了解JS中的Event Loop这篇挺足的

lgmyxbjfu
发布于 2020-9-6 10:58
浏览
0收藏

 


在开始Event Loop之前,我们先来介绍下JS线程方面。

JS是一门单线程语言,在最新的 HTML5 中提出了Web-Worker,但JS是单线程这一核心仍未改变。所以一切Javascript版的"多线程"都是用单线程模拟出来的,一切JS多线程都是纸老虎!

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

故JS任务也得一个一个顺序执行,如果遇到耗时比较久,那往后的任务是不是要一直等待下去?在此,我们将任务分为:同步任务、异步任务。

 

同步与异步任务
主线程类似一个加工厂,它只有一条流水线,待执行的任务就是流水线上的原料,只有前一个加工完,后一个才能进行。Event Loops就是把原料放上流水线的工人。 只要已经放在流水线上的,它们会被依次处理,称为同步任务。一些待处理的原料,工人会按照它们的种类排序,在适当的时机放上流水线,这些称为异步任务。

了解JS中的Event Loop这篇挺足的-鸿蒙开发者社区
导图的意图: 

任务分为同步或异步,两者进入不同的环境。同步进入主线程,异步进入Event Table并注册函数。
当指定的事件完成时,Event Table会将这个函数移入Event Queue。
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。
示例代码:

$.ajax({
    ...
    success:() => {
        console.log('b!');
    }
})
console.log('a');


