Vue 中的事件循环
父子组件钩子执行顺序
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
父组件包含多个子组件时,父组件和兄弟组件钩子执行顺序如下
父beforeCreate -> 父created -> 父beforeMount -> A组件beforeCreate -> A组件created -> A组件beforeMount-> B组件beforeCreate -> B组件created -> B组件beforeMount -> A组件mounted -> B组件mounted -> 父mounted
我们一般会在 父created / 父mounted 中去发送请求,那么执行顺序会变成
父beforeCreate -> 父created[request1] -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted[request2] -> request1_callback -> request2_callback
我们还可以在 Vue 中使用 setTimeout / setInterval / nextTick,那么执行顺序就会变成
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created[setTimeout -> nextTick] -> 子beforeMount -> 子mounted -> 父mounted[setInterval] -> nextTick_callback -> setTimeout_callback -> setInterval_callback
这其中涉及了 Javascript Event Loop 的知识,script(整体代码) / setTimeout / setInterval / request 都是宏任务(macro-tasks / tasks),nextTick 是微任务(micro-tasks / jobs)。事件循环从宏任务 script(整体代码) 开始第一次循环,之后全局上下文进入函数调用栈,直到调用栈清空(只剩全局),然后执行所有的 micro-tasks,当所有可执行的 micro-tasks 执行完毕之后,第一次事件循环结束。循环再次从 macro-tasks 开始,找到其中一个任务队列执行完毕,然后再执行所有的 micro-tasks,这样一直循环下去。 更加具体的解释可参考 详解事件循环
了解以上内容后,我们来看看下面这个例子打印的是什么?
<html>
<head>
<title>
Event Loop in Vue
</title>
<meta name="description" content="Shopee Seller Data Center" />
</head>
<body>
<div id="app">
<div>I am Parent Compoent : name is {{ name }}</div>
<div>-----------------------------------------</div>
<comp :metric-data="metricData" />
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const comp = {
template: `<div>I am Child Component : name is Child<div><ul v-for="item in metricData">{{ item.name }}</ul></div></div>`,
props: {
metricData: {
type: Array,
required: true,
},
},
data() {
return {
name: 'child',
}
},
beforeCreate() {
console.log('子beforeCreate')
},
created() {
console.log('子created')
setTimeout(() => {
console.log('子created: setTimeout')
}, 1000)
this.$nextTick(() => {
console.log('子created: nextTick')
})
},
beforeMount() {
console.log('子beforeMount')
},
mounted() {
console.log('子mounted')
setInterval(() => {
console.log('子mounted: setInterval')
}, 1000)
}
}
const vm = new Vue({
el: '#app',
data: {
name: 'Parent',
metricData: []
},
components: {
comp
},
methods: {
getDataInCreated () {
// 模拟请求
const metricData = [
{ name: 'jack' },
{ name: 'lucy' }
]
setTimeout(() => {
console.log('父created: getDataInCreated')
this.metricData = metricData
}, 1000)
},
getDataInMounted () {
// 模拟请求
setTimeout(() => {
console.log('父mounted: getDataInMounted')
}, 1000)
}
},
beforeCreate() {
console.log('父beforeCreate')
},
created() {
console.log('父created')
this.getDataInCreated()
},
beforeMount() {
console.log('父beforeMount')
},
mounted() {
console.log('父mounted')
this.getDataInMounted()
}
})
</script>
</body>
</html>
打印结果:
父beforeCreate ---第一次事件循环
父created
父beforeMount
子beforeCreate
子created
子beforeMount
子mounted
父mounted
子created: nextTick
父created: getDataInCreated ---第二次事件循环
子created: setTimeout ---第三次事件循环
子mounted: setInterval ---第四次事件循环
父mounted: getDataInMounted ---第五次事件循环
beforeCreate & created 时选项的初始化
各个选项的初始化顺序:
beforeCreate -> initState[initProps -> initMethods -> initData -> initCompouted -> initWatch] -> created
beforeCreate:
-
props:属性(e.g. metricData)已经代理到 vm 上,但是访问会报错,这是因为
vm._props === undefined
,访问 metricData 会执行 vm._props.metricData,所以会报 Exception: TypeError: Cannot read property 'metricData' of undefined at VueComponent.proxyGetter 的错误。相关源码如下:// 源码 function initProps$1 (Comp) { var props = Comp.options.props; for (var key in props) { proxy(Comp.prototype, "_props", key); } } var sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }; function proxy (target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }
-
methods:属性还未代理到 vm 上,即 vm 上没有这个属性。
-
data:属性还未代理到 vm 上。
-
computed:属性已经代理到 vm 上,即 vm 上有这个属性,但是值全是 undefined。
-
watch:vm 上没有 _watchers 属性。
created:
-
props:如果父组件中有使用这个prop:即传值 !== undefined,那么它的值就是父组件传递的值;如果父组件中没有使用这个 prop,定义时有默认值就等于默认值,没有默认值就是 undefined。
if (父组件有使用:即传值 !== undefined) { value = 父组件传值 } else { if (定义时有默认值) { value = 默认值 } else { value = undefined } }
-
methods:在 initMethods 初始化并挂载到 vm 上。
-
data:属性在 initData 中被代理到 vm 上,值等于定义的初始值。
-
computed:每一个 computed 属性,都会生成一个对应的 watcher 对象(computed-watcher),这类 watcher 有个特点,我们拿下面的 b 举例:属性 b 依赖 a,当 a 改变的时候,b 并不会立即重新计算,而是之后其他地方读取 b 的时候,它才会真正计算,即具备 lazy(懒计算)特性。computed-watcher 会保存在 _watchers 中。
{ data () { return { a: 12 } }, computed: { b () { return this.a * 2 } } }
-
watch:每一个 watch 属性,都会生成一个对应的 watcher 对象(normal-watcher / user-watcher), 只要监听的属性改变了,就会触发定义好的回调函数。normal-watcher 也会保存在 _watchers 中。
watch: { immediate: true }
现在有个需求,两个功能点:展示列表;有一个输入框可以筛选列表。代码有两种实现:
-
定义一个searchValue,searchValue 绑定输入框,在 created 中发送请求,watch searchValue,当它发生变化时,再去发送请求,代码如下:
{ data () { return { searchValue: '' } } watch: { searchValue(newVal, oldVal) { if (newVal !== oldVal) { this.getList() } } }, methods: { getList() {} }, created () { this.getList() } }
-
无需在 created 中发送请求,使用 watch immediate: true
{ data () { return { searchValue: '' } } watch: { searchValue: { handler: this.getList, immediate: true } }, methods: { getList() {} }, }
写法一是在初始化 watch 的时候(initWatch),不会执行 handler(默认 immediate: false),接下来执行钩子 created,在其中发送请求,之后 searchValue 有变化再去发送请求。
写法二设置了 immediate: true,所以在初始化 watch 时就会立即执行 handler,接下来再去调用钩子函数(如果有的话),之后 searchValue 有变化再去发送请求。
两种的差别在于第一次触发请求的地方,写法一是在 created 中,写法二是在 watch 中,写法二第一次请求触发的时间早于写法一。