乐趣区

关于eventloop:从event-loop规范探究javaScript异步及浏览器更新渲染时机

异步的思考

event loops暗藏得比拟深,很多人对它很生疏。但提起异步,置信每个人都晓得。异步背地的“靠山”就是 event loops。这里的异步精确的说应该叫浏览器的event loops 或者说是 javaScript 运行环境的 event loops,因为 ECMAScript 中没有event loopsevent loops 是在 HTML Standard 定义的。

event loops标准中定义了浏览器何时进行渲染更新,理解它有助于性能优化。

思考下边的代码运行程序:

console.log('script start');
​
setTimeout(function () {console.log('setTimeout');
}, 0);
​
Promise.resolve()
 .then(function () {console.log('promise1');
 })
 .then(function () {console.log('promise2');
 });
​
console.log('script end');

下面的程序是在 chrome 运行得出的,乏味的是在 safari 9.1.2 中测试,promise1 promise2会在 setTimeout 的后边,而在 safari 10.0.1 中失去了和 chrome 一样的后果。为何浏览器有不同的体现,理解 tasks, microtasks 队列就能够解答这个问题。

很多框架和库都会应用相似上面函数:

function flush() {...}
function useMutationObserver() {
 var iterations = 0;
 var observer = new MutationObserver(flush);
 var node = document.createTextNode('');
 observer.observe(node, { characterData: true});
​
 return function () {node.data = iterations = ++iterations % 2;};
}

首次看这个 useMutationObserver 函数总会很有纳闷,MutationObserver不是用来察看 dom 的变动的吗,这样凭空造出一个节点来重复批改它的内容,来触发察看的回调函数有何意义?

答案就是应用 Mutation 事件 能够异步执行操作(例子中的 flush 函数),一是能够尽快响应变动,二是能够去除反复的计算。然而setTimeout(flush, 0) 同样也能够执行异步操作,要晓得其中的差别和抉择哪种异步办法,就得理解event loop

定义

先看看它们在标准中的定义。

Note:本文的援用局部,就是对标准的翻译,有的局部会概括或者省略的翻译,有误请斧正。

事件循环

event loop翻译进去就是 事件循环 ,能够了解为实现异步的一种形式,咱们来看看 event loop 在HTML Standard 中的定义章节:

第一句话:

为了协调事件,用户交互,脚本,渲染,网络等,用户代理必须应用本节所述的event loop

事件,用户交互,脚本,渲染,网络 这些都是咱们所相熟的货色,他们都是由 event loop 协调的。触发一个 click 事件,进行一次 ajax 申请,背地都有 event loop 在运作。

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.

工作队列是汇合,而不是队列,因为事件循环解决模型的第一步从选定的队列中获取第一个可运行工作,而不是使第一个工作出队。

task

一个 event loop 有一个或者多个 task 队列。

当用户代理安顿一个工作,必须将该工作减少到相应的 event loop 的一个 tsak 队列中。

每一个 task 都来源于指定的工作源,比方能够为鼠标、键盘事件提供一个 task 队列,其余事件又是一个独自的队列。能够为鼠标、键盘事件调配更多的工夫,保障交互的晦涩。

task也被称为 macrotasktask 队列还是比拟好了解的,就是一个先进先出的队列,由指定的工作源去提供工作。

哪些是 task 工作源呢?

标准在 Generic task sources 中有提及:

DOM 操作工作源: 此工作源被用来相应 dom 操作,例如一个元素以非阻塞的形式插入文档。

用户交互工作源: 此工作源用于对用户交互作出反应,例如键盘或鼠标输出。响应用户操作的事件(例如 click)必须应用 task 队列。

网络工作源: 网络工作源被用来响应网络流动。

history traversal 工作源: 当调用 history.back()等相似的 api 时,将工作插进 task 队列。

