• 又到了battle EventLoop的季节


    金秋飒爽,丹桂飘香,我们又迎来了一年一度金九银十招聘季,有一个问题也是经常要cue到,逢人就要问:你知道EventLoop吗,展开说说?

    简介

    Event Loop ,“事件循环

    浏览器 or Node的一种解决JavaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。

    为什么要弄懂Event Loop?

    • 增加技术深度,懂得JS运行机制
    • 底层原理掌握好,万变不离其宗
    • 面试官最爱

    Event Loop

    1、宏任务:MacroTask

    • script全部代码
    • setTimeOut
    • setInterval
    • setImmediate
    • I/O
    • UI Rendering

    2、微任务:MicroTask

    • Process.nextTick(Node独有)
    • Promise
    • MutationObserver

    浏览器中的Event Loop

    JS有一个主线程main thread和调用栈call-stack,所有的任务哦度会被放到调用栈里等待主线程执行。

    1、调用栈

    采用后进先出的原则, 当函数执行的时候,会被添加到栈的顶部,当操作完成后,就会从栈顶移除,直到栈内被清空。

    2、 同步任务和异步任务

    JS单线程任务分为:同步任务 and 异步任务

    同步任务:在调用栈中按顺序等待主线程依次执行

    异步任务:在异步任务有结果后,将注册的回调函数放入任务队列中等待主线程空闲,被读取到栈内等待执行

    任务队列Task Queue:队列,先进先出

    3、事件循环的进程模型

    • 选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即null,则执行跳转到微任务的执行步骤
    • 将事件循环中的任务设置为已选择任务
    • 执行任务
    • 将事件循环中当前运行任务设置为null
    • 将已运行完成的任务从任务队列中删除
    • microtasks步骤:进入microtask检查点
    • 更新界面渲染
    • 返回第一步

    4、执行进入microtask检查点时,用户代理会执行以下步骤

    • 设置microtask检查点标志为true
    • 当事件循环microtask执行不为空时,选择一个最先进入的microtask队列的microtask,将事件循环的microtask设置为已选择的microtask并运行,将已执行完的microtask设置为null,移除microtask
    • 清理indexDB事务
    • 设置进入microtask检查点标志为false

    执行栈完成同步任务后,查看执行栈是否为空,如果执行栈为空,就会检查微任务队列是否为空,如果是空的话,执行宏任务,否则就一次性执行完所有的微任务。

    举个例子🌰

    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');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    第一次执行

    执行同步代码,将宏任务和微任务划分到各自队列中

    Tasks宏任务:run script、 setTimeout callback
    Microtasks微任务:Promise then
    JS stack: script
    
    Log: script start、script end。	
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第二次执行

    执行宏任务后,检测到微任务队列中不为空,执行Promise1,执行完后调用Promise2.then,放入到微任务中,再执行调用Promise.then

    Tasks宏任务:run script、 setTimeout callback
    Microtasks微任务:Promise2 then
    JS stack: Promise2 callback	
    
    Log: script start、script end、promise1、promise2
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第三次执行

    当微任务队列为空时,执行宏任务,执行setTmeout callback,打印日志

    Tasks宏任务:setTimeout callback
    Microtasks微任务:	
    JS stack: setTimeout callback
    
    Log: script start、script end、promise1、promise2、setTimeout
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第四次执行

    清空Tasks队列和JS Stack

    Tasks宏任务:setTimeout callback
    Microtasks微任务:	
    JS stack:
    
    Log: script start、script end、promise1、promise2、setTimeout
    
    • 1
    • 2
    • 3
    • 4
    • 5

    浏览器中的Event Loop总结

    • 执行一个宏任务(栈中没有就从事件队列中获取)
    • 执行过程遇到微任务,添加到微任务队列中
    • 宏任务执行完毕后,立即执行微任务队列的所有微任务
    • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
    • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

    Node中的Event Loop

    Node中的Event Loop是基于libuv实现的,而libuv是Node的新跨平台抽象层,libuv使用异步、事件驱动的编程方式,核心是提供i/o事件循环和异步回调。

    一共分为6个阶段:

    1、timers:执行setTimeout和setInterval中到期的callback

    执行这两个回调需要设置一个毫秒数,由于system的调度可能会延时,达不到预期时间。

    2、pending callback:上一轮循环中少数的callback会放在这一阶段执行

    此阶段执行某些操作系统回调。

    3、idle,prepare:仅在内部使用

    4、poll:最重要的阶段,执行pengding callback,在适当的情况下会阻塞在这个阶段

    主要的两个功能:

    执行I/O回调、处理轮询队列中的事件

    当事件循环进入poll阶段并且在timers中没有可执行的定时器,将发生:

    如果poll队列不为空,则事件循环将遍历其同步执行它们的callback队列,直到队列为空,或者达到system-dependent(系统相关限制)。

    如果队列为空,将发生:

    • 如果有setImmediate()回调需要执行,则会立即停止执行poll阶段并进入执行check阶段以执行回调
    • 如果没有setImmediate()回调,poll阶段将等待callback被添加到队列中,然后立即执行

    5、check:执行setImmediate

    此阶段运训人员在poll阶段完成后立即执行回调。

    如果poll阶段闲置并且script已排队setImmediate(),则事件循环到达check阶段执行而不是继续等待。

    setImmediate()是一个特殊的计时器,它在事件循环的一个单独阶运行,使用libuv API来调度在poll阶段完成后执行的回调。

    通常,当代码被执行时,事件循环最终将达到poll阶段,它将等待传入连接,请求等。

    但是如果已经调度了回调setImmediate(),并且轮询阶段变为空闲,则它将结束并且达到check阶段,而不是等到poll事件。

    setImmediate() 的setTimeout()的区别

    根据被调用的时间以不同的方式表现:

    • setImmediate()设计用于在当前poll阶段完成后check阶段执行脚本
    • setTimeout()安排在经过最小ms后运行的搅拌,在timers阶段执行
    setTimeout(() => {
      console.log('timeout');
    }, 0);
    
    setImmediate(() => {
      console.log('immediate');
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    执行定时器的顺序将根据调用它们的上下文而有所不同。如果从主模块中调用两者,那么时间将收到进程性能的限制。其结果也不相同。

    Process.nextTick()

    Process.nextTick()虽然是异步api的一部分,单不是事件循环的一部分。

    Process.nextTick()callback添加到next tick队列。一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。

    也就是说,如果存在nextTick队列,就会清空队列中的所有回调函数,并且优先于其他microtask执行。

  • 相关阅读:
    Python实战项目:俄罗斯方块(源码分享)(文章较短,直接上代码)
    2 亿 + 数据打开方式:人工智能数字疗法
    Docker的安装、启动和配置镜像加速
    ClickHouse入门手册1.0
    随想录一刷Day48——动态规划
    基于C++实现一个支持简单交互绘图小程序
    Spring的Factories机制介绍
    JavaEE进阶3
    【数据密集型系统设计】软件系统的可靠性、可伸缩性、可维护性
    用户级线程和内核级线程
  • 原文地址:https://blog.csdn.net/weixin_42224055/article/details/126584698