当前位置 博文首页 > 九号窗口的博客:前端面试题总结(vue)
vue是个轻量级的框架,是一个构建数据的视图集合,大小只有几十Kb
vue是组件化开发,适合多人开发
vue中的双向数据绑定更方便操作表单数据
因为vue是MVVM的框架,视图,数据,结构分离使数据的更改更为简单
vuex可以管理所有组件用到的数据,不需要借助之前props在组件间传值
官方文档通俗易懂,易于理解和学习;
数据驱动和组件化。
理解
model是数据模型,管理数据和处理业务逻辑
view是视图,负责显示数据
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,负责监听Model中数据的改变并且控制视图的更新
model中的数据改变,view也会跟着改变;用户通过交互操作改变view中的数据,model也会跟着改变。其中的dom操作viewmodel帮我们完成了,不需要自己操作dom
优点
mvvm 主要解决了 mvc 中大量的 DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。
当 Model 频繁发生变化,开发者需要主动更新到 View
首先,使用Vue.extend()创建一个组件
然后,使用Vue.component()方法注册组件
接着,如果子组件需要数据,可以在props中接受定义
最后,子组件修改好数据之后,想把数据传递给父组件,可以使用emit()方法
通过props,例子如下
<template>
<div>
<div>{{message}}(子组件)</div>
</div>
</template>
<script>
export default {
props: {
message: { type: String, default: '默认值' } ,//定义传值的类型
}
</script>
<template>
<div>
<div>父组件</div>
<child :message="parentMsg"></child>
</div>
</template>
<script>
import child from './child' //引入child组件
export default {
data() {
return {
parentMsg: 'a message from parent' //在data中定义需要传入的值
}
},
components: {
child
}
}
</script>
父:
<child ref="childMethod"></child>
子:
method: {
test() {
alert(1)
}
}
在父组件里调用test即 this.$refs.childMethod.test()
<template>
<div class="app">
<input @click="sendMsg" type="button" value="给父组件传递值">
</div>
</template>
<script>
export default {
data () {
return {
//将msg传递给父组件
msg: "我是子组件的msg",
}
},
methods:{
sendMsg(){
//func: 是父组件指定的传数据绑定的函数,this.msg:子组件给父组件传递的数据
this.$emit('func',this.msg)
}
}
}
</script>
<template>
<div class="app">
<child @func="getMsgFormSon"></child> //父组件调用方法
</div>
</template>
<script>
import child from './child.vue'
export default {
data () {
return {
msgFormSon: "this is msg"
}
},
components:{
child,
},
methods:{
getMsgFormSon(data){
//此处的data即为子组件传过来的数据
this.msgFormSon = data
console.log(this.msgFormSon)
}
}
}
</script>
直接在子组件中通过this.$parent.event来调用父组件的方法
bus方式
1.新建bus.js
import Vue from 'vue'
export default new Vue
2.在需要传值和接受值的vue文件中,各自引入bus.js
import bus from '../util/bus'
3.定义传值的方法,使用bus.$emit(‘methodName’,data), methodName是自定义的方法名
<button @click="trans()">传值</button>
methods: {
trans(){
bus.$emit('test',this.helloData)
}
},
4.在要接收值的组件里,使用bus.on(‘methodName’,val =>{ }) ,val 就是传过来的值
mounted(){
bus.$on('test',val=>{
console.log(val);
this.cdata = val
})
}
1、安装vuex :cnpm install vuex --save
2、创建一个 vuex 文件夹,并在里面新建一个 store.js 写入以下代码:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
3、state 定义数据:state在vuex中用于存储数据
var state={ //存放数据,数据中心
count:1,
// 其他数据格式:orderList: [],
// 其他数据格式:params: {}
}
4、 getters 类似计算属性:
var getters= {
computedCount: (state) => {
return state.count*2
}
}
5、mutations里面放的是方法,方法主要用于改变state里面的数据
var mutations={
incCount(){
++state.count;
}
}
6、异步操作,Action 提交的是 mutation,而不是直接变更状态
var actions= {
incMutationsCount(context) { /*因此你可以调用 context.commit 提交一个 mutation*/
context.commit('incCount'); /*执行 mutations 里面的incCount方法 改变state里面的数据*/
//此处按照实际情况扩展~
}
}
7、暴露参数
const store = new Vuex.Store({
state,
mutations,
getters,
actions
})
export default store;
8、 组件里去使用 Vuex:
(1). 获取state里面的数据
this.$store.state.数据
(2). 获取 getters里面方法返回的的数据 (一般vue 和 store 进行交互 用 $store.getters, getters的值放在计算属性里,动态绑定在计算属性computed里)
this.$store.getters.computedCount
(3). 触发 mutations 改变 state里面的数据
this.$store.commit('incCount');
(4). 触发 actions里面的方法
this.$store.dispatch('incMutationsCount');
//这个 incMutationsCount 会再去 执行 mutations 里面的incCount方法
1.区别:vuex存储在内存,localstorage(本地存储)则以文件的方式存储在本地,永久保存;
localStorage和sessionStorage只能存储字符串类型,对于复杂的对象可以使用ECMAScript提供的JSON对象的stringify和parse来处理
2.应用场景:vuex用于组件之间的传值,localstorage,sessionstorage则主要用于不同页面之间的传值。
3.永久性:当刷新页面(这里的刷新页面指的是 --> F5刷新,属于清除内存了)时vuex存储的值会丢失,sessionstorage页面关闭后就清除掉了,localstorage不会。
1.使用vue-router通过跳转链接带参数传参。
2.使用本地缓存localStorge。
3.使用vuex数据管理传值。
1:hash 模式下,仅hash符号之前的内容会被包含在请求中,如http://www.abc.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误。
history模式下,前端的URL必须和实际向后端发起请求的URL一致。
history 模式需要后端配合将所有访问都指向 index.html,否则用户刷新页面,会导致 404 错误
实现路由权限控制的两种方式
配置静态的路由表,比如登录、注册页,其他路由通过动态注入
登录的时候过滤后台返回的路由列表,拿到符合路由规则的路由表
通过 addRoutes() 这个方法把路由给注入到路由表,这样就可以访问已注入的路由了
1、第一次登录的时候,前端调后端的登陆接口,发送用户名和密码
2、后端收到请求,验证用户名和密码,验证成功,就给前端返回一个token
3、前端拿到token,将token存储到localStorage和vuex中,并跳转路由页面
4、前端每次跳转路由,就判断 localStroage 中有无 token ,没有就跳转到登录页面,有则跳转到对应路由页面
5、每次调后端接口,都要在请求头中加token
6、后端判断请求头中有无token,有token,就拿到token并验证token,验证成功就返回数据,验证失败(例如:token过期)就返回401,请求头中没有token也返回401
7、如果前端拿到状态码为401,就清除token信息并跳转到登录页面
// 导航守卫
// 使用 router.beforeEach 注册一个全局前置守卫,判断用户是否登陆
router.beforeEach((to, from, next) => {
if (to.path === '/login') {
next();
} else {
let token = localStorage.getItem('Authorization');
if (token === 'null' || token === '') {
next('/login');
} else {
next();
}
}
});
// 添加请求拦截器,在请求头中加token
axios.interceptors.request.use(
config => {
if (localStorage.getItem('Authorization')) {
config.headers.Authorization = localStorage.getItem('Authorization');
}
return config;
},
error => {
return Promise.reject(error);
});
//如果前端拿到状态码为401,就清除token信息并跳转到登录页面
localStorage.removeItem('Authorization');
this.$router.push('/login');
调微信的授权登录地址,成功之后微信会重定向回到我们开发的页面并返回code在回调的url中
拿到code以后,传给后台,让后台去获取用户信息再传给前端。我们拿到用户信息后,比如openId,头像等,可以用localStorage缓存起来
v-show 本质就是通过控制 css 中的 display 设置为 none,控制隐藏,只会编译一次;v-if 是动态的向 DOM 树内添加或者删除 DOM 元素,若初始值为 false ,就不会编译了。而且 v-if 不停的销毁和创建比较消耗性能。
总结:如果要频繁切换某节点,使用 v-show (切换开销比较小,初始开销较大)。如果不需要频繁切换某节点使用 v-if(初始渲染开销较小,切换开销比较大)
v-model 双向数据绑定;
v-for 循环;
v-if v-show 显示与隐藏;
v-on 事件;v-once : 只绑定一次。
当data 有变化的时候它通过Object.defineProperty()方法中的set方法进行监控,并调用在此之前已经定义好data 和view关系的回调函数,来通知view进行数据的改变
而view 发生改变则是通过底层的input 事件来进行data的响应更改
概念:Vue 实例从创建到销毁的过程
生命周期钩子的一些使用方法:
beforecreate : 可以在这加个loading事件,在加载实例时触发
created : 初始化完成时的事件写在这里,如在这结束loading事件,异步请求也适宜在这里调用
mounted : 挂载元素,获取到DOM节点
updated : 如果对数据统一处理,在这里写上相应函数
beforeDestroy : 可以做一个确认停止事件的确认框 nextTick : 更新数据后立即操作dom
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
计算属性和侦听属性都可以实现当某一个数据(称它为依赖数据)发生变化的时候,所有依赖这个数据的“相关”数据“自动”发生变化,但在特定场景下的处理还是有一些微妙的区别
data: {
firstName: 'Liu',
lastName: 'lu'
},
computed: {
fullName:{
get(){//回调函数 当需要读取当前属性值时执行,根据相关数据计算并返回当前属性的值
return this.firstName + ' ' + this.lastName
},
set(val){//监视当前属性值的变化,当属性值发生变化时执行,更新相关的属性数据
//val就是fullName的最新属性值
console.log(val)
const names = val.split(' ');
console.log(names)
this.firstName = names[0];
this.lastName = names[1];
}
}
}
watch: {
pagination: {
handler (val) {
this.paginationInfo = val
},
immediate: true,
deep: true
}
}
computed常用于值的计算,如简化tempalte里面{{}}计算和处理props或$emit的传值,页面重新渲染值不变化,计算属性会立即返回之前的计算结果,而不必再次执行函数。
watch常用来观察动作,听props,$emit或本组件的值执行异步操作,页面重新渲染时值不变化也会执行
computed名称不能与data里对象重复,只能用同步,必须有return,是多个值变化引起一个值变化,多多对一
watch名称必须与data里对象一样,可以用于异步,没有return,是一对多,监听一个值,一个值变化引起多个值变化
1、watch中的函数名称必须是所依赖data中的属性名称
2、watch中的函数是不需要调用的,只要函数所依赖的属性发生了改变 那么相对应的函数就会执行
3、watch中的函数会有2个参数 一个是新值,一个是旧值
4、watch默认情况下无法监听对象的改变,如果需要进行监听则需要进行深度监听 深度监听需要配置handler函数以及deep为true。(因为它只会监听对象的地址是否发生了改变,而值是不会监听的)
5、watch默认情况下第一次的时候不会去做监听,如果需要在第一次加载的时候也需要去做监听的话需要设置immediate:true
6、watch在特殊情况下是无法监听到数组的变化
- 通过下标来更改数组中的数据
- 通过length来改变数组的长度
原因:v-for比v-if优先,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候。
不推荐:
<ul>
<li v-for="user in users" v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
</ul>
推荐:
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isActive
})
}
}
<ul>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</ul>
可以让当前组件或者路由不经历创建和销毁,而是进行缓存,凡是被keep-alive组件包裹的组件,除了第一次以外。不会经历创建和销毁阶段的。第一次创建后就会缓存到缓存当中
初次进入时:created > mounted > activated;退出后触发 deactivated
再次进入:会触发 activated;事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
routers: [{
path: '/',
name: 'Home',
meta: {
keepAlive: false // 不需要缓存
}
},{
path: '/page',
name: 'Page',
meta: {
keepAlive: true // 需要缓存
}
},]
$router为VueRouter的实例,是一个全局路由对象,包含了路由跳转的方法、钩子函数等。
$route 是路由信息对象||跳转的路由对象,每一个路由都会有一个route对象,是一个局部对象,包含path,params,hash,query,fullPath,matched,name等路由信息参数
state:存储数据,存储状态;在根实例中注册了store 后,用 this.$store.state 来访问;对应vue里面的data;存放数据方式为响应式,vue组件从store中读取数据,如数据发生变化,组件也会对应的更新。
组件访问State中数据的两种方式
1、this.$store.state.全局数据名称
2、从vuex中按需导入mapState函数,将当前组件需要的全局数据映射为当前组件的computed计算属性
import { mapState } from 'vuex'
computed:{
...mapState( ['count'] )
}
getter:可以认为是 store 的计算属性,它的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
const store = new Vuex.store({
state:{
count : 0
},
getters:{
countAfter: state=>{
return count*2
}
}
})
//使用getters的第一种方式
this.$store.getters.countAfter
mutation:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
const store = new Vuex.store({
state:{
count : 0
},
mutation:{
add(state){
//变更状态
state.count++
}
}
})
nethods:{
handle(){
//触发mutation的第一种方式
this.$store.commit('add')
}
//1、从vuex中按需导入mapMutation函数
import { mapMutation } from 'vuex'
//2、将指定的mutation函数,映射为当前组件的methods函数
methods:{
...mapMutation(['add','addN'])
handle(){
//触发mutation的第二种方式
this.add()
}
}
action:包含任意异步操作,通过提交 mutation 间接更变状态。
const store = new Vuex.store({
mutation:{
add(state){
state.count++
}
},
actions:{
addAsync(context){
//在actions中不能直接修改state中的数据
//必须通过context.commit()触发某个mutation才行
setTimeout(()=>{
context.commit('add')
},1000)
}
}
})
//触发Action
methods:{
handle(){
//触发actions的第一种方式
this.$store.dispatch('addAsync')
}
}
//1、从vuex中按需导入mapActions函数
import { mapActions} from 'vuex'
//2、将指定的mapAtions函数,映射为当前组件的methods方法
methods:{
...mapMutation(['addAsync','addNAsync'])
handle(){
//触发mutation的第二种方式
this.addAsync()
}
}
module:将 store 分割成模块,每个模块都具有state、mutation、action、getter、甚至是嵌套子模块。
<input type="text" v-model.trim="msg">
<input type="text" v-model.lazy="msg" @input="input" @change="change">
首先,sass和less都是css的预编译处理语言,他们引入了mixins,参数,嵌套规则,运算,颜色,名字空间,作用域,JavaScript赋值等 加快了css开发效率,当然这两者都可以配合gulp和grunt等前端构建工具使用,但是他们两者有什么不同呢?
1.编译环境不同
less是通过js编译 是在客户端处理
sass同通过ruby 是在服务器端处理
2.变量符不一样
less是用@,sass是用$
3.sass支持条件语句,可以使用if{}else{},for{}循环等等。而less不支持。
var 存在变量提升而let和const不存在变量提升
var存在变量覆盖,而let和const在同级作用域中不能重复定义
var声明的变量会挂载到window上,会放在全局,let 和 const声明的变量不会
let和const声明的变量会形成块级作用域,var不会
const有一个很好的应用场景,当我们引用第三方库的时声明的变量,用const来声明可以避免未来不小心重命名而导致出现bug
console.log(a);//undefined
var a = "hey I am now hoisting";
console.log(a);//Uncaught ReferenceError: a is not defined
let a = "hey I am now hoisting";
var a="show";
function hah(){
alert(a);//undefined
var a=4;
alert(a);//4
}
hah();
function hah(number){
var a="show";
while(number!=0){
alert(a);//show
var a=4;
alert(a);//4
number--;
}
}
hah(1);
$("#result").append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
function(x, y) {
x++;
y--;
return x + y;
}
(x, y) => {x++; y--; return x+y}
// 3.新增字符串方法
let str = "hello.vue";
// 开头
console.log(str.startsWith("hello"));//true
// 后缀
console.log(str.endsWith(".vue"));//true
// 包含
console.log(str.includes("e"));//true
console.log(str.includes("hello"));//true
当我们使用箭头函数时,函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,它的this是继承外面的,因此内部的this就是外层代码块的this
普通函数中的this总是代表它的直接调用者,在默认情况下,this指的是window
依赖管理:方便引用第三方模块、让模块更容易复用、避免全局注入导致的冲突、避免重复加载或加载不需要的模块。
合并代码:把各个分散的模块集中打包成大文件,减少 HTTP 的请求链接数,配合 UglifyJS 可以减少、优化代码的体积。
各路插件:babel 把 ES6+ 转译成 ES5 ,eslint 可以检查编译期的错误……
由于 webpack 并不支持除 .js 以外的文件,
从而需要使用 loader 转换成 webpack 支持的模块,
plugin 用于扩展 webpack 的功能,
在 webpack 构建生命周期的过程在合适的时机做了合适的事情。
简单的说就是分析代码,找到“require”、“exports”、“define”等关键词,并替换成对应模块的引用。
把你的项目当成一个整体,通过一个给定的主文件(index.js),
webpack将从这个文件开始找到你的项目的所有的依赖文件,
使用loaders处理他们,最后打包为一个浏览器可以识别的js文件
file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
source-map-loader:加载额外的 Source Map 文件,以方便断点调试
image-loader:加载并且压缩图片文件
babel-loader:把 ES6 转换成 ES5
css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
eslint-loader:通过 ESLint 检查 JavaScript 代码
process.env是Node.js用于存放当前进程环境变量的对象;
而NODE_ENV则可以让开发者指定当前的运行时环境,
当它的值为production时即代表当前为生产环境
库和框架在打包时如果发现了它就可以去掉一些开发环境的代码,如警告信息和日志等。这将有助于提升代码运行速度和减小资源体积
首先可以优化Loader的搜索范围,只在src文件夹下查找,node_modules下的代码是编译过的,没必要再去处理一遍
使用HappyPack可以将Loader的同步执行转为并行,从而执行Loader时的编译等待时间
代码压缩相关,启用gzip压缩
概念
虚拟dom就是一个能代表dom树的js对象,通常含有标签名、标签属性还有一些子元素等等
优点
虚拟dom借助dom diff算法可以减少不必要的dom操作
diff算法是在新虚拟DOM和老虚拟DOM进行diff(精细化比对),实现最小量更新,最后反映到真正的DOM上
v-for默认使用就地复用策略,列表数据修改的时候,他会根据key值去判断某个值是否修改,
如果修改,则重新渲染这一项,否则复用之前的元素
key的作用主要是为了高效的更新虚拟DOM
函数作为对象本身属性调用的时候,this 指向对象
1.call:参数1 this指向,参数2 任意类型
2.apply:参数1 this指向,参数2 数组 (参数一为null指向的是本身)
3.var一个变量保存this指向
4.使用es6的箭头函数
let obj={
a:222,
fn:function(){
console.log(this) //obj
setTimeout(function(){
console.log(this) //window
})
}
};
obj.fn();
let obj={
a:222,
fn:function(){
setTimeout(()=>{
console.log(this) //obj
console.log(this.a) //222
});
}
};
obj.fn();
//被嵌套的函数独立调用时this默认指向window
var obj = {
a:2,
foo:function(){
console.log(this) // obj
function text(){
console.log(this) //window
}
text();
}
}
obj.foo()
//自执行函数内部中的this指向window
var a = 10;
function foo(){
(function test(){
console.log(this); //window
})()
}
var obj = {
a:2,
foo:foo
}
obj.foo();
//闭包 this默认指向了window
var a = 10;
var obj = {
a:2,
foo:function(){
var c = this.a
return function test(){
console.log(this)
return c
}
}
}
var fn = obj.foo()
fn()
// 隐式丢失this的五种情况
// 1、函数赋值给另外变量
var a = 0;
function foo(){
console.log(this) //window
console.log(this.a)
}
var obj = {
a:1,
foo:foo
}
var bar = obj.foo;
bar();
//2、参数传递
var a = 0;
function foo(){
console.log(this)
}
function bar(fn){
fn()
}
var obj={
a:1,
foo:foo,
}
bar(obj.foo);
// 3、内置函数 setTimeout、setInterval第一个参数的回调函数中的this默认指向window
var a = 0;
var obj = {
a:1,
foo:function(){
console.log(this.a)
}
}
setTimeout(obj.foo, 2000)
// 4、间接调用
function foo(){
console.log(this.a)
}
var a = 0;
var obj={
a:1,
foo:foo
}
var p = {a:4};
obj.foo(); //1
(p.foo = obj.foo)(); //0
// 显式绑定
1、call,apply, bind
2、数组的forEach等方法
promise对象采用链式的 then方法,可以指定一组按照次序调用的回调函数。
前一个 then 里的一个回调函数,返回的可能还是一个 Promise对象,(即有异步操作)这时后面的回调函数,就会等待该 Promise对象的状态发生变化才会被调用,由此实现异步操作按照次序执行。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
// 采用ajax异步回调的方式改装成promise
function getIp(){
var promise = new Promise(function(resolve,reject){
var xhr = new XMLHttpRequest()
xhr.open('GET','https://easy-mock.com/mock/5ac2f80c3d211137b3f2843a/promise/getIp', true) //设置一个ajax的参数)
xhr.onload = function(){
var retJson = JSON.parse(xhr.responseText) // {"ip":"58.100.211.137"} 数据到来对数据进行解析
resolve(retJson.ip) //初始化-完成状态-变为成功状态
}
xhr.onerror = function(){
reject('获取IP失败') //初始化-拒绝状态-变为失败状态
}
xhr.send()
})
return promise
}
function getCityFromIp(ip){
var promise = new Promise(function(resolve,reject){
var xhr = new XMLHttpRequest()
xhr.open('GET','https://easy-mock.com/mock/5ac2f80c3d211137b3f2843a/promise/getCityFromIp?ip='+ip, true)
xhr.onload = function(){
var retJson = JSON.parse(xhr.responseText) // {"city": "hangzhou","ip": "23.45.12.34"}
resolve(retJson.city)
}
xhr.onerror = function(){
reject('获取city失败')
}
xhr.send()
})
return promise
}
function getWeatherFromCity(city){
var promise = new Promise(function(resolve,reject){
var xhr = new XMLHttpRequest()
xhr.open('GET','https://easy-mock.com/mock/5ac2f80c3d211137b3f2843a/promise/getWeatherFromCity?city='+city, true)
xhr.onload = function(){
var retJson = JSON.parse(xhr.responseText) // {"weather": "晴天","city": "beijing"}
reslove(retJson)
}
xhr.onerror = function(){
reject('获取天气失败')
}
xhr.send()
})
return promise
}
// getIp获取IP-IP获取城市-城市获取天气
getIp().then(function(ip){
return getCityFromIp(ip) // 得到ip
}).then(function(city){
return getWeatherFromCity(city) // 得到城市
}).then(function(){
console.log(data) // 得到具体的城市其他状况(如天气、人口等等)
}).catch(function(e){
console.log('出现了错误',e)
})