event loop 与 vue

写意风流

结论

对于event loop 可以抽象成一段简单的代码表示

for (macroTask of macroTaskQueue) {
    // 1. Handle current MACRO-TASK
    handleMacroTask();
      
    // 2. Handle all MICRO-TASK
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}

js事件机制

javascript是一个单线程语言,同一时间只能执行一个任务。
对于javascript的事件处理机制,我们可以简单理解成“主线程+任务队列”模式。主要步骤如下

(1)所有同步任务都在主线程上执行,形成一个执行栈。

(2)主线程之外,还存在一个 "任务队列"(task queue)。只要异步任务有了运行结果,就在 "任务队列" 之中放置一个事件。
(3)一旦 "执行栈" 中的所有同步任务执行完毕,系统就会读取 "任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

任务队列

任务队列分为task queue和microtask queue。执行栈任务清空后会先从Microtasks中取任务,Microtasks中执行完之后才会执行task中的任务。

因此一个event loop主要流程如下:

  1. 开始一个Event loop
  2. 执行栈从tasks queue中取任务,并执行。
  3. 执行完后,执行栈清空
  4. 执行栈从microtasks queue中取任务执行
  5. 执行完成,执行栈清空
  6. 判断microtasks queue是否还有任务,有则重复步骤3。
  7. 进入 Update the rendering(更新渲染)阶段
  8. Event loop结束

流程图如下:

clipboard.png

再仔细理解一下开头列出的代码:

for (macroTask of macroTaskQueue) {
    // 1. Handle current MACRO-TASK
    handleMacroTask();
      
    // 2. Handle all MICRO-TASK
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}

VUE与EVENT_LOOP

为什么使用Microtask queue

对于VUE这类web2.0框架而言,最主要做的应该还是把对data的修改映射到DOM上。如果只修改一个数据,就刷新一次DOM,那么在一个同步过程中,同时修改好几个数据,必然会导致多次渲染。这肯定是不可取的。
从上面的event loop我们了解到,一个event loop对应一次render。理想状况当然就是,一次event loop产生的所有改动最好再render之前将DOM都先更新好。这样在一个周期中就可以只render一次。

从这点需求出发很容易发现,microtask queue很符合要求。

  1. microtask queue肯定在render ui之前执行完
  2. 不像task queue存在多个,microtask只存在一个queue。
  3. 最关键的是: microtask queue只有都清空了才能进入下一步,无论queue里是什么时候塞进来的。

实际代码中,不管是鼠标点击还是键盘输入或是网络时间,触发了哪些方法,这些触发都可以看成开启一个event loop。这些触发造成的任何修改都放到microtask queue中,就可以保证在这一轮的evnet loop走到render ui时可以拿到最新的DOM。

要说明的是,这里的render并不是维护虚拟DOM,也不是把虚拟DOM的变化投射到真实DOM上。而是将真实DOM更新到UI的过程。

这么说是因为:Event Loop 并不是在 ECMAScript 标准中定义的,而是在 HTML 标准中定义的:

To coordinate events, user interaction, scripts, rendering, networking, and so forth...

在 JavaScript Engine 中(以 V8 为例),只是实现了 ECMAScript 标准,而并不关心什么 Event Loop。也就是说 Event Loop 是属于 JavaScript Runtime 的,是由宿主环境提供的(比如浏览器)。
浏览器可不会关心什么虚拟DOM。只负责DOM改变后渲染UI。

为什么不用task queue

同上: 在开启一个event loop后,如果将任务放到task queue中,那么这个task任务只会在本轮Event loop结束后才会执行,并开启新一轮event loop。这无疑会导致两次render UI。
实际上,尤大为了修复一些bug,曾经将VUE.nexttick用task queue实现。但是导致了很明显的性能问题。
可以看看两个列子: 例一 , 例二
两个fiddle的实现一模一样,就是让那个绝对定位的黄色元素起到一个fixed定位的效果:绑定scroll事件,每次滚动的时候,计算当前滚动的位置并更改到那个绝对定位元素的top属性上去。大家自己试试滚动几下,对比下效果,你就会发现第一个fiddle中的黄元素是稳定不动的,fixed很好。而后一个fiddle中就有问题了,黄色元素上下晃动,似乎跟不上我们scroll的节奏,总要慢一点,虽然最后停下滚动时位置是对的。

上述两个例子其实是在这个issue中找到的,第一个jsfiddle使用的版本是Vue 2.0.0-rc.6,这个版本的nextTick实现是采用了MO,而后因为IOS9.3的WebView里的MO有bug,于是尤雨溪更改了实现,换成了window.postMessage,也就是后一个fiddle所使用的Vue 2.0.0-rc.7。后来尤雨溪了解到window.postMessage是将回调放入的macrotask 队列。这就是问题的根源了。

参考:

  1. Tasks, microtasks, queues and schedules
  2. 深入理解 JavaScript Event Loop
  3. Vue源码详解之nextTick:MutationObserver只是浮云,microtask才是核心!
阅读 733

前端成长之路
记录前端成长路上的心得体会。

诸事烦于心今日岂能浪迹天涯,

175 声望
8 粉丝
0 条评论

诸事烦于心今日岂能浪迹天涯,

175 声望
8 粉丝
宣传栏