async和await
async和await的实现,要从回调地狱开始说起。我们通过之前的文章已经知道了Promise通过链式调用来解决回调地狱的问题,但是就算使用了then()函数来进行链式调用,代码仍然非常沉重,如果有一个非常复杂的异步流程,也还是需要很多的链式调用进行支撑。那有没有方法可以让异步的写法变得像同步一样呢?
这时候就不得不提一种函数——Generator函数
Generator函数
Generator函数是ES6引入的,也称生成器函数。总体结构如下所示,在函数名的前面会有一个*
,但是箭头函数是不可以用来定义生成器函数的。
function * funName() {
yield ***
yield ***
}
复制代码
它可以通过yield关键字把函数的执行流挂起,提供了让函数可以分步执行的能力。如果遇到了yield关键字,执行就会停止。
当然,它也有一个next()方法,通过调用这个方法可以让生成器开始执行或者继续执行。
有一个需要注意的地方,生成器对象一开始调用的时候并不会直接去执行,而是会处于suspened状态(暂停执行)。所以需要用next()方法来让它执行。
function* test() {
console.log(1);
return 123;
}
var value = test();
console.log(value);
复制代码
如上面所示,我们定义了一个叫test的生成器函数,函数内部会打印1,同时返回123。因此我们像常规的函数一样去调用这个生成器函数,并且定义了一个叫value的变量去接受这个函数的返回值。
当我们执行后,如下图所示:
我们发现,test函数执行了,但是内部的console并没有执行,也没有返回123。而test是一个显示suspended
的对象。因为生成器对象一开始调用的时候并不会直接去执行,而是会处于suspened状态即暂停执行。所以需要用next()方法来让它执行。
next()
然后我们使用了next(),如下面代码所示:
function* test() {
console.log(1);
return 123;
}
var value = test();
console.log(value.next());
console.log(value);
复制代码
当我们运行后会发现test函数的代码正常执行了,打印
test.next()
运行的结果时发现,返回的是一个对象,对象内部有一个done属性和一个value属性,value属性就是return回来的值。
当我们再次打印value变量时,发现变成了一个test对象,状态是closed。
因为,生成器函数实际上没有传统的返回值,调用的时候会生成一个实例化的Generator对象。这个对象有一个next方法,可以通过调用next方法来执行生成器函数内部的代码,直到yield会暂停执行,等待下一次的next(),然后会继续执行到下一个的yield。
而next方法的调用会返回一个对象,对象中存在2个属性,分别是done
和value
。
- done:该属性的值是一个布尔值,表示是否可以继续执行next(),如果时true则表示已经执行完毕,否则则表示可以继续执行next()。
- value:如果done属性的值是false,value通常代表下⼀个yield右侧的值。如果done属性的值是true,那么这个属性的值就是return的值。
yield
我们将代码改造一下,加入yield关键字。
function* test() {
console.log(1);
yield 2;
yield 3;
yield 4;
return 5;
}
var value = test();
// 这时候的value是实例化的Generator函数
console.log(value);
// 第一次调用next(),代码执行到第一个yield,value的值是2,done是false
console.log(value.next());
// 第二次调用next(),value的值是3,done是false
console.log(value.next());
// 第三次调用next(),value的值是4,done是false
console.log(value.next());
// 第四次调用next(),value的值是5,done是true
console.log(value.next());
// 第五次调用next(),value的值是undefined,done是true
console.log(value.next());
复制代码
我们发现,当调用next()的时候,代码会执行到yield时暂停,状态为false,值为yield右侧的值。当代码执行完毕后,则done属性变为true,value会是函数return的值。如果代码执行完毕了继续执行next(),会发现done属性并没有发生变化,但是value属性已经变成了undefined了。
传值问题
function* test() {
console.log(1);
var a = yield 2;
console.log(a);
var b = yield 3;
console.log(b);
var c = a + b;
console.log(c);
}
var value = test();
console.log(value);
const step1 = value.next();
console.log(step1);
const step2 = value.next();
console.log(step2);
const step3 = value.next();
console.log(step3);
复制代码
如上述代码所示,想给a b c赋值,但是通过yield实现了分布执行。但是发现a 和 b都变成了undefined。
这是因为在分步执⾏过程中,我们是可以在程序中对运⾏的结果进⾏⼈为⼲预的,也就是说yield返回的结果和他左 侧变量的值都是我们可以⼲预的。
当我们对代码进行下面这样的改造:
function* test() {
console.log(1);
var a = yield 2;
console.log(a);
var b = yield 3;
console.log(b);
var c = a + b;
console.log(c);
}
var value = test();
console.log(value);
const step1 = value.next();
console.log(step1);
const step2 = value.next(step1.value);
console.log(step2);
const step3 = value.next(step2.value);
console.log(step3);
复制代码
当我们这样子改造后,就能正常赋值了。也就是说next函数执⾏的过程中我们是需要传递参数的,当下⼀次next执⾏的时候我们如果不传递参数,那么本次yield左侧变量的值就变成了undefined,所以我们如果想让yield左侧的变量有值就必须在next中传⼊指定的结果。
用Generator将Promise异步流程同步化
function* test() {
var a = yield 1;
console.log(a);
var res = yield setTimeout(function () {
return 6666666666;
}, 1000);
console.log(res);
var res1 = yield new Promise(function (resolve) {
setTimeout(function () {
resolve(456);
}, 1000);
});
console.log(res1);
}
var generator = test();
console.log(generator);
var step1 = generator.next();
console.log(step1);
var step2 = generator.next();
console.log(step2);
var step3 = generator.next();
console.log(step3);
var step4 = generator.next();
console.log(step4);
复制代码
如上所示,我们使用Generator函数并结合了异步函数以及Promise,发现对异步的setTimeout无法正确打印出return返回的值,并且输出没有任何延时。而对于Promise来说,能够拿到Promise本身,展开也能发现Promise状态变成了fulfilled,值就是1s后resolve的参数。
因此,我们是不是可以通过递归调用的方式,通过next()来推动函数的执行,用done属性作为是否结束的依据,如果碰到了Promise,就等promise执行完毕了再执行下一步。
function generatorFunctionRunner(fn) {
//定义分步对象
let generator = fn();
//执⾏到第⼀个yield
let step = generator.next();
//定义递归函数
function loop(stepArg, generator) {
//获取本次的yield右侧的结果
let value = stepArg.value;
//判断结果是不是Promise对象
if (value instanceof Promise) {
//如果是Promise对象就在then函数的回调中获取本次程序结果
//并且等待回调执⾏的时候进⼊下⼀次递归
value.then(function (promiseValue) {
if (stepArg.done === false) {
loop(generator.next(promiseValue), generator);
}
});
} else {
//判断程序没有执⾏完就将本次的结果传⼊下⼀步进⼊下⼀次递归
if (stepArg.done == false) {
loop(generator.next(stepArg.value), generator);
}
}
}
//执⾏动态调⽤
loop(step, generator);
}
function* test() {
var res1 = yield new Promise(function (resolve) {
setTimeout(function () {
resolve("第⼀秒运⾏");
}, 1000);
});
console.log(res1);
var res2 = yield new Promise(function (resolve) {
setTimeout(function () {
resolve("第⼆秒运⾏");
}, 1000);
});
console.log(res2);
var res3 = yield new Promise(function (resolve) {
setTimeout(function () {
resolve("第三秒运⾏");
}, 1000);
});
console.log(res3);
}
generatorFunctionRunner(test);
复制代码
上述代码中我们封装了一个generatorFunctionRunner函数用来将Promise的异步流程同步化。打印结果会每隔1s进行打印,符合我们的预期。yield后面的Promise对象在运行时,在运行到当前行会等待Promise对象编程完成状态,然后我们通过判断Generator函数是否执行完了确定是否要继续递归调用。
Async和Await
后来,在ES8中实现了全新的异步控制流程,通过Async和Await,async和await其实就相当于是Promise的语法糖。
// 使用方式
async function test() {
await ...
await ...
}
复制代码
我们可以发现,这种写法和Generator函数结构很像,我们可以通过async修饰一个函数,被修饰过的函数的子作用域中,我们可以是用await来控制函数的流程,await右侧可以编写任意变量或者对象,如果右侧是一个Promise时,会等待Promise的状态变成fulfilled,才会继续往下执行。
我们尝试写一个被async修饰的函数:
async function test() {
console.log(123);
return "这是一个测试函数";
}
const result = test();
console.log(result); // Promise { '这是一个测试函数' }
复制代码
当我们接受到被async修饰的函数的返回值时,我们可以发现被async修饰的函数,本身是一个Promise对象。因此返回了一个值为1的Promise对象。
async function test() {
console.log(3);
return 1;
}
console.log(1);
test();
console.log(2);
复制代码
我们知道,new Promise是一个同步的流程,回调函数执行实际上也是同步执行的。而Promise.then则是一个微任务。因此,我们可以大胆的猜测,上面的打印结果是1 3 2
接下来看下面的代码:
async function test() {
console.log(3);
var a = await 4;
console.log(a);
return 1;
}
console.log(1);
test();
console.log(2);
复制代码
很多人会想,他会按照顺序打印1 3 4 2。
但是实际上输出的是1 3 2 4,为什么呢?
因为async把函数翻译成了Promise对象,所以实际上上述的代码类似于
console.log(1);
new Promise(function (resolve) {
console.log(3);
resolve(4);
}).then(function (a) {
console.log(a);
});
console.log(2);
复制代码
async函数中有⼀个最⼤的特点,就是第⼀个await会作为分⽔岭⼀般的存在,在第⼀个await的右侧和上⾯的代码,全部是同步代码区域相当于new Promise的回调,第⼀个await的左侧和下⾯的代码,就变成了异步代码区域相当于then的回调.
最终的Promise流程控制
async function test() {
var res1 = await new Promise(function (resolve) {
setTimeout(function () {
resolve("第⼀秒运⾏");
}, 1000);
});
console.log(res1);
var res2 = await new Promise(function (resolve) {
setTimeout(function () {
resolve("第⼆秒运⾏");
}, 1000);
});
console.log(res2);
var res3 = await new Promise(function (resolve) {
setTimeout(function () {
resolve("第三秒运⾏");
}, 1000);
});
console.log(res3);
}
test();
复制代码