Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【Vue】异步更新 - nextTick为什么要microtask优先? #15

Open
qingzhou729 opened this issue Aug 18, 2019 · 1 comment
Open
Labels

Comments

@qingzhou729
Copy link
Owner

qingzhou729 commented Aug 18, 2019

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

  • 我们的同步任务在主线程上运行会形成一个执行栈。
  • 如果碰到异步任务,比如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的大致循环过程:

image

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

image

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

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


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

总结

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

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

参考文章

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

侵权删 ^^

@qingzhou729 qingzhou729 changed the title Vue异步更新 - nextTick为什么要microtask优先? 【Vue】异步更新 - nextTick为什么要microtask优先? Aug 18, 2019
@maogongzi
Copy link

您好,请问我可以转载或者翻译您的文章吗(会注明出处)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants