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

Vue源码解读系列篇javascript

1、Vue异步更新队列

(1)Vue异步更新

相信你们都知道,Vue能够作到数据驱动视图更新,好比咱们就简单写一个事件以下:html

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

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

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

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

Vue官方文档也印证了咱们的想法,以下:react

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的全部数据变动。若是同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做是很是重要的。git

以上能够详见Vue官方文档 - 异步更新队列github

(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的源码究竟作了什么?

2、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知识。

3、eventLoop

(1)任务队列

用2张图带你们简单回忆一下,可是就不细讲了,你们能够自行查找资料。

eventLoop

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

(2)微任务、宏任务

eventLoop、微任务、宏任务

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

4、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 升级踩坑小记

文章内容基本都是在其余大佬的基础上进行的理解,讲错的你们能够批评指正~

参考文章

文章有一些结论直接参考其余文章,本身实在是懒得写啦~~

侵权删 ^^

相关文章
相关标签/搜索