JavaScript运行时
在执行 JavaScript 代码的时候,JavaScript 运行时实际上维护了一组用于执行 JavaScript 代码的代理。每个代理由一组执行上下文的集合、执行上下文栈、主线程、一组可能创建用于执行 worker 的额外的线程集合、一个任务队列以及一个微任务队列构成。
1.事件循环(Event loops)
每个代理都是由事件循环驱动的,事件循环负责收集事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript 任务(宏任务),然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。
单线程的JS通过事件循环来实现异步。
(宏)任务 vs 微任务
宏任务:计划由标准机制来执行的任何 JavaScript。
在以下时机,宏任务会被添加到任务队列:
- 一段新程序或子程序被直接执行时。
- 触发了一个事件,将其回调函数添加到任务队列时。
- setTimeout() 或 setInterval()
注意:setTimeout()不要理解成x ms后执行,要理解成x ms后将函数添加至宏任务队列,由事件循环决定其什么时候执行。
微任务:只有当 Javascript 调用栈为空,而控制权尚未返还给代理用来驱动脚本执行环境的事件循环之前才会被执行的任务。 - Promise
- queueMicrotask(fn)
- process.nextTick()
执行顺序 - 当执行来自宏任务队列中的宏任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的首个宏任务。
- 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行,直到微任务队列为空才会停止执行。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。
function test() {
setTimeout(() => {
new Promise((resolve) => {
console.log(1);
resolve();
}).then(() => {
Promise.resolve().then(() => {
console.log(2);
})
console.log(3);
})
console.log(4)
}, 1000)
setTimeout(() => {
console.log(5)
}, 1000)
}
test();
async函数
async函数是使用async关键字声明的函数,其中允许使用await关键字。
async和await关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise。
await的行为:
await后面可以跟任何表达式,函数执行至await并不是立即交出控制权,而是先计算出该表达式的值,也就是说如果是函数,将继续调用该函数计算出返回值。计算出表达式的值后控制权交回给调用async函数的外层函数。
一般情况下await后应该跟一个promise对象
async function foo() {
const result = await new Promise(
(resolve) => setTimeout(() => resolve(1))
)
console.log(result)
}
foo()
近似于:
function foo() {
return new Promise(
(resolve) => setTimeout(() => resolve(1))
).then((res)=> {
const result = res;
console.log(result);
})
}
foo()
await后面的对象可以扩展为thenable对象(即定义了then方法的对象),await会将其视为Promise处理,调用其then方法并将后续代码作为回调函数,如果没有定义合适的then方法会出现意想不到的错误,所以最好使用promise对象。
其他情况:await后不是thenable对象
async function foo() {
const result = await 1
await 2
await 3
console.log(result)
}
foo()
console.log(2)
近似于:
function foo() {
return Promise.resolve(1).then((res)=> {
const result = res;
console.log(result);
})
}
foo()
也就是对于非thenable对象,await后的表达式会被隐式地包装在一个promise中,并且此promise的状态直接是fulfilled完成状态。结合上面说的js执行机制可知,函数执行到此处await后面的代码会立即加入微任务队列中。
await表达式的值就是await后面的promise的解决值。
async函数返回值:
async函数一定会返回一个promise对象。如果一个async函数的返回值看起来不是promise,那么它将会被隐式地包装在一个promise中。
如下代码:
async function foo() {
return 1
}
等价于:
function foo() {
return Promise.resolve(1)
}
注意:js会在没有返回值的函数最后添加return undefined。
例子:
来分析下面这段代码,调用runGame()
private needNum = [];
private l = [[1, 2], [3, 4], [5, 6]];
public async runGame() {
this.needNum = [];
await this.onFinish();
console.log(this.needNum);
}
async onFinish() {
if (this.l.length <= 0) {
return true;
}
let s = this.l.pop();
s.forEach(async (value) => {
let s = await this.onAddNumber(value);
if (s) {
this.needNum.push(s);
await this.onFinish();
}
})
}
async onAddNumber(value) {
return value;
}
let needNum = [];
let l = [[1, 2], [3, 4], [5, 6]];
async function runGame() {
setTimeout(() => {
console.log('setTimeout');
})
needNum = [];
await onFinish();
console.log('runGame');
console.log(needNum);
}
async function onFinish() {
if (l.length <= 0) {
return true;
}
let s = l.pop();
console.log("onFinish")
console.log(s)
s.forEach(async (value) => {
let s = await onAddNumber(value);
console.log("forEach")
console.log(s)
if (s) {
needNum.push(s);
await onFinish();
}
})
}
async function onAddNumber(value) {
return value;
}
runGame();
总结
单线程的js利用任务队列+事件循环的机制实现异步;
1个事件循环 = 1个宏任务 + 所有微任务;
await之后的值永远会被包装成promise,async函数永远返回一个promise;
使用async函数时要仔细地推算一下程序的执行顺序,否则很容易出现意想不到的情况。
转自路过的不知名假面骑士~~~