Generator函数的简单介绍
从形式上来看,Generator函数和普通函数别无二致。只有两个区别,一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
表达式,定义不同的内部状态。
js
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
const hw = helloWorldGenerator();
上面代码定义了一个 Generator 函数helloWorldGenerator
,它内部有两个yield
表达式(hello
和world
),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
Generator函数执行之后会返回一个遍历器对象,这个遍历器对象主要有next、throw、return这三个方法,实现async和await主要用到next、throw这两个方法。
next方法
执行完Generator函数返回的遍历器对象可以看作是一个指针对象,每次调用next方法,就会使得指针对象指向下一个状态,函数就会从上一次停下的状态继续往后执行,直到遇到下一个yield
或者return
,next方法返回一个对象,这个对象有value、done这两个属性。
js
console.log(hw.next()); // { value: 'hello', done: false }
console.log(hw.next()); // { value: 'world', done: false }
console.log(hw.next()); // { value: 'ending', done: true }
console.log(hw.next()); // { value: undefined, done: true }
value属性的值就是yield
表达式的值,当函数遇到return,返回的对象的value属性就是return的返回值,done属性就会变成true,因为return就标志着这个函数完成了全部的执行;当一个函数内部没有return的时候,相当于return undefined
,所以在遍历完最后一个yield
状态之后还要执行一遍next方法,done属性才变为true。
next方法的参数
next方法可以携带一个参数,这个参数会作为上一个yield
表达式的返回值(注意yield表达式的值和返回值的区别)。
js
function* foo(x) {
const y = 2 * (yield (x + 1));
const z = yield (y / 3);
return (x + y + z);
}
const b = foo(5);
console.log(b.next());// { value:6, done:false }
console.log(b.next(12)); // { value:8, done:false }
console.log(b.next(13)); // { value:42, done:true }
上面代码中,在调用第2个和第3个next方法的时候分别传入12和13作为参数,则函数中的y的值就为2 * 12
,z的值就为13
,最后函数返回的值就为5 + 24 + 13
。
throw方法
通过调用throw方法,可以在Generator函数外部抛出错误,然后在函数内部被捕获。
js
const g = function* () {
try {
yield;
} catch (e) {
console.log(e);
}
};
const i = g();
i.next();
i.throw(new Error('出错了!')); // Error: 出错了!(...)
具体实现
基础版本
了解了Generator函数的上述特性之后,我们就可以来着手实现async/await了。
js
function fn(nums) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(nums * 2);
}, 1000);
});
}
function* gen() {
const num1 = yield fn(1);
console.log(num1); // 2
const num2 = yield fn(num1);
console.log(num2); // 4
const num3 = yield fn(num2);
console.log(num3); // 8
return num3;
}
const testGAsync = asyncToGenerator(gen);
实现async/await核心主要是要实现在函数的内部将异步操作转为继发操作,即要等到上一个异步操作完成并返回值,再进行下一步操作,很显然我们的Generator函数是可以实现的,我们将我们的每一个异步操作用Promise包装,只有当当前的promise对象状态变为fulfilled,我们才继续对Generator实例执行next()方法,同时将上一步中promise对象的返回值作为next参数传入Generator函数。
js
function asyncToGenerator(generatorFunc) {
return function(...args) {
// 执行Generator函数,返回遍历器对象。
const gen = generatorFunc.apply(this,args);
function step(arg) {
// 对遍历器对象执行next方法
const generatorResult = gen.next(arg);
const { value , done } = generatorResult;
if(done) {
resolve(value);
} else {
Promise.resolve(value).then(val => step(val));
}
}
step();
}
}
上面的代码初步实现了async/await,核心思路就是利用递归,不断地去执行遍历器对象的next方法,执行next方法返回的是一个Promise对象,我们在Promise的then回调函数中将我们异步操作得到的值传递给下一个next方法作为参数传入Generator函数中,成为yield表达式的返回值。
错误捕获
我们的基础版本和真正的async/await还有两个区别,第一,我们无法捕获我们异步操作过程中抛出的错误;第二,async/await返回的是一个Promise对象,其可以对async函数内部的同步代码的错误进行捕获。
js
// 1. 无法在async函数中捕获异步操作的错误
function fn(nums) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('promiseError');
}, 1000);
});
}
function* gen() {
try {
const num1 = yield fn(1);
console.log(num1);
} catch(err) {
console.log(err);
}
}
const testGAsync = asyncToGenerator(gen);
testGAsync(); // Uncaught (in promise) promiseError
// 2. 无法在返回的Promise对象中捕获同步错误
function* gen() {
const num1 = yield fn(1);
console.log(num1);
throw new Error('genError')
}
const testGAsync = asyncToGenerator(gen);
testGAsync().then(res => {
console.log(res);
}, err => {
console.log(err)
}); // Uncaught (in promise) Error: genError
为了能够捕获到同步错误和异步错误,我们需要做两个操作,一个操作是去尝试捕获在调用next方法时候可能有的错误错误,捕获到的话就在Promise对象内部reject出去,第二个操作是用Promise对象对next方法返回值进行再次包装时添加then方法的第二个回调函数,进行错误捕获,捕获到错误就调用遍历器对象的throw方法让错误在async函数内部被捕获。
js
function asyncToGenerator(generatorFunc) {
return function () {
let gen = generatorFunc.apply(this, arguments)
return new Promise((resolve, reject) => {
function step(name, arg) {
let generatorResult;
try {
generatorResult = gen[name](arg)
} catch (err) {
reject(err)
}
const { value, done } = generatorResult
if (done) {
resolve(value)
} else {
Promise.resolve(value).then(
(val) => {
step('next', val)
},
(err) => {
step('throw', err)
}
);
}
}
step('next')
})
}
}
参考: