进击的 JavaScript 之 事件循环

事件循环(Event Loop)

这玩意可以说是js 中的重中之重吧,基本上哪里都能遇到它,搞不懂它怎么行!内容较长,干货满满!

一、单线程

JavaScript 是 单线程 的(这个很重要),也就是说js 引擎中负责执行 js 代码的只有一个线程,称为主线程。主线程执行时,进入函数调用栈执行代码。

单线程意味着: 同一时间片断内只能执行单个任务。所有任务都要排队,只有等前一个任务结束后,才会执行下一个任务。

console.log('1111');
console.log('2222');
console.log('3333');

主线程


二、浏览器不是单线程的

虽然JS运行在浏览器中,是单线程的,但浏览器不是单线程的,例如Webkit或是Gecko引擎,都可能有如下线程:

下面这段可以简单略过,等到看完文章再来细品就好。

  • javascript引擎: 是单线程执行的,浏览器无论什么时候都只有一个JS线程在运行JS程序。

  • GUI渲染线程: 负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。(所以页面优化时,会把不重要的js放到页面尾部加载

  • 浏览器事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等。可以看成一个任务派送线程,把对应的异步任务派送到队列中或者其他线程,等待 js 引擎执行。

  • 定时触发器线程:浏览器定时计数器并不是由 JavaScript 引擎计数的, 因为 javaScript 引擎是单线程的, 如果用js引擎计时,那就不存在异步了。 我认为通过单独线程来计时并触发定时是更为合理的方案。当计时结束后把回调函数派送到队列。

  • 异步 http 请求线程:在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。

三、任务

单线程效率很低,那么怎么解决这个问题呢?那就是事件循环啦!事件循环是怎么做到的呢?那就是采用两种任务执行:

  1. 同步任务(synchronous)。主线程中的任务。
  2. 异步任务(asynchronous):
    • 启动相应的线程,执行异步任务,所以不会阻塞主线程的执行。等到线程把任务完成后,触发事件派送,把回调函数放入任务队列中等待执行。(比如setTimeout)
    • 或者,直接触发事件派送,把回调函数放入任务队列中等待执行。(比如process.nextTick)

我个人感觉,第一种异步是宏任务,第二种异步是微任务,符合逻辑。哈哈。

下面我就把第一种异步称为 宏异步任务,第二种异步称为 微异步任务 啦。

异步任务就是其中的回调函数不会立即在主线程中执行,而是被派送到任务队列中等待执行。

异步任务是立即执行的。真正异步的是其中的回调函数。

四、异步任务与事件的关系

一、宏异步任务

不知道大家对事件循环理解的时候有没有疑惑,如果js 的是单线程的,那么为什么要搞事件循环呢,搞来搞去还不是主线程去执行吗,效率哪里快了。

那么关键就在这了,宏异步任务会启动浏览器相应的线程去执行任务,当宏异步任务完成后,就会触发相应事件,把其中的回调函数添加到任务队列中去等待执行。

就拿setTimeout 异步函数来说:

setTimeout(function(){
    console.log("i am timeout.")
}, 0)

console.log("i am end.")

//i am end.
//i am timeout.

这段代码执行时,主线程遇到了setTimeout 异步函数,它启动定时触发器线程,然后继续执行,“i am end.” 输出,然后,0秒后,计时结束,定时触发器线程触发 计时结束事件 ,把里面的回调函数放入 任务队列中,事件循环开始,回调函数执行输出 “i am timeout”。

所以说为什么是异步呢,就是它会启动相应的线程去执行任务,不阻塞主线程执行,也会节省很多时间。当然也可以用来改变函数执行顺序。

可以把宏异步任务看成以下三个步骤:

  1. 启动相应的浏览器线程去执行异步的操作。
  2. 等待异步操作完成后,触发相应事件,把回调函数派送到任务队列中。
  3. 事件循环,读取任务队列,执行回调函数。
二、微异步任务

微异步任务呢,就是效率很高的异步了,它直接触发事件派送,在任务队列中排队等待。

注意: setTimeout,promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务(回调函数)。


五、任务队列

当任务多了,就需要排队处理,那么这个排的队伍就是任务队列。
任务队列 又分两种,为macro-task(宏任务)micro-task(微任务),新标准中分别称为taskjobs

  • 宏任务——MacroTask(Task):
    setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI
    rendering ,ajax ,DOM, BOM 事件

  • 微任务——MicroTask(在ES2015规范中称为Job):

    process.nextTick, Promise, MutationObserver

宏任务是在下轮循环的开始执行,微任务是在本轮循环的结尾执行
事件循环
注意:这里是为了方便理解,不管哪种异步任务都是要进入主线程(函数调用栈)中执行的(很重要)。还有,队列和栈是不同的数据结构,别搞混了。不清楚的可以看本系列二,js中的数据结构。

任务源队列:

属于同一种任务源的任务,会放入一个任务源队列中。比如settimeout 源队列,Promise 源队列。
事件循环


六、事件循环

事件循环就是: 主线程的任务执行完之后(栈空了以后),去读取微任务(MicroTask) 队列的任务,按队列顺序读取到函数调用栈中执行。 微任务队列为空后,把宏任务(MacroTask) 队列的任务 按顺序读取到函数调用栈中执行。

更简单的说:只要主线程空了(同步),就会去读取”任务队列”,这就是JavaScript的事件运行机制。

有栗子有真相:

console.log("glob");

setTimeout(function(){
    console.log("timeout");
});
// setTimeout 函数,不加时间,默认为0;

// promise 函数本身不是异步,resolve, reject 等函数是异步的(也就是后面then中的回调函数)。
new Promise(function(resolve, reject) {
    console.log('promise');
    resolve();
}).then(function() {
    console.log('then');
})

// glob1
// promise
// then
// timeout

开始分析:

  1. 从头执行这段代码,首先"glob1" 输出
  2. 然后遇到setTimeout,开启定时触发器线程,开始计时。0秒后,计时结束,触发计时结束事件,把setTimeout 回调函数,放入 setTimeout 源任务队列。
  3. promise 函数执行,“promise” 输出
  4. 然后遇到 resolve ,立即触发事件,把then 中的回调函数放入 promise 源任务队列。
  5. 主线程为空,开始事件循环。

现在的情况是这样的:
事件循环

一、读取微任务(macro-task) 队列
本轮循环开始。主线程读取Promise 源队列,执行console.log(“then”); “then” 输出。
事件循环

二、此时,微任务(macro-task)队列为空,开始下轮循环
主线程读取宏任务(macro)队列,执行 console.log(“timeout”), 输出"timeout"。
在这里插入图片描述

三、所有队列为空,主线程为空,执行结束。

其实,事件循环的难点在于微任务。因为它是在本轮循环的结尾开始循环的,也就是说,只有在微任务队列为空时,才会开始下轮的循环。

来个大难度的:
注意:下面的代码只能在node 的环境中执行。方法:建一个js文件,比如test.js,然后node test.js

console.log('golb1');

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})

