很多时候,我们都不清楚该什么时候使用 Vue 的 computed 计算属性,何时该使用 watch 监听属性。现在让我们尝试从源码的角度来看看,它们两者的异同吧。
computed
计算属性的初始化过程,发生在 Vue 实例初始化阶段的 initState() 函数中,其中有一个 initComputed 函数。该函数的定义在 src/core/instance/state.js中:
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 (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.
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') {
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)
}
}
}
}
首先创建一个空对象,接着遍历 computed 属性中的每一个 key, 为每一个 key 都创建一个 Watcher。这个 Watcher 与普通的 Watcher 不一样的地方在于:它是 lazy Watcher。关于 lazy Watcher 与普通 Watcher 的区别,我们待会展开。然后对判断如果 key 不是实例 vm 中的属性,调用defineComputed(vm, key, userDef),否则报相应的警告。
接下来重点看defineComputed(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
} else {
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
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
这里的逻辑也是很简单,其实就是利用 Object.defineProperty给计算属性对应的key值添加 getter 和 setter。我们重点来关注一下 getter 的情况,缓存的配置也先忽略,最终 getter 对应的是 createdComputedGetter(key)的返回值,我们来看它的定义:
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
}
}
}
createdComputedGetter(key)返回一个函数computedGetter,它就是计算属性对应的 getter。
至此,整个计算属性的初始化过程到此结束。我们知道计算属性对应的 Watcher 是一个 lazy Watcher,它和普通的 Watcher 有什么区别呢?由一个例子来分析 lazy Watcher 的实现:
var vm = new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
当初始化整个 lazy Watcher 实例的时候,构造函数的逻辑有稍微的不同:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
//...
this.value = this.lazy
? undefined
: this.get()
}
可以发现 lazy Watcher 并不会立刻求值,而是返回的是 undefined。
然后当我们的 render 函数执行访问到 this.fullname 的时候,就出发了计算属性的 getter:
function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
这简短的几行代码是核心逻辑。我们先来看:此时的 Watcher.dirty 属性为 true。会执行 Watcher.evaluate() :
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
这里,会通过调用 this.get() 方法,执行对应属性的 get 函数。在我们的例子中,就是执行:
function () {
return this.firstName + ' ' + this.lastName
}
这个时候,会触发对应变量firstName和lastName的获取,触发对应的响应式过程。得到了最新的值之后,将 this.dirty 属性设置为false。
更加关键的代码在这里:
if (Dep.target) {
watcher.depend()
}
Vue 实例存在一个 Watcher,它会调用计算属性。计算属性中有 lazy Watcher,它会调用响应式属性。每一个 Watcher 的 get() 方法中,都有pushTarget(this)和popTarget()的操作。
在上面的代码中,此时的 Dep.target是 Vue 的实例 Watcher,此时的 watcher 变量是计算属性的 lazy Watcher,通过执行代码watcher.depend(),将计算属性的 lazy Watcher 关联的 dep 都与 Dep.target 发生关联。
在我们的例子中,即把this.firstName、this.lastName与实例 Watcher关联起来。这样就可以实现:
- 当
this.firstName、this.lastName发生变化的时候,实例Watcher就会收到更新通知,此时的计算属性也会触发 get 函数,从而更新。 - 当
this.firstName、this.lastName未发生变化的时候,实例Watcher调用计算属性,因为lazy Watcher对应的dirty属性为false,那么就会直接返回缓存的value值。
由此可以看出:计算属性中的 lazy Watcher 有以下作用:
- 保存 computed 属性的 get 函数(方法)
- 保存计算结果
- 控制缓存计算结果是否有效(通过 this.dirty 属性)
Watch
侦听属性的初始化过程,与计算属性类似,都发生在 Vue 实例初始化阶段的 initState() 函数中,其中有一个 initWatch 函数。该函数的定义在 src/core/instance/state.js中:
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:
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)
}
这里的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 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) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
也就是说,侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。
最终都会执行const watcher = new Watcher(vm, expOrFn, cb, options)实例化一个 Watcher。这里需要注意的一点是这是一个 user Watcher,因为 options.user = true。通过实例化 Watcher 的方式,一旦我们 watch 的数据发生了变化,它最终会执行 Watcher 的 run方法,执行回调函数 cb。
通过 vm.$watch 创建的 watcher 是一个 user watcher,其实它的功能很简单,在对 watcher 求值以及在执行回调函数的时候,会处理一下错误,比如:
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
关于 Vue 内部的错误处理,有新文章做对应的讨论,可戳这里。
总结
就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。
vue源码解读文章目录:
Vue 更多系列: