杂谈:vue中created、watch和computed的执行顺序

面试题:vuecreatedwatch(immediate: true)computed的执行顺序是啥?

先看个简单的例子:

// main.js
import Vue from "vue";

new Vue({el: "#app",template: `<div><div>{{computedCount}}</div></div>`,data() {return {count: 1,}},watch: {count: {handler() {console.log('watch');},immediate: true,}},computed: {computedCount() {console.log('computed');return this.count + 1;}},created() {console.log('created');},
}); 

当前例子的执行顺序为:watch --> created --> computed


为什么?

new Vue的实例化过程中,会执行初始化方法this._init,其中有代码:

Vue.prototype._init = function (options) {// ...initState(vm);// ...callHook(vm, 'created');// ...if (vm.$options.el) {vm.$mount(vm.$options.el);}
}

function initState (vm) {vm._watchers = [];var opts = vm.$options;if (opts.props) { initProps(vm, opts.props); }if (opts.methods) { initMethods(vm, opts.methods); }if (opts.data) {initData(vm);} else {observe(vm._data = {}, true /* asRootData */);}if (opts.computed) { initComputed(vm, opts.computed); }if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch);}
} 

猛一看代码,是不是发现先执行的initComputed(vm, opts.computed),然后执行initWatch(vm, opts.watch),再执行callHook(vm, 'created'),那为啥不是computed --> watch --> created呢?


不要着急,听我娓娓道来。

1、关于initComputed

const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {// $flow-disable-lineconst watchers = vm._computedWatchers = Object.create(null)// computed properties are just getters during SSRconst isSSR = isServerRendering()for (const key in computed) {const userDef = computed[key]const getter = typeof userDef === 'function' ? userDef : userDef.get// ...if (!isSSR) {// create internal watcher for the computed property.watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions)}// component-defined computed properties are already defined on the// component prototype. We only need to define computed properties defined// at instantiation here.if (!(key in vm)) {defineComputed(vm, key, userDef)} else if (process.env.NODE_ENV !== 'production') {// ...}}
} 

在通过initComputed初始化计算属性的时候,通过遍历的方式去处理当前组件中的computed。首先,在进行计算属性实例化的时候,将{ lazy: true }作为参数传入,并且实例化的Watcher中的getter就是当前例子中的computedCount函数;其次,通过defineComputed(vm, key, userDef)的方式在当前组件实例vm上为key进行userDef的处理。具体为:

export function defineComputed (target: any,key: string,userDef: Object | Function
) {const shouldCache = !isServerRendering()if (typeof userDef === 'function') {sharedPropertyDefinition.get = shouldCache? createComputedGetter(key): createGetterInvoker(userDef)sharedPropertyDefinition.set = noop}// ...Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {if (watcher.dirty) {watcher.evaluate()}if (Dep.target) {watcher.depend()}return watcher.value}}
} 

从以上可以看出,这里通过Object.defineProperty(target, key, sharedPropertyDefinition)的方式,将函数computedGetter作为get函数,只有当对key进行访问的时候,才会触发其内部的逻辑。内部逻辑watcher.evaluate()为:

evaluate () {this.value = this.get()this.dirty = false
} 

get中有主要逻辑:

value = this.getter.call(vm, vm) 

这里的this.getter就是当前例子中的:

computedCount() {console.log('computed');return this.count + 1;
} 

也就是说,只有当获取computedCount的时候才会触发computed的计算,也就是在进行vm.$mount(vm.$options.el)阶段才会执行到console.log('computed')

2、关于initWatch

function initWatch (vm: Component, watch: Object) {for (const key in watch) {const handler = watch[key]if (Array.isArray(handler)) {for (let i = 0; i < handler.length; i++) {createWatcher(vm, key, handler[i])}} else {createWatcher(vm, key, handler)}}
}
function createWatcher (vm: Component,expOrFn: string | Function,handler: any,options?: Object
) {if (isPlainObject(handler)) {options = handlerhandler = handler.handler}if (typeof handler === 'string') {handler = vm[handler]}return vm.$watch(expOrFn, handler, options)
} 

在通过initWatch初始化侦听器的时候,如果watch为数组,则遍历执行createWatcher,否则直接执行createWatcher。如果handler是对象或者字符串时,将其进行处理,最终作为参数传入vm.$watch中去,具体为:

Vue.prototype.$watch = function ( expOrFn: string | Function,cb: any,options?: Object ): Function {const vm: Component = thisif (isPlainObject(cb)) {return createWatcher(vm, expOrFn, cb, options)}options = options || {}options.user = trueconst watcher = new Watcher(vm, expOrFn, cb, options)if (options.immediate) {try {cb.call(vm, watcher.value)} catch (error) {handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}return function unwatchFn () {watcher.teardown()}} 

这里获取到的options中会有immediate: true的键值,同时通过options.user = true设置usertrue,再将其作为参数传入去进行Watcher的实例化。

当前例子中options.immediatetrue,所以会执行cb.call(vm, watcher.value),也就是以vm为主体,立刻执行cb。当前例子中cb就是handler

handler() {console.log('watch');
}, 

这里就解释了当前例子中console.log('watch')是最先执行的。

然后,执行完initComputedinitWatch以后,就会通过callHook(vm, 'created')执行到生命周期中的console.log('created')了。

最后通过vm.$mount(vm.$options.el)进行页面渲染的时候,会先去创建vNode,这时就需要获取到computedCount的值,进而触发其get函数的后续逻辑,最终执行到console.log('computed')

总结

关于vuecreatedwatch的执行顺序相对比较简单,而其中computed是通过Object.defineProperty为当前vm进行定义,再到后续创建vNode阶段才去触发执行其get函数,最终执行到计算属性computed对应的逻辑。

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值