原稿:语雀 · 【原理五】computed & watch · 原目录:Vue › 【原理五】computed & watch
计算属性
计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中,执行了 if (opts.computed) initComputed(vm, opts.computed),initComputed 的定义在 src/core/instance/state.js 中:
const computedWatcherOptions = { computed: true }
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
// ...
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
)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {/*...*/}
}
}
- 函数首先创建 vm._computedWatchers 为一个空对象,接着遍历 computed 对象,获取每个计算属性的 getter 函数(暂不考虑 setter)
- 接下来为每一个计算属性 key 创建一个 computed watcher,默认进行惰性观察,设置 value 为 lazy,而不是立刻获取值
// Watcher()
this.value = this.lazy
? undefined
: this.get();
- 最后调用 defineComputed(vm, key, userDef)
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
defineComputed() 中利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,计算属性有 setter 的情况比较少,重点关注 getter。getter 是 createComputedGetter(key) 的返回值:createComputedGetter 返回一个函数 computedGetter,实际上就是 watchers[] 元素的包装
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {watcher.evaluate();}
if (Dep.target) {watcher.depend();}
return watcher.value
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};
/**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
}
/.../
return value
};
当首次渲染时,调用 getter:
- dirty 为真,执行 watcher.evaluate(),通过 watcher.get() 调用 userDef.get,因为计算属性依赖于别的属性,这一步又回去调用其他属性的 getter,根据响应式原理,则 computed watcher 会订阅到依赖属性的变化上
- 之后执行 depend(),渲染 watcher 订阅到 computed watcher 的变化上
/** * Subscriber interface. * Will be called when a dependency changes. */Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); }};
当计算属性依赖的数据修改,触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:
- computed watcher 属于惰性观察,会先设置 this.dirty = true,而不是立刻重新计算
- 当再次通过 computedGetter 访问这个计算属性时,才会执行 evaluate(),通过 get() 更新计算值
计算属性的缓存即通过上述过程实现,当依赖属性未修改时,this.dirty == false ,调用 computedGetter 时不会执行 evaluate(),而是直接返回缓存的 value
侦听
侦听属性的初始化发生在 Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化之后
if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch)}
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) } }}
这里就是对 watch 对象做遍历,拿到每一个 handler,因为 Vue 是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用 createWatcher 方法,否则直接调用 createWatcher:
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)
}
首先对 handler 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watch 是 Vue 原型上的方法,它是在执行 stateMixin 的时候定义的
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) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
- 侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数
- 接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化一个 user watcher,其 lazy 为 false,则构造函数中就会调用 get(),这个 watcher 会订阅在侦听数据的变化上;当侦听项改变,调用 notify() 通知订阅的 watcher,user watcher 也会调用 watcher.update() ,接着执行 watcher.run(),调用回调函数 cb,并且如果设置了 immediate 为 true,则直接会执行回调函数 cb
- 最后返回 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher
Watcher.prototype.update = function update () {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
watcher 是贯穿 vue 响应式中的角色
|