代码的分类
我们将每一句要执行的 js 代码当做一个任务,则 js 代码可以按照其执行方式的不同,按下图分类
- 同步任务:立即执行的代码
- 异步任务:延迟执行的代码
-
微任务:被放入微任务队列(micro task queue)中等待执行的代码
因为Promise、async 、await 都是 ES6 语法定义的
-
宏任务:被放入 Web APIs 中等待执行的代码
因为 setTimeout 、setInterval、ajax、Dom事件都是浏览器定义的
-
不同类型代码执行的顺序和过程
1. 同步任务
- 将其放入调用栈(Call Stack)中
- 执行该段代码
- 将其从调用栈中移除
除 Promise、async 、await、setTimeout 、setIntervall、ajax、Dom事件之外的代码都是可立即执行的同步任务
2. 微任务
-
将其放入微任务队列中等待执行
-
待所有同步任务执行完毕,开始按微任务队列依次执行微任务
将第1个进入微任务队列的微任务放入调用栈中,执行微任务内的代码,将其从调用栈中移除,再将第2个进入微任务队列的微任务放入调用栈中,执行,移除…… 以此类推,直到清空微任务队列。
3. DOM 渲染
微任务队列清空后,便暂停 js 代码的执行,开始尝试渲染 DOM, 若没有 DOM操作,则跳过此步。
因 JS 可修改 DOM 结构 , 所以 js 代码的执行和 DOM 渲染必须共用一个线程,这便导致 js 代码的执行和 DOM 渲染无法同时进行。
4. Event Loop 事件轮询
DOM渲染完毕后,便触发Event Loop,开始事件轮询
5. 宏任务
-
将其放入 Web APIs 中,并开始计时
-
计时结束后,将其放入回调队列(Callback Queue)
-
回调队列中的宏任务,依次被事件轮询到
将第1个进入回调队列的宏任务放入调用栈中,执行宏任务内的代码,将其从调用栈中移除,再将第2个进入回调队列的宏任务放入调用栈中,执行,移除…… 依此类推,直到清空回调队列。
最终 js 代码的执行顺序
因不同类型的代码可能层层嵌套,所以最终 js 代码的执行顺序可能非常复杂,但总的运行方式,如上文所言,根据代码的不同,将其放入不同的队列或栈中,然后依次执行,核心要领在于
- 依次执行可执行的同步任务,直到清空调用栈
- 依次执行微任务队列中的微任务,直到清空微任务队列
- 暂停 js 代码的执行,尝试渲染DOM,若无DOM操作,则直接进入第4步,若有DOM操作,则待DOM渲染完成,进入第4步
- 开始事件轮询,依次执行回调队列中的宏任务(事件轮询会一直进行下去,一旦有新的宏任务计时结束进入回调队列,就会被送去调用栈执行)
简单概括如下图所示:
自测题
console.log(100)
// 宏任务
setTimeout(() => {
console.log(200)
})
// 微任务
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
答案 100 400 300 200
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
答案
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
解析:详见代码注释
// 定义函数,先跳过
async function async1() {
console.log("async1 start"); // 2
await async2(); // 先执行 async2() , 进入 async2 函数内
// await 后的代码都是微任务,将其放入微任务队列 --- 微任务 1
console.log("async1 end"); // 6
}
// 定义函数,先跳过
async function async2() {
console.log("async2"); // 3
}
// 同步任务
console.log("script start"); // 1
// 宏任务 1
setTimeout(function () {
console.log("setTimeout"); // 8
}, 0);
// 同步任务
async1(); // 进入 async1 函数内
// 同步任务 -- Promise 函数体内的代码会立刻执行
new Promise(function (resolve) {
console.log("promise1"); // 4
resolve(); // Promise 状态变为 resolved , 立即触发了 then 函数
}).then(function () {
// then 函数是个微任务,将其放入微任务队列 --- 微任务 2
console.log("promise2"); // 7
});
// 同步任务
console.log("script end"); // 5
// 同步任务执行完毕,开始执行微任务
// 依次执行微任务1 和 微任务 2
// 最后执行宏任务