ajax进入Event Table,注册回调函数success。
执行`console.log('代码执行结束')``。
ajax事件完成,回调函数success进入Event Queue。
主线程从Event Queue读取回调函数 success 并执行。
上例只是简单的介绍事件循环,我们来看看更加详细的介绍。

Event Loop 的处理过程
event loop翻译出来就是事件循环,可以理解为实现异步的一种方式。

事件,用户交互,脚本,渲染,网络这些都是我们所熟悉的东西,他们都是由event loop协调的。触发一个click事件,进行一次ajax请求,背后都有event loop在运作。

(task 另外一个名称为:macrotask)

了解JS中的Event Loop这篇挺足的-鸿蒙开发者社区

 
 

在规范的Processing model定义了event loop的循环过程: 一个 event loop 只要存在,就会不断执行下边的步骤:

1.在tasks队列中选择最老的一个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会不断循环的去取tasks队列的中最老的一个任务推入栈中执行,并在当次循环里依次执行并清空microtask队列里的任务。
执行完microtask队列里的任务,有可能会渲染更新。(浏览器很聪明,在一帧以内的多次dom变动浏览器不会立即响应,而是会积攒变动以最高60HZ的频率更新视图)。
microtasks 检查点

event loop运行的第 6 步,执行了一个microtask checkpoint,看看规范如何描述microtask checkpoint: 当用户代理去执行一个 microtask checkpoint,如果 microtask checkpoint 的 flag(标识)为 false,用户代理必须运行下面的步骤:

1.将microtask checkpoint的flag设为true。

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

3.在microtask队列中选择最老的一个任务。

4.将上一步选择的任务设为event loop的currently running task。

5.运行选择的任务。

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

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

8.Done: 每一个environment settings object它们的 responsible event loop就是当前的event loop,会给environment settings object发一个rejected promises 的通知。

9.清理IndexedDB的事务。

10.将microtask checkpoint的flag设为flase。
microtask checkpoint所做的就是执行microtask队列里的任务。什么时候会调用microtask checkpoint呢?

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

 

macro-task(宏任务):

setTimeout
setInterval
setImmediate
I/O
UI rendering

 

micro-task(微任务):

process.nextTick
promises
Object.observe
MutationObserver
我们来看看示例代码:

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:

macrotask
microtask
script
 
1.从macrotask队列中取出script任务,推入栈中执行。

2.promise1列为microtask,setTimeout1列为task,setTimeout2列为macrotask。

macrotask
microtask
setTimeout1 setTimeout2
promise1
3.script任务执行完毕,执行microtask checkpoint,取出microtask队列的promise1执行。

循环 2:

macrotask
microtask
setTimeout1 setTimeout2
 
4.从macrotask队列中取出setTimeout1,推入栈中执行,将promise2列为microtask。

macrotask
microtask
setTimeout2
promise2
5.执行microtask checkpoint取出microtask列的promise2行。

循环 3

macrotask
microtask
setTimeout2
 
6.acrotask列中取出setTimeout2推入栈中执行。

7.Timeout2务执行完毕,执行microtask checkpoint。

macrotask
microtask
 
 
每次执行完microtask,视图就会更新吗?在我们日常开发中,好像并不是,让我们看看具体原因吧。

event loop 中的 Update the rendering(更新渲染)
这是Event Loop中很重要部分,在第 7 步会进行Update the rendering(更新渲染),规范允许浏览器自己选择是否更新视图。也就是说可能不是每轮事件循环都去更新视图,只在有必要的时候才更新视图。

这篇文章较详细的讲解了渲染机制。

验证更新渲染(Update the rendering)的时机
不同机子测试可能会得到不同的结果,这取决于浏览器,CPU、GPU性能以及它们当时的状态。

例 1:

<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 个macrotask,分别是click1、setTimeout1、setTimeout2,所以会分别在 3 次event loop中进行。

当点击后,一共产生 3 个macrotask,分别是click1、setTimeout1、setTimeout2,所以会分别在 3 次event loop中进行。

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

 

例 2:

<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 才会进行绘制呢?

 

例 3:

var con = document.getElementById('con');
con.onclick = function () {
   setTimeout(function  setTimeout1() {
        con.textContent = 0;
   }, 0);
    setTimeout(function  setTimeout2() {
            con.textContent = 1;
    }, 16.7);
};



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

可否认为相邻的两次 event loop 的间隔很短,浏览器就不会去更新渲染了呢?继续我们的实验

 

例 4: 我们在同一时间执行多个setTimeout来模拟执行间隔很短的task。

<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>



 

 
图中一共绘制了两帧,第一帧 910ms,第二帧 920ms,都远远高于每秒 60HZ(16.7ms)的频率,所以两次 event loop 的间隔很短同样会进行绘制。 

 

例 5:

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

<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>



 
 

可以看到一大块黄色区域,上半部分有一根绿线就是点击后的第一次绘制,脚本的运行耗费大量的时间,并且阻塞了渲染。

看看setTimeout2的运行情况。

 

 
 

可以看到setTimeout2这轮event loop没有run microtasks,microtasks在setTimeout1被全部执行完了。

25000 个microtasks不能说明event loop对microtasks数量没有限制,有可能这个限制数很高,远超 25000,但日常使用基本不会使用那么多了。

对 microtasks 增加数量限制,一个很大的作用是防止脚本运行时间过长,阻塞渲染。

 

例 6:

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



总体的Timeline:

了解JS中的Event Loop这篇挺足的-鸿蒙开发者社区

 
看看单个 requestAnimationFrame的Timeline: 

了解JS中的Event Loop这篇挺足的-鸿蒙开发者社区

 

和setTimeout很相似,可以看出requestAnimationFrame也是一个task,在它完成之后会运行run microtasks。

小结 上边的例子可以得出一些结论:

在一轮event loop 中多次修改同一dom,只有最后一次会进行绘制。
渲染更新(Update the rendering)会在event loop中的tasks和microtasks完成后进行,但并不是每轮event loop都会更新渲染,这取决于是否修改了dom和浏览器觉得是否有必要在此时立即将新状态呈现给用户。如果在一帧的时间内(时间并不确定,因为浏览器每秒的帧数总在波动,16.7ms 只是估算并不准确)修改了多处dom,浏览器可能将变动积攒起来,只进行一次绘制,这是合理的。
如果希望在每轮event loop都即时呈现变动,可以使用requestAnimationFrame。

作者:前端安然
来源:掘金

分类
标签
已于2020-9-6 10:58:50修改
收藏
回复
举报
回复
    相关推荐