本文将介绍Generator的一些基础使用,执行过程,以及
co
库的实现。
Generator
使用function*
的形式申明Generator函数,并使用yield
关键字返回值;执行Generator函数会返回一个遍历器对象,遍历器对象带有next
方法,执行后返回的值包含value
与done
属性:
const g = function* g() {
yield 'hello';
yield 'world';
}
const iterable = g();
let v;
do {
v = iterable.next();
console.log(v.value, v.done);
} while(!v.done)
输出为:
hello false
world false
undefined true
如果代码为:
const g = function* g() {
yield 'hello';
return 'world';
}
则输出为:
hello false
world true
Generator中可以使用for
循环、while
循环等:
const g = function* g() {
const arr = ['hello', 'world']
for (let item of arr) {
yield item
}
}
我们可以用for of
遍历迭代器:
for (let item of g()) { console.log(item) }
// hello
// world
可以通过设置[Symbol.iterator]
来创造迭代器:
const iterable = {
[Symbol.iterator]: function* () {
const arr = ['hello', 'world']
for (let item of arr) {
yield item
}
}
};
for (let item of iterable) { console.log(item) }
Generator可以通过next
传入参数(从第二次调用next
起,后面会解释为什么从第二次起):
const g = function* g() {
const arr = [];
let v
while(v = yield arr) { arr.push(v) }
}
const iterable = g();
console.log(iterable.next().value); // []
console.log(iterable.next(1).value); // [1]
console.log(iterable.next(2).value); // [1, 2]
可以使用throw
来抛出错误(在throw
前确保先执行一次next
):
const g = function* g() {
const arr = [];
let v
while(true) {
try {
v = yield arr
arr.push(v)
} catch (err) {
console.log('err', err)
}
}
}
const iterable = g();
console.log(iterable.next().value);
console.log(iterable.next(1).value);
iterable.throw('whoops');
console.log(iterable.next(2).value);
// []
// [1]
// err, whoops
// [1, 2]
也可以由Generator抛出错误,外部捕获:
const g = function* g() {
const hasErr = yield 1
if (hasErr) throw 'whoops'
yield 2
}
const iterable = g();
iterable.next();
try {
iterable.next(true);
} catch(err) {
console.log(err);
}
可以用return
提前结束Generator:
const g = function* g() {
yield 1;
yield 2;
yield 3;
}
var iterable = g();
console.log(iterable.next()) // { value: 1, done: false }
console.log(iterable.return('foo')) // { value: 'foo', done: true }
console.log(iterable.next()) // { value: undefined, done: true }
return
会被finally
捕获:
const g = function* g() {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
const iterable = g();
iterable.next() // { value: 1, done: false }
iterable.next() // { value: 2, done: false }
iterable.return(7) // { value: 4, done: false }
iterable.next() // { value: 5, done: false }
iterable.next() // { value: 7, done: true }
Generator函数被称为“半协程”(semi-coroutine),只有Generator函数的调用者,才能将程序的执行权还给Generator函数。
执行过程
- 第一次
next
的执行,代码运行到第一个第一个yield
之前,返回yield
之后的值/表达式 - 第二次执行
next
,传入的参数将替换第一个yield
位置的语句,并向后执行,直到下一个yield
,返回值(因此第一次next
传入的值是被抛弃的;同理throw
之前也需要至少执行过一次next
) - 按照第二次起的逻辑,继续重复执行,直到
return
或不再yield
在执行yield
之后,Generator等待外部调用next
再执行之后的代码,在这个过程中,可以执行一些异步操作,再调用next
将结果返回Generator同时将程序的执行权还给Generator并继续执行,这也是可以用Generator模拟类似async/await的同步写法的原因。
generator与异步
因此,我们可以进行这样的操作:
const getUserId = (token) => {
setTimeout(() => {
iterable.next(parseInt(token.split(' ')[1], 10));
}, 0);
};
const getUserInfo = (userId) => {
setTimeout(() => {
iterable.next({ userId });
}, 0);
};
const g = function* (token) {
const userId = yield getUserId(token);
console.log(userId);
const data = yield getUserInfo(userId);
console.log(data);
return data;
};
const iterable = g('Bearer 123');
iterable.next();
还可以使用co
库:
const co = require('co');
const getUserId = (token) => {
return Promise.resolve(parseInt(token.split(' ')[1], 10));
};
const getUserInfo = (userId) => {
return Promise.resolve({ userId });
};
const fn = co.wrap(function* (token) {
const userId = yield getUserId(token);
const data = yield getUserInfo(userId);
return data;
});
fn('Bearer 123').then(data => {
console.log(data);
});
自己动手实现co
库
这里我们实现一个简化版的co
库,我们所要做的,是实现如下过程的自动化:
- 调用Generator的
next
方法 - 在
next
返回的Promise
完成时,用Promise的返回值再次调用next
方法 - 反复执行第二步,直到
next
返回的done
为true
以下是实现代码:
co
函数将返回一个Promise:
const co = function (fn, ...args) {
return new Promise((resolve, reject) => {
...
});
};
首先我们调用传入的fn
来生成带有next
方法的generator
实例:
gen = fn.apply(this, args);
我们需要实现next
函数:在调用gen.next
方法之后,我们会获得带有value
和done
的结果,这个结果作为参数调用next
;如果done
为true
,则直接resolve(value)
;否则,value
应该是一个Promise
实例,我们通过value.then
注册回调。
在value.then
的回调中,我们将使用(Generator外部的异步或同步方法的)返回的值继续调用gen.next
,并用其返回的结果继续调用next
进行处理:
const onFulfilled = function (res) {
let ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
};
const next = function (p) {
const { done, value } = p;
if (done) {
resolve(value);
} else {
(value.then ? value : Promise.resolve(value)).then(onFulfilled, reject);
}
};
最后,别忘了调用一次gen.next
来启动整个过程:
onFulfilled();
这里判断value.then
并使用Promise.resolve
,是为了兼容外部调用可以是同步方法的情况,例如:
const getUserId = (token) => {
return parseInt(token.split(' ')[1], 10);
};
最后我们实现co.wrap
函数,这里进行了柯里化:
co.wrap = function (fn) {
return function(...args) {
return co.call(this, fn, ...args);
};
};
完整示例代码
const co = function (fn, ...args) {
return new Promise((resolve, reject) => {
gen = fn.apply(this, args);
const onFulfilled = function (res) {
let ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
};
const next = function (p) {
const { done, value } = p;
if (done) {
resolve(value);
} else {
(value.then ? value : Promise.resolve(value)).then(onFulfilled, reject);
}
};
onFulfilled();
});
};
co.wrap = function (fn) {
return function(...args) {
return co.call(this, fn, ...args);
};
};
// demo
const getUserId = (token) => {
return parseInt(token.split(' ')[1], 10);
};
const getUserInfo = (userId) => {
return Promise.resolve({ userId });
};
const fn = co.wrap(function* (token) {
const userId = yield getUserId(token);
const data = yield getUserInfo(userId);
return data;
});
fn('Bearer 123').then(data => {
console.log(data);
});