最近工作中遇到个bug,半天找不到为什么,场景大概是这样的
<template>
<div id="app">
<p>{{data.user_info.nick}}</p>
</div>
<template>
<script>
export default {
name: 'app',
data() {
return {
data:{},
}
},
created() {
this.init();
},
methods:{
init() {
ajax({
url:'hhh',
type:'get',
success() {
this.data = res;
}
})
}
}
}
</script>
运行时报错
TypeError: Cannot read property ‘nick’ of undefined
[图片上传失败...(image-2d58c6-1541755736318)]
为了快速解决问题,我做了这样的修改
data() {
return {
data:{
user_info:{}
},
}
},
对象里面有对象,那我就先定义成想要的结构
或者,下面这种解决方法也是可行的
<div id="app">
<p>{{data.user_info.nick}}</p>
</div>
后来,在项目做完了以后,我尝试着找到了原因
需要关注两点,一是生命周期,二是vue的异步dom更新
下面,我们来回顾一下vue的生命周期
在我上面的代码中,是把init放到了created阶段去执行,我们注意created阶段的描述,组件实例创建完成,属性已绑定,但是dom还未生成,$el属性还不存在。所以如果执行下面代码,将不会报错
export default {
name: 'app',
data() {
return {
data:{},
}
},
created() {
this.init();
},
methods:{
init() {
this.data = {
user_info:{
nick:'ggg'
}
}
//ajax({
// url:'hhh',
// type:'get',
// success() {
// this.data = res;
// }
//})
}
}
}
因为在init中的代码是同步执行的,所以在created阶段我们更改了data但是dom并没有加载,所以不会报错。
但是,因为我把data的改变放在了一个异步请求中,所以data没有立即被改变,在它得到数据的时候已经错过了上一班车了。(错过了上一班车的概念等会会解释);
然后我们在来看一下如果把init函数放在mounted中呢。
mounted阶段,模版编译挂载之后
这个时候dom已经生成,而生成的dom读取的是原始的data数据,所以不管是不是放到请求中去改变data都会报错
export default {
name: 'app',
data() {
return {
data:{},
}
},
mounted() {
this.init();
},
methods:{
init() {
this.data = {
user_info:{
nick:'ggg'//TypeError: Cannot read property ‘nick’ of undefined
}
}
}
}
}
export default {
name: 'app',
data() {
return {
data:{},
}
},
mounted() {
this.init();
},
methods:{
init() {
ajax({
url:'hhh',
type:'get',
success() {
this.data = res;//TypeError: Cannot read property ‘nick’ of undefined
}
})
}
}
}
所以其实其实我把init写在created中是不好的,因为vue的生命周期的执行和请求数据是两个同时在执行的线程,因为网速的快慢,对于不同用户,他们可能会在不同的生命周期得到数据。而在created阶段因为dom还未生成,所以就加大了不确定性。所以最好把请求数据写到mounted中。
然后在来说说刚才的错过班车的问题
这里我们要来看看vue的内部实现机制
在new Vue()之后,Vue()会调用_init函数(不是自己写的那个init函数)进行初始化,它会初始化生命周期(beforeCreated阶段)、事件、props、methods、data、computed与watch等。其中最重要的是通过Object.defineProperty设置setter和getter函数,用来监控data数据的改变,实现所谓的数据双向绑定。
编译
complie编译可以分成parse、optimize与generate三个阶段
parse
parse负责解析template模版中的指令、class、style等数据,形成AST(抽象语法树)
optimize
optimize的主要作用是标记static静态节点,这是vue在编译过程中的一处优化,后面update更新界面时,会有一个patch的过程,diff算法会直接跳过静态节点,从而减少了比较的过程,优化了patch的性能。
generate
generate是将AST转化成render function字符串的过程,得到结果是render的字符串以及staticRenderFns字符串。
在经历过 parse、optimize 与 generate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。
响应式
当render function被渲染的时候,因为会读取所需对象的值,所以会触发getter函数进行依赖收集,依赖收集的目的是将观察者watcher对象存放到当前闭包中的订阅者Dep的subs中。在修改对象的值的时候,会触发对应的 setter, setter 通知之前「依赖收集」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。
需要注意的是,vue并不会在以后对象的修改的时候就更新视图,它会将这些更新收集起来,一段时间去更新一次视图。
因为我们其实并不很清楚的知道会更新视图的具体时间,所以如果有些操作一定要在dom更新之后执行,可以使用$nextTick函数。
$nextTick
它接收一个回调函数,回调函数会在这次更新完后执行。