返回

专栏第九篇-异步渲染机制

一、异步渲染机制

在上一节的内容中,我们知道在数据发生变化的时候,会重新更新依赖,最终会执行Watcher实例的update方法。

但是其实Vue中并不是在每次执行update方法时都会执行渲染,他会将多次变化做一次合并渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vue from "vue";

new Vue({
template:`<div>{{ msg }}</div>`,
data(){
return {
msg:"inited"
}
},
mounted(){
this.msg = '第一次渲染';
this.msg = '第二次渲染';
}
}).$mount("#app")

按照我们上节所学的知识:每一个data都被代理劫持,每次data修改后都会重新执行更新方法。

那么在 mounted方法中 data修改了 2 次,是不是应该渲染 2 次呢?

但是从实际角度明显我们这里只需要渲染第二次的内容,也就是只渲染一次,而 Vue内部也是这么操作的。

由于Vue内部的异步渲染机制,实际上页面只会渲染一次,把第一次的赋值所带来的的响应与第二次的赋值所带来的的响应进行一次合并,将最终的val只做一次页面渲染,而且页面是在执行所有的同步代码之后才能得到渲染。

二、为什么需要异步渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let data = {msg:'inited'}
let _msg = data.msg
Object.defineProperty(data,"msg",{
set(val){
console.log("我被设置了")
_msg = val
},
get(){
console.log("我被访问了")
return _msg
}
})
data.msg = '第一次渲染'
console.log("获取此时data.msg",data.msg)
data.msg = '第二次渲染'
// 执行结果
// 我被设置了
// 我被访问了
// 获取此时data.msg 第一次渲染
// 我被设置了

通过执行上面这段代码,我们可以知道代理是同步操作。

也就是说如果在 Vue中不进行异步处理的话,可能最终执行逻辑就是下图这样。

这样的话每次 data变化就会重新渲染一次,可能会导致浏览器的闪烁卡顿。

所以我们可以从用户体验和性能 2 个角度进行分析:

  • 用户体验:从上述例子可以看出,实际上页面只需要展示最终的值变化。第一次的值变化只是一个过渡状态,如果将其渲染并显示给用户,可能会导致页面出现闪烁现象,从而影响用户体验。通过 Vue 的异步更新队列机制,可以有效避免这种情况的发生,确保用户看到的是稳定且最新的页面状态。
  • 性能:在上述例子中,最终需要展示的数据实际上是第二次对 val 赋的值。如果第一次赋值也触发页面渲染,那么在最终结果渲染之前,页面会进行一次不必要的渲染。这无疑增加了性能的消耗。

三、Vue异步渲染原理

数据每次变化时,将其引起页面变化的操作都会放到一个异步API的回调函数中。当同步代码执行完毕后,异步回调开始执行。此时 Vue 会将所有需要渲染的变化合并在一起,最终执行一次渲染操作。

四、queueWatcher

每一次data变化都会执行Watcher实例上的 update方法。

1
2
3
4
5
6
7
8
9
10
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

默认 lazy、sync都是 false。

所以默认每一次都会调用queueWatcher方法,那么这个 queueWatcher函数都做了什么操作呢?

4.1 has对象

1
2
3
4
5
6
7
8
9
let has = {};
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
if (has[id] != null) {
return
}
has[id] = true
// 省略部分代码
}

has 对象用于存储已经加入队列的 watcher 的 ID,以确保每个 watcher 只被加入队列一次。这是一种避免重复处理相同 watcher 的机制。

4.2 noRecurse选项

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

如果当前的 watcher 就是全局的 Dep.target(即当前正在执行的 watcher),并且这个 watcher 设置了 noRecurse 标志(表示不希望递归触发),那么就直接返回,不执行任何操作。

4.3 queue

1
2
3
4
5
6
7
8
9
const queue: Array<Watcher> = []
export function queueWatcher(watcher: Watcher) {
// 省略部分代码
if (!flushing) {
queue.push(watcher)
} else {
queue.splice(i + 1, 0, watcher)
}
}

queue是一个数组,用于存储需要执行更新渲染的 watcher实例。

当数据变化时,相关的 watcher实例会被添加到这个数组中,等待执行。

4.4 flushing状态

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
let flushing = false;
let index = 0;
export function queueWatcher(watcher: Watcher) {
// 省略部分代码
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
nextTick(flushSchedulerQueue);
}

function flushSchedulerQueue(){
flushing = true;
// 省略代码
// 执行完更新操作后重置内部状态
resetSchedulerState();
}
function resetSchedulerState(){
flushing = false;
}

如上述代码所示:flushing是一个内部状态,表示当前是否正在执行 watcher 的队列。默认为 false。

当第一次执行 queueWatch时,flushing为 false,表示当前没有执行 watcher队列,这个时候处理比较简单,将 watcher 添加到队列的末尾,等待后续的执行。

此时调用nextTick(flushSchedulerQueue)方法。

在同步任务执行以后,执行异步时在flushSchedulerQueue方法中将 flushing置为 true,表示当前正在执行 watcher队列。

如果在执行flushSchedulerQueue方法时再次调用了 queueWatcher方法,找到第一个 id小于或等于 watch.id,然后插入他的后面,保证了 watcher按照它们被创建的顺序执行,即使在执行过程中有 watcher加入。

4.5 waiting状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let waiting = false;
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue)
}

function resetSchedulerState(){
// 将 waiting重置为 false
waiting = false;
}

function flushSchedulerQueue(){
resetSchedulerState()
}

queueWatcher方法最主要的功能就是执行 nextTick(flushSchedulerQueue)方法,从而达到异步批量更新的目的。

