面试题:
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-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
设置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
中created
和watch
的执行顺序相对比较简单,而其中computed
是通过Object.defineProperty
为当前vm
进行定义,再到后续创建vNode
阶段才去触发执行其get
函数,最终执行到计算属性computed
对应的逻辑。
最后
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。
有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:
文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取