await可以让JavaScript进行等待,直到一个promise执行并返回它的结果,JavaScript才会继续往下执行。这个特性能让我们以同步的写法来写异步调用。下面来一步步的模拟进行实现。
一、Iterator遍历器介绍
Array、Object都是可以遍历的,但是Set、Map却不能用for循环遍历,为了弥补这个遗憾,es6加入了Iterator遍历器,来为Set、Map、Array、Object新增一个统一的遍历API,这样只要这个对象上面具有[Symbol.iterator]方法,便可以遍历。由于Iterator中next()方法可以传入数组的length进行控制,每次执行next()能返回包含value/done属性的Iterator对象。这使得伪数组中无法被遍历的属性可以被剔除,因而使用es6中新增加的for...of循环来遍历具有[Symbol.iterator]方法的对象,变得更加安全。几乎可以用来全面替代for...in(除非遍历对象时要获取key、value值)。
//next模拟实现:
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
Iterator 具体详解可看此篇:Iterator 和 for...of 循环
二、Generator迭代器介绍
调用 Generator 函数,会返回一个遍历器指针g(想象成空数组就好了),而每个yield相当于一个断点,执行后会有一次返回。如下:
function* gen() {
yield console.log(1)
yield console.log(2)
console.log(3)
}
const g = gen() //遍历器指针
g.next() //1
g.next() //2
g.next() //3
由于yield 的断点返回功能,这使得我们可以使用返回的遍历器临时存储数据,类似于闭包功能,如下:
function* a() {
let n = 0;
while (n < 3) {
yield n++;
}
return;
}
let m = a(); //m 为返回的遍历器
let arr = [];
for (let s of m) { //使用for...of遍历
arr.push(s);
}
console.log(arr); //[ 0 ,1 ,2 ]
Generator 具体详解可看此篇:Generator 函数的异步应用
既然yield相当于断点,而await的作用也是让js等待,两者的功能相似,似乎实现await功能有些头绪了,下面看一下需求:去掉promise中的一堆then,实现从原始到后来的过渡
function sleep(time) {
return new Promise((resolve, rej) => {
setTimeout(resolve, time);
});
}
原始:
sleep(1000).then(()=>{console.log("休眠结束")})
后来:
await sleep(1000)
执行原始函数后,首先返回一个new Promise对象,该Promise被压入microTask数组,在下一个时间片到来时,Promise中的函数执行,setTimeout(resolve, time)
被压入macroTask堆,在至少1000ms之后,setTimeout执行,Promise返回,此时触发then函数,resolve被替换成()=>{console.log("休眠结束")}
,输出"休眠结束"。
这里看出Promise封装了回调函数,避免了回调地狱,但却要写一堆then,感觉依旧比较混乱。下面对sleep进行一层迭代器封装
function sleep(time) {
return new Promise((resolve, rej) => {
setTimeout(resolve, time);
});
}
function* s() {
yield sleep(1000).then(()=>{console.log(1000);});
yield sleep(500).then(()=>{console.log(500);});
yield sleep(200).then(()=>{console.log(200);});
}
let m = s();
console.log(m.next())
// { value: Promise { <resolved> }, done: false }
// 1000
let n = s();
n.next() //200
n.next() //500
n.next() //1000
这里先返回了迭代器对象,然后迭代器中的value执行,在resolved之后,输出resolve函数,打印1000。但是在测试3个迭代器时,我们发现数据并没有出现我们预期的结果,这是因为,在yield时返回的promise虽然在microTask中正确排列,但在顺序迭代执行microTask时,setTimeout又被压入macroTask,各个函数从新计算等待时间,从而造成等待异步失败。因此要对迭代器进行异步改写,如下:
n.next().value.then(() => {
n.next().value.then(()=>{
n.next()
});
});
此时输出正常,然后使用深度遍历把他抽象成通用函数:
function deepScan(iteraor){
let item=iteraor.next()
if(item.done){
return
}else{
let {value,done}=item
if(value instanceof Promise){
value.then(()=>{deepScan(iteraor)})
}else{
deepScan(iteraor)
}
}
}
这样,一个使用同步写法的异步功能就实现了,可以把async/await函数近似理解为如下Generator 和Promise的语法糖就可以了。
//完整代码
function sleep(time) {
return new Promise((resolve, rej) => {
setTimeout(resolve, time);
});
}
function* s() {
yield sleep(2000).then(() => {
console.log(1000);
});
yield sleep(500).then(() => {
console.log(500);
});
yield sleep(200).then(() => {
console.log(200);
});
}
function deepScan(iteraor){
let item=iteraor.next()
if(item.done){
return
}else{
let {value,done}=item
if(value instanceof Promise){
value.then(()=>{deepScan(iteraor)})
}else{
deepScan(iteraor)
}
}
}
let n = s();
deepScan(n)