javascript中异步任务的执行顺序
- 经常会遇到一些异步任务的执行顺序问题,接下来就来看几个常见的例子,答案解析在后面,可以自己先分析一下,看看执行结果,这样比较有利于查漏补缺。
// 微任务,宏任务的场景
setTimeout(() => {
console.log(1)
Promise.resolve(7).then(res => {
console.log(res);
})
})
const promise = new Promise((resolve) => {
console.log(2);
resolve(3);
console.log(4);
})
promise.then(res => {
console.log(res)
Promise.resolve(5).then(res => {
console.log(res);
})
setTimeout(() => {
console.log(6);
})
})
console.log(5);
// async/await的场景
async function test1() {
console.log(1);
await test2();
console.log(3)
}
async function test2() {
console.log(2);
// await console.log(2); // 有await和没有await结果是不一样的
}
test1();
new Promise(resolve => {
console.log(4);
resolve(5);
}).then(res => {
console.log(res)
})
console.log(6);
// 在遍历方法中的场景
const list = [1, 2, 3];
function fn(n) {
return new Promise(resolve => {
setTimeout(() => {
resolve(n);
}, 1000)
})
}
list.forEach(async item => {
const res = await fn(item);
console.log(res);
})
- 需要弄明白上面这几个例子的正确执行顺序,首先需要去了解浏览器的事件循环的原理,以及宏任务和微任务的执行过程。关于这个概念我总结了几点:
- 微任务有哪些:Promise, Object.observe, MutationObserver,process.nextTick(这个是node.js中的)
- 宏任务有哪些:渲染线程,js引擎线程,定时器触发线程,事件触发线程,异步http线程,这个太迷糊了,就记住比较常见的就是:主代码块,setTimeout, setInterval,setImmidate等
- 执行过程:宏任务和微任务都有自己的一个队列,当执行完一个宏任务时,它会去将当前微任务队列中的所有微任务执行并清空,然后再进行下一次的宏任务。代码执行过程中,遇到了setTimeout/setInterval时,会把该任务丢到宏任务队列中,等待当前循环的宏任务以及微任务全部执行完毕才会从队列拿下一个执行,遇到了promise的微任务,会放到微任务队列中,而且,如果在微任务队列执行过程中,又新建了微任务,也就是promise里面再创建一个promise,后一个promise还是要在当前的时间循环中去完成。
- 了解了这个机制,我们就可以去分析一下第一个例子了
// 微任务,宏任务的场景
// 1. 首先遇到setTimeout,把它丢到了宏任务队列的下一个宏任务
// 2. 遇到promise,Promise的构造函数是一个同步任务,也就是属于当前的主代码块的宏任务,就会立即执行,所以先输出了3, 5
// 3. resolve它调用的时候是把当前操作放进了一个微任务队列中,其实就算没有then方法,这个resolve依旧会在下一个事件循环去执行
// 4. promise.then其实相当于注册了对应的resolve的回调,也就是说在then的时候会等待微任务执行了之后把结果的回调丢给它
// 5. 同步任务console执行,输出8
// 6. 此次事件队列的宏任务执行完成,开始清空微任务队列
// 7. 执行console打印resolve的回调,输出4
// 8. 新建了一个promise,也就是丢到了当前微任务队列,然后此次队列必须清空,所以继续输出6
// 9. 新建了一个setimeout,这个就丢到了宏任务队列的下下一个宏任务。
// 10. 微任务执行完毕,拿出第一个放入宏任务的setTimeout的回调,输出1,并且创建了一个微任务
// 11. 由于没有其它的任务了,就开始执行微任务,输出2
// 12. 最后输出宏任务队列的最后一个setTimeout,输出7
// 13. 整体分析下来,它的顺序是3, 5, 8, 4, 6, 1, 2, 7
setTimeout(() => {
console.log(1)
Promise.resolve(2).then(res => {
console.log(res);
})
})
const promise = new Promise((resolve) => {
console.log(3);
resolve(4);
console.log(5);
})
promise.then(res => {
console.log(res)
Promise.resolve(6).then(res => {
console.log(res);
})
setTimeout(() => {
console.log(7);
})
})
console.log(8);
- 接下来,为了去分析第二个例子,需要去了解,async和await究竟是怎么实现的,做了哪些处理。为了分析这个问题,我去typescript官网的练习里面写了一个async方法,拿到了编译成js的代码,可以看看typescript是怎么去profill这个async的。
// 这个是写的ts代码
async function test() {
await console.log(1);
}
// 下面是输出的js代码,我们来慢慢分析
// 首先定义一个awaiter方法,返回一个promise对象,接受一个generate函数
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
// 这两个方法就是把generate的结果或者异常抛出,主要看下面的step方法
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
// 接受{ value, done }这样的一个generate函数的一次运行结果,然后判断
// 如果generate执行完成,就使用resolve抛出结果
// 如果还没有执行完成就继续调用fullfill,间接地继续调用step去让generate继续next,
// 这个函数的逻辑我个人感觉就是实现一个generate函数迭代器的语法糖,使得一个generate函数在完全执行完成或出现异常时拿到了最后的结果才进行接下来的操作
function step(result) {
result.done
? resolve(result.value)
: new P(function (resolve) {
resolve(result.value);
}).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next()); // 要注意的是这个地方,promise的构造函数中是同步的,所以第一步的执行过程是同步的,只是它的结果会在下一个微任务中通过resolve抛出来,所以在第二个例子中,await test2(),其实已经执行完了它的方法了,只是还没拿到结果,所以它的console也是同步的,但是如果在test2()中去await console,那么就会先丢进下一个异步任务
});
};
// 然后实现了一个generate的函数
// 看下它的两个参数,
// thisArg就是body的执行上下文
// body就是需要在每一步执行的代码块,看代码是一个函数,然后把定义的内部对象_丢给它
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = {
label: 0, // 标记它每一步的代码
sent: function() {
if (t[0] & 1)
throw t[1];
return t[1];
},
trys: [],
ops: []
},
f,
y,
t,
g;
// f作为当前函数是否已经在while的执行中的一个标识
//
return g = {
next: verb(0),
"throw": verb(1),
"return": verb(2)
}, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) {
return function (v) {
return step([n, v]);
};
}
// 这个方法一时半会看得不是很通透,但是大致得能看到它的实现过程
function step(op) {
// f应该是用来记录当前方法是否已经执行了
if (f) throw new TypeError("Generator is already executing.");
while (_)
try {
// 刚进入就把f置为1,这样下次重复调用就会报错了
if (
f = 1,
// 第一次进来,y还是没有值的,所以不执行
y && (
t = op[0] & 2
? y["return"]
: op[0]
? y["throw"] || ((t = y["return"]) && t.call(y), 0)
: y.next
) && !(t = t.call(y, op[1])).done)
return t;
if (
y = 0, // 第一次给y赋值
t // 判断t有没有值,第一次进来没有t值
)
op = [op[0] & 2, t.value];
// 根据返回结果中的[ status, 返回值 ],分别去做对应的操作
switch (op[0]) {
case 0:
case 1:
t = op;
break;
case 4: // 第一次进入循环,4,label++,表示先执行了第一步,并且抛出返回值{ value: undefined, done: false },交给了awaiter去处理
// 在awaiter里面发现done是false没救会继续开启一个promise去,在下一个微任务中去执行
_.label++;
return { value: op[1], done: false };
case 5:
_.label++; y = op[1]; op = [0];
continue;
case 7:
op = _.ops.pop(); _.trys.pop();
continue;
default:
// 返回值为[ 2, undefiend]时,把_变成0,跳出了while循环
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
_ = 0;
continue;
}
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
_.label = op[1];
break;
}
if (op[0] === 6 && _.label < t[1]) {
_.label = t[1];
t = op;
break;
}
if (t && _.label < t[2]) {
_.label = t[2];
_.ops.push(op);
break;
}
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _); // 第一次进来应该是执行函数,拿到第一次的返回结果,这个拿到的应该是 op = [ 4, undefined ],继续回调,console.log()的返回值就是undefined
} catch (e) {
op = [6, e];
y = 0;
} finally {
f = t = 0;
}
if (op[0] & 5)
throw op[1];
// 执行结束的标识
return {
value: op[0] ? op[1] : void 0,
done: true
};
}
};
// 高阶函数嵌套之后的方法
function test() {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
// 这个地方用了await
switch (_a.label) {
case 0: return [4 /*yield*/, console.log(1)];
case 1:
_a.sent();
return [2 /*return*/];
}
// 如果没有用await,就是下面的,也不存在switch的判断
// console.log(1)
// return [2 /*return*/];
});
});
}
- 了解到上面的程度,其实就能去理解第二个例子了,还是自己太菜了,下次找点时间再研究研究,今天不太想看
- 从上面的源码可以看出,async是返回了一个promise对象,然后await是立即执行当前await的内容,然后把它的返回结果丢给resolve处理,也就是一个微任务,然后把后面的代码块放到了一个新的微任务中执行
// async/await的场景
// 1. 执行test1,先输出1
// 2. 遇到await,执行test2的方法,在遇到一个await之前的代码都会立即执行,输出2,会把后面的代码块丢到下一个微任务中
// 3. promise构造函数执行,输出4,遇到resolve,丢进微任务队列中
// 4. 主代码块输出6
// 5. 把第一个微任务拿出来,await之后的内容,输出3
// 6. 把第二个微任务拿出来,promise的resolve,输出5
// 最终结果:1 2 4 6 3 5
async function test1() {
console.log(1);
await test2();
console.log(3)
}
async function test2() {
console.log(2);
// await console.log(2); // 有await和没有await结果是不一样的
}
test1();
new Promise(resolve => {
console.log(4);
resolve(5);
}).then(res => {
console.log(res)
})
console.log(6);
// 如果,test2中的console是被await的,就需要换一个思路,console.log依旧是立即执行了,因为都是在第一个await里面,但是区别在于两次await,会把console.log(3)丢进一个嵌套了两层的promise中,也就是在promise中的then里面又定义了promise,这样的话,3就被搁置到了5的后面了,顺序就变成了1 2 4 6 5 3
// 不太理解的话可以看看这个例子
// 最外层的两个promise是同步执行的,所以他们的顺序是先后,然后内层的需要等外层的执行完了再去放到微任务队列,所以上面加一个await就会导致3被搁置得更后面
new Promise(resolve => {
resolve(1);
}).then(res => {
console.log(res)
Promise.resolve(2).then(r => {
console.log(r);
})
})
new Promise(resolve => {
resolve(5);
}).then(res => {
console.log(res)
})
- 最后一个例子的话,就需要去理解一下forEach的内部原理了,forEach有一个很大的特点,就是:除了抛出异常以外,没有办法中止或跳出 forEach() 循环。详情可见MDN
- 所以最后一个例子
// 在遍历方法中的场景
const list = [1, 2, 3];
function fn(n) {
return new Promise(resolve => {
setTimeout(() => {
resolve(n);
}, 1000)
})
}
list.forEach(async item => {
const res = await fn(item); // 这个地方不会等到await执行完了才执行下一个await,而且同步开启了三个await的执行,所以结果就是等待一秒钟之后连续输出了1, 2, 3,而不是每隔一秒输出一个
console.log(res);
})
小结
- 异步任务的核心就是事件循环的机制,async/await的原理,以及一些遍历器的处理,反正就是多了解,多理解,遇到了复杂的异步交互场景,比较有利于去梳理业务