task工作源十分宽泛,比方 ajaxonloadclick事件,基本上咱们常常绑定的各种事件都是 task 工作源,还有数据库操作 (IndexedDB),须要留神的是setTimeoutsetIntervalsetImmediate 也是 task 工作源。总结来说 task 工作源:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

HTML parser 是一个典型的 task

<script> 标签的解析和执行是在 parse HTML 这个阶段,是这个阶段的一部分,也就是最后构建 DOM trees 的时候。在标准的 8.1.4 Event loops 中的 8.1.4.1 Definitions 里有这样的话:

An event loop has one or more task queues. A task queue is an ordered list of tasks, which are algorithms that are responsible for such work as:

Parsing

The HTML parser tokenizing one or more bytes, and then processing any resulting tokens, is typically a task.

Callbacks

Calling a callback is often done by a dedicated task.

提到了 HTML parser是一个典型的task,当咱们解析和执行完 <script> 标签里的内容,后续执行的回调函数才会分到指定的 task 中执行。

能够做个测试,在文档两头插入一个执行的脚本,应用 Developer tools 里的 performance(原timeline) 看到后果是parser HTML -> 执行脚本 -> parser HTML….. , 执行脚本是会中断文档解析的,因为脚本可能会批改 dom trees,所以最后的文档解析和脚本执行应该是一个间断的过程,所以 <script> 标签的代码也是一个task

一个 loop 不是只蕴含一个 task 队列和一个微工作队列吗?为什么说‘’一个 event loop 有一个或者多个 task 队列‘’?

不要混同 nodejs 和浏览器中的 event loop 这里有一篇文章能够看看。

浏览器的确是这样的,只有一个宏队列。然而在 NodeJS 中,不同的 macrotask 对应着不同的宏队列:

  1. Timers Queue:setTimeout() 和 setInterval() 的回调
  2. IO Callbacks Queue:用户输出的回调,如键盘、鼠标事件
  3. Check Queue:setImmediate 的回调
  4. Close Callbacks Queue:一些筹备敞开的回调函数,如:socket.on(‘close’, …)

取出下一个宏工作的时候,会 从上一个宏工作所在队列开始往后查看是否有下一个工作 。也就是说,如果以后宏队列还有工作,那么取出一个执行;如果以后宏队列没有工作,会执行下一个宏队列的工作(而不是回到第一个Timers Queue 中)。

同样,浏览器中只有一个微队列,而 node 中有两个:

  1. Next Tick Queue:是搁置 process.nextTick(callback) 的回调工作的
  2. Other Micro Queue:搁置其余 microtask,比方Promise

两个微队列的工作也是按队列程序顺次执行。

要留神 NodeJS 与浏览器的执行程序的不同,NodeJS的执行阶段是这样的:

  1. 先执行全局 Script 代码
  2. 执行完同步代码调用栈清空后,先从微工作队列 Next Tick Queue 中顺次取出所有的工作放入调用栈中执行,再从微工作队列 Other Microtask Queue 中顺次取出所有的工作放入调用栈中执行
  3. 而后开始宏工作的 6 个阶段,每个阶段都将该宏工作队列中的所有工作都取出来执行(留神,这里和浏览器不一样,浏览器只取一个)
  4. 6 个阶段执行结束后,再开始执行微工作,以此形成事件循环

microtask

每一个 event loop 都有一个 microtask 队列,一个 microtask 会被排进 microtask 队列而不是 task 队列。

有两种 microtasks:别离是solitary callback microtaskscompound microtasks。标准值只笼罩solitary callback microtasks

如果在初期执行时,spin the event loop,microtasks 有可能被挪动到惯例的 task 队列,在这种状况下,microtasks 工作源会被 task 工作源所用。通常状况,task 工作源和 microtasks 是不相干的。

microtask队列和 task 队列有些类似,都是先进先出的队列,由指定的工作源去提供工作,不同的是一个 event loop里只有一个 microtask 队列。

HTML Standard没有具体指明哪些是 microtask 工作源,通常认为是 microtask 工作源有:

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

