理解EventLoop(事件循环)

262 views次阅读
没有评论

EventLoop是什么?

EventLoop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJs基于不同的技术实现了各自的EventLoop。

  • JS是单线程语言,JS的Event Loop是JS的执行机制。深入了解JS的执行,就等于深入了解JS里的event loop。
  • 浏览器的Event Loop是在html5的规范中明确定义。
  • NodeJS的Event Loop是基于libuv实现的。可以参考Node的官方文档以及libuv的官方文档。
  • libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商。

JS中的EventLoop

(1)JS为什么是单线程的? 因为JS最初是被设计用在浏览器中,如果JS是多线程的,比如说:有两个线程process1和process2,由于是多线程的JS,两个线程同时对一个dom进行操作,process1删除了该dom,而process2修改了该dom,此时浏览器就没法执行了。所以说JS是单线程的。
(2)JS中为什么需要异步呢? 因为JS中代码自上而下执行,如果不存在异步,当一行代码执行的时间过长时,后边的代码就会被阻塞,对于用户而言,意味着页面卡死,体验极其不好。
(3)JS中如何实现异步的呢? 通过事件循环(EventLoop),理解了EventLoop机制,就理解了JS的执行机制。
(4)JS中的EventLoop 举个栗子1:观察下面代码的执行顺序

    console.log(1)
    setTimeout(funciton(){
      console.log(2)
    },0)
    console.log(3) // 1 3 2
  

很显然,setTimeout里的函数并没有立即执行,而是延迟了一段时间,满足一定条件后才执行的,这类代码就叫做异步代码。在JS中就是将任务分为同步任务和异步任务。 按照这种分类方式,JS的执行机制就是:

  • 首先判断JS是同步还是异步代码,同步就进入主线程,异步就进入event table
  • 异步任务在event table中注册函数,当满足触发条件后,被推入event queue
  • 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程中

以上三步循环就是JS中的EventLoop。

举个栗子2:观察下面代码

    setTimeout(function(){
      console.log("定时器开始啦")
    })
    new Promise(function(resolve){
      console.log("马上执行for循环啦")
      for(var i = 0;i < 10000;i++){
        i==99 && resolve()
      }
    }).then(function(){
      console.log("执行then函数啦")
    })
    console.log("代码执行结束")
  

如果按照上面例1的结论来分析,就是:

    setTimeout 是异步任务,被放到event table

    new Promise 是同步任务,被放到主线程里,直接执行打印 console.log('马上执行for循环啦')
    
    .then里的函数是 异步任务,被放到event table
    
    console.log('代码执行结束')是同步代码,被放到主线程里,直接执行
  

所以结果是 【马上执行for循环啦 — 代码执行结束 — 定时器开始啦 — 执行then函数啦】吗? 然而执行后的结果居然不是这样,而是【马上执行for循环啦 — 代码执行结束 — 执行then函数啦 — 定时器开始啦】

那么,难道是异步任务的执行顺序,不是前后顺序,而是另有规定? 事实上,按照异步和同步的划分方式,并不准确。 更准确的划分方式是:

  • macro-task(宏任务):script,setTimeout,setInterval,setimmediate,requestAnimationFrame (浏览器独有),I/O,UI rendering(浏览器独有)
  • micro-task(微任务):promise,promise.nextTick,MutationObserver,Object.observe,其中promise.nextTick为node独有

那么按照这种分类方式,JS的执行机制就是

  • 先执行一个宏任务,过程中如果遇到微任务就将其放入微任务的【事件队列】里
  • 当前宏任务执行完毕后,会查看微任务的【事件队列】,并将队列里的微任务依次执行完
  • 重复前面两步

再来分析下这个例子的执行顺序:

    首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里

    遇到 new Promise直接执行,打印"马上执行for循环啦"
    
    遇到then方法,是微任务,将其放到微任务的【队列里】
    
    打印 "代码执行结束"
    
    本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印"执行then函数啦"
    
    到此,本轮的event loop 全部完成。
    
    
    下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印"定时器开始啦"
  

关于setTimeout 对于这段代码:

    setTimeout(function(){
      console.log('开始执行')
    },3000)
  

我们通常的理解是三秒后会打印“开始执行”。其实更准确的说法是:3秒后setTimeout里的函数会被推入event queue中,而event queue(事件队列)里的任务,只有在主线程空闲时才会执行。所以只有当满足3秒后并且主线程空闲时,才会在3秒后执行setTimeout里的函数。如果主线程的内容很多,执行事件超过了3秒,比如10秒,那么setTimeout里的函数只能在10秒后执行了。

浏览器的EventLoop

  • 1. 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等)
  • 2. 全局Script代码执行完毕后,调用栈Stack会清空
  • 3. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1
  • 4. 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
  • 5. microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空
  • 6. 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行
  • 7. 执行完毕后,调用栈Stack为空
  • 8. 重复第3-7个步骤
  • 9. 重复第3-7个步骤
  • 10. ……

需要注意的是:

  • 1. 宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务
  • 2. 微任务队列中所有的任务都会被依次取出来执行,直到microtask queue为空
  • 3. UI rendering的节点,是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。

举个栗子:

    console.log(1)

    setTimeout(() => {
      console.log(2)
      Promise.resolve().then(() => {
        console.log(3)
      })
    })
    
    new Promise((resolve, reject) => {
      console.log(4)
      resolve(5)
    }).then((data) => {
      console.log(data)
    })
    
    setTimeout(() => {
      console.log(6)
    })
    
    console.log(7) //执行结果: 1 4 7 5 2 3 6
  
guxuerui
版权声明:本站原创文章,由guxuerui于2019年06月25日发表,共计4662字。
转载提示:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
Loading...