参考文章
最后一次搞懂 Event Loop
从一道面试题谈谈对EventLoop的理解
setTimeout+Promise+Async输出顺序?很简单呀!
要想引出EventLoop,我们先从一句话开始
1.JS的运行机制
有人说:“JS是一门单线程的非阻塞脚本语言”。
1.1 JS为什么是单线程语言,那它是怎么实现异步编程(非阻塞)运行的?
先在脑海里记住上面两个问题,我们往下走~
2.进程和线程
进程:进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程:线程是 CPU 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位)
线程依赖进程,一个进程可以有一个或者多个线程,但是线程只能是属于一个进程。
2.1 JS的单线程
js的单线程指的是javaScript引擎只有一个线程。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
这就决定了它只能是单线程的,否则会带来很复杂的同步问题。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。为了防止主线程的阻塞,JavaScript 有了 同步 和 异步 的概念。
2.2 同步和异步
- 同步:你给朋友打电话咨询一件事,朋友说他查查,你不挂电话在等待,朋友把查到的结果告诉你,这期间你不能做自己的事情
// 下面这段段代码首先会弹出 alert 框,如果你不点击 确定 按钮,所有的页面交互都被锁死,并且后续的 console 语句不会被打印出来。
alert('Yancey');
console.log('is');
console.log('the');
console.log('best');
- 异步:你给朋友打电话咨询一件事,朋友说他查查,回头告诉你,你把电话挂了,先去做自己的事情
2.3 继续学习几个概念~
先来解释上图中出现的几个单词所要表达的含义。
==Heap(堆)、Stack(栈)、Queue(队列)、Event Loop(事件轮询)==
2.3 程序中的堆栈队列
Heap 堆
堆, 是一种动态存储结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为最大堆,一种为最小堆,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。 堆是线性数据结构,相当于一维数组,有唯一后继。
Stack 栈
栈在程序中的设定是限定仅在表尾进行插入或删除操作的线性表。 栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。栈是只能在某一端插入和删除的特殊线性表。
Queue 队列
队列特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。
进行插入操作的端称为队尾,进行删除操作的端称为队头。 队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出
2.4 JS中的堆栈队列
堆
堆, 动态分配的内存,大小不定也不会自动释放,存放引用类型,指那些可能由多个值构成的对象,保存在堆内存中,包含引用类型的变量,实际上保存的不是变量本身,而是指向该对象的指针。可以简单理解为存储代码块。
堆的作用:存储引用类型值的数据
let obj = {
name: 'Even',
remark: '登峰造极'
}
let func = () => {
console.log('hello world')
}
栈(执行栈)
js中的栈准确来将应该叫调用栈(ECStack),会自动分配内存空间,会自动释放,存放基本类型,简单的数据段,占据固定大小的空间。
栈的作用:存储基本类型值,还有一个很要的作用。提供代码执行的环境。
队列(任务队列,事件队列)
js中的队列可以叫做任务队列或异步队列,任务队列里存放各种异步操作所注册的回调,里面分为两种任务类型,宏任务(macroTask)和微任务(microTask)。
终于引出了Event Loop(事件轮询)~
3. Event Loop
事件轮询就是解决javaScript单线程对于异步操作的一些缺陷,让 javaScript做到既是单线程,又绝对不会阻塞的核心机制,是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制。
event-loop-processing-model 规范定义了 Event Loop 的循环过程:
3.1 Event Loop 的循环过程
一个Event Loop只要存在,就会不断执行下边的步骤:
- 在tasks(任务)队列中选择最老的一个task,用户代理可以选择任何task队列,如果没有可选的任务,则跳到下边的microtasks步骤。
- 将上边选择的task设置为正在运行的task。
- Run: 运行被选择的task。
- 将Event Loop的currently running task变为null。
- 从task队列里移除前边运行的task。
- Microtasks: 执行microtasks任务检查点。(也就是执行microtasks队列里的任务)
- 更新渲染(Update the rendering):可以简单理解为浏览器渲染...
- 如果这是一个worker event loop,但是没有任务在task队列中,并且WorkerGlobalScope对象的closing标识为true,则销毁Eveent Loop,中止这些步骤,然后进行定义在Web workers章节的run a worker。 9.返回到第一步。
==这么多字 相当于不讲人话 我们不看也罢!!== 我们直接看大佬们总结概括之后的结论。
概括来说:
- Eveent Loop会不断循环的去取tasks队列的中最老的一个task(可以理解为宏任务)推入栈中执行,并在当次循环里依次执行并清空microtask队列里的任务。
- 执行完microtask队列里的任务,有可能会渲染更新。(浏览器很聪明,在一帧以内的多次dom变动浏览器不会立即响应,而是会积攒变动以最高60HZ(大约16.7ms每帧)的频率更新视图)
上代码!
console.log(1) // 同步
setTimeout(() => {
console.log(2) // 异步
}, 2000);
console.log(3) // 同步
setTimeout(() => {
console.log(4) // 异步
}, 0);
console.log(5) // 同步
输出:1 3 5 4 2
前面说了,等所有同步代码都执行完,再从事件队列里依次执行所有异步回调函数。
其实事件队列也是一个小团体,人家也有自己的规则。为什么事件队列里需要有自己的规则呢?要不你先想想为什么学校里的社团里要有自己的规则要分等级,是因为有的人能力强有的人能力弱,所以也就有了等级的高低。其实事件队列也一样,事件队列是用来存异步回调的,但是异步也分类型啊,异步任务分为宏任务和微任务,并且==微任务执行时机先于宏任务==
3.2 宏任务和微任务
3.2.1 我们先看下宏任务和微任务分别都有哪些?
宏任务 (macrotask)
- script(整体代码)
- setTimeout/setInterval
- setImmediate(Node环境)
- requestAnimationFrame
微任务 (microtask)
- Promise的then()、catch()、finally()里面的回调
- process.nextTick (Node 环境)
3.2.2 执行流程
3.3 光说不练假把式!
习题一
console.log(1)
setTimeout(() => {
console.log(2)
Promise.resolve().then(() => {
console.log(3)
})
});
console.log(4)
new Promise((resolve,reject) => {
console.log(5)
resolve()
}).then(() => {
console.log(6)
setTimeout(() => {
console.log(7)
})
})
console.log(8)
解析:
console.log(1) // 同步
setTimeout(() => { // 异步:宏任务 setTimeout1
console.log(2)
Promise.resolve().then(() => { // 异步:微任务 then2
console.log(3)
})
});
console.log(4) // 同步
new Promise((resolve,reject) => {
console.log(5) // 同步
resolve()
}).then(() => { // 异步:微任务 then1
console.log(6)
setTimeout(() => { // 异步:宏任务 setTimeout2
console.log(7)
})
})
console.log(8) // 同步
- 执行外层同步任务:1 4 5 8;
- 执行到 setTimeout 放到宏任务队列,此时宏任务队列 setTimeout1;
- 执行到 then 放到微任务队列,微任务队列 then 1;
- 执行 then1 :6 ,碰到宏任务 setTimeout2,此时宏任务队列 setTimeout1,setTimeout2;
- 执行 setTimeout1 :2 ,碰到微任务 then2 ,此时宏任务队列 setTimeout2,微任务队列 then2;
- 执行 then2 :3;
- 执行 setTimeout2 : 7;
所以最终输出: 1 4 5 8 6 2 3 7
再来一道测试一下!
习题二
setTimeout(() => {
console.log('A');
}, 0);
var obj = {
func: function() {
setTimeout(function() {
console.log('B');
}, 0);
return new Promise(function(resolve) {
console.log('C');
resolve();
});
},
};
obj.func().then(function() {
console.log('D');
});
console.log('E');
- 第一个 setTimeout 放到宏任务队列,此时宏任务队列为 ['A']
- 接着执行 obj 的 func 方法,将 setTimeout 放到宏任务队列,此时宏任务队列为 ['A', 'B']
- 函数返回一个 Promise,因为这是一个同步操作,所以先打印出 'C'
- 接着将 then 放到微任务队列,此时微任务队列为 ['D']
- 接着执行同步任务 console.log('E');,打印出 'E'
- 因为微任务优先执行,所以先输出 'D'
- 最后依次输出 'A' 和 'B'
所以最终输出:C E D A B
习题三
当遭遇 Async/Await 时又会变成什么样?
async function async1() {
console.log(1);
await async2();
console.log(2);
}
async function async2() {
console.log(3);
}
console.log(4);
setTimeout(function () {
console.log(5);
});
async1()
new Promise(function (resolve, reject) {
console.log(6);
resolve();
}).then(function () {
console.log(7);
});
console.log(8);
转化为
console.log(4); // 同步
setTimeout(function () {
console.log(5); // 异步:宏任务 setTimeout
});
// async1函数可转换成
console.log(1) // 同步
new Promise((resolve, reject) => {
console.log(3) // 同步
resolve()
}).then(() => { // 异步:微任务 then1
console.log(2)
})
// async1函数结束
new Promise(function (resolve, reject) {
console.log(6); // 同步
resolve();
}).then(function () { // 异步:微任务 then2
console.log(7);
});
console.log(8); // 同步
这样再去按照之前的解题思路走一遍
正确答案 4 1 3 6 8 2 7 5
最后一题
async function async1() {
console.log('A')
await async2()
console.log('B')
}
async function async2() {
console.log('C')
return new Promise((resolve, reject) => {
resolve()
console.log('D')
})
}
console.log('E')
setTimeout(() => {
console.log('F')
}, 0);
async1()
new Promise((resolve) => {
console.log('G')
resolve()
}).then(() => {
console.log('H')
}).then(() => {
console.log('I')
})
console.log('J')
正确输出顺序(下方拖动鼠标查看答案)↓
4. 推荐阅读学习
Tasks, microtasks, queues and schedules
深入介绍了MutationObserver在EventLoop的应用场景,感兴趣的可以去了解学习