不少时候,咱们都不清楚该何时使用 Vue 的 computed 计算属性,什么时候该使用 watch 监听属性。如今让咱们尝试从源码的角度来看看,它们二者的异同吧。vue
计算属性的初始化过程,发生在 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)
,不然报相应的警告。express
接下来重点看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
实例的时候,构造函数的逻辑有稍微的不一样:oop
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
:post
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
有如下做用:
侦听属性的初始化过程,与计算属性相似,都发生在 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 更多系列: