Vue源码解读系列篇
- 1. Vue响应式原理-理解Observer、Dep、Watcher
- 2. Vue响应式原理-如何监听Array的变化
- 3. Vue响应式原理-如何监听Array的变化?详细版
- 4. Vue异步更新 && nextTick源码解析
一、Vue异步更新队列
(1)Vue异步更新
相信大家都知道,Vue
可以做到数据驱动视图更新,比如我们就简单写一个事件如下:
methods: {
tap() {
for (let i = 0; i < 10; i++) {
this.a = i;
}
this.b = 666;
},
},
当我们触发这个事件,视图中的a
和b
肯定会发现一些变化。
那我们思考一下,Vue
是如何管理这个变化的过程?比如上面这个案例,a
被循环了10次,那Vue
会去渲染视图10次吗?显然不会,毕竟这个性能代价非常大。毕竟我们只需要a
最后一次的赋值。
实际上Vue
是异步更新视图的,也就是说会等这个tap
事件执行完,检查发现只需要更新a
和b
,然后再一次性更新,避免无效的更新。
Vue
官方文档也印证了我们的想法,如下:
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
以上可以详见Vue官方文档 - 异步更新队列。
(2)派发更新中的异步队列
Vue
通知视图更新,是通过dep.notify
,相信你读到这里肯定是了解Vue
响应式原理的。那么来查看下dep.notify
都做了什么?耐心点,离真相越来越近了。
// dep.js
notify () {
const subs = this.subs.slice();
// 循环通知所有watcher更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
首先循环通知所有watcher
更新,我们发现watcher
执行了update
方法。
// watcher.js
update () {
if (this.lazy) {
// 如果是计算属性
this.dirty = true
} else if (this.sync) {
// 如果要同步更新
this.run()
} else {
// 进入更新队列
queueWatcher(this)
}
}
update
方法首先判断是不是计算属性或开发者定义了同步更新,这些我们先不看,直接进入正题,进入异步队列方法queueWatcher
。
那么再来看下queueWatcher
,我省略了绝大部分代码,毕竟代码是枯燥的,为了方便大家理解,都是一些思路性代码。
export function queueWatcher (watcher: Watcher) {
// 获取watcherid
const id = watcher.id
if (has[id] == null) {
// 保证只有一个watcher,避免重复
has[id] = true
// 推入等待执行的队列
queue.push(watcher)
// ...省略细节代码
}
// 将所有更新动作放入nextTick中,推入到异步队列
nextTick(flushSchedulerQueue)
}
function flushSchedulerQueue () {
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
watcher.run()
// ...省略细节代码
}
}
通过上述代码可以看出我们将所有要更新的watcher
队列放入了nextTick
中。
nextTick
的官方解读为:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
这里的描述其实限制了nextTick
的技能,实际上nextTick
就是一个异步方法,也许和你使用的setTimeout
没有太大的区别。
那来看下nextTick
的源码究竟做了什么?
二、nextTick源码浅析
nextTick
源码很少,翻来翻去没几行,但是我也不打算展开讲,因为看代码真的很枯燥。
下面的代码只有几行,其实你可以选择跳过看结论。
// timerFunc就是nextTick传进来的回调等... 细节不展开
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
// 当原生 Promise 不可用时,timerFunc 使用原生 MutationObserver
// MutationObserver不要在意它的功能,其实就是个可以达到微任务效果的备胎
)) {
timerFunc = () => {
// 使用 MutationObserver
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 如果原生 setImmediate 可用,timerFunc 使用原生 setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 最后的倔强,timerFunc 使用 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
总结就是Promise
> MutationObserver
> setImmediate
> setTimeout
。
果然和setTimeout
没有太大的区别~
再总结一下优先级:microtask (jobs)
优先。
nextTick
源码为什么要microtask
优先?再理解这个问题答案之前,我们还要复习eventLoop
知识。
三、eventLoop
(1)任务队列
用2张图带大家简单回忆一下,但是就不细讲了,大家可以自行查找资料。
- 我们的同步任务在主线程上运行会形成一个执行栈。
- 如果碰到异步任务,比如
setTimeout
、onClick
等等的一些操作,我们会将他的执行结果放入队列,此期间主线程不阻塞。 - 等到主线程中的所有同步任务执行完毕,就会通过
event loop
在队列里面从头开始取,在执行栈中执行event loop
永远不会断。 - 以上的这一整个流程就是
Event Loop
(事件循环机制)。
(2)微任务、宏任务
- 每次执行栈的同步任务执行完毕,就会去任务队列中取出完成的异步任务,但是队列中又分为微任务
microtask
和宏任务tasks
队列 - 等到把所有的微任务
microtask
都执行完毕,注意是所有的,他才会从宏任务tasks
队列中取事件。 - 等到把队列中的事件取出一个,放入执行栈执行完成,就算一次循环结束。
- 之后
event loop
还会继续循环,他会再去微任务microtask
执行所有的任务,然后再从宏任务tasks
队列里面取一个,如此反复循环。
四、nextTick为什么要尽可能的microtask优先?
简单的回忆了eventLoop
、微任务、宏任务后,我们还要再抛出一个结论。
我们发现,原来在执行微任务之后还会执行渲染操作!!!(当然并不是每次都会,但至少顺序我们是可以肯定的)。
- 在一轮
event loop
中多次修改同一dom
,只有最后一次会进行绘制。 - 渲染更新(
Update the rendering
)会在event loop
中的tasks
和microtasks
完成后进行,但并不是每轮event loop
都会更新渲染,这取决于是否修改了dom
和浏览器觉得是否有必要在此时立即将新状态呈现给用户。如果在一帧的时间内(时间并不确定,因为浏览器每秒的帧数总在波动,16.7ms只是估算并不准确)修改了多处dom
,浏览器可能将变动积攒起来,只进行一次绘制,这是合理的。 - 如果希望在每轮
event loop
都即时呈现变动,可以使用requestAnimationFrame
。
这里我抛出结论,原因和理论知识可以看这篇文章 从event loop规范探究javaScript异步及浏览器更新渲染时机 ,这位大神写的很好。
不知道大家有没有猜出【nextTick
为什么要尽可能的microtask
优先?】
这里又盗了大神的图,event loop
的大致循环过程:
假设现在执行到某个 task,我们对批量的dom
进行异步修改,我们将此任务插进tasks
,也就是用宏任务实现。
显而易见,这种情况下如果task
里排队的队列比较多,同时遇到多次的微任务队列执行完。那很有可能触发多次浏览器渲染,但是依旧没有执行我们真正的修改dom
任务。
这种情况,不仅会延迟视图更新,带来性能问题。还有可能导致视图上一些诡异的问题。
因此,此任务插进microtasks
:
task
队列如果有大量的任务等待执行时,将dom
的变动作为microtasks
而不是宏任务(task
)能更快的将变化呈现给用户。
参考文章
文章有一些结论直接参考其他文章,自己实在是懒得写啦~~
侵权删 ^^