前面我们在 vue2源码系列-响应式原理 中介绍了 vue
中的整个响应式实现及流程,其中跳过了某些细节性的代码,现在我们再去好好学习研究一番。
学习目标
我们先来梳理下本篇文章的学习目标
-
整明白
Watcher
的每一行代码(PS:有点夸张了) -
明白
renderWatcher
和userWatcher
-
清楚
watch
和computed
选项实现原理,两者的参数实现及差异 -
清楚
asyncWatcher
的排队执行机制
Watcher实现
前面学习 vue响应式原理
的时候其实是有对 Watcher
的实现进行一个比较完整的分析的,只是部分细节没有深入。这次争取弄明白全部实现。
let uid = 0
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
// renderWatcher 一个实例对应唯一一个renderWatcher
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep // deepWatcher
this.user = !!options.user // userWatcher
this.lazy = !!options.lazy // computed实现
this.sync = !!options.sync // syncWatcher
this.before = options.before // 前置函数 比如在渲染前会调用 hook:mounted
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
// deps newDeps 用于dep收集
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
// 非computed执行get()
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
// 执行getter添加订阅
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// watch deep 参数实现原理
if (this.deep) {
// traverse就是递归遍历value触发各个属性的getter
traverse(value)
}
popTarget()
// 清空本轮deps
this.cleanupDeps()
}
return value
}
// 添加订阅
addDep (dep: Dep) {
const id = dep.id
// 防止重复收集
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
// 清空本轮deps
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
// 接收订阅中心的更新通知
update () {
// computed使用
if (this.lazy) {
this.dirty = true
// 同步watcher
} else if (this.sync) {
this.run()
// 异步watcher队列
} else {
queueWatcher(this)
}
}
run () {
// 执行回调
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// computed的更新通知
evaluate () {
this.value = this.get()
this.dirty = false
}
// 用于computed依赖其它属性deps
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// 移除deps
teardown () {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
有了上次的基础,Watcher
的代码理解起来其实不难。上面我们对每个函数及重点代码都有注释分析,下面我们再看看一些容易看不明白的点。
newDepIds/depIds
值得一说的是收集 dep
的作用。很简单,就是防止重复收集。那为什么需要两个数组来实现了,单单防止重复收集的话,一个 Set
数组无疑是最简单的。
我们知道每个更新的时候都会执行 this.get() -> this.getter.call(vm, vm)
, 其实使用两个数组的原因就在于在 getter
函数中,是会触发不同属性的 getter
的,他们会将属性的对应的 dep
添加当前 watcher
订阅。
所以每次更新的时候 getter
函数有可能触发不同属性的 getter
,这时候应该添加新一轮的订阅,而老一轮的订阅可能已经不存在了,所以需要及时移除,这就是使用两个数组而不是一个数组实现的原因。具体复现可以通过指令 v-if
来试试,加深理解。
cleanupDeps () {
let i = this.deps.length
while (i--) {
// 移除上一轮不必要的订阅
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
// 这里的实现有点绕 将newDepIds赋值为depIds,而后再通过clear方法清空
// 让人难免觉得多此一举,为什么不直接 newDepIds = [] 呢
// 按照我个人的理解是 newDepIds = [] 势必要新建数组
// 而下面的方式不需要开辟新内存,也不用回收旧内存,一直是原来两个内存地址,属于优化内存的写法
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
computed的实现
上面 Watcher
的实现其实很多是和实现 computed
相关的,所以我们来看看 computed
的实现原理
initComputed
我们之前分析 vue
初始化的时候,知道在 initState
中会调用 initComputed
初始计算属性
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// 收集计算属性
const watchers = vm._computedWatchers = Object.create(null)
// 判断服务端渲染
const isSSR = isServerRendering()
// 遍历computed
for (const key in computed) {
const userDef = computed[key]
// 兼容函数及对象写法
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// 实例化watcher
// 特别
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// 定义getter
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
可以看到 initComputed
主要是对 computed
选项进行遍历初始化 watcher
实例,和其它 watcher
不同之处就在于选项中传了参数 lazy:true
。我们再看看 defineComputed
defineComputed
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// 这段代码比较多但是逻辑不复杂 主要就是判断计算属性的set get设置
// 我们重点关注 get 就行
// 其实主要逻辑就是通过createComputedGetter创建getter函数再定义到vm对应属性中
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
// 这边有个cache参数可以用于关闭缓存
sharedPropertyDefinition.get = userDef.get
? shouldCache && cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
// 定义到vm对应属性中
Object.defineProperty(target, key, sharedPropertyDefinition)
}
createComputedGetter
createComputedGetter
函数应该是精华之处了,这边我们能看到其和 watcher
的联系,以及方法 evaluate
和 depend
的使用,而这也是实现计算属性缓存的原理。
function createComputedGetter (key) {
return function computedGetter () {
// 获取我们在函数initComputed中设置的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 我们之前说过computed watcher设置了lazy=true
// 这样同时dirty也会设置为true
if (watcher.dirty) {
// 正常的watcher在实例化的时候就会调用get()进行依赖收集及获取值
// 而计算属性需要在属性的get函数中主动计算值
watcher.evaluate()
}
// 这里是实现computed数据驱动的原理
// 我们举个例子 name() {return this.firtName + this.lastName}
// 在渲染watcher中因为会调用到 this.name 就会走到当前函数
// 在当前函数中 Dep.target 则会指向渲染 watcher
// 而在计算属性new Watcher的时候
// 我们说过其会收集对应的dep数组也就是firtName和lastName对应的dep
// 此时调用watcher.depend()则会将收集的dep分别订阅当前的渲染watcher
// 所以当触发firtName或者lastName的时候,就会触发dep.notify进而通知渲染watcher更新
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
computed缓存原理
上面我们解析了计算属性依赖于其它属性更新的原理,我们再来分析分析其缓存实现
缓存原理的实现其实在于 dirty
属性的运用。
if (watcher.dirty) {
// 正常的watcher在实例化的时候就会调用get()进行依赖收集及获取值
// 而计算属性需要在属性的get函数中主动计算值
watcher.evaluate()
}
如果访问计算属性的 get
函数,会进行 watcher.dirty
的判断,如果为 true
才会调用 evaluate
获取新值,否则则返回旧值,那么他是如何判断值更新呢?
update () {
// 和普通watcher不同的是
// 依赖的值更新时不会调用run函数
// 而是仅仅将dirty设置true来表明值已更新
// 这样就不会调用get()进行更新
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
在实际使用到计算属性的访问函数 get
中,才会通过 dirty
判断进入 evaluate
evaluate () {
// 在此才会调用get更新value 同时将dirty设置为fasle表明值已经更新
this.value = this.get()
this.dirty = false
}
watch实现
我们再来看看 watch
选项的实现,会比计算属性简单许多
initWatch
同样从入口开始 initState -> 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 {
// 创建watcher
createWatcher(vm, key, 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]
}
// 最终调用vm.$watch
return vm.$watch(expOrFn, handler, options)
}
$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 || {}
// 将user参数设置为true 用于标志开发者watcher
options.user = true
// 正常的实例化watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
// 一个值得注意的参数
// 如果配置了immediate则在此时(vue初始化)进行调用回调cb
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
// 为啥这边要进行pushTarget呢
// 而且target为undefined
// 因为当前可能处于其它watcher实例化当中,例如渲染watcher
// 如果这边不推入其它watcher得话会导致cb中得某些属性dep添加了渲染watcher
// 则可能引起没必要得渲染watcher get执行
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
// 移除deps解除订阅
return function unwatchFn () {
watcher.teardown()
}
}
}
异步watcher
在 Watcher
的 update
中有这么个判断
update () {
// lazy用于computed
if (this.lazy) {
} else if (this.sync) {
// 同步watcher
this.run()
// 异步watcher
} else {
queueWatcher(this)
}
}
看来只有同步 watcher
才会直接调用 run
进行更新。而异步 watcher
则会调用 queueWatcher
,实际上大部分 watcher
都是异步 watcher
,因为 sync
默认值就是 false
queueWatcher
那么我们主要来研究 queueWatcher
是怎么个实现,watcher
更新函数是以怎么样一个排队机制来进行更新的。
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 通过watcherID判断防止重复执行
// 这里也是设计为异步的一个重要原因吧
// 开发中肯定会存在某些修改触发同一个渲染watcher的情况
// 通过异步队列则可以很好的防止重复,大大优化效率
if (has[id] == null) {
has[id] = true
// flushing表明当前queue正在执行更新
if (!flushing) {
// 如果不是正在更新,则推入队列即可
queue.push(watcher)
} else {
// 如果正在更新,则要对比watcherID的大小 将watcher推入正确位置
// 这种情况是由于watcher的回调cb又通知了新的watcher更新
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 异步执行
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
// 使用nextTick来实现异步执行
// 其中执行函数flushSchedulerQueue通过回调方式传递、
// nextTick的原理以后分析 知道它是将函数添加到异步队列中就行了
nextTick(flushSchedulerQueue)
}
}
}
flushSchedulerQueue
我们再来看看实际更新函数 flushSchedulerQueue
function flushSchedulerQueue () {
// 这边会设置flushing=true
// 表明当前正在执行更新
flushing = true
let watcher, id
// 注意这边会将watcher进行排序
// 和上面的排序其实是对应的,正因为上面的queueWatcher在排序之后
// 所以才需要对比watcherID插入特定位置
// 这边的排序主要为了处理
// 1. 父组件的更新总是应该先于子组件
// 2. userWatcher总是应该先于渲染watcher
// 3. 如果父组件已经销毁,其实不再需要执行更新
queue.sort((a, b) => a.id - b.id)
// 这边要注意queue.length不会通过len=queue.length的方式缓存
// 因为queue的长度其实是实时变化的,和前面说的原因一样
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
// before执行
if (watcher.before) {
watcher.before()
}
id = watcher.id
// 重置has[id] 不然下一轮不会添加到队列了
has[id] = null
// 执行更新函数
watcher.run()
}
// keepAlive组件
const activatedQueue = activatedChildren.slice()
// 用于获取普通组件 通过watcher.vm
const updatedQueue = queue.slice()
// 重置参数 见下文
resetSchedulerState()
// 触发组件update及activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
}
resetSchedulerState
正常的重置队列参数
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
waiting = flushing = false
}
总结
本文主要分析了 Watcher
的实现原理及与其相关的 computed
和 watch
选项的实现。同时分析了 watcher
的更新函数是如何添加到异步队列中及其执行机制。内容实际上是比较多的,可能有些地方没说明白,希望多多理解。good good staduy day day up