本篇文章主要是《浅析Vue.js依赖收集和派发更新中观察者模式的应用》的扩展,主要是介绍计算属性和监听属性,建议先阅读上篇👆。
计算属性 computed
computed的初始化定义在文件中: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属性在SSR期间只是getter
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) {
// 为computed属性创建内部watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// 组件定义的计算属性已经在组件原型定义过。
// 我们只需要定义在此处实例化定义过的计算属性。
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)
}
}
}
}
initComputed的主要逻辑是遍历所有的computed属性,然后为每个computed属性创建watcher,并保存在vm._computedWatchers
上,最后,若computed的key未在vm上定义过,则执行defineComputed(vm, key, userDef)
。
下面是defineComputed的实现
function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
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
}
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)
}
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的核心是为计算属性设置getter,getter的逻辑是找到其watcher,调用其evaluate
方法和depend
方法,并返回watcher.value
。
class Watcher {
// ...
// 计算watcher的值。只有lazy watchers才需要这样做。
evaluate () {
this.value = this.get()
this.dirty = false
}
// 取决于被此watcher收集的所有deps。
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
computed watcher与渲染watcher不同的是lazy: true
,this.dirty = this.lazy
,所以其dirty= true
。
如果组件的模板中有使用到computed,依赖收集和派发更新的流程如下:
- 当组件在挂载时,其渲染watcher在计算时会读取到computed的值时,就会触发computed的getter,然后找到computed watcher,因为
dirty= true
,所以会执行其evaluate
方法; evaluate
会执行this.get()
重新计算computer的值,在这个过程中,Dep.target
是computed watcher,同时会触发computed所有依赖项的getter,然后收集所有依赖项的dep,保存在newDeps
中,执行cleanupDeps
后,newDeps
会保存到deps
中,这就是依赖收集的过程;evaluate
执行完毕之后,Dep.target
变为渲染watcher,这时执行computed watcher的depend
方法,会遍历computed watcher的所有deps
,并执行dep.denped
将渲染watcher保存在所有依赖项的订阅者列表subs
中,当依赖项发生变化时,便可以直接通知到对应的渲染watcher进行更新了。
监听属性 watch
监听属性的初始化在initState
中
function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// ...
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
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属性的主要逻辑是遍历每一个watch属性,为每一个key执行createWatcher
,因为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]
}
return vm.$watch(expOrFn, handler, options)
}
createWatcher的逻辑比较简单,主要是获取到最终的handler,并返回vm.$watch
方法的结果,其定义在文件中:/src/core/instance/state.js
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
方法的核心逻辑是创建Watcher,若设置了immediate
,则立即执行回调函数,最后返回取消监听的方法。
通过$watch
方法创建的是user watcher,其依赖收集和派发更新的流程如下:
- 创建user watcher时,在
get
的过程中会获取到监听属性的值,这时Dep.target
是user watcher,监听属性会将这个user watcher收集到其dep的订阅列表中,这就完成了依赖收集; - 当监听属性发生变化时,会通知user watcher进行更新,便会执行用户传入的回调函数,这就是派发更新的过程。