JavaScript的一个特点就是单线程,意味着同一时间只能做一件事。
这导致了后面的任务就要等到前面的任务完成后才能执行,如果前面的任务耗时很长,就会导致后面的任务一直处于等待中,可能带来进程阻塞的问题。
为了解决这个问题,js有两种任务的执行模式:同步模式和异步模式。
1. 同步任务与异步任务
1.1 同步任务(Synchronous)
在主线程上排队执行的任务只有前一个任务执行完毕,才能在执行下一个任务,形成一个执行栈。
1.2 异步任务(Asynchronous)
异步任务分为宏任务和微任务。
不进入主线程,而是进入任务队列。主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列中放置一个事件。
执行栈中的所有同步任务执行完毕(此时js引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
2. 任务队列和事件循环
- 所有的同步任务都在主线程上执行,形成一个执行栈。
- 除了主线程之外,还存在一个任务队列,只要异步任务有了运行结果,就在任务队列中植入一个时间标记。
- 主线程完成所有任务(执行栈清空),就会读取任务队列,先执行微任务队列再执行宏任务队列。
- 重复以上三步。 只要主线程空了,就会读取任务队列,这就是js的运行机制,也被称为event loop(事件循环)
2.1 事件循环
由于主线程不断重复的获得任务、执行任务、再获取再执行,所以这种机制被叫做事件循环(Event Loop)。
Event Loop 包含两类:一类是基于 Browsing Context,一种是基于 Worker。
2.2. 任务队列
根据规范,事件循环是通过任务队列的机制进行协调的。
一个Event Loop中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;
任务队列也分为宏任务队列和微任务队列。
每个任务都有一个任务源(task source),源自同一个任务源的 task 必须需放到同一个任务队列,从不同源来的则被添加到不同队列。setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。
在事件循环中,每进行一次循环操作称为 tick ,每一次tick的任务处理模型是比较复杂的,但关键步骤如下:
- 在此次 tick 中选择最先进入队列的任务(oldest task),如果有则执行(一次)
- 检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtasks Queue
- 更新 render
- 主线程重复执行上述步骤
3. 宏任务与微任务
在异步模式下,创建异步任务主要分为宏任务和微任务两种。ES6规范中,宏任务(Macrotask)称为Task,微任务(Microtask)称为Jobs。宏任务是由宿主(浏览器、Node)发起的,而微任务由js自身发起。
注意:
- 微任务比宏任务的执行时间要早。
- 宏任务里如果有宏任务,不会执行里面的那个宏任务,而是被丢进任务队列后面,所以会最后执行。
- 同步任务->微任务->宏任务
3.1 宏任务(Macrotask)
每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得js内部 macrotask 与 DOM 任务能够有序的执行,会在一个 macrotask 执行结束后,在下一个 macrotask 执行开始前,对页面进行重新渲染。
macrotask -> 渲染 -> macrotask
3.1.1 宏任务包含
- script(整体代码)
- setTimeout
- setInterval
- Ajax
- DOM事件
- I/O
- UI交互事件
- postMessage
- MessageChannel
- setImmediate(Node.js 环境)
3.2 微任务(Microtask)
在当前 task 执行结束后立即执行的任务。在当前 task 任务后,在下一个 task 之前,在渲染之前。task -> microtask -> 渲染 -> task
在某个 macrotask 执行完成后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染之前)。
3.2.1 微任务包含
- Promise.then
Promise的状态决定Promise.then的执行顺序,若状态为pending,执行靠后;若状态为fulfilled,执行靠前。(两种相对而言)- 若
.then返回一个非Promise对象,其执行时间(变更状态的时间)会快于返回一个Promise对象。 - 参考:Promise和async/await题目解析
- async/await
- 执行完
await修饰的对象后,先执行async外的同步代码,然后再回到原处继续执行。即遇到await就阻塞,执行完 async 外面的同步代码后,再回到内部。
- 执行完
- Object.observe
- MutationObserver
- process.nextTick(Node.js 环境)
3.3 任务执行机制
在事件循环中,每进行一次循环操作称为 tick ,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
- 渲染完毕后,js 线程继续接管,开始下一个宏任务(从事件队列中获取)
3.3.1 执行顺序练习题
搜集了部分题目,仅供参考,侵删
blog.csdn.net/m0_44973790…
注意:
Promise.resolve()和new Promise()都是实例化过程,同步执行
setTimeout(function(){
console.log('setTimeout')
},0)
console.log('script start')
async function async1(){
console.log('async1 start');
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2 end')
}
new Promise(function(resolve){
console.log('promise')
for (var i = 0; i <10000; i++) {
if (i === 10) {
console.log("3")
}
i == 9999 && resolve('4')
}
resolve();
}).then(function(val){
console.log(val)
console.log('promise1')
}).then(function(res){
console.log(res)
console.log('promise2')
})
async1();
console.log('script end')
setTimeout(() => {//宏任务队列1
console.log('1');//宏任务队1的任务1
setTimeout(() => {//宏任务队列3(宏任务队列1中的宏任务)
console.log('2')
}, 0)
new Promise(resolve => {
resolve()
console.log('3')//宏任务队列1的任务2
}).then(() => {//宏任务队列1中的微任务
console.log('4')
})
}, 0)
setTimeout(() => {//宏任务队列2
console.log('5')
}, 0)
console.log('6')//同步主线程
async function test1() {
console.log("test1 begin");
const result = await test2();
console.log("result", result);
console.log("test1 end");
}
async function test2() {
console.log("test2");
}
console.log("script begin");
test1();
console.log("script end");
console.log(1)
setTimeout(()=>{
console.log(2)
Promise.resolve().then(()=>{
console.log(3)
})
}
new Promise((resolve,reject)=>{
console.log(4)
resolve(5)
}).then((data)=>{
console.log(data)
})
setTimeout(()=>{
console.log(6)
})
console.log(7)