任务队列( Event Queue )
同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入任务队列。主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是 Event Loop (事件循环)。
在事件循环中,每进行一次循环操作称为tick,每一次 tick 的任务处理模型关键的步骤可以总结如下:
- 1.在此次 tick 中选择最先进入队列的任务,如果有则执行(一次)
- 2.检查是否存在 Microtasks ,如果存在则执行,直至清空Microtask Queue
- 3.更新 render
- 4.主线程重复执行上述步骤
什么是宏任务和微任务
宏任务 macrotasks:
- setTimeout
- setInterval
- requestAnimationFrame
- I/O
- UI 交互事件
- setImmediate (Node.js 环境)
- script (整体代码)
requestAnimationFrame 和 setInterval对比说明
微任务 microtasks:
- process.nextTick (Node.js 环境)
- Promises
- Object.observe (实时监测js中对象的变化)
- MutationObserver (监视对DOM树所做更改)
microtask会在两种情况下执行:
- 任务队列(macrotask = task queue)回调后执行,前提条件是当前没有其他执行中的代码。
- 每个task末尾执行。
另外在处理microtask期间,如果有新添加的microtasks,也会被添加到队列的末尾并执行。
结合来看事件循环
执行 宏任务 => 微任务的Event Queue => 宏任务的Event Queue
- 事件循环从宏任务开始;
- 一个事件循环有一个或者多个任务队列;
- 每个事件循环都有一个 microtask 队列;
- macrotask队列就是我们常说的任务队列(task queue),microtask队列不是任务队列;
- 一个任务可以被放入到 macrotask 队列,也可以放入 microtask 队列;
- 当一个任务被放入 microtask 或者 macrotask 队列后,准备工作就已经结束,这时候可以开始执行任务了。
简要说明:开始 -> 取 task queue第一个task执行 -> 取microtask全部任务依次执行 -> 取task queue下一个任务执行 -> 再次取出microtask全部任务执行 -> ... 这样循环往复
Promise运行顺序总结:
- promise的构造函数是同步执行,promise.then中的函数是异步执行。
- 构造函数中的 resolve 或 reject 只有第一次执行有效,多次调用没有任何作用。promise状态一旦改变则不能再变。
- promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。
- 如果在一个then()中没有返回一个新的promise,则 return 什么下一个then就接受什么,如果then中没有return,则默认return的是 undefined.
- then()的嵌套会先将内部的then()执行完毕再继续执行外部的then();
- catch和then的连用,如果每一步都有可能出现错误,那么就可能出现catch后面接上then的情况。如果在catch中也抛出了错误,则后面的then的第一个函数不会执行,因为返回的 promise状态已经为rejected了
async、await执行顺序
使用 async 定义的函数,当它被调用时,它返回的其实是一个 Promise 对象。
当这个 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值。
await是一个让出线程的标志 。await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面js栈的代码,等本轮事件循环执行完了之后又会跳回到async函数中等待await后面表达式的返回值,如果返回值为非promise则继续执行async函数后面的代码,否则将返回的promise放入promise队列。 点击查看
示例
示例一
setTimeout(function(){
console.log(1)
},0);
new Promise(function(resolve){
console.log(2)
for( var i= 100000; i > 0 ; i-- ){
i==1 && resolve()
}
console.log(3)
}).then(function(){
console.log(4)
});
console.log(5);
1、遇到 setTimeout,其回调函数console.log(1) 被分发到宏任务 Event Queue (任务队列) 中
2、遇到 Promise,先执行 console.log(2),输出 2;下一步执行for循环,即使for循环要累加到10万,也是在执行栈里面,等待for循环执行完毕以后,将Promise的状态从fulfilled切换到resolve,随后把回调函then里面的 console.log(4) 推到 微任务队列 里面去。接下来马上执行马上 console.log(3) ,输出 3。
3、遇到 console.log(5),输出 5
4、第一轮执行完毕,没有正在执行的代码。符合上面讲的 microtask 执行条件,因此会将 microtask 中的任务优先执行,因此执行 console.log(4) ,输出 4
5、最后还剩 macrotask 里的setTimeout放入的函数 console.log(1) 最后执行,输出 1
6、至此,所有的都队列都已清空,执行完毕
// 2 3 5 4 1
示例二
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
1、整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start
2、遇到 setTimeout,其回调函数被分发到宏任务 Event Queue (任务队列) 中
3、遇到 Promise,其 then函数被分到到 微任务队列 中,记为 then1,之后又遇到了 then 函数,将其分到 微任务队列 中,记为 then2
4、遇到 console.log,输出 script end
5、第一轮执行完毕,没有正在执行的代码。符合上面讲的 microtask 执行条件,因此会将 microtask 中的任务优先执行,执行 then1,输出 promise1; 接着执行 then2,输出 promise2, 微任务清空
6、检查宏任务队列, 任务队列的setTimeout放入的函数 console.log('setTimeout') 最后执行,输出 setTimeout
7、至此,所有的都队列都已清空,执行完毕
// script start
// script end
// promise1
// promise2
// setTimeout
示例三
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
1、事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 script(整体代码)任务;当遇到任务源 (task source) 时,则会先分发任务到对应的任务队列中去。所以,就和上面例子类似,首先遇到了console.log,输出 script start;
2、遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1;
3、遇到 promise,new promise 中的代码立即执行,输出 promise1, 然后执行 resolve ,遇到 setTimeout ,将其分发到任务队列中去,记为 timemout2, 将其 then 分发到微任务队列中去,记为 then1;
4、接着遇到 console.log 代码,直接输出 script end
5、第一轮结束,接着检查微任务队列,发现有个 then1 微任务,执行,输出then1, 再检查微任务队列,发现已经清空
6、开始检查宏任务队列,执行 timeout1,输出 timeout1; 接着执行 timeout2,输出 timeout2
7、至此,所有的都队列都已清空,执行完毕
// script start
// promise1
// script end
// then1
// timeout1
// timeout2
可看流程图
示例四
setTimeout(function () {
console.log("1");
}, 0);
async function async1() {
console.log("2");
const data = await async2();
console.log("3");
return data;
}
async function async2() {
return new Promise((resolve) => {
console.log("4");
resolve("async2的结果");
}).then((data) => {
console.log("5");
return data;
});
}
async1().then((data) => {
console.log("6");
console.log(data);
});
new Promise(function (resolve) {
console.log("7");
// resolve()
}).then(function () {
console.log("8");
});
1、事件循环从宏任务 (macrotask) 队列开始,遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1;
2、遇到async1(),立即执行,输出 输出 2 ;
3、执行await 右边的async2(),遇到promise,new promise 中的代码立即执行,输出 4, 然后执行 resolve ,将其 then 分发到微任务队列中去,记为 then1,resolve处理的data为 "async2的结果" ;
4、跳出async1,先执行后面的内容,遇到promise,一样立即执行new promise中的代码 输出 7,没有resolve,then失效( 只有resolve()之后,传递给then时,then中的函数才会被推到微任务中 );
5、第一轮结束,接着检查微任务队列,首先遇到then1, 输出 5,返回值data 为"async2的结果" ;
6、async1()中的await执行完毕,现在执行async1()后面的代码, 输出 3, 函数最后有返回值是Promise对象,可以用then方法指定下一步的操作,resolve处理的data为 "async2的结果",记为then2;
7、执行then2 输出 6,下一步输出data 输出 async2的结果;再检查微任务队列,发现已经清空
8、开始检查宏任务队列,执行 timeout1,输出 1;
9、至此,所有的都队列都已清空,执行完毕
结果
tips
JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。