# js 事件循环机制
# JAVASCRIPT 为什么是单线程的?
- 作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM,决定了 js 只能为单线程
- 比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
TIP
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质
# 什么是异步
什么是异步
代码在执行过程中,会遇到一些无法立即处理的任务,比如以下;
- 计时完成后需要执行的任务 -- setTimeout、setInterval
- 网络通信完成后需要执行的任务 -- XHR、Featch
- 用户操作后需要执行的任务 -- addEventListener
如果让渲染线程等待这些任务的执行时机没达到、就会导致主线程长期处于【阻塞】的状态,从而导致浏览器【卡死】
# 为什么需要异步?
单线程只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞,不向下执行。会造成页面卡死现象。
面试题:如何理解js中的异步?
- Js是一门单线程语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。而渲染主线程又承担着诸多工作,例如渲染页面【布局、计算样式、处理图层】、事件处理、执行js、回调、解析HTML等等。 如果使用同步的方式,就极有可能使主线程,产生阻塞,从而使消息队列中的很多其他任务无法执行。这样一来主线程中就会因为阻塞浪费时间。 导致页面无法及时更新,造成页面卡顿的现象。
- 所以浏览器采用异步的方式避免,具体做法就是当某些任务例【计时器、网络请求、事件监听】主线程将任务交给其他线程去处理,自身立即结束任务的执行, 从而去执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调用执行。
- 在这种异步的模式下,浏览器不阻塞,最大限度的保证了主线程的流畅运行。
# 单线程又是如何实现异步的呢?
通过事件循环实现异步
::: tip 面试:简述事件循环
- 事件循环又叫消息循环,是浏览器主线程的工作方式。
- 在Chrome源码中, 开启了一个不会结束的for循环,每次循环从消息队列中取出第一个任务,而其他线程只需要在合适的时间将任务添加到队列末尾即可。
- 过去把消息队列分为宏队列和微队列,根据W3c最新的解释来看,每个任务有不同的类型,同类型的任务必须在同一个队列中,不同的任务可以属于不同的队列。
- 不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务,但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行 :::
# 宏任务&微任务
宏任务(macro-task) | 微任务(micro-task) |
---|---|
整体的script代码 | promise.[ then/catch/finally ]((非new Promise)) |
setTimeout | process.nextTick(Node.js 环境) |
setInterval | MutaionOberver(浏览器环境) |
setImmediate(Node.js 环境) | Object.observe |
IO操作 | x |
UI交互事件 | x |
postMessage | x |
MessageChannel | x |
# 宏任务(macro-task)
整体代码 script、setTimeOut、setInterval、setImmediate
TIP
setImmediate()只有一个的时候永远最后执行,多个的时候每次循环只执行一个
# 微任务(micro-task)
Promise 的回调(promise.then) 、async/await、process.nextTick()永远最先执行,存在多个的时候先进先出
# 单个 setImmediate()
setTimeout(function timeout() {
console.log("setTimeout");
}, 0);
setImmediate(function A() {
console.log(1);
});
process.nextTick(function A() {
console.log(2);
});
new Promise(function(resolve) {
console.log("3");
resolve();
}).then(function() {
console.log("4");
});
// 输出 3 2 4 setTimeout 1
分析执行过程
- 1、
setTimeout
异步任务进入event table
- 2、
setImmediate
异步任务进入event table
- 3、
process.nextTick
异步任务进入event table
- 4、
promise
同步任务执行console.log(3)
,.then回调
异步任务 进入event table
- 5、此时同步任务执行完毕,优先执行微任务 process.nextTick 该任务被推入
event loop
,经判断 主线程为空,则进入主线程执行。 - 6、
then()回调函数
为微任务 等待process.nextTick
结束 执行 - 7、执行宏任务
setTimeout
- 8、
setImmediate
最后执行
# 多个 setImmediate()
setTimeout(function timeout() {
console.log("setTimeout");
setImmediate(function() {
console.log(9);
});
}, 0);
setImmediate(function() {
console.log(1);
setImmediate(function() {
console.log(7);
});
});
process.nextTick(function() {
setImmediate(function() {
console.log(6);
});
console.log(2);
});
new Promise(function(resolve) {
console.log("3");
resolve();
}).then(function() {
console.log("4");
});
setImmediate(function() {
console.log(5);
});
// 输出结果: 3 2 4 setTimeout 1 5 6 9 7
分析执行过程
- 1、
setTimeout
宏任务进入event table
记作setTimeout1
- 2、
setImmediate
微任务 进入event table
记作setImmediate1
- 3、
process.nextTick
微任务 进入event table
记作process1
- 4、
promise
执行 console.log(3)then()回调 微任务
进入event table
记作promise1
- 5、
setImmediate
微任务 进入event table
记作setImmediate2
event table:宏任务 微任务 setTimeout1 setImmediate1 process1 promise1 setImmediate2
第一轮执行结束输出 3
第二轮开始
- 1、
process.nextTick
在第一轮执行过程中已经 存在event table
了,由于它在循环开始执行,故优先执行 process1
- 1、
setImmediate
微任务进入event table
记作setImmediate3
- 2、 执行 console.log(2)
- 1、
- 2、执行当前
event table
中的 微任务 promise1 的 then()回调函数console.log(4)
- 3、执行当前
event table
中的 宏任务setTimeout1
- 1、 执行 console.log(‘setTimeout’)
- 2、
setImmediate
进入 event lable记作
setImmediate4`
- 4、
setImmediate 最后执行
故执行setImmediate1
- 1、执行 console.log(1)
- 2、
setImmediate
进入event lable
记作setImmediate5
- 5、event table 中还有 setImmediate2 执行 console.log(5)
宏任务 微任务 setImmediate3 setImmediate4 setImmediate5
第二轮 执行结束 结果为: 2 4 setTimeout 1 5; 进入第三轮
- 1、执行 setImmediate3 中 console.log(6)
- 2、执行 setImmediate4 中 console.log(9)
- 3、执行 setImmediate5 中 console.log(7)
第三轮 的执行结果为 6 9 7 至此 该程序执行完毕
# 执行顺序
WARNING
- 1、渲染主线程
- 2、微队列
- 3、延时队列
- 4、交互队列
- 1、同步任务
- 2、promise.nextTick (Node 独有)
- 3、微任务
- 4、Dom 渲染
- 5、宏任务
- 6、setImmediate (Node 独有)