举个例子:
// 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中created和watch的执行顺序相对比较简单,而其中computed是通过Object.defineProperty为当前vm进行定义,再到后续创建vNode阶段才去触发执行其get函数,最终执行到计算属性computed对应的逻辑。