NOTE: Promise的定义在 ECMAScript标准而不是在 HTML 标准中,然而 ECMAScript 标准中有一个 jobs 的概念和 microtasks 很类似。在 Promises/A+ 标准的 Notes 3.1 中提及了 promise 的 then 办法能够采纳“宏工作(macro-task)”机制或者“微工作(micro-task)”机制来实现 。所以结尾提及的promise 在不同浏览器的差别正源于此,有的浏览器将 then 放入了 macro-task 队列,有的放入了 micro-task 队列。在jake 的博文 Tasks, microtasks, queues and schedules 中提及了一个探讨 vague mailing list discussions,一个广泛的共识是 promises 属于 microtasks 队列。

进一步理解 event loops

晓得了 event loops 大抵做什么的,咱们再深刻理解下event loops

有两种event loops,一种在浏览器上下文,一种在 workers 中。

每一个用户代理必须至多有一个浏览器上下文event loop,然而每个单元的类似源浏览器上下文至少有一个event loop

event loop总是具备至多一个浏览器上下文,当一个 event loop 的浏览器上下文全都销毁的时候,event loop也会销毁。一个浏览器上下文总有一个 event loop 去协调它的流动。

Workerevent loop 绝对简略一些,一个 worker 对应一个 event loop,worker 过程模型治理event loop 的生命周期。

重复提到的一个词是 browsing contexts(浏览器上下文)。

浏览器上下文 是一个将 Document对象出现给用户的环境。在一个Web 浏览器内,一个标签页或窗口常蕴含一个浏览上下文,如一个 iframe 或一个 frameset 内的若干frame

联合一些材料,对上边标准给出一些了解(有误请斧正):

  • 每个线程都有本人的event loop
  • 浏览器能够有多个 event loopbrowsing contextsweb workers就是互相独立的。
  • 所有同源的 browsing contexts 能够共用event loop,这样它们之间就能够互相通信。

event loop 的处理过程(Processing model)

在标准的 Processing model 定义了 event loop 的循环过程:

1. 一个 event loop 只有存在,就会一直执行下边的步骤:1. 在 tasks 队列中抉择最老的一个 task, 用户代理能够抉择任何task 队列,如果没有可选的工作,则跳到下边的 microtasks 步骤。

2. 将上边抉择的 task 设置为正在运行的 task。

3.Run: 运行被抉择的task

4. 将 event loop 的 currently running task 变为null

5. 从 task 队列里移除前边运行的 task。

6.Microtasks: 执行 microtasks 工作检查点。(也就是执行 microtasks 队列里的工作)

7. 更新渲染(Update the rendering)…

8. 如果这是一个 worker event loop,然而没有工作在task 队列中,并且 WorkerGlobalScope 对象的 closing 标识为true,则销毁event loop,停止这些步骤,而后进行定义在 Web workers 章节的 run a worker。

9. 返回到第一步。

event loop会一直循环下面的步骤,概括说来:

  • event loop会一直循环的去取 tasks 队列的中最老的一个工作推入栈中执行,并在当次循环里顺次执行并清空 microtask 队列里的工作。
  • 执行完 microtask 队列里的工作,有 可能 会渲染更新。(浏览器很聪慧,在一帧以内的屡次 dom 变动浏览器不会立刻响应,而是会积攒变动以最高 60HZ 的频率更新视图)

microtasks 检查点(microtask checkpoint)

event loop运行的第 6 步,执行了一个microtask checkpoint,看看标准如何形容microtask checkpoint

1. 当用户代理去执行一个 microtask checkpoint,如果microtask checkpointflag(标识)为 false,用户代理必须运行上面的步骤:1. 将microtask checkpointflag设为true

2.Microtask queue handling: 如果 event loopmicrotask队列为空,间接跳到第八步(Done)。

3. 在 microtask 队列中抉择最老的一个工作。

4. 将上一步抉择的工作设为 event loop 的 currently running task。

5. 运行抉择的工作。

