代码的分类
我们将每一句要执行的 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步
- 开始事件轮询,依次执行回调队列中的宏任务(事件轮询会一直进行下去,一旦有新的宏任务计时结束进入回调队列,就会被送去调用栈执行)
简单概括如下图所示:
自测题 1
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 // 最后执行宏任务
自测题 2
console.log('start') setTimeout(() => { console.log('a') Promise.resolve().then(() => { console.log('c') }) }) Promise.resolve().then(() => { console.log('b') setTimeout(() => { console.log('d')}) }) console.log('end')
答案
start end b a c d