早上好,各位未来的 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 应用。希望今天的讲座对大家有所帮助!下次再见!