阅读 2926

Vue异步更新 - nextTick为什么要microtask优先?

Vue源码解读系列篇

一、Vue异步更新队列

(1)Vue异步更新

相信大家都知道,Vue可以做到数据驱动视图更新,比如我们就简单写一个事件如下:

methods: {
    tap() {
        for (let i = 0; i < 10; i++) {
            this.a = i;
        }
        this.b = 666;
    },
},
复制代码

当我们触发这个事件,视图中的ab肯定会发现一些变化。

那我们思考一下,Vue是如何管理这个变化的过程?比如上面这个案例,a被循环了10次,那Vue会去渲染视图10次吗?显然不会,毕竟这个性能代价非常大。毕竟我们只需要a最后一次的赋值。

实际上Vue是异步更新视图的,也就是说会等这个tap事件执行完,检查发现只需要更新ab,然后再一次性更新,避免无效的更新。

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张图带大家简单回忆一下,但是就不细讲了,大家可以自行查找资料。

eventLoop

  • 我们的同步任务在主线程上运行会形成一个执行栈。
  • 如果碰到异步任务,比如setTimeoutonClick等等的一些操作,我们会将他的执行结果放入队列,此期间主线程不阻塞。
  • 等到主线程中的所有同步任务执行完毕,就会通过event loop在队列里面从头开始取,在执行栈中执行 event loop永远不会断。
  • 以上的这一整个流程就是Event Loop(事件循环机制)。

(2)微任务、宏任务

eventLoop、微任务、宏任务

  • 每次执行栈的同步任务执行完毕,就会去任务队列中取出完成的异步任务,但是队列中又分为微任务microtask和宏任务tasks队列
  • 等到把所有的微任务microtask都执行完毕,注意是所有的,他才会从宏任务tasks队列中取事件。
  • 等到把队列中的事件取出一个,放入执行栈执行完成,就算一次循环结束。
  • 之后event loop还会继续循环,他会再去微任务microtask执行所有的任务,然后再从宏任务tasks队列里面取一个,如此反复循环。

四、nextTick为什么要尽可能的microtask优先?

简单的回忆了eventLoop、微任务、宏任务后,我们还要再抛出一个结论。

执行顺序

我们发现,原来在执行微任务之后还会执行渲染操作!!!(当然并不是每次都会,但至少顺序我们是可以肯定的)。

  • 在一轮event loop中多次修改同一dom,只有最后一次会进行绘制。
  • 渲染更新(Update the rendering)会在event loop中的tasksmicrotasks完成后进行,但并不是每轮event loop都会更新渲染,这取决于是否修改了dom和浏览器觉得是否有必要在此时立即将新状态呈现给用户。如果在一帧的时间内(时间并不确定,因为浏览器每秒的帧数总在波动,16.7ms只是估算并不准确)修改了多处dom,浏览器可能将变动积攒起来,只进行一次绘制,这是合理的。
  • 如果希望在每轮event loop都即时呈现变动,可以使用requestAnimationFrame

这里我抛出结论,原因和理论知识可以看这篇文章 从event loop规范探究javaScript异步及浏览器更新渲染时机 ,这位大神写的很好。

不知道大家有没有猜出【nextTick为什么要尽可能的microtask优先?】 这里又盗了大神的图,event loop的大致循环过程:

event loops

假设现在执行到某个 task,我们对批量的dom进行异步修改,我们将此任务插进tasks,也就是用宏任务实现。

task

显而易见,这种情况下如果task里排队的队列比较多,同时遇到多次的微任务队列执行完。那很有可能触发多次浏览器渲染,但是依旧没有执行我们真正的修改dom任务。

这种情况,不仅会延迟视图更新,带来性能问题。还有可能导致视图上一些诡异的问题。 因此,此任务插进microtasks

可以看到如果task队列如果有大量的任务等待执行时,将dom的变动作为microtasks而不是宏任务(task)能更快的将变化呈现给用户。

总结

之所以讲这篇文章,是因为在最近在读Vue的源码,我看的是2.6.10, 发现nextTick和2.5版本的实现不太一样。大家可以看下这位大佬的文章 Vue.js 升级踩坑小记

文章内容基本都是在其他大佬的基础上进行的理解,讲错的大家可以批评指正~

参考文章

文章有一些结论直接参考其他文章,自己实在是懒得写啦~~

侵权删 ^^

关注下面的标签,发现更多相似文章
评论