这里我们只简单学习计算属性和侦听属性的初始化,后续响应式原理会继续深入研究学习这两个点的内容。它们形虽不同,但是本质都是依赖于 Watcher 类及与其他类模块。
Computed 计算属性的初始化
接上一节的流程,在 initState 函数的代码中我们看到有初始化计算属性的代码,代码如下:
const opts = vm.$options
if (opts.computed) initComputed(vm, opts.computed) // 初始化 computed
它把我们在实例化 Vue 应用时传入的计算属性的配置继续传入 initComputed 调用,它在文件 /src/core/instance/state.js 中,我们来看它的源码:
// 计算属性的Watcher 对象的实例化选项
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
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]
// 求值函数赋给 getter
const getter = typeof userDef === 'function' ? userDef : userDef.get
...
if (!isSSR) {
// 为每一个计算属性的 键 创建 Watcher 观察者对象
// create internal watcher for the computed property.
watchers[key] = new Watcher(
// Vue 实例
vm,
// 用户定义的求值函数 或 空函数
getter || noop,
// 空函数
noop,
// 上面定义的选项 { lazy: true }
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)
}
...
}
}
这个函数里面大概做了两件事,一是为每个计算属性的元素创建一个 Watcher 对象,且其创建时传入了 lazy 为真的选项。二是对每一个计算属性的元素调用函数 defineComputed 定义为响应式的变量。我们跟进 defineComputed 函数看看它的代码:
// 计算属性的 Object.defineProperty 默认选项
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any, key: string, userDef: Object | Function
) {
const shouldCache = !isServerRendering()
// 配置Object.defineProperty 选项的 get 属性
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
}
...
// 调用 Object.defineProperty 设置为响应式属性
Object.defineProperty(target, key, sharedPropertyDefinition)
}
// 创建计算属性的 getter 函数
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
}
}
}
在函数 defineComputed 中,最终就是调用 Object.defineProperty 把计算属性设置成了响应式的属性。因为我们定义的计算属性多数都是设置键和对应的求值函数,所以我们主要看 userDef 为求值函数的情况。
在这个情况下,它调用了 createComputedGetter 生成了用于 sharedPropertyDefinition.get 的 getter 函数——computedGetter,它就类似于响应式数据项的 reactiveGetter。在这个 computedGetter 中,获取该属性对应的 Watcher 对象,然后判断是否为脏状态,是则执行 Watcher.evaluate() 计算值;接着判断 Dep.target 是否为真,是则处于依赖收集状态,执行 Watcher.depend() 收集依赖。最后返回计算出的值。
Watch 侦听属性的初始化
在初始化函数 initState 中还有侦听属性的初始化调用,代码如下:
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
它直接把侦听属性传入 initWatch 调用,我们看 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
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]
}
// 最终逐元素传入调用 $watch 方法
return vm.$watch(expOrFn, handler, options)
}
这个代码调用比较简单,在最终把侦听属性的每个元素传入 $watch 设置好侦听回调,我们继续看 $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) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}
在这个 $watch 方法中,就是把要侦听的属性和回调函数在 new Watcher 实例化时传入构造函数中,与此同时,还传入了 Watcher 的选项配置,其中 user 选项属性为真,这表示我们实例化了一个用户定义的观察者对象,这个后面在 Watcher 类详解会细说,这里我们只用知道在这个 Watcher 对象已经帮我们配置好了侦听属性,它在侦听到属性有变动时会主动调用配置的回调函数即可。
总结:
对比数据对象的处理,计算属性和侦听属性的处理就简单多了。计算属性先经过 initComputed 给属性创建对应的 Watcher 对象,然后调用 defineComputed 定义为响应式属性,在其 computedGetter 中计算求值并收集依赖。侦听属性最终就是调用 Vue.prototype.$watch 对每一个属性都创建了一个 Watcher 对象,在属性有变动的时候会执行用户定义的回调函数。
从计算属性和侦听属性的初始化流程来看,Watcher 是实现这个功能的核心,这个类我们在接下来会研究学习,同时也会继续研究学习计算属性和侦听属性与 Watcher 的关联与配合工作的细节。