各位观众,晚上好!我是你们今晚的导游,将带你们深入 Vue 2 的源码腹地,探秘 Watcher
的 "get" 和 "update" 方法,看看它们是如何在依赖收集和渲染队列中翩翩起舞的。准备好迎接一场源码级别的探险了吗?Let’s go!
第一站:Watcher 的 "get" 方法:依赖收集的幕后推手
首先,我们要明确一点:依赖收集的目的是什么?简单来说,就是让 Vue 知道当哪些数据发生变化时,需要通知哪些 Watcher
进行更新。而 Watcher
的 get
方法,就是负责启动这场 "数据与观察者" 之间浪漫邂逅的关键人物。
让我们先来看看 Watcher
的 get
方法的代码(简化版):
// src/core/observer/watcher.js
get() {
pushTarget(this) // 重要!将当前 watcher 推入全局的 targetStack
let value
try {
value = this.getter.call(vm, vm) // 执行 getter,触发依赖收集
} catch (e) {
// 处理错误
} finally {
popTarget() // 重要!将当前 watcher 从 targetStack 中弹出
}
return value
}
这段代码的核心在于 pushTarget(this)
、this.getter.call(vm, vm)
和 popTarget()
这三行。它们共同完成了一项伟大的任务:建立数据和 Watcher
之间的连接。
-
pushTarget(this)
:吹响依赖收集的号角pushTarget
函数的作用是将当前Watcher
实例推入一个全局的栈targetStack
中。这个targetStack
就像一个临时的 "观察者登记处",告诉我们当前哪个Watcher
正在 "工作"。// src/core/observer/dep.js let targetStack = [] export function pushTarget (_target: ?Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { Dep.target = targetStack.pop() }
这里
Dep.target
扮演着 "当前活跃的 Watcher" 的角色。每次只有一个Watcher
能够成为Dep.target
。pushTarget
就像是说:“嘿,现在this
这个Watcher
要开始工作了,大家都注意了!”。popTarget
则是说:“好了,this
这个Watcher
工作结束了,可以下班了!” -
this.getter.call(vm, vm)
:触发依赖收集的导火索this.getter
是一个函数,它负责获取Watcher
所观察的数据。这个函数通常是由 Vue 编译生成的,它会访问组件的data
、computed
等属性。关键在于,当
this.getter
函数执行时,它会访问到响应式数据。而响应式数据的getter
会执行依赖收集的操作。让我们来看看响应式数据的
getter
是如何工作的:// src/core/observer/index.js (observe 函数中定义的 getter) get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { // 关键!判断是否存在活跃的 Watcher dep.depend() // 执行依赖收集 if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }
这里的
Dep.target
就是我们刚刚pushTarget
进去的Watcher
实例。如果Dep.target
存在,说明当前有Watcher
正在 "监听"。dep.depend()
就像是说:“嘿,Dep.target
(也就是当前的Watcher
) 对我(这个数据)感兴趣,我记下来了!”dep.depend()
的作用是:- 将当前的
Watcher
实例添加到Dep
对象的subs
数组中。subs
数组存储了所有依赖于该数据的Watcher
。 - 同时,将当前的
Dep
对象添加到Watcher
实例的newDeps
数组中。
这样,数据和
Watcher
之间就建立了双向的连接。 - 将当前的
-
popTarget()
:清理现场popTarget
函数的作用是将targetStack
栈顶的Watcher
弹出,恢复Dep.target
的状态。 这就像是说:“好了,这个Watcher
已经完成了它的工作,现在轮到下一个Watcher
了!”
依赖收集的完整流程,用表格梳理一下更清晰:
步骤 | 操作 | 作用 | 参与者 |
---|---|---|---|
1 | pushTarget(watcher) |
将当前 Watcher 推入 targetStack ,设置为 Dep.target |
Watcher , Dep |
2 | 执行 watcher.getter |
访问响应式数据 | Watcher , 组件实例 |
3 | 响应式数据的 getter 中的 dep.depend() |
将当前 Watcher 添加到 Dep 的 subs 数组中,同时将 Dep 添加到 Watcher 的 newDeps 数组中 |
Dep , Watcher |
4 | popTarget() |
将当前 Watcher 从 targetStack 中弹出,恢复 Dep.target 的状态 |
Watcher , Dep |
第二站:Watcher 的 "update" 方法:将变化推入渲染队列
当响应式数据发生变化时,Dep
对象会通知所有依赖于它的 Watcher
进行更新。而 Watcher
的 update
方法,就是处理这些更新通知的关键人物。
让我们来看看 Watcher
的 update
方法的代码(简化版):
// src/core/observer/watcher.js
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this) // 重要!将 watcher 推入队列
}
}
这段代码根据 Watcher
的不同类型,采取不同的更新策略。
-
this.lazy
:懒执行的 Watcher (例如 computed property)如果
Watcher
是一个懒执行的Watcher
(例如computed property
对应的Watcher
),那么只需要将this.dirty
设置为true
,表示该computed property
的值已经过期,下次访问时需要重新计算。 -
this.sync
:同步更新的 Watcher如果
Watcher
是一个同步更新的Watcher
,那么直接调用this.run()
方法进行更新。 这种方式通常用于一些需要立即更新的场景,例如在watch
选项中使用sync: true
。 -
默认情况:异步更新,推入队列
在大多数情况下,
Watcher
都会采用异步更新的方式。queueWatcher(this)
的作用是将当前的Watcher
实例推入一个队列中,等待 Vue 在下一个tick
时统一进行更新。// src/core/observer/scheduler.js const queue: Array<Watcher> = [] let has: { [key: number]: ?true } = {} let waiting = false let flushing = false let index = 0 function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // 如果正在 flushing,则根据 watcher.id 插入到队列的合适位置 // 保证: // 1. 组件的 update 由父到子执行 // 2. 用户定义的 watcher 比 render 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 nextTick(flushSchedulerQueue) } } }
queueWatcher
函数的作用是:- 将
Watcher
添加到queue
队列中。 - 使用
has
对象来避免重复添加同一个Watcher
。 - 如果当前没有正在刷新队列 (
flushing
为false
),则直接将Watcher
添加到队列的末尾。 - 如果当前正在刷新队列 (
flushing
为true
),则根据Watcher
的id
将其插入到队列的合适位置,以保证更新的顺序。 - 如果当前没有正在等待刷新队列 (
waiting
为false
),则调用nextTick(flushSchedulerQueue)
函数,触发 Vue 在下一个tick
时刷新队列。
nextTick
函数的作用是将flushSchedulerQueue
函数推入一个微任务队列中,等待浏览器空闲时执行。// src/core/util/next-tick.js let callbacks = [] let pending = false function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } let timerFunc if (typeof Promise !== 'undefined' && isNative(Promise)) { // 使用 Promise const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) } } else if (typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // 使用 MutationObserver let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // 使用 setImmediate timerFunc = () => { setImmediate(flushCallbacks) } } else { // 使用 setTimeout timerFunc = () => { setTimeout(flushCallbacks, 0) } } export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
nextTick
函数会优先使用Promise
、MutationObserver
或setImmediate
,如果都不支持,则使用setTimeout
。当浏览器空闲时,
flushSchedulerQueue
函数会被执行,它会遍历queue
队列,依次执行每个Watcher
的run
方法。// src/core/observer/scheduler.js 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 component's update. queue.sort((a, b) => a.id - b.id) // 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 invoke watcher's callback if (has[id] != null) { cleanupDeps(watcher) } } resetSchedulerState() } function resetSchedulerState () { index = queue.length = 0 has = {} waiting = flushing = false }
flushSchedulerQueue
函数的作用是:- 将
flushing
设置为true
,表示当前正在刷新队列。 - 对
queue
队列进行排序,以保证更新的顺序。- 父组件的更新先于子组件。
- 用户定义的
Watcher
先于渲染Watcher
。
- 遍历
queue
队列,依次执行每个Watcher
的run
方法。 - 在开发环境下,会检查并调用
Watcher
的回调函数。 - 调用
resetSchedulerState
函数,重置调度器的状态。
Watcher
的run
方法会真正执行更新操作:// src/core/observer/watcher.js 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) { 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
方法的作用是:- 调用
this.get()
方法,重新求值,触发依赖收集。 - 比较新值和旧值,如果发生了变化,则调用
this.cb
方法,执行更新操作。
- 将
总结:Watcher
的 "get" 和 "update" 方法的协作流程
- 依赖收集 (get):当组件首次渲染或数据变化时,
Watcher
的get
方法被调用。get
方法会将当前Watcher
推入targetStack
,然后执行getter
函数,访问响应式数据,触发依赖收集。 - 数据变化:当响应式数据发生变化时,
Dep
对象会通知所有依赖于它的Watcher
。 - 推入队列 (update):
Watcher
的update
方法被调用,将Watcher
推入更新队列queue
中。 - 异步更新:在下一个
tick
时,Vue 会遍历更新队列queue
,依次执行每个Watcher
的run
方法。 - 执行更新 (run):
Watcher
的run
方法会重新求值,并执行更新操作,例如更新 DOM。
用表格总结 Watcher
的 update
方法的流程:
步骤 | 操作 | 作用 | 参与者 |
---|---|---|---|
1 | 数据变化,Dep 通知 Watcher |
触发 Watcher 的更新 |
Dep , Watcher |
2 | Watcher 的 update 方法被调用 |
根据 Watcher 的类型决定更新策略 |
Watcher |
3 | 如果是异步更新,则将 Watcher 推入 queue |
等待下一个 tick 时统一更新 |
Watcher , 调度器 |
4 | nextTick 触发 flushSchedulerQueue |
遍历 queue ,执行每个 Watcher 的 run 方法 |
调度器 |
5 | Watcher 的 run 方法被调用 |
重新求值,比较新旧值,执行更新操作 | Watcher , 组件实例 |
一些需要注意的点:
- Vue 的依赖收集是细粒度的,只有当组件实际使用了某个数据时,才会建立依赖关系。
- Vue 的更新是异步的,采用队列的方式,可以有效地减少不必要的 DOM 操作,提高性能。
- Vue 的更新顺序是有保证的,父组件的更新先于子组件,用户定义的
Watcher
先于渲染Watcher
。
好了,今天的源码探险就到这里。希望通过这次旅程,大家对 Vue 2 中 Watcher
的 "get" 和 "update" 方法有了更深入的理解。下次再见!