July 8, 2023

Vue2源码(1):异步更新 nextTick原理

nexttick源码

[TOC]

异步更新

1 要实现什么?

用户可能连续几行代码中,多次修改同一变量数据。若每次修改,都进行dom更新,显然是性能浪费

fix:将DOM更新操作进行延迟,也就是把DOM更新操作暂存起来。等同步代码执行完后,再进行异步的DOM更新(可以说使用了promise)

2 将DOM更新操作进行延迟

如何将DOM更新延迟呢

修改数据后,会进入 watcher 的 update() 方法

然后执行 queueWatcher() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 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) // 把当前watcher暂存起来
}
}

3 queueWatcher()

首先要把当前watcher暂存起来

暂存这里,就引出了几个问题:

Answer:

到这里,暂存就搞定了

那么就可以进行刷新渲染操作了

刷新操作关键点是:vue 希望不论 watcher 的 update 执行多少次,它的刷新渲染操作仅执行一次

因此,这里使用了一个锁机制:waiting

1
2
3
4
5
6
let waiting = false
// 锁
if (!waiting) {
waiting = true
执行异步渲染操作...
}

初始化的时候,waiting = false

因此第一次,会执行 if 语句的代码, 执行异步渲染操作

后面即使多次触发 queueWatcher ,也不会进入 if 语句的代码了

里面的代码,我在上面简述为:执行异步渲染操作

那么这个过程是什么呢?

其实就是:异步的、把 queue 中存储的 watcher 一个一个拿出来进行渲染操作

比如用计时器实现异步,传入一个方法做渲染操作:

1
setTimeout( 执行渲染操作, 0 )

(当前,vue 中并不是简单的用 setTimeout 来实现的,后面再说)

具体来说:

执行渲染操作的方法叫做: flushSchedulerQueue()

它做了2件事情:

以上面为基础,举一个 DEMO:

用户在界面多次修改:

1
2
3
vm.name = 'xx'
vm.name = 'ww'
vm.name = 'end'

执行第一行代码:

触发了 watcher 的 update,进入 queueWatcher(),vue 会将 这个 watcher 加入到 queue 中

此时锁是打开的,会进行异步渲染操作(因为它是异步的,所以不会立刻渲染),并将锁关上了

执行第一行、第三行代码:

虽然多次触发了 update,但是这个 watcher 已经加入了,所以不会重复加入,并且锁是关上的

同步代码执行完了,可以执行异步代码了

执行 flushSchedulerQueue()

会调用 watcher.run()

run() 实际上又执行力 get() 方法(watcher的通用求值方法),即

入栈;

执行回调(vm_update vm_render); // 这里拿到的值是 name 的最新值:’end’,进行渲染

出栈;

渲染完了,将锁打开

所以,即使多次修改值,异步渲染操作仅会执行一次,且拿到最新值

现在这样看起来,还挺完美

但如果执行下面这些代码:

1
2
vm.name = 'icheng'
console.log(app.innerHTML); // 打印 dom 上的文本 name

此时,界面上的数据是新的,但是打印出来的数据,是旧的

因为:这两行同步代码会先执行,然后才会异步更新渲染,所以第二行去拿 dom 上的数据,这个数据根本还没更新

如何解决这个问题

1
2
3
4
vm.name = 'icheng'
vm.&nextTick(() => {
console.log(app.innerHTML);
})

引出了 vue 中一个核心方法:nextTick()

有了nextTick(),我们可以把前面的执行异步渲染操作的 setTimeout() 改为 nextTick()

1
setTimeout( flushSchedulerQueue(), 0 )

​ 👇

1
nextTick( flushSchedulerQueue(), 0 )

nextTick 中,会传入 flushSchedulerQueue() 渲染更新,也会传入用户定义的的方法,然后依次执行

因此,vue 中有一个 callbacks 队列,维护所有这些任务

4 nextTick()

首先将传入的任务,推入 callbacks 队列

然后,以异步的方式、执行任务

所以,nextTick() 不是创建一个异步任务,而是将这个任务维护到了队列中,依次执行

考虑兼容性,它内部没有确定采用了哪个异步 API,而是采用了优雅降级的方式:

源码

nextTick() 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
// 将任务推入 callbacks 队列
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
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
})
}
}

优雅降级的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 注:flushCallbacks是遍历 callbacks 队列按照顺序依次执行

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) ||
// PhantomJS and iOS 7.x
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)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

queueWatcher 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 将一个观察者推入观察者队列中,且具有重复id的观察者将被跳过
*/
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
// 去重
if (has[id] != null) {
return
}

if (watcher === Dep.target && watcher.noRecurse) {
return
}

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 (__DEV__ && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}

About this Post

This post is written by Duan WeiCheng, licensed under CC BY-NC 4.0.