而 waiting决定了是否应该执行flushSchedulerQueue方法。

在第一次执行 queueWatcher(this)时,会调用nextTick(flushSchedulerQueue)方法。

并且将 waiting设置为 true。

然后立即执行异步更新操作,所以 waiting表示目前存在异步更新操作正在等待执行。

然后在一次事件循环内再次调用 queueWatcher方法,此时已经有异步更新操作等待执行。

所以不再调用异步更新操作,这意味着在一次事件循环内,该方法只会被执行一次。

该变量在异步更新后会被重置为 false。

4.6 flushSchedulerQueue

专门用于执行处理queue队列。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
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 watcher run,
// its watchers can be skipped.
queue.sort(sortCompareFn)

// 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]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (__DEV__ && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' +
(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),
watcher.vm
)
break
}
}
}

// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()

resetSchedulerState()

// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
cleanupDeps()

// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}

4.6.1 使用sortCompareFn进行排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const sortCompareFn = (a: Watcher, b: Watcher): number => {
if (a.post) {
if (!b.post) return 1
} else if (b.post) {
return -1
}
return a.id - b.id
}
let queue = []
function flushSchedulerQueue() {
// 省略部分代码
// 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 watcher run,
// its watchers can be skipped.
queue.sort(sortCompareFn)
}

因为在一次事件循环内多次触发 queueWatcher 函数会给队列添加 watcher。

此时 watcher的执行顺序需要重新整理,sortCompareFn用于给 watcher排序。

post表示这是一个在 DOM更新后执行的 watcher

排序目的主要是为了确保下面三种情况的顺序:

  1. 父子组件更新顺序:组件的父 watcher 应该在子 watcher 之前执行。这是因为父组件通常在子组件之前创建,因此其 watcher 也应该先执行。
  2. 用户 watcher 与渲染 watcher 的顺序:用户定义的 watcher(通常在组件的 watch 选项中定义)应该在组件的渲染 watcher 之前执行。这是因为用户 watcher 通常在渲染 watcher 之前创建。
  3. 处理组件销毁:如果一个组件在父组件的 watcher 执行过程中被销毁,其 watcher 可以被跳过,以避免对已销毁组件的无效操作。

4.6.2 执行watcher实例的更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (__DEV__ && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' +
(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),
watcher.vm
)
break
}
}
}

在将 watcher顺序排列好后,立即执行遍历队列中的每个 watcher并执行他们。

同时在开发模式下,检查是否有无限循环更新的情况。

如果 has[id] 仍然不为 null(这意味着在当前 watcher 执行后,又有新的更新请求),则增加 circular[id] 的计数,用于跟踪同一个 watcher 被重新触发的次数。

如果 circular[id] 的次数超过了 MAX_UPDATE_COUNT(一个设定的最大更新次数),则发出警告,提示可能存在无限更新循环,并中断当前的执行循环。

4.6.3 调用keep-alive组件的activated钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const activatedChildren: Array<Component> = []

function resetSchedulerState() {
index = queue.length = activatedChildren.length = 0
has = {}
if (__DEV__) {
circular = {}
}
waiting = flushing = false
}
const activatedQueue = activatedChildren.slice()

callActivatedHooks(activatedQueue)

function callActivatedHooks(queue) {
for (let i = 0; i < queue.length; i++) {
queue[i]._inactive = true
activateChildComponent(queue[i], true /* true */)
}
}

当一个组件被插入到 DOM 中时,它的 activated 钩子会被调用。

这个函数处理的是 activated 钩子的调用逻辑,特别是在处理动态组件( 标签)或 keep-alive 缓存的组件时。

4.6.4 调用普通组件的 updated钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const updatedQueue = queue.slice()

callUpdatedHooks(updatedQueue)

function callUpdatedHooks(queue: Watcher[]) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm && vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}

4.6.5 调用resetSchedulerState重置状态

1
2
3
4
5
6
7
8
function resetSchedulerState() {
index = queue.length = activatedChildren.length = 0
has = {}
if (__DEV__) {
circular = {}
}
waiting = flushing = false
}

在执行完 watcher实例的渲染方法后,需要将状态重置。

4.6.6 清除无效的订阅

1
2
3
4
5
6
7
8
9
10
cleanupDeps()

export const cleanupDeps = () => {
for (let i = 0; i < pendingCleanupDeps.length; i++) {
const dep = pendingCleanupDeps[i]
dep.subs = dep.subs.filter(s => s)
dep._pending = false
}
pendingCleanupDeps.length = 0
}

这个函数的主要作用是移除那些已经无效的订阅(subs),这些订阅可能来自于已经被销毁的 Watcher 实例。以下是对 cleanupDeps 函数的详细解释:

五、异步函数 nextTick

我们知道Vue使用异步渲染的方式来提高效率。

而 Vue中就是使用 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
26
27
const callbacks = []
let pending = false
export function nextTick(cb, ctx) {
let _resolve
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
})
}
}

可以看出来 nextTick 就是将传入的回调函数放入 callbacks这个数组中,然后再使用异步的 API进行调用。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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)
}
}

timerFunc 用于将回调函数排队到浏览器的微任务(microtask)队列中,以确保在当前脚本执行完毕后,下一次事件循环开始前执行这些回调。

  1. 优先使用 Promise.then
  2. 如果环境不支持 Promise,则尝试使用 MutationObserver
  3. 如果上面 2 种都不支持,则尝试使用 setImmediate
  4. 如果上面 3 种都不支持,则最终使用 setTimeout

六、总结

本文介绍了Vue的异步渲染机制以及异步渲染机制带来的好处。


本站总访问量