computed vs. watch
通过源码学习解决何时用computed,何时用watch的问题。
computed
计算属性初始化:
// 定义 computed watcher 标志,lazy 属性为 true
const computedWatcherOptions = { lazy: true }
function initComputed(vm: Component, computed: Object) {
// $flow-disable-line
// 定义一个 watchers 为空对象
// 并且为 vm 实例上也定义 _computedWatchers 为空对象,用于存储 计算watcher
// 这使得 watchers 和 vm._computedWatchers 指向同一个对象
// 也就是说,修改 watchers 和 vm._computedWatchers 的任意一个都会对另外一个造成同样的影响
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
// 遍历 computed 中的每一个属性值,为每一个属性值实例化一个计算 watcher
for (const key in computed) {
// 获取 key 的值,也就是每一个 computed
const userDef = computed[key]
// 用于传给 new Watcher 作为第二个参数
// computed 可以是函数形式,也可以是对象形式,对象形式的则取里面的 get
// computed: { getName(){} } | computed: { getPrice: { get(){}, set() {} } }
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
// 为每一个 computed 添加上 计算watcher;lazy 为 true 的 watcher 代表 计算watcher
// 在 new watcher 里面会执行 this.dirty = this.lazy; 所以刚开始 dirty 就是 true
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions // const computedWatcherOptions = { lazy: true }
)
}
// 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)) {
// 将 computed 属性代理到 vm 上,使得可以直接 vm.xxx 的方式访问 computed 的属性
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
// 在非生产环境会判重,computed 的属性不能和 data、props 中的属性重复
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
defineComputed的定义:
// 将 computed 的 key 代理到 vm 实例上
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
// shouldCache 用来判断是客户还是服务端渲染,客户端需要缓存
const shouldCache = !isServerRendering()
// 如果是客户端,使用 createComputedGetter 创建 getter
// 如果是服务端,使用 createGetterInvoker 创建 getter
// 两者有很大的不同,服务端渲染不会对计算属性缓存,而是直接求值
if (typeof userDef === 'function') {
// computed 是函数形式
sharedPropertyDefinition.get = shouldCache ?
createComputedGetter(key) :
createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
// 如果 computed 是对象形式
sharedPropertyDefinition.get = userDef.get ?
shouldCache && userDef.cache !== false ?
createComputedGetter(key) :
createGetterInvoker(userDef.get) :
noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
// 拦截对 computed 的 key 访问,代理到 vm 上
Object.defineProperty(target, key, sharedPropertyDefinition)
}
通过Object.defineProperty设置getter和setter,并将computed的属性代理到vm上,可以直接访问。
通过createComputedGetter得到客户端的getter函数:
// 用于创建客户端的 conputed 的 getter
// 由于 computed 被代理了,所以当访问到 computed 的时候,会触发这个 getter
function createComputedGetter(key) {
// 返回一个函数 computedGetter 作为劫持 computed 的 getter 函数
return function computedGetter() {
// 每次读取到 computer 触发 getter 时都先获取 key 对应的 watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// dirty 是标志是否已经执行过计算结果;dirty=true,代表有脏数据,需要重新计算
// dirty 初始值是 true(在 new Watcher 时确定),所以 computed 首次会进行计算,与 watch 略有差别
// 如果执行过并且依赖数据没有变化则不会执行 watcher.evaluate 重复计算,这也是缓存的原理
// 在 watcher.evaluate 中,会先调用 watcher.get 进行求值,然后将 dirty 置为 false
// 在 watcher.get 进行求值的时候,访问到 data 的依赖数据,触发 data 数据的 get,收集 计算watcher
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
// 进行依赖收集
// 注意,这里收集的是 渲染watcer,而不是 计算watcher
watcher.depend()
}
// 返回结果
return watcher.value
}
}
}
触发渲染watcher:
update() {
/* istanbul ignore else */
// lazy 为 true 代表是 computed
if (this.lazy) {
// 如果是 计算watcher,则将 dirty 置为 true
// 当页面渲染对计算属性取值时,触发 computed 的读取拦截 getter
// 然后执行 watcher.evaluate 重新计算取值
this.dirty = true;
}
}
watch
watch的初始化:
function initWatch(vm: Component, watch: Object) {
// 遍历 watch 对象
for (const key in watch) {
// 获取 handler = watch[key]
const handler = watch[key]
// handler可以是数组的形式,执行多个回调
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
vue支持一个key对应多个handler(没用过)。
createWatcher的实现:
function createWatcher(
vm: Component,
expOrFn: string | Function,
handler: any,
options ? : Object
) {
// 如果 handler(watch[key]) 是一个对象,那么获取其中的 handler 方法
// watch: {
// a: {
// handler(newName, oldName) {
// console.log('obj.a changed');
// },
// immediate: true, // 立即执行一次 handler
// // deep: true
// }
// }
if (isPlainObject(handler)) {
// 如果是对象,那么 options 就是 watch[key]
options = handler
// handler 是 watch[key].handler
handler = handler.handler
}
// watch 也可以是字符串形式
// methods: {
// userNameChange() {}
// },
// watch: {
// userName: 'userNameChange'
// }
// 如果 handler(watch[key]) 是字符串类型
if (typeof handler === 'string') {
// 找到 vm 实例上的 handler
handler = vm[handler]
}
// handler(watch[key]) 不是对象也不是字符串,那么不需要处理 handler,直接执行 vm.$watch
// 例如:watch: { a(newName, oldName) {} }
/**
* expOrFn: 就是每一个 watch 的名字(key 值)
* handler: watch[key]
* options: 如果是对象形式,options 有值,不是,可能是 undefined
*/
return vm.$watch(expOrFn, handler, options)
}
watch最终会调用vm.$watch
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options ? : Object
): Function {
const vm: Component = this
// 先判断一下 handler 会不会是对象,是对象,继续调用 createWatcher 处理
// 这里是因为有这种情况:this.$watch('msg', { handler: () => {} }) 直接调用
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
// 如果 options 是 undefined,将 options 赋值为空对象 {}
options = options || {}
// options.user 这个是用户定义 watcher 的标志
options.user = true
// 创建一个user watcher
// 在实例化 user watcher 的时候会执行一次 getter 求值,这时,user watcher 会作为依赖被数据所收集
const watcher = new Watcher(vm, expOrFn, cb, options)
// 如果有 immediate,立即执行回调函数 handler
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 返回 unwatch 函数,用于取消 watch 监听
return function unwatchFn() {
watcher.teardown()
}
}
}
深度依赖-deep
如果设置了deep:true,在get时会执行traverse。
const seenObjects = new Set()
/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
在深层递归遍历时,对每个子对象进行访问,触发它们的getter过程。具体实现中,会把对象的dep.id记录到set数据结构中,避免重复访问。deep的设置可以在深层数据变化时触发回调,但是深层递归会有一定的性能开销(Vue3.5中deep可以设为整数,用来指定监听的深度)。
computed vs. watch
- computed支持缓存,watch不支持
- watch支持异步,computed不支持
- computed默认首次监听,wacth默认首次不执行回调,可以更改immediate属性
- computed直接使用整个对象不会深度监听,需要调用到对象的具体属性(a.b.c),watch可以设置deep属性实现深度细致监听
- computed适合属性依赖其他属性计算而来,watch监听的值必须是data声明过的值或父组件传递的props中的数据