通过上一节分析我们了解了响应式数据依赖收集过程,收集的目的就是为了当我们修改数据的时候,可以对相关的依赖派发更新,那么这一节我们来详细分析这个过程。
我们看下 defineReactive
中 setter
的逻辑:
1 | export function defineReactive ( |
setter 的逻辑有 2 个关键的点:
- 一个是
childOb = !shallow && observe(newVal)
,如果shallow
为false
的情况,会对新设置的值变成一个响应式对象; - 另一个是
dep.notify()
,通知所有的订阅者
1. 触发setter
当我们在组件中对响应的数据做了修改,就会触发 setter 的逻辑,最后调用 dep.notify()
方法, 它是 Dep 的一个实例方法,定义在 src/core/observer/dep.js
中:
1 | class Dep { |
这里的逻辑非常简单:
- 首先浅拷贝 subs 返回一个新的数组
- 遍历所有的
subs
,也就是Watcher
的实例数组,然后调用每一个watcher
的update
方法,它的定义在src/core/observer/watcher.js
中:
1 | class Watcher { |
在这里我们只关心 update函数
其实就是调用了 queueWatcher(this)
2. queueWatcher
实现:
1 | const queue: Array<Watcher> = [] // watcher 队列 |
- 这里引入了一个
队列
的概念,这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在nextTick
后执行flushSchedulerQueue
。 has
对象保证同一个 Watcher 只添加一次,但是当执行flushSchedulerQueue
的过程中,watcher 是可以被添加进队列的,因为在flushSchedulerQueue
遍历queue
的时候会执行has[id] = null
- 接着对
flushing
的判断:- 为 false:表示还没有调用
flushSchedulerQueue
,此时将 watcher 推入queue
队列。 - else 部分的逻辑稍后再说。
- 为 false:表示还没有调用
waiting
:保证对nextTick(flushSchedulerQueue)
的调用逻辑只有一次。nextTick
的实现之后会抽一小节专门去讲,目前就可以理解它是在下一个 tick,也就是异步的去执行flushSchedulerQueue
。
3. flushSchedulerQueue
接下来我们来看 flushSchedulerQueue
的实现,它的定义在 src/core/observer/scheduler.js
中。
1 | // src/core/observer/scheduler.js |
1 | let flushing = false |
3.1 队列排序
queue.sort((a, b) => a.id - b.id)
对队列做了从小到大的排序,这么做主要有以下要确保以下几点:
- 组件的更新由父到子;因为父组件的创建过程是先于子的,所以
watcher的创建
也是先父后子,执行顺序也应该保持先父后子。 - 用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。
- 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。
3.2 队列遍历
在对 queue 排序后,接着就是要对它做遍历,拿到对应的 watcher,执行 watcher.run()
。
1 | for (index = 0; index < queue.length; index++) { |
这里需要注意一个细节,在遍历的时候每次都会对 queue.length
求值,因为在 watcher.run()
的时候,很可能用户会再次添加新的watcher
:修改数据值从而触发 setter,这样会再次执行到 queueWatcher
,如下:
1 | export function queueWatcher (watcher: Watcher) { |
flushing
为true
,就会执行到 else 的逻辑,然后就会从后往前找- 找到第一个
待插入 watcher的id
比当前队列中 watcher的id
大的位置 - 因此 queue 的长度发生了变化。
3.2.1 watcher.run()
1 | class Watcher { |
通过 this.get()
得到它当前的值,然后做判断,如果满足以下条件之一:
- 新旧值不等
- 新值是对象类型
- deep 模式
执行 watcher的回调
,注意回调函数执行的时候会把第一个和第二个参数传入新值value
和 旧值oldValue
,这就是当我们添加自定义watcher
的时候能在回调函数的参数中拿到新旧值的原因。
对么对于渲染 watcher
而言,它在执行 this.get()
方法求值的时候,会执行 this.getter
方法,也就是 updateComponent
:
1 | updateComponent = () => { |
所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行patch
的过程,但它和首次渲染有所不同,在之后的章节会介绍。
3.2.2 循环判断
1 | if (process.env.NODE_ENV !== 'production' && has[id] != null) { |
每次遍历 queue 时,用 circular[id]
来记录 watcher 对象的循环次数,当大于 MAX_UPDATE_COUNT
时认为是死循环
举个栗子🌰
1 | <script> |
3.3 状态恢复
这个过程就是执行 resetSchedulerState
函数,它的定义在 src/core/observer/scheduler.js
中。
1 | const queue: Array<Watcher> = [] |
总结:
通过这一节的分析,我们对 Vue 数据修改派发更新的过程也有了认识,实际上就是当数据发生变化的时候,触发 setter 逻辑
,把在依赖过程中订阅的的所有观察者
,也就是 watcher
,都触发它们的 update过程
,这个过程又利用了队列做了进一步优化,在 nextTick后
执行所有 watcher.run
,最后执行它们的回调函数
。nextTick 是 Vue 一个比较核心的实现了,下一节我们来重点分析它的实现。