前言
众所周知,vue 中的更新时异步的,比如 this.msg = xxx,你看起来他是立马更新了,其实并没有。它会异步执行,接下来就来看看怎么实现的吧。
先上图
首先从数据改动开始说起
- 调用this.msg = xxx 数据发生变更
- 在数据初始化阶段已经收集了依赖的watcher 到 dep 中, 执行 dep.notify 通知watcehr变更
- notify 方法遍历调用 所有以来的watcher 的update 方法,把当前watcher 实例放入 queueWatcher 函数中执行,接下来就是异步更新的关键了,看代码
queueWatcher 函数代码 在 src\core\observer\scheduler.js
主要作用:把当前 watcher 实例添加到一个 queue 中
export function queueWatcher (watcher: Watcher) {
// 拿到 watcher 的唯一标识
const id = watcher.id
// 无论有多少数据更新,相同的 watcher 只被压入一次
// 我理解这就是为什么在一次操作中,多次更改了变量的值,但是只进行了一次页面更新的原因,
// 同一变量 依赖它的 watcher 是一定的,所以已经存在了就不再放进watcher 队列中了,也不会走后面的逻辑
if (has[id] == null) {
// 缓存当前的watcher的标识,用于判断是否重复
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.
// 此处能走到这儿,说明 flushSchedulerQueue 函数被执行了 watcher队列已经正在开始被更新了,
// 并且 在执行某个watcher.run方法的时候又触发的数据响应式更新,重新触发了 queueWatcher
// 因为在执行的时候回有一个给 watcher 排序的操作,所以,当 watcher 正在更新时已经是排好顺序了的,此时需要插入到特定的位置,保持 watcher 队列依然是保持顺序的
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
// waiting 表示当前的 flushSchedulerQueue 还没有被执行,因为还没有重置状态, waiting 仍然 为 true
// 所以 waiting 的意义就是 表明是否执行了flushSchedulerQueue,
if (!waiting) {
waiting = true
// 直接同步刷新队列
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 同步执行
flushSchedulerQueue()
return
}
// 把更新队列函数放到异步队列中
nextTick(flushSchedulerQueue)
}
}
}
复制代码
flushSchedulerQueue 代码在相同目录下
// 主要作用: 遍历执行每一个 watcher 的 run 方法,进而实现数据和视图的更新,并在执行完所有的 方法之后,重置状态,表示正在刷新队列的 flushing, 表示 watcher 是否存在的 has,表示是否需要执行 nexttick 的 waiting
function flushSchedulerQueue () {
// 当方法被执行时,设置为正在刷新状态,以示可以继续执行 nextTick 方法
flushing = true
// 把队列中的 watcher 排个序,
/**
* 排序的作用:(此句照搬照抄而来)
* 1. 保证父组件的watcher比子组件的watcher先更新,因为父组件总是先被创建,子组件后被创建
* 2. 组件用户的watcher在其渲染watcher之前执行。
* 3. 如果一个组件在其父组件执行期间被销毁了,会跳过该子组件。
*/
queue.sort((a, b) => a.id - b.id)
// 中间略去若干代码
...
// 遍历 queue 中存的 所有的 watcher,执行 run 方法更新
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
watcher.run()
}
// 因为 queue 是在一个闭包中,所以当遍历执行完毕了,就把 队列清空
queue.length = 0;
// has 是判断 当前 watcher 是否重复,作为是否把 watcher 放进 queue 的依据
// 此时已经执行完了 queue 中的所有 watcher了,之前已经执行过的watcher 如果发生了变更,可以重新加入了
has = {}
// waiting 是判断是否 执行 nextTick 的标识,当前的刷新队列已经执行完毕了,说以,可以设置为 false 了,执行下一轮的的添加异步事件队列的方法
// flushing 是判断是否当前异步事件正在执行的标志,当前更新完毕,作为判断 watcher 入队的形式
waiting = flushing = false
}
复制代码
nextTick 方法 源码src\core\util\next-tick.js
export function nextTick(cb ? : Function, ctx ? : Object) {
let _resolve
// 把执行更新操作之后的回调函数添加到队列里
// 用try catch包装一下传进来的函数,避免使用$nextTick时,传入的回调函数出错能够及时的捕获到
// 只要执行了nextTick函数,就把回调函数添加到回调列表里
// 这里的 cb 回调函数就是 flushSchedulerQueue 函数,里面执行了 queue 中存放的所有的 watcher.run 方法
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 通过pending来判断是否需要向任务队列中添加任务
// 如果上一个清空回调列表的当flushCallbacks函数还在任务队列中,就不往任务队列中添加
// 第一次执行时,就默认就添加一个进任务队列,一旦添加进任务队列,就表明暂时不在需要往任务队列中添加flush函数
// 当执行了上一个 flushCallbacks 函数的时候,pending修改为false,表明可以重新添加一个清空回调列表的flush函数到任务队列了
if (!pending) {
pending = true
// 这里是调用清空 callbacks 数组中方法,并执行的函数,
timerFunc()
}
// $flow-disable-line
// 判断当前环境是否支持promise,如果支持的话,可以返回一个期约对象,
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
复制代码
timerFunc() 方法,主要是做一些降级操作,实现异步的关键
timerFunc = () => {
Promise.resolve().then(flushCallbacks)
}
// 如果当前环境不支持的话,会进行一定的降级操作,直到最后,用 宏任务settimeout来处理
复制代码
看看 flushCallbacks, 任务就是执行了所有的 callbacks函数
function flushCallbacks() {
// 如果开始执行了 flushCallbacks 说明,当前的异步任务已经为空了,如果此时再 nextTick 方法会添加新的 任务进去了
pending = false
// 拷贝一份callbacks中的所有回调函数,用于执行
const copies = callbacks.slice(0)
// 随即删除所有callbacks
callbacks.length = 0
// 当微任务队列中的flushCallbacks添加到执行栈中了,就执行callbacks中的所有的函数
// 也就是调用执行每一个 flushSchedulerQueue 函数,然后遍历执行每一个函数
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
复制代码
基本关键变量的作用
- waiting: 变量,作为是否执行 nextTick ,添加 flushSchedulerQueue 方法的关键,标志着 callbacks 中 是否有 flushSchedulerQueue 方法, 比如同一个变量的改变,可能会影响多个 watcher,因为执行 flushSchedulerQueue 是异步的,遍历dep.update 先把所有的 watcher 都放入到 queue 中,也才只执行了一次 nextTick,callbacks中也只有一个方法。虽然当第一次方如 watcher 时就会执行 nexttick 把 flushSchedulerQueue方法放入callbacks 中,看起来好像已经要执行了,但是因为 queue 是闭包变量,所以,后续的变量仍然可以添加queue 中,
- flushing:: 表示是否正在执行 flushSchedulerQueue 方法,如果是正在执行更新方法的话,对向已经排好序的 watcher 队列中添加新的 watcher,需要把新 watcher 插入到排好序的指定的位置,这也就是为什么遍历 watdher 那块儿会 直接使用 queue.length 的原因,这个长度会发生变化。
- pending:: pending 是决定是否把更新 callbacks 数组的方法放入异步队列的关键,保证了异步队列中只有一个清空callbacks 的任务, 也就解释了,连续手动执行多个 $nextTick 方法不会立即执行,也还是会把他们的回调 放入 callbacks 中,然后等到任务都执行完毕了,一下把所有的 回调函数都执行掉。
参考