6. 将 event loop 的 currently running task 变为null

7. 将后面运行的 microtaskmicrotask队列中删除,而后返回到第二步(Microtask queue handling)。

8.Done: 每一个 environment settings object 它们的 responsible event loop 就是以后的 event loop,会给environment settings object 发一个 rejected promises 的告诉。

9. 清理 IndexedDB 的事务。

10. 将 microtask checkpointflag设为flase

microtask checkpoint所做的就是执行 microtask 队列里的工作。什么时候会调用 microtask checkpoint 呢?

  • 当上下文执行栈为空时,执行一个 microtask checkpoint。
  • event loop 的第六步(Microtasks: Perform a microtask checkpoint)执行 checkpoint,也就是在运行task 之后,更新渲染之前。

执行栈(JavaScript execution context stack)

taskmicrotask 都是推入栈中执行的,要残缺理解 event loops 还须要意识JavaScript execution context stack,它的标准位于 tc39.github.io/ecma262/#ex…

javaScript是单线程,也就是说只有一个主线程,主线程有一个栈,每一个函数执行的时候,都会生成新的execution context(执行上下文),执行上下文会蕴含一些以后函数的参数、局部变量之类的信息,它会被推入栈中,running execution context(正在执行的上下文)始终处于栈的顶部。当函数执行完后,它的执行上下文会从栈弹出。

举个简略的例子:

function bar() {console.log('bar');
}
​
function foo() {console.log('foo');
bar();}
​
foo();

执行过程中栈的变动:

残缺异步过程

标准艰涩难懂,做一个形象的比喻:主线程相似一个加工厂,它只有一条流水线,待执行的工作就是流水线上的原料,只有前一个加工完,后一个能力进行。event loops就是把原料放上流水线的工人。只有曾经放在流水线上的,它们会被顺次解决,称为 同步工作 。一些待处理的原料,工人会依照它们的品种排序,在适当的机会放上流水线,这些称为 异步工作

过程图:

举个简略的例子,假如一个 script 标签的代码如下:

Promise.resolve().then(function promise1 () {console.log('promise1');
 })
setTimeout(function setTimeout1 (){console.log('setTimeout1')
 Promise.resolve().then(function  promise2 () {console.log('promise2');
 })
}, 0)
​
setTimeout(function setTimeout2 (){console.log('setTimeout2')
}, 0)

运行过程:

script里的代码被列为一个 task,放入task 队列。

循环 1:

  • 【task 队列:script;microtask 队列:】
  1. 从 task 队列中取出 script 工作,推入栈中执行。
  2. promise1 列为 microtask,setTimeout1 列为 task,setTimeout2 列为 task。
  • 【task 队列:setTimeout1 setTimeout2;microtask 队列:promise1】
  1. script 工作执行结束,执行 microtask checkpoint,取出 microtask 队列的 promise1 执行。

循环 2:

  • 【task 队列:setTimeout1 setTimeout2;microtask 队列:】
  1. 从 task 队列中取出 setTimeout1,推入栈中执行,将 promise2 列为 microtask。
  • 【task 队列:setTimeout2;microtask 队列:promise2】
  1. 执行 microtask checkpoint,取出 microtask 队列的 promise2 执行。

循环 3:

  • 【task 队列:setTimeout2;microtask 队列:】
  1. 从 task 队列中取出 setTimeout2,推入栈中执行。
  2. setTimeout2 工作执行结束,执行 microtask checkpoint。
  • 【task 队列:;microtask 队列:】

event loop 中的 Update the rendering(更新渲染)

这是 event loop 中很重要局部,在第 7 步会进行Update the rendering(更新渲染),标准容许浏览器本人抉择是否更新视图。也就是说可能不是每轮事件循环都去更新视图,只在有必要的时候才更新视图。

