谈谈个人对js事件循环的理解

知乎文章地址:https://zhuanlan.zhihu.com/p/50003943

在这之前,首先谈谈js的单线程机制:众所周知,js是单线程的语言,也就是说同一时间只能做一件事情。不过,现在这个年代还不能多线程开发吗?答案肯定是否定的,而关于为什么js选择用单线程,只要举一个栗子就好了:比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以嘛,javaScript的主要用途是与用户互动,以及操作DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

接下来我们分五个部分来聊聊js的Event Loop。

 

一.js中的为什么会分同步操作和异步操作呢?

举个栗子:

var i, t = Date.now()
for (i = 0; i < 100000000; i++) {}
console.log(Date.now() - t) // 79

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。像上面的这种情况,中间的for循环耗时太长,并且在它还未执行完之前,之后的语句都不能执行,这不是我们想要看到的。如果是换成http请求,导致的体验感应该是极差的。

因此,为了解决这些问题,js脚本的全部操作分为同步和异步:同步就是按照顺序执行操作,异步是在同步操作执行完之后执行异步操作。对于前端页面,通过异步的ajax http请求,保证了基础脚本代码的执行,同时保证了一个体验效果。

二.js异步操作执行流程

首先举个栗子:

setTimeout(function () {
  console.log(1);
}, 0);

console.log(2) // 2 1

上面的setTimeout是一个异步函数,虽然里面设置的回调延时为0,但回调不会立即执行,我的一个理解是:该函数执行并返回后,仅仅是注册一个异步任务,回调函数的执行只有在异步操作完成后才能执行。

在网上看到的一个异步回调函数执行的流程是这样的:首先是我理解的异步函数的异步任务注册、执行异步操作、执行完之后通知主线程、主线程调用回调函数。

关于这个流程,再举个栗子:

box.onclick = function () {console.log(1)}
console.log(2);

onclick注册异步任务(点击事件),当执行完最后一条语句,我点击box这个元素(异步操作执行),点击完(通知主线程),然后回调被执行(主线程调用回调函数)。

三.同步转异步

假设这样一个场景吧:我目前有一个列表,列表中有100000项,我需要通过dom元素将它们渲染在页面中。

如果我们单单通过一个for循环将dom元素全部渲染到页面中,在短时间内往页面中大量添加DOM节点显然也会让浏览器吃不消,看到的结果往往就是浏览器的卡顿甚至假死。为了解决这个问题,我们可以通过异步的方式将这100000个dom节点分成不同的组进行渲染,比如:

// 原先我们是这样的
var ary = [];
for ( var i = 1; i <= 1000; i++ ){
  ary.push( i );    // 假设 ary 装载了 1000 条数据
};
var renderFriendList = function( data ){
  for ( var i = 0, l = data.length; i < l; i++ ){
    var div = document.createElement( 'div' );
    div.innerHTML = i;
    document.body.appendChild( div );
  }
};
renderFriendList( ary );

 

// 现在我们是这样的
var ary = [];
for ( var i = 1; i <= 1000; i++ ){
  ary.push( i );    // 假设 ary 装载了 100000 条数据
};
var renderFriendList = function( data ){
  var i = 0, l = data.length;
  setTimeout(function(){
    var div = document.createElement( 'div' );
    div.innerHTML = i;
    document.body.appendChild( div );
    i++;
    if(i < l) {
      setTimeout(arguments.callee, 10);
    }
  }, 10);
};
renderFriendList( ary );

你会发现,这是一个很有趣的过程。

四.事件循环

首先,盗取两张图:

事件循环是当js脚本中的同步操作执行完之后,异步回调函数循环执行的一个过程。

想必大家也发现了上面的图片分了三块:Heap、Stack、Queue,其中Heap即堆,是用于存放对象的内存块,Stack指的是函数的执行栈,函数的调用需要在该栈里面才能够被执行,Queue指的是任务队列或者叫做消息队列或事件队列。

针对这种结构,分析一下,js脚本的执行过程,以之前的onclick为栗:

  1. js脚本中的语句会从后往前的顺序推入执行栈中,并执行栈中的每一条语句;
  2. 当我点击box,会将一个异步任务推入消息队列中,等待被执行;
  3. 当执行栈为空,主线程会去读取任务队列,并将其中的异步任务推入执行栈中执行;
  4. 之后一直重复2、3两步;

五.异步任务分类

其实,异步任务不是单单说推入一个消息队列中,它其实分为两类——微任务(micro task)和宏任务(macro task)。(micro task优于macro task执行)

以下事件属于宏任务:

  • setInterval()
  • setTimeout()

以下事件属于微任务

  • new Promise()
  • new MutaionObserver()

前面我们介绍过,在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈...如此反复,进入循环。

我们只需记住当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

现在举一个栗子:

setTimeout(function () {
  console.log(1);
});

new Promise(function(resolve,reject){
  console.log(2)
  resolve(3)
}).then(function(val){
  console.log(val);
})

console.log(4);
// 结果为 2 4 3 1

原因很简单,new Promise中的操作会立即执行,然后接着执行微任务输出3,执行宏任务输出4。

 

以上文章参考以下两篇博客:

深入理解javascript中的事件循环event-loop - 小火柴的蓝色理想 - 博客园​www.cnblogs.com

详解JavaScript中的Event Loop(事件循环)机制​www.cnblogs.com

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值