vue中created、watch和computed的执行顺序详解
qb 人气:1前言
面试题:
vue
中created
、watch(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-line const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const 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 = handler handler = 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 = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const 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
设置user
为true
,再将其作为参数传入去进行Watcher
的实例化。
当前例子中options.immediate
为true
,所以会执行cb.call(vm, watcher.value)
,也就是以vm
为主体,立刻执行cb
。当前例子中cb
就是handler
:
handler() { console.log('watch'); },
这里就解释了当前例子中console.log('watch')
是最先执行的。
然后,执行完initComputed
和initWatch
以后,就会通过callHook(vm, 'created')
执行到生命周期中的console.log('created')
了。
最后通过vm.$mount(vm.$options.el)
进行页面渲染的时候,会先去创建vNode
,这时就需要获取到computedCount
的值,进而触发其get
函数的后续逻辑,最终执行到console.log('computed')
。
附:为什么vue中的watch在mounted之后执行
首先,在调用mounted的时候,会进入到defineReactive函数,然后调用函数里面的set方法,将mounted中赋的新的值给传递过去,并通过调用dep.notify( )把消息发送给订阅者,继而更新订阅者的列表
后面才开始渲染watch进入Watcher的class
总结
关于
vue
中created
和watch
的执行顺序相对比较简单,而其中computed
是通过Object.defineProperty
为当前vm
进行定义,再到后续创建vNode
阶段才去触发执行其get
函数,最终执行到计算属性computed
对应的逻辑。
加载全部内容