Vue学习系列之二、nextTick

1,171 阅读7分钟

一、事件循环

1、提问

首先,都2021了,大家对taskmircotasktask queue这些概念都很清楚了。但大家能否回答以下的问题:

  1. 任务队列是一个队列吗,task是否会分优先级?什么样的task优先级更高?
  2. requestAnimateFrame在什么阶段执行?requestIdleCallback呢?
  3. 事件循环浏览器渲染之间有什么关系?

ok,如果这些问题你都能回答上来,大佬请私信我您的微信,咱们♂深入交流一下♀😂;如果有不清楚,也别慌,这篇文章就是为了把这些东西都搞明白。

以下的结论全部都根据HTML5 规范,英文好的小伙伴可以直接去看

2、定义

2.1 事件循环

为了协调事件、用户交互、脚本、渲染、网络等,UA必须使用事件循环。每个UA都有一个关联的事件循环,这是该UA独有的。

这里的UA指的是user Agnet,其包括了代码运行所需的环境——ECMAScript执行上下文、执行对战、执行线程等,但是执行线程并不属于UA,所以有可能出现一个线程协同调度着多个事件循环。

这样说其实就很复杂了,我们可以简单认为一个tab页面对应一个事件循环即可。

2.2 任务队列、任务

An event loop has one or more task queues. A task queue is a Set of tasks.

Task queues are sets,not queues, because step one of the event loop processing model grabs the first runnable task from the chosen queue, instead of dequeuing the first task.

一个事件循环有一个或多个任务队列,任务队列是多个任务的Set

这里解决了我们的第一个问题:任务队列并不是一个队列,因为在事件循环执行流程的第一步,是从选中的任务队列中拿出第一个可以执行的任务,而不是出队第一个任务。

2.3 不止一个任务队列

For example, a user agent could have one task queue for mouse and key events to which the user interaction task source is associated, and another to which all other task sources are associated. Then, using the freedom granted in the initial step of the event loop processing model, it could give keyboard and mouse events preference over other tasks three-quarters of the time, keeping the interface responsive but not starving other task queues. Note that in this setup, the processing model still enforces that the user agent would never process events from any one task source out of order.

刚刚上面也提到了,一个任务循环会有多个任务队列,每个任务队列中保存着不同类型的任务,比如:

  1. 鼠标、键盘这类的用户交互任务
  2. 其他的任务,比如postMessage、setTimeout、setIntervel、http

浏览器会在按照任务顺序的前提下,将四分之三的优先级分配给用户交互事件,来保证响应的及时性,同时也不会把其余的任务饿死

也就是因为这个原因,在Vue中产生了这个issue,这个问题的具体原因其实就是因为当时Vue的nextTick的实现使用的是postMessage,它作为一个task优先级很低,当遇到浏览器的滚动时,事件循环优先选择了的用户交互的任务队列,导致postMessage中的渲染函数迟迟没有执行,视图没有更新,在之后的版本中,尤大把nextTick的实现改为了Mutation ObserverPromise.resolve的方案。

2.4 事件循环流程

  1. 从选中的任务队列中取出一个宏任务执行

  2. 检查微任务队列,执行微任务,清空微任务队列,在执行过程中产生的微任务均在本次事件循环中执行完毕

  3. 检查是否需要更新,这里有个概念叫做Rendering opportunities,用来判断本次task执行完毕后是否需要进行更新,也就是说并不是完成一次task就要进行视图的更新,很有可能是在多次事件循环后,才进行一次视图更新

    • 通常来说视图更新的间隔是固定的,对应着屏幕刷新的频率60fps,也就是说每次视图更新的间隔是在16.7ms,当页面性能无法维持在这个频率的时候,浏览器会将频率下降到30fps,以避免丢帧。

    • 当浏览器上下文不可见时,频率甚至可以降到4fps。这里的浏览器上下文指的就是页面,不可见的情况比如iframe、当前tab不在此页面等情况

    • 当浏览器认定当前修改渲染,不会有任何的改变,并且map of animation frame callbacks 为空,也就是没有调用requestAnimationFrame时,也会跳过渲染

    • 最后还有一种情况,就是当浏览器认为由于其他原因最好跳过更新渲染(主要是为了合并定时器回调)。

      This step enables the user agent to prevent the steps below from running for other reasons, for example, to ensure certain tasks are executed immediately after each other, with only microtask checkpoints interleaved (and without, e.g., animation frame callbacks interleaved). Concretely, a user agent might wish to coalesce timer callbacks together, with no intermediate rendering updates.

    • 当出现上述的任意一种情况是,跳过3.x的全部步骤。

      3.1 如果页面出现了窗口大小改变,直接执行resize方法

      3.2 如果页面出现了滚动,直接执行scroll方法

      3.3 执行requestAnimationFrame的回调

      3.4 执行Intersection Observer的回调

      3.5 重新绘制用户界面

  4. 启动空闲期算法,也就是执行requestIdleCallback的回调。

对于resizescroll来说,并不会等到重新绘制用户界面这一步,这样会有很多的延迟,在CSSOM VIEW中有提到,如果是resize,浏览器会立即触发resize事件;scroll相对复杂一些,浏览器会初始化一个pending scroll event targets,当事件循环触发了scroll方法的时候,会将targets收集到的元素队列依次触发scroll事件,当然是冒泡的,如果是元素的scroll事件的话还会将document的事件取消掉。

task1.jpeg

以上就是基本的事件循环的流程,大家可以对看着图,对照的流程慢慢梳理。希望大家看完之后可以完美回答,本文一开始的问题。

其中其实还有很多的细节,但是太过繁琐,我并没有全部列出来,如果还想深入了解的话,可以去看上文提到的HTML5的规范

二、Vue中的nextTick

1、 Vue的更新DOM是异步的

Vue文档中有这样一段描述,当Vue修改数据后,并不会立即触发watcher的更新,而是将其推入一个异步的队列中,这个队列将watcher进行排序,并且会异步执行这些watcher,当然这其中就包含更新DOM。

那我们思考一下,这个所谓的异步应该处于哪个阶段呢?答案是,微任务。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。这也就是nextTick的实现。

2、举个🌰

<div id="example">{{message}}</div>
<script>
    var vm = new Vue({ el: '#example', data: { message: '123' } })

    vm.message = 'new message' // 更改数据 

    vm.$el.textContent === 'new message' // false 

    Vue.nextTick(
        function () { 
            vm.$el.textContent === 'new message' // true 
        }
     )
</script>
  1. 事件循环正在执行宏任务

  2. vm.message = 'new message'触发setter,将render Watcher放入异步队列,Vue内部调用了nextTick将异步队列放入微任务队列 nt-2.png

  3. 由于DOM没有更新所以,第一次判断textContent的值是123,所以是false nT-1.png

  4. 用户调用了nextTick将回调推入微任务队列,这里注意并不是将函数推入了异步队列,异步队列中只有watcher,且将队列中的watcher全部执行完,是在一个微任务中完成的。 nt-3.png

  5. Task执行完毕,开始执行清空微任务队列,第一个微任务就是将异步队列中的watcher更新一遍 nt-4.png

  6. watcher全部更新,DOM更新,这里注意,是DOM更新而不是视图更新,因为是否更新视图是浏览器来决定的。如果这里有点蒙,请再看一遍事件循环的流程。 nt-5.png

  7. 执行下一个微任务,由于此时DOM已经更新,所以判断结果为true。 nt-6.png

三、延伸阅读