JS的事件循环
本文是我自己对事件循环的理解和总结,不会有太多的理论知识,已经有太多文章写过了,自己搜索下就能找到很多;
同时,文章中的观点仅是本人自己理解,比较白话,不用太较真啊!
事件循环是什么?
事件是什么?
事件是特定情况下触发的操作。这里的操作是用函数封装起来的,所以,事件就是在特定情况下触发的函数。
例如:
- dom元素相关事件,在用户单击页面上的按钮或页面加载完成时触发;
- 定时器中,在特定时间间隔后触发其绑定函数;
- ajax请求中,在请求响应时,才会触发回调函数;
- promise中,在其内代码有了结果后,再触发 then 函数;
上面的这些情况的回调函数,都是在特定情况下触发的函数,所以都可以称为事件
特定情况触发的函数
注意,这里指的是“特定情况”,而不是按代码顺序执行时触发的,也就不是同步触发的,而是异步触发的。
所以,事件是异步触发的。
事件循环是什么?
事物循环,顾名思义,就是事件的循环触发。没错,事件循环的核心就是“事件的循环触发”;
同时事件循环还包括,这些异步操作的触发,回调函数的收集以及回调函数的触发等一系列操作。这些操作其实贯穿整个JS代码的运行
综上所述:事件循环就是JS代码的运行机制。
另:
异步操作的触发, 其实是 同步执行的。
而回调函数的触发,是异步执行的
事件循环是如何实现的?
JS是单线程的
JS是单线程的,所以浏览器只会分配一个线程用来解析执行JS代码,同时所有的JS代码也只能在这一个线程中执行。此线程为JS引擎线程。
因为是单线程的,所以代码是同步执行的,也就是按代码顺序执行的,只有前一个任务执行完成,才能执行下一个任务。
那么如果实现在特定情况下触发函数呢?这就需要用于事件循环了
JS如何实现异步的
1)JS是解释型语言。所以JS是需要在宿主环境中才能运行的。而宿主环境是多线程的。
2)JS是单线程语言。所以宿主环境分配一个线程来解析运行JS代码。
3)宿主环境再利用其他线程来辅助完成其他的一些异步功能。同时创建相关任务队列,用来存储当异步功能完成后要调用的回调函数。
4)最后再循环将这些回调函数放入到JS引擎中的执行栈中,进行调用。
所以:JS的异步功能,是宿主环境的各线程相关联合实现的。而这整个过程可以称为事件循环
不同的宿主环境对JS的事件循环的实现是不同的
JS引擎运行JS代码涉及的几个概念
- 执行栈
- 执行上下文
宿主环境实现事件循环涉及的几个概念
- 事件注册表 event table
- 任务队列 task queue
- 微任务队列
- 宏任务队列
任务分类
- 同步任务
- 异步任务
- 宏任务(在ES5版本中,异步任务只有宏任务,都放在任务队列中)
- 微任务(在ES6版本后,异步任务中添加了微任务,此后异步任务就有了两种)
JS中的任务指的是什么?
JS中的任务通常指的是需要通过代码完成的操作或功能。这些任务可以是简单的计划操作,也可以是复杂的功能;可以是一行代码,也可以是一个函。
浏览器环境中的“事件循环”实现
异步任务中如何产生宏任务/微任务?
- 宏任务
- 全局作用域(script整体代码) :可以将整个script代码看成一个宏任务。算默认宏任务,在执行此任务时,再触发其他异步任务,并注册事件信息。
- setTimeout
- setInterval
- 微任务
- promise 的 then 方法
- async 方法中,await 语句行后面的代码
- window.queueMicroTask(fn) 的 fn 方法 中
- new window.MutationObserve(fn) 的方法中
个人总结的执行顺序图解法:
图解法中的几条规则:
- JS一开始是执行全局同步代码,后面再开始异步代码执行;但在“事件循环”中,可以将全局同步代码看作成一个默认执行的宏任务。所以在执行图中,同步代码作为宏任务列表的第一个宏任务。
- 所有的宏任务都是独立,隔离的,不存在上下级关系,只有进入宏任务列表的先后关系。所以构建了一个宏任务列表,宏任务队列为事件循环的第一层循环。
- 宏任务中的同步任务和微任务之间存在上下级关系,根据这些关系可以构建一棵以宏任务为根节点的任务树。不需要关系宏任务,它会添加到宏任务列表中
图解法评分分两个步骤:
- 构建执行图
- 创建一个宏任务列表;此列表为事件循环的第一层循环
- “script全文宏任务”作为宏任务队列的第一个任务,也是默认执行的宏任务(其实就是最外层的同步代码),可命名为 h0 。
- 在执行同步代码时,若遇到宏任务,则在列表的后面假加任务,可依次命名为 h1, h2, … , hn。
- 创建宏任务的下级树;将宏任务中的同步任务和微任务以上下级关系构建一棵任务树。
- 以宏任务为任务树的根节点。
- 将宏任务中所有的同步任务的输出写在此节点上,可用 t 标识了点。如:t : 1,3,4
- 若遇到微任务,则新建一个微任务节点,以“w+次序“标识节点名;
- 若微任务中只有同步任务或只有一个微任务,则可将输出直接写上,如:w0 : 5,6
- 若同时存在同步/微任务,可分别创建节点,并写上输出。
- 若微任务中还有下级微任务,则可继续向下创建节点。
- 若有多个宏任务,则重复上一步骤。(注意:在之前代码的执行过程中,可添加宏任务)
- 创建一个宏任务列表;此列表为事件循环的第一层循环
2)根据任务节点图得出最终输出;
- 根据宏任务列表自上而上,从默认任务开始;
- 从宏根节点开始,向下级记录输出,每层节点中自上向下记录输出。
- 一棵宏任务树记录完成后,再向后开始新的宏任务树的输出记录
- 最终得出整段代码的执行顺序输出。
先上几个示例-用图解法
一、示例1
// 默认宏任务 :h0 ----- 同步任务
console.log("0");
setTimeout(function() { // 宏任务1 : h1
console.log("1");
new Promise(function(resolve, reject) {
console.log("2");
resolve();
}).then(() => {
console.log("3");
});
}, 0);
new Promise(function(resolve, reject) {
console.log("4"); // 同步任务
resolve();
}).then(() => {
console.log("5"); // 微任务
});
console.log("6"); // 同步任务
// 输出:0 4 6 5 1 2 3
图解
图解说明:
1)h0, h1 组成宏任务列表,若还有宏任务,则在下面添加节点
2)以 “t:” 为前缀的节点中,冒号后面的为同步任务输出。
3)以 “w:” 为前缀的节点中,冒号后面的为微任务输出。这里只有一个任务,所以就没有加序号了。
4)最后输出:先算出每个宏任务输出,然后再将每个宏任务输出按先后累加起来。
示例2
// h0 - 最外层同步任务
console.log(0)
// h1
setTimeout(() => {
// h3
setTimeout(()=>{console.log(6)},0)
console.log(1) //
var p2 = new Promise((n1, n2) => {
n1(1000)
})
p2.then(()=>{console.log(7)}) //
}, 0)
// h2
setTimeout(() => {
// h4
setTimeout(() => {console.log(2)}, 200) //
var p3 = new Promise((n1, n2) => {
n1(1000)
})
p3.then(()=>{console.log(8)})//
console.log(2)//
}, 0)
var p1 = new Promise((n1, n2) => {
n1(1000)
})
p1.then(() => {console.log(3)}) //
console.log(5) //
// 输出:4 5 3 1 7 2 8 6 2
图解-执行顺序
示例3
setTimeout(() => { // ----------- h1
console.log(0);
});
new Promise(resolve => {
console.log(1);
setTimeout(() => { // ----------- h2
resolve(); //这里!!!!!!!
var p1=new Promise((n1,n2)=>{n1(20)})
p1.then(() => console.log(2));
console.log(3);
setTimeout(()=>{console.log(9)},0) // ----------- h3
});
new Promise((n1,n2)=>{n1(20)}).then(() => console.log(4));
}).then(() => { //这里的then函数要等resolve()执行后才能执行 一点注意!!!
console.log(5);
var p2=new Promise((n1,n2)=>{n1(20)})
p2.then(() => console.log(8));
setTimeout(() => console.log(6)); // ----------- h4
});
console.log(7);
// 输出: 1 7 4 0 3 5 2 8 9 6
示例4
console.log(1)
setTimeout(() => {
setTimeout(()=>{console.log(2)},0)
console.log(3)
var p2 = new Promise((n1, n2) => {
n1(1000)
})
p2.then(()=>{console.log(4)})
}, 0)
setTimeout(() => {
setTimeout(() => {console.log(5)}, 200)
var p3 = new Promise((n1, n2) => {
n1(1000)
})
p3.then(()=>{console.log(6)})
console.log(7)
}, 0)
var p1 = new Promise((n1, n2) => {
n1(1000)
})
p1.then(() => {console.log(8)})
console.log(9)
// 输出 : 1 9 8 3 4 7 6 2 5
Nodejs环境中的“事件循环”实现
异步任务中如何产生宏任务/微任务?
- 宏任务
- script全文。
- setTimeout
- setInterval
- setImmediate,浏览器中没有
- 微任务
- promise 的 then 方法
- async 方法中,await 语句行后面的代码
- queueMicroTask(fn) 的 fn 方法 中
- new MutationObserve(fn) 的方法中
- process.nextTick,浏览器中没有
执行顺序:
- 执行默认宏任务(script全文代码) - 在此过程中添加其下级任务到队列中
- 循环执行 nextTick微任务队列
- 循环执行 其他微任务列表
- 再重复上面1,2,3 这3个步骤。不过不是执行默认宏任务,则是新添加的宏任务。
- 当所有其他宏任务执行完毕后,再循环执行 setImmediate 队列中的宏任务。
示例1(包括 setTimeout宏任务、nextTick微任务,普通微任务)
console.log('1');
async function async1() {
console.log('2');
await async2();
console.log('3');
}
async function async2() {
console.log('4');
}
process.nextTick(function() {
console.log('5');
})
setTimeout(function() {
console.log('6');
process.nextTick(function() {
console.log('7');
})
new Promise(function(resolve) {
console.log('8');
resolve();
}).then(function() {
console.log('9')
})
})
async1();
new Promise(function(resolve) {
console.log('10');
resolve();
}).then(function() {
console.log('11');
});
console.log('12');
// 输出 : 1 2 4 10 12 5 3 11 6 8 7 9
示例2
包括:setTimeout宏任务,setImmediate宏任务,process.nextTick微任务,promise.then微任务
console.log('1'); // 同1
setTimeout(function () { // 宏1
console.log('2'); // 宏1-同1
process.nextTick(function () { // 宏1-n微1
console.log('3');
})
new Promise(function (resolve) {
console.log('4'); // 宏1-同2
resolve();
}).then(function () {
console.log('5') // 宏1-微1
})
})
new Promise(function (resolve) {
console.log('6'); // 同2
resolve();
}).then(function () { // 微1
console.log('7')
})
process.nextTick(function () { // n微1
console.log('8'); //
})
setImmediate(() => { // 宏2
console.info('9') // 主线程和事件队伍的函数执行完成之后立即执行 和setTimeOut(fn,0)差不多
})
new Promise(function (resolve) {
console.log('10'); // 同3
resolve();
}).then(function () { // 微2
console.log('11')
})
setTimeout(function () { // 宏3
console.log('12');
setImmediate(() => {
console.info('13')
})
process.nextTick(function () {
console.log('14')
})
new Promise(function (resolve) {
console.log('15');
resolve();
}).then(function () {
console.log('16')
})
})
process.nextTick(function () { // n微2
console.log('17');
})