咱们都晓得 javaScript 是单线程,渲染计算应该是浏览器 GUI 渲染线程负责,是由浏览器用 c ++ 编写的模块负责的。GUI渲染线程与 JS 引擎是互斥的,当 JS 引擎执行时 GUI 线程会被挂起,GUI更新会被保留在一个队列中等到 JS 引擎闲暇时立刻被执行。脚本运行工夫过长会阻塞渲染。

www.html5rocks.com/zh/tutorial… 这篇文章较具体的解说了渲染机制。

渲染的根本流程:

  1. 解决 HTML标记并构建 DOM树。
  2. 解决 CSS 标记并构建 CSSOM 树,将 DOMCSSOM 合并成一个渲染树。
  3. 依据渲染树来布局,以计算每个节点的几何信息。
  4. 将各个节点绘制到屏幕上。

Note: 能够看到渲染树的一个重要组成部分是 CSSOM 树,绘制会期待 css 款式全副加载实现才进行,所以 css 款式加载的快慢是首屏出现快慢的关键点。

上面讨论一下渲染的机会。标准定义在一次循环中,Update the rendering会在第六步 Microtasks: Perform a microtask checkpoint 后运行。

验证更新渲染(Update the rendering)的机会

不同机子测试可能会失去不同的后果,这取决于浏览器,cpu、gpu 性能以及它们过后的状态。

例子 1

咱们做一个简略的测试

<div id='con'>this is con</div>
<script>
var t = 0;
var con = document.getElementById('con');
con.onclick = function () {setTimeout(function setTimeout1 () {con.textContent = t;}, 0)
};
</script>

chromeDeveloper toolsTimeline 查看各局部运行的工夫点。当咱们点击这个 div 的时候,下图截取了局部工夫线,黄色局部是脚本运行,紫色局部是更新 render 树、计算布局,绿色局部是绘制。

绿色和紫色局部能够认为是Update the rendering

在这一轮事件循环中,setTimeout1是作为 task 运行的,能够看到 paint 的确是在 task 运行完后才进行的。

例子 2

当初换成一个 microtask 工作,看看有什么变动

<div id='con'>this is con</div>
<script>
var con = document.getElementById('con');
con.onclick = function () {Promise.resolve().then(function Promise1 () {con.textContext = 0;})
};
</script>

和上一个例子很像,不同的是这一轮事件循环的 taskclick的回调函数,Promise1则是 microtaskpaint 同样是在他们之后实现。

规范就是那么定义的,答案仿佛不言而喻,咱们把例子变得略微简单一些。

例子 3

<div id='con'>this is con</div>
<script>
var con = document.getElementById('con');
con.onclick = function click1() {setTimeout(function setTimeout1() {con.textContent = 0;}, 0)
 setTimeout(function setTimeout2() {con.textContent = 1;}, 0)
};
</script>

当点击后,一共产生 3 个 task,别离是click1、setTimeout1、setTimeout2,所以会别离在 3 次event loop 中进行。上面截取的是 setTimeout1、setTimeout2 的局部。

咱们批改了两次 textContent,奇怪的是setTimeout1、setTimeout2 之间没有 paint,浏览器只绘制了textContent=1,难道setTimeout1、setTimeout2 在同一次 event loop 中吗?

例子 4

在两个 setTimeout 中减少microtask

<div id='con'>this is con</div>
<script>
var con = document.getElementById('con');
con.onclick = function () {setTimeout(function setTimeout1() {
 con.textContent = 0;
 Promise.resolve().then(function Promise1 () {console.log('Promise1')
 })
 }, 0)
 setTimeout(function setTimeout2() {
 con.textContent = 1;
 Promise.resolve().then(function Promise2 () {console.log('Promise2')
 })
 }, 0)
};
</script>

run microtasks 中能够看进去,setTimeout1、setTimeout2应该运行在两次 event loop 中,textContent = 0的批改被跳过了。

setTimeout1、setTimeout2的运行距离很短,在 setTimeout1 实现之后,setTimeout2马上就开始执行了,咱们晓得浏览器会尽量放弃每秒 60 帧的刷新频率(大概 16.7ms 每帧),是不是只有两次 event loop 距离大于 16.7ms 才会进行绘制呢?

