难受就摸头盖骨

Event Loop

来源:Loong Panda

  

概念

Event Loop即事件循环,是解决javaScript单线程运行阻塞的一种机制。 主要是为了协调单线程下,事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞。

因为JavaScript 是单线程,也就是说,所有任务需要排队,前一个任务结束,才会执行后一个任务。

但是IO设备(输入、出设备)可能会因为网络等因数导致速度很慢(比如Ajax)继而CPU没有充分利用,所以设计者将IO设备的任务挂起,先执行后面的任务,等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。于是,就把所有任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

 

同步任务:

只有前一个任务执行完毕,才能执行后一个任务;直接在主线程上排队执行且最先执行,形成一个执行栈

 

异步任务:

不进入主线程、而是进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

一、microtask(微任务)
1、Promise.then
2、MutationObserver(Mutation Observer API 用来监视 DOM 变动)
3、Object.observe()(已废弃)
4、nextTick(Node.js 环境)

二、macrotask(宏任务)
1、script(整体代码) 注: 这个很容易忽略掉,就是指script根环境,而且它永远是第一个执行的宏任务(包含了同步的任务).
2、setTimeout
3、setInterval
4、setImmediate
5、I\O
6、UI rendering(DOM event)

执行机制:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",执行一个宏任务, 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中 宏任务执行完毕后,再依次执行执行当前微任务队列中的所有微任务,当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
(4)主线程不断重复上面的第三步。

 

任务队列

"任务队列"是一个先进先出的数据结构,也是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

例子1

案例来源:https://juejin.cn/post/6844903512845860872
setTimeout(function() { 
  console.log('setTimeout'); 
}) 
new Promise(function(resolve) { 
  console.log('promise'); 
}).then(function() { 
  console.log('then'); 
}) 
console.log('console');

1、整体代码script这段代码作为宏任务,进入主线程。
2、先遇到setTimeout,将分发到宏任务Event Queue。
3、接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
4、遇到console.log(),立即执行。
5、整体代码script作为第一个宏任务执行结束,然后会看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
6、第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
7.结束。

 

例子2:

setTimeout(() => {
    console.log(3)
    new Promise((resolve, reject) => {
        console.log(5)
        resolve()
    }).then(console.log(6))
}, 0)

setTimeout(() => {
    console.log(4)
}, 0)

new Promise((resolve, reject) => {
    console.log(1)
    resolve()
}).then(console.log(2))
输出是1 2 3 5 6 4
分析:
1、前面的两个setTimeout都是宏任务,所以现在宏任务队列有2个任务
2、Promise里面的代码是同步任务,所以现在会马上执行 输出1
3、Promise的then是微任务,所以现在微任务队列有1个任务
4、在执行完同步任务之后,开始执行微任务,也就是console.log(2), 输出2
5、在执行完微任务之后,会执行宏任务,第一个宏任务也就是第一个setTimeout
6、第一个setTimeout会先输出3,然后输出5,因为这两个都是同步任务,然后遇到then,加入微任务队列,宏任务执行完重新开始下一个循环。
7、因为没有同步代码,所以接着执行微任务,此时微任务队列有1个任务(第6步加入), 宏任务队列还有1个任务(第6步执行完了第一个宏任务)
8、执行微任务,输出6
9、再执行宏任务,输出4

 

例子3:

//主线程直接执行
console.log('1');
//丢到宏事件队列中
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
//微事件1
process.nextTick(function() {
    console.log('6');
})
//主线程直接执行
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    //微事件2
    console.log('8')
})
//丢到宏事件队列中
setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

结果:
    输出为1,7,6,8,2,4,3,5,9,11,10,12。

分析:
    首先浏览器执行js进入第一个宏任务进入主线程, 直接打印console.log('1')
        • 遇到 setTimeout  分发到宏任务Event Queue中
        • 遇到 process.nextTick 丢到微任务Event Queue中
        • 遇到 Promise, new Promise 直接执行 输出 console.log('7');
        • 执行then 被分发到微任务Event Queue中

        •第一轮宏任务执行结束,开始执行微任务 打印 6,8
        •第一轮微任务执行完毕,执行第二轮宏事件,执行setTimeout
        •先执行主线程宏任务,在执行微任务,打印'2,4,3,5'
        •在执行第二个setTimeout,同理打印 ‘9,11,10,12’
        •整段代码,共进行了三次事件循环

 

误区

nodejs事件循环和浏览器的事件循环不一样的。浏览器的Event loop是在HTML5中定义的规范,而node中则由libuv库实现

原文来源:Loong Panda

 

posted @ 2022-05-13 16:36  longpanda_怪怪哉  阅读(70)  评论(0编辑  收藏  举报
下定决心了,要把写不出代码就摸后脑勺的习惯给改了! 如果可以~单身待解救~~