process.nextTick(function() {
    console.log('glob1_nextTick');
})
new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})
一、主线程从头开始执行这段代码

注意啊,任务队列不是固定的,谁先被派送,谁排前面,谁先执行。队列就是这个意思了。

我的意思就是说,setTimeout 源队列不一定一直排第一个,Promise 源队列,也不一定一直排第二个!

执行完的情况是这样的:
事件循环
输出的情况如下:

golb1
glob1_promise
glob2_promise
二、第一轮循环开始

也就是说,一开始,第一轮循环是从主线程开始往后的。

主线程读取 微任务(micro-task) 队列,先读取process.nextTick 源队列,第一个回调函数执行。
事件循环
输出的情况如下:

golb1
glob1_promise
glob2_promise
glob1_nextTick

然后是process.nextTick第二回调函数:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick

读取Promise 源任务队列,执行第一个回调函数。
事件循环

此时结果如下:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then

然后读取Promise 源队列的第二个回调函数执行:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then

现在队列的情况是这样的:
事件循环

微任务队列为空,开始第二轮循环。

第二轮循环开始,读取setTimeout 源队列的第一个回调函数。

在这里插入图片描述

而这个回调函数内,又含有其他异步任务,然后触发事件派送,排队等待。函数执行完的队列是这样的。
事件循环

结果如下:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise

这时候就是关键了!!!重要的事说三遍!!!
这时候就是关键了!!!重要的事说三遍!!!
这时候就是关键了!!!重要的事说三遍!!!

因为此时的setTimeout 源队列不为空,所以主线程不会去读微任务队列,而是继续读取setTimeout 源任务队列的第二个回调函数执行。

执行结果队列如下:
事件循环

结果如下:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise

再次关键了!!!重要的事说三遍!!!
再次关键了!!!重要的事说三遍!!!
再次关键了!!!重要的事说三遍!!!

此时,主线程不会去读取宏任务队列的第二个队伍:setImmediate 源队列,而是继续往后,因为第一个宏任务队列(setTimeout源队列)为空了。所以读取微任务队列的process.nextTick 源队列的第一个回调函数执行。
事件循环

结果如下:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick

继续读process.nextTick 源队列 第二个回调函数执行。然后是Promise 源队列,我这里就直接简化了,不一一列了,反正读取到微任务队列为空。
结果队列如下:
事件循环
结果如下:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then

第二轮循环结束。

第三轮循环开始,读取setImmediate 源队列第一个回调函数执行。

事件循环

结果如下:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
immediate1
immediate1_promise

然后是读取宏任务队列的 setImmediate 源队列 的第二个回调函数执行。
在这里插入图片描述

结果如下:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
immediate1
immediate1_promise
immediate2
immediate2_promise

setImmediate 源队列为空,开始读取微任务队列。我就不细说了,上面说过了。逐个读取微任务队列回调函数执行。
最终队列为空,事件循环结束。
在这里插入图片描述

最终结果如下:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
immediate1
immediate1_promise
immediate2
immediate2_promise
immediate1_nextTick
immediate2_nextTick
immediate1_then
immediate2_then
所以应该看明白了吧,微任务队列为空,则标志着本次循环结束。而且微异步任务 执行的效率是很高的,它是在本次循环结尾,下次循环开始之前执行的!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值