例子 5

将工夫距离加大一些。

<div id='con'>this is con</div>
<script>
var con = document.getElementById('con');
con.onclick = function () {setTimeout(function  setTimeout1() {con.textContent = 0;}, 0);
    setTimeout(function  setTimeout2() {con.textContent = 1;}, 16.7);
};
</script>

两块黄色的区域就是 setTimeout,在 1224ms 处绿色局部,浏览器对 con.textContent = 0 的变动进行了绘制。在 1234ms 处绿色局部,绘制了con.textContent = 1

可否认为相邻的两次 event loop 的距离很短,浏览器就不会去更新渲染了呢?持续咱们的试验

例子 6

咱们在同一时间执行多个 setTimeout 来模仿执行距离很短的 task。

<div id='con'>this is con</div>
<script>
var con = document.getElementById('con');
con.onclick = function () {setTimeout(function(){con.textContent = 0;},0)
   setTimeout(function(){con.textContent = 1;},0)
   setTimeout(function(){con.textContent = 2;},0)
   setTimeout(function(){con.textContent = 3;},0)
   setTimeout(function(){con.textContent = 4;},0)
    setTimeout(function(){con.textContent = 5;},0)
   setTimeout(function(){con.textContent = 6;},0)
};
</script>

图中一共绘制了两帧,第一帧 4.4ms,第二帧 9.3ms,都远远高于每秒 60HZ(16.7ms)的频率,第一帧绘制的是 con.textContent = 4,第二帧绘制的是 con.textContent = 6。所以两次event loop 的距离很短同样会进行绘制。

例子 7

有说法是一轮 event loop 执行的 microtask 有数量限度(可能是 1000),多余的 microtask 会放到下一轮执行。上面例子将 microtask 的数量减少到 25000。

<div id='con'>this is con</div>
<script>
var con = document.getElementById('con');
con.onclick = function () {setTimeout(function  setTimeout1() {
        con.textContent = 'task1';
        for(var i = 0; i  < 250000; i++){Promise.resolve().then(function(){con.textContent = i;});
        }
   }, 0);
    setTimeout(function  setTimeout2() {con.textContent = 'task2';}, 0);
};
</script>

总体的timeline

能够看到一大块黄色区域,上半局部有一根绿线就是点击后的第一次绘制,脚本的运行消耗大量的工夫,并且阻塞了渲染。

