Appearance
JS异步和事件循环
何为异步?
代码在执行的过程中,会遇到一些无法立即处理的任务,例如:
- 计时完成后需要执行的任务 --
setTimeout
、setInterval
- 网络通信完成后需要执行的任务 --
XHR
、Fetch
- 用户操作后需要执行的任务 --
addEventListener
如果让渲染主线程等待这些任务的时机到达,就会导致主线程长期处于阻塞
状态,从而导致浏览器卡死
。
基于以上的问题,就出现了JS的事件循环机制。
事件循环
事件循环的机制就在渲染主线程中执行的,目的是为了处理异步任务。又被称作消息循环,是浏览器渲染主线程的工作方式。
- 最开始的时候,渲染主线程会进入一个无限循环。
- 每一次循环会检查消息队列中是否有任务存在,如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
- 其他所有线程(包括其他进程的线程),可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务。
整个过程,被称为事件循环(消息循环)。
如何理解JS异步
JS是一门单线程语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。渲染主线程承担着诸多的工作,例如渲染页面、执行JS等等。
如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死的现象。
所以浏览器采用异步的方法来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
经典问题
js
var h1 = document.querySelector('h1');
var btn = document.quertSelector('button');
function delay(duration) {
var start = Date.now();
while (Date.now() - start < duration) {}
}
btn.onclick = function () {
h1.textContent = 'xdd';
delay(3000);
}
点击按钮后,3s过后页面上的元素才会重新绘制成xdd。
用事件循环的机制来说明:
- 用户点击按钮,执行方法,将h1.textContent 设置为'xdd'。
- 修改html的元素,会导致页面重新渲染,此时需要开启一个任务去做渲染的操作,将任务添加到事件队列中。
- 执行3s的死循环,按钮的点击事件执行完毕。
- 渲染主线程去事件队列中查找任务,取出刚刚生成的渲染任务,进行渲染。
优先级
任务是没有优先级的,在消息队列中先进先出。但消息队列是有优先级的。
根据w3c最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
- 浏览器必须准备好一个微队列,微队列的任务优先所有其他任务执行。
https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
在目前chrome的实现中,至少包含了下面的队列:
- 延时队列: 用于存放计时器到达后的回调任务,如定时器,优先级[中]
- 交互队列: 用于存放用户操作后产生的事件处理任务,如用户点击事件/键盘事件等,优先级[高]
- 微队列: 用于存放需要最快执行的任务,如promise,优先级[最高]
总结
在浏览器的地址栏中输入url并键入回车。
首先,网络进程会开启一个线程,发送网络请求并接收响应。一旦服务器响应到达,网络线程将接收到的数据传递给渲染引擎线程。
然后,渲染引擎线程会解析接收到的响应文件,一般为html文件。解析html的过程中,遇到link标签链接着其他的资源文件,则会开启新的网络线程去请求对应的资源文件。遇到script标签后,会开启JS线程,创建一个任务,放入到事件循环的执行栈中并执行,因为渲染线程与JS引擎线程互斥,所以渲染线程此时会挂起,这也是为什么JS的执行会阻塞浏览器渲染的原因之一。然后开始对执行栈中的任务进行处理。
由于JS是单线程,代码的执行方式是解释执行,即从上到下从左到右。JS是在渲染进程中的JS引擎线程中执行。在执行的过程中,如果碰到同步的代码,则正常执行。如果碰到异步的代码,则会在队列中创建对应的任务。
事件循环中的队列,称为消息队列,也叫任务队列。该队列里的任务都处于等待执行的状态。当执行JS的过程中碰到异步的任务,会将任务放入到对应的异步处理模块中执行,然后执行栈不做等待,继续执行后续的代码。异步处理模块中的异步任务执行完毕后,会注册一个回调函数放入到任务队列中等待调度执行。
异步的任务也分为两种,微任务和宏任务。可以粗糙的理解为,任务队列中有两个子队列,分别为微任务队列和宏任务队列。当执行栈中的任务执行完毕后,通过事件循环的机制来到任务队列中查看是否有任务等待调度,此时会优先查看微任务队列,如果存在等待调度的任务,则会将整个微任务队列里的任务取出放入到执行栈中执行。如果没有则会查看宏任务队列中是否有等待调度的任务,如果有则会根据排队顺序取出第一个任务放入到执行栈中执行。这里有一个重点就是,微任务队列会全部取出,而宏任务队列是一个一个排队取出。
当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。
题目1:
js
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
})
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0)
最后输出结果是Promise1,setTimeout1,Promise2,setTimeout2
题目2:
js
async function async1 () {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2 () {
console.log('async 2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise((resolve) => {
console.log('promise 1');
resolve();
}).then(() => {
console.log('promise 2');
})
console.log('script end');
script start -> async1 start -> async 2 -> promise 1 -> script end -> async1 end -> promise 2 -> setTimeout