持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 13 天,点击查看活动详情
9.响应式原理-7.queueWatcher
start
上一个章节查看了 Watcher 的源码,了解到 dep 中会存储 Watcher。那么当我们 set 数据的时候,dep 和 Watcher 是如何工作的呢?
从set到update
/* 1. defineReactive中的set */
dep.notify()
/* 2.dep的notify */
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
/* 3.Watcher实例的update*/
update() {
/* istanbul ignore else */
if (this.lazy) {
// 懒
this.dirty = true;
} else if (this.sync) {
// 同步
this.run();
} else {
// 主要是执行 queueWatcher
queueWatcher(this);
}
}
/* 4. queueWatcher */
看上述的逻辑,可以了解到当修改数据的时候,触发了以下操作:
- 触发 defineReactive 中的 set;
- Dep 实例的 notify;
- Watcher 实例的 update
- queueWatcher
queueWatcher
-
看一下 queueWatcher 英文单词的释义
queue队列Watcher观察者scheduler时间调度员;程序机,调度机;调度程序; (scheduler是存放queueWatcher方法的 js 文件名)
-
好,开始研究
queueWatcher以及其相关的逻辑
\src\core\observer\scheduler.js 中的 queueWatcher
const queue: Array<Watcher> = []
let index = 0
let has: { [key: number]: ?true } = {}
let flushing = false
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
* 将一个观察者推入观察者队列。
* 有重复id的工作将被跳过,除非它是
* 当队列被刷新时推送。
*/
export function queueWatcher(watcher: Watcher) {
// 1. 拿到 watcher的id
const id = watcher.id
// 2. has 是一个对象, 用来存储 watcher,同一个 watcher不用重复推入
if (has[id] == null) {
has[id] = true
// flushing 冲洗
if (!flushing) {
// queue是一个数组,存储watcher
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.
// 如果已经在刷新,则根据监视程序的id拼接它
// 如果已经超过了它的id,接下来将立即运行它。
let i = queue.length - 1 // 拿到最后一个watcher的索引
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
// 排队刷新
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
// watcher上有 before ,先执行对应的 before
if (watcher.before) {
watcher.before()
}
// 拿到当前 watcher id
id = watcher.id
// 清空
has[id] = null
// 执行watcher的 run
watcher.run()
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
\src\core\observer\next-tick.js 中的 nextTick
// 定义一个数组,存储用户注册的回调
const callbacks = []
// 声明一个变量标记,标记是否已经向任务队列中添加了一个任务
let pending = false
// 其实就是 下次微任务执行时 更新 dom
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()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve) => {
_resolve = resolve
})
}
}
\src\core\observer\next-tick.js 中的 timerFunc
let timerFunc // 定义一个变量
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
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)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
/*
通过下面的方法
promise
MutationObserver
setImmediate
setTimeout
*/
function flushCallbacks() {
// 1. 重置为false
pending = false
// 2. 浅拷贝一份函数
const copies = callbacks.slice(0)
callbacks.length = 0 // 清空数组???有点意思 var a=[1,2,3]; a.length = 0; 打印a []
// 3. for 循环执行callbacks中存储的每一项
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
首先,上方的代码块展示了 queueWatcher 相关的主要代码。
\src\core\observer\scheduler.js
\src\core\observer\next-tick.js
这里统一梳理一下相关逻辑。
前面提到,当我们触发 data 中一个属性的 set,会触发queueWatcher
-
queueWatcher的主要逻辑- 在 has,queue 存储我们的 watcher 的 id;
nextTick(flushSchedulerQueue)
-
flushSchedulerQueue- for 循环遍历 queue,依次执行 watcher.run
-
nextTick- 将传入的
flushSchedulerQueue,存储在 callbacks 数组中。 - 如果
!pending,执行timerFunc
- 将传入的
-
timerFunc- 异步执行
flushCallbacks - 这里的异步实现优先级依次为
Promise,MutationObserver,setImmediate,setTimeout
- 异步执行
-
flushCallbacks- 遍历并执行
callbacks的每一项(flushSchedulerQueue)。
- 遍历并执行
watcher.run
/**
* Scheduler job interface.
* Will be called by the scheduler.
* 调度器的工作界面。
* 将被调度程序调用。
*/
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) {
const info = `callback for watcher "${this.expression}"`;
invokeWithErrorHandling(
this.cb,
this.vm,
[value, oldValue],
this.vm,
info
);
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
}
watcher.run的逻辑也很简单,调用this.get(),调用vm._update(vm._render(), hydrating);_update,_render后续会去学习,这里简单理解,视图更新的方法。
思考
- 异步队列
阅读了上述内容,我了解到当修改数据,其实最终还是执行 watcher.run。 但是为什么要引入异步队列? 先看看官方文档对这里的解释。 vue2_异步队列
结合现有我们了解到的
并不是当我修改数据,就马上更新 dom,而是开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
举个例子,我点击一个按钮,分别修改
a,b,c三个属性的属性值,代码如下所示
{
data () {
return {
a: 1,
b: 1,
c: 1
}
},
methods: {
say () {
this.a = "111"
this.b = "222"
this.c = "333"
}
}
}
- 此时会分别触发
a,b,c三个属性的 set。如果没有异步队列,会执行三次 watcehr.run , 重新渲染三次页面? - 加入了异步队列,在一次事件循环中,收集去重后的 watcher,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
这么做,可以避免不必要的性能消耗。但是也会有一些影响。正如官网所提到的
如果你想基于更新后的 DOM 状态做点什么?
this.$nextTick(() => {
// 这里写你的逻辑
})
end
- 本节主要是了解了 通知更新实际是 执行 watcher.run,然后触发视图更新。
- Vue 在更新 DOM 时是异步执行的。