看看 setTimeout2 的运行状况。[](https://github.com/aooy/aooy….setTimeout2这轮 event loop 没有 run microtasks,microtaskssetTimeout1被全副执行完了。

25000 个 microtasks 不能阐明 event loopmicrotasks数量没有限度,有可能这个限度数很高,远超 25000,但日常应用根本不会应用那么多了。

对 microtasks 减少数量限度,一个很大的作用是避免脚本运行工夫过长,阻塞渲染。

例子 8

应用requestAnimationFrame

<div id='con'>this is con</div>
<script>
var con = document.getElementById('con');
var i = 0;
var raf =  function(){requestAnimationFrame(function() {
         con.textContent = i;
         Promise.resolve().then(function(){
            i++;
            if(i < 3) raf();});
    });
}
con.onclick = function () {raf();
};
</script>

总体的Timeline: 点击后绘制了 3 帧,把每次变动都绘制了。

看看单个 requestAnimationFrameTimeline

setTimeout 很类似,能够看出 requestAnimationFrame 也是一个task,在它实现之后会运行run microtasks

例子 9

验证 postMessage 是否是task

setTimeout(function setTimeout1(){console.log('setTimeout1')
}, 0)
var channel = new MessageChannel();
channel.port1.onmessage = function onmessage1 (){console.log('postMessage')
    Promise.resolve().then(function promise1 (){console.log('promise1')
    })
};
channel.port2.postMessage(0);
setTimeout(function setTimeout2(){console.log('setTimeout2')
}, 0)
console.log('sync')

执行程序:

sync
postMessage
promise1
setTimeout1
setTimeout2

timelime:

第一个黄块是 onmessage1,第二个是setTimeout1,第三个是setTimeout2。不言而喻,postMessage 属于 task,因为setTimeout 的 4ms 标准化了,所以这里的 postMessage 会优先 setTimeout 运行。

小结

上边的例子能够得出一些论断:

  • 在一轮 event loop 中屡次批改同一dom,只有最初一次会进行绘制。
  • 渲染更新(Update the rendering)会在 event loop 中的 tasksmicrotasks实现后进行,但并不是每轮 event loop 都会更新渲染,这取决于是否批改了 dom 和浏览器感觉是否有必要在此时立刻将新状态出现给用户。如果在一帧的工夫内(工夫并不确定,因为浏览器每秒的帧数总在稳定,16.7ms 只是估算并不精确)批改了多处dom,浏览器可能将变动积攒起来,只进行一次绘制,这是正当的。
  • 如果心愿在每轮 event loop 都即时出现变动,能够应用requestAnimationFrame

利用

event loop的大抵循环过程,能够用下边的图示意:

假如当初执行到 currently running task,咱们对批量的dom 进行异步批改,咱们将此工作插进task:

此工作插进microtasks

能够看到如果 task 队列如果有大量的工作期待执行时,将 dom 的变动作为 microtasks 而不是 task 能更快的将变动出现给用户。

同步简简单单就能够实现了,为啥要异步去做这些事?

对于一些简略的场景,同步齐全能够胜任,如果得对 dom 重复批改或者进行大量计算时,应用异步能够作为缓冲,优化性能。

举个小例子:

当初有一个简略的元素,用它展现咱们的计算结果:

<div id='result'>this is result</div>

有一个计算平方的函数,并且会将后果响应到对应的元素

function bar (num, id) {
  var  product  = num  * num;
  var resultEle = document.getElementById(id);
  resultEle.textContent = product;
}

当初咱们制作些问题,假如当初很多同步函数援用了 bar,在一轮event loop 里,可能 bar 会被调用屡次,并且其中有几个是对 id='result' 的元素进行操作。就像下边一样:

...
bar(2, 'result')
...
bar(4, 'result')
...
bar(5, 'result')
...

仿佛这样的问题也不大,然而当计算变得复杂,操作很多 dom 的时候,这个问题就不容忽视了。

用咱们上边讲的 event loop 常识,批改一下bar

var store = {}, flag = false;
function bar (num, id) {store[ id] = num;
  if(!flag){Promise.resolve().then(function () {for( var k in store){var num = store[k];
            var product  = num  * num;
            var resultEle = document.getElementById(k);
            resultEle.textContent = product;
       }
    });
    flag = true;
  }
}

当初咱们用一个 store 去存储参数,对立在 microtasks 阶段执行,过滤了多余的计算,即便同步过程中屡次对一个元素批改,也只会响应最初一次。

写了个简略插件 asyncHelper,能够帮忙咱们异步的插入 taskmicrotask

例如:

// 生成 task
var myTask = asyncHelper.task(function () {console.log('this is task')
});
// 生成 microtask
var myMicrotask = asyncHelper.mtask(function () {console.log('this is microtask')
});

// 插入 task
myTask()
// 插入 microtask
myMicrotask();

对之前的例子的应用 asyncHelper:

var store = {};
// 生成一个 microtask
var foo = asyncHelper.mtask(function () {for( var k in store){var num = store[k];
            var product  = num  * num;
            var resultEle = document.getElementById(k);
            resultEle.textContent = product;
       }
}, {callMode: 'last'});

function bar (num, id) {store[ id] = num;
  foo();}

如果不反对 microtask 将回退成task

参考

https://github.com/aooy/blog/issues/5

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

https://html.spec.whatwg.org/multipage/webappapis.html#event-loop

退出移动版