早上好,各位未来的 Vue.js 大师们!今天要和大家深入挖掘 Vue 2 源码中 Watcher 类的精髓,特别是 get 方法如何巧妙地触发依赖收集,以及 update 方法如何将变化推送到渲染队列。准备好了吗?我们开始咯!
开场白:Watcher 是什么?为什么重要?
在开始之前,我们先来简单回顾一下 Watcher 在 Vue 2 中的角色。你可以把它想象成一个勤劳的工人,它的任务是监视某个表达式(比如 data 中的属性)的变化,一旦发现变化,就通知相应的组件进行更新。没有 Watcher,数据变化了,视图却纹丝不动,整个 Vue 应用就瘫痪了,所以说它非常重要!
第一幕:Watcher 的构造函数:生而不同
我们先来看看 Watcher 构造函数,了解一下 Watcher 对象在创建时都经历了什么。
/**
* A watcher parses an expression and notifies the component when the
* expression value changes. This is used for both the $watch() api and
* directives.
*
* @param {Vue} vm - Vue instance
* @param {String|Function} expOrFn - Watcher expression or function
* @param {Function} cb - Callback when value changes
* @param {Object} options - Watcher options
* {
* deep: boolean,
* user: boolean,
* lazy: boolean,
* sync: boolean,
* before: Function
* }
* @param {Boolean} isRenderWatcher - internal marker for render watchers
*/
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} 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
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)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
构造函数主要做了这些事情:
- 保存关键信息: 比如 Vue 实例
vm,要监听的表达式expOrFn,以及回调函数cb。 - 处理选项: 解析
options,设置deep、user、lazy等标志位。这些标志位决定了Watcher的行为。 - 生成
getter: 这是最关键的一步。根据expOrFn生成getter函数。这个getter函数的作用是获取表达式的值。如果expOrFn是一个函数,那么getter就是它本身;如果expOrFn是一个字符串,那么getter就是一个解析字符串路径的函数(parsePath)。 - 如果是非
lazy的Watcher,会立即调用get()方法: 这意味着Watcher在创建时就会立即执行依赖收集。
第二幕:get() 方法:依赖收集的启动器
接下来,我们进入今天的主题:get() 方法。这个方法是依赖收集的核心。
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
让我们一步一步地分析 get() 方法的实现:
-
pushTarget(this): 这是依赖收集的启动信号!pushTarget函数会将当前的Watcher对象(this)推入一个全局的栈Dep.targetStack中。这个栈的作用是保存当前正在执行的Watcher。 -
执行
getter:value = this.getter.call(vm, vm)。这里调用了之前生成的getter函数。getter函数会访问组件实例vm上的数据。关键就在这里:当getter函数访问data中的属性时,会触发data属性的getter方法。 -
data属性的getter方法:data属性的getter方法会做两件事:- 返回属性的值。
- 调用
dep.depend()方法。 这里的dep是data属性对应的Dep对象。Dep对象负责管理所有依赖于该属性的Watcher。
-
dep.depend()方法:dep.depend()方法会将当前正在执行的Watcher(也就是Dep.targetStack栈顶的Watcher)添加到dep的subs数组中。subs数组存储了所有依赖于该属性的Watcher。 -
deep选项的处理: 如果Watcher设置了deep选项,那么会调用traverse(value)函数。traverse函数会递归地访问value的所有属性,从而触发这些属性的依赖收集。 -
popTarget(): 在getter函数执行完毕后,popTarget函数会将Dep.targetStack栈顶的Watcher移除。这意味着依赖收集已经结束。 -
cleanupDeps(): 这个方法用于清理旧的依赖关系,并建立新的依赖关系。我们稍后会详细讲解。 -
返回
value:get()方法最终返回getter函数计算出的值。
总结一下,get() 方法通过 pushTarget 和 popTarget 两个函数,将 Watcher 对象推入和弹出 Dep.targetStack 栈,从而标记当前正在执行的 Watcher。当 getter 函数访问 data 属性时,会触发 data 属性的 getter 方法,进而调用 dep.depend() 方法,将当前的 Watcher 添加到 dep 的 subs 数组中。这就是依赖收集的过程!
第三幕:update() 方法:变化的传递者
现在我们来看看 update() 方法。这个方法负责将变化推送到渲染队列,最终触发视图的更新。
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
update() 方法的实现非常简单,但是根据 Watcher 的不同类型,它的行为也会有所不同:
lazy的Watcher: 如果Watcher是lazy的,那么会将this.dirty设置为true。lazy的Watcher不会立即更新,而是等到需要时才更新。sync的Watcher: 如果Watcher是sync的,那么会立即调用this.run()方法。sync的Watcher会同步更新,这意味着视图会立即更新。- 默认情况: 如果
Watcher既不是lazy的,也不是sync的,那么会将this传递给queueWatcher(this)函数。queueWatcher函数会将Watcher添加到渲染队列中。
第四幕:queueWatcher() 函数:渲染队列的管理者
queueWatcher() 函数是 Vue 2 中一个非常重要的函数。它负责管理渲染队列,确保视图的更新以高效的方式进行。
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
queueWatcher() 函数的实现逻辑如下:
- 检查
Watcher是否已经存在: 使用has对象来记录已经添加到队列中的Watcher的id。如果Watcher已经存在,那么直接返回,避免重复添加。 - 添加到队列: 如果
Watcher不存在,那么将其添加到queue数组中。- 如果当前没有在刷新队列(
!flushing),那么直接将Watcher添加到队列的末尾。 - 如果当前正在刷新队列(
flushing),那么需要根据Watcher的id将其插入到队列的合适位置,以确保更新的顺序正确。
- 如果当前没有在刷新队列(
- 触发队列刷新: 如果当前没有在等待刷新(
!waiting),那么会将waiting设置为true,并调用nextTick(flushSchedulerQueue)函数。nextTick函数会将flushSchedulerQueue函数添加到下一个事件循环中执行。
第五幕:flushSchedulerQueue() 函数:渲染队列的执行者
flushSchedulerQueue() 函数负责执行渲染队列中的所有 Watcher。
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher. (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's update,
// we can skip the child's update.
queue.sort(sort)
// do not cache length because more watchers might be pushed
// as we run existing watchers.
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and warn for recursive updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
recursiveUpdateCount++
if (recursiveUpdateCount > RECURSION_LIMIT) {
warn(
'You may have an infinite update loop in a component. ' +
'Make sure you do not have an infinite recursive updates.'
)
break
}
}
}
// keep copies of the queue so that lifecycle hooks can be used to selectively
// invoke wathcers.
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
}
flushSchedulerQueue() 函数的实现逻辑如下:
- 设置
flushing为true: 表示当前正在刷新队列。 - 对队列进行排序: 调用
queue.sort(sort)函数对队列进行排序。排序的目的是确保组件的更新顺序是从父组件到子组件,并且用户Watcher在渲染Watcher之前执行。 - 遍历队列: 遍历队列中的所有
Watcher,并依次执行它们的run()方法。 - 重置状态: 调用
resetSchedulerState()函数重置调度器的状态。
第六幕:Watcher.run() 方法:触发回调函数
最后,我们来看看 Watcher 的 run() 方法。这个方法负责执行 Watcher 的回调函数。
/**
* This is where the scheduler starts.
* It is called recursively to run all queued watchers,
* until there are no more watchers left to run.
*/
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)
}
}
}
}
run() 方法的实现逻辑如下:
- 检查
Watcher是否处于激活状态: 只有处于激活状态的Watcher才能执行。 - 获取新值: 调用
this.get()方法获取表达式的新值。这个过程会再次触发依赖收集,但是这次的依赖收集是为了更新Watcher的依赖关系。 - 比较新值和旧值: 如果新值和旧值不相等,或者
Watcher是深度监听的,或者监听的是对象或数组,那么需要执行回调函数。 - 执行回调函数: 调用
this.cb.call(this.vm, value, oldValue)执行回调函数,并将新值和旧值作为参数传递给回调函数。
第七幕:cleanupDeps() 方法:依赖的清理工
cleanupDeps() 方法是用来清理依赖的,防止内存泄漏,并保证依赖关系的正确性。
/**
* Clean up for dependency collection.
*/
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
}
这个方法主要做了一下几件事:
- 遍历旧的依赖: 遍历
this.deps数组,这个数组包含了上一次依赖收集中收集到的所有Dep对象。 - 检查依赖是否仍然存在: 对于每个
Dep对象,检查它是否仍然存在于this.newDepIds中。this.newDepIds包含了本次依赖收集中收集到的所有Dep对象的id。 - 移除不再存在的依赖: 如果
Dep对象不再存在于this.newDepIds中,说明该依赖已经失效,需要从Dep对象的subs数组中移除当前的Watcher。 - 更新依赖关系: 交换
this.depIds和this.newDepIds,交换this.deps和this.newDeps,并清空this.newDepIds和this.newDeps。
总结:依赖收集和更新流程
我们来总结一下整个依赖收集和更新的流程:
- 创建
Watcher: 在创建Watcher时,会生成getter函数,并立即调用get()方法(除非是lazy的Watcher)。 get()方法触发依赖收集:get()方法会将当前的Watcher推入Dep.targetStack栈,并执行getter函数。data属性的getter方法收集依赖: 当getter函数访问data属性时,会触发data属性的getter方法,进而调用dep.depend()方法,将当前的Watcher添加到dep的subs数组中。- 数据变化: 当
data属性的值发生变化时,会触发dep.notify()方法。 dep.notify()方法通知Watcher:dep.notify()方法会遍历dep的subs数组,并依次调用每个Watcher的update()方法。update()方法将Watcher添加到渲染队列:update()方法会将Watcher添加到渲染队列中(除非是lazy或sync的Watcher)。flushSchedulerQueue()函数执行渲染队列:flushSchedulerQueue()函数会将渲染队列中的所有Watcher按照一定的顺序执行。Watcher.run()方法触发回调函数:Watcher.run()方法会获取表达式的新值,并比较新值和旧值,如果需要更新,则执行回调函数。
表格总结
| 方法/函数 | 作用 |
|---|---|
Watcher 构造函数 |
创建 Watcher 对象,保存关键信息,生成 getter 函数。 |
Watcher.get() |
触发依赖收集,将 Watcher 推入 Dep.targetStack 栈,执行 getter 函数,获取表达式的值。 |
data 属性的 getter |
当访问 data 属性时触发,返回属性的值,并调用 dep.depend() 方法,将当前的 Watcher 添加到 dep 的 subs 数组中。 |
dep.depend() |
将当前的 Watcher 添加到 dep 的 subs 数组中。 |
dep.notify() |
当 data 属性的值发生变化时触发,遍历 dep 的 subs 数组,并依次调用每个 Watcher 的 update() 方法。 |
Watcher.update() |
将 Watcher 添加到渲染队列中(除非是 lazy 或 sync 的 Watcher)。 |
queueWatcher() |
管理渲染队列,将 Watcher 添加到队列中,并触发队列刷新。 |
flushSchedulerQueue() |
执行渲染队列中的所有 Watcher,按照一定的顺序执行,确保组件的更新顺序是从父组件到子组件,并且用户 Watcher 在渲染 Watcher 之前执行。 |
Watcher.run() |
获取表达式的新值,并比较新值和旧值,如果需要更新,则执行回调函数。 |
cleanupDeps() |
清理旧的依赖关系,并建立新的依赖关系。 |
结语:掌握 Watcher,玩转 Vue
掌握 Watcher 的工作原理,你就能更深入地理解 Vue 的响应式系统,也能更好地调试和优化 Vue 应用。希望今天的讲座对大家有所帮助!下次再见!