我曾一度认为没有必要去学习 Javascript 的生成器( Generator ),认为它只是解决异步行为的一种过渡解决方案,直到最近对相关工具库的深刻学习,才逐渐认识到其强大之处。可能你并无手动去写过一个生成器,可是不得不否定它已经被普遍使用,尤为是在 redux-saga 和 RxJS 等优秀的开源工具。javascript
首先须要明确的是生成器其实来源于一种设计模式 —— 迭代器模式,在 Javascript 中迭代器模式的表现形式是可迭代协议,而这也是 ES2015 中迭代器和可迭代对象的来源,这是两个容易让人混淆的概念,但事实上 ES2015 对其作了明确区分。java
实现了 next
方法的对象被称为迭代器。next
方法必须返回一个 IteratorResult
对象,该对象形如:node
{ value: undefined, done: true }
复制代码
其中 value
表示这次迭代的结果,done
表示是否已经迭代完毕。react
实现了 @@iterator
方法的对象称为可迭代对象,也就是说该对象必须有一个名字是 [Symbol.iterator]
的属性,这个属性是一个函数,返回值必须是一个迭代器。git
String
, Array
, TypedArray
, Map
和 Set
是 Javascript 中内置的可迭代对象,好比,Array.prototype[Symbol.iterator]
和 Array.prototype.entries
会返回同一个迭代器:es6
const a = [1, 3, 5];
a[Symbol.iterator]() === a.entries(); // true
const iter = a[Symbol.iterator](); // Array Iterator {}
iter.next() // { value: 1, done: false }
复制代码
ES2015 中新增的数组解构也会默认使用迭代器进行迭代:github
const arr = [1, 3, 5];
[...a]; // [1, 3, 5]
const str = 'hello';
[...str]; // ['h', 'e', 'l', 'l', 'o']
复制代码
既然可迭代对象是实现了 @@iterator
方法的对象,那么可迭代对象就能够经过重写 @@iterator
方法实现自定义迭代行为:编程
const arr = [1, 3, 5, 7];
arr[Symbol.iterator] = function () {
const ctx = this;
const { length } = ctx;
let index = 0;
return {
next: () => {
if (index < length) {
return { value: ctx[index++] * 2, done: false };
} else {
return { done: true };
}
}
};
};
[...arr]; // [2, 6, 10, 14]
复制代码
从上面能够看出,当 next
方法返回 { done: true }
时,迭代结束。json
有两种方法返回生成器:redux
function*
声明的函数是一个生成器函数,生成器函数返回的是一个生成器const counter = (function* () {
let c = 0;
while(true) yield ++c;
})();
counter.next(); // { value: 1, done: false },counter 是一个迭代器
counter[Symbol.iteratro]();
// counterGen {[[GeneratorStatus]]: "suspended"}, counter 是一个可迭代对象
复制代码
上面的代码中的 counter
就是一个生成器,实现了一个简单的计数功能。不只没有使用闭包也没有使用全局变量,实现过程很是优雅。
生成器的强大之处在于能方便地对生成器函数内部的逻辑进行控制。在生成器函数内部,经过 yield
或 yield*
,将当前生成器函数的控制权移交给外部,外部经过调用生成器的 next
或 throw
或 return
方法将控制权返还给生成器函数,而且还可以向其传递数据。
yield
和 yield*
只能在生成器函数中使用。生成器函数内部经过 yield
提早返回,前面的计数器就是利用这个特性向外部传递计数的结果。须要注意的是前面的计数器是无限执行的,只要生成器调用 next
方法,IteratorResult
的 value
就会一直递增下去,若是想计数个有限值,须要在生成器函数里面使用 return
表达式:
const ceiledCounter = (function* (ceil) {
let c = 0;
while(true) {
++c;
if (c === ceil) return c;
yield c;
}
})(3);
ceiledCounter.next(); // { value: 1, done: false }
ceiledCounter.next(); // { value: 2, done: false }
ceiledCounter.next(); // { value: 3, done: true }
ceiledCounter.next(); // { value: undefined, done: true }
复制代码
yield
后能够不带任何表达式,返回的 value
为 undefined
:
const gen = (function* () {
yield;
})();
gen.next(); // { value: undefined, done: false }
复制代码
生成器函数经过使用 yield*
表达式用于委托给另外一个可迭代对象,包括生成器。
委托给 Javascript 内置的可迭代对象:
const genSomeArr = function* () {
yield 1;
yield* [2, 3];
};
const someArr = genSomeArr();
greet.next(); // { value: 1, done: false }
greet.next(); // { value: 2, done: false }
greet.next(); // { value: 3, done: false }
greet.next(); // { value: undefined, done: true }
复制代码
委托给另外一个生成器(仍是利用上面的 genGreet
生成器函数):
const genAnotherArr = function* () {
yield* genSomeArr();
yield* [4, 5];
};
const anotherArr = genAnotherArr();
greetWorld.next(); // { value: 1, done: false}
greetWorld.next(); // { value: 2, done: false}
greetWorld.next(); // { value: 3, done: false}
greetWorld.next(); // { value: 4, done: false}
greetWorld.next(); // { value: 5, done: false}
greetWorld.next(); // { value: undefined, done: true}
复制代码
yield
表达式是有返回值的,接下来解释具体行为。
生成器函数外部正是经过这三个方法去控制生成器函数的内部执行过程的。
生成器函数外部能够向 next
方法传递一个参数,这个参数会被看成上一个 yield
表达式的返回值,若是不传递任何参数,yield
表达式返回 undefined
:
const canBeStoppedCounter = (function* () {
let c = 0;
let shouldBreak = false;
while (true) {
shouldBreak = yield ++c;
console.log(shouldBreak);
if (shouldBreak) return;
}
};
canBeStoppedCounter.next();
// { value: 1, done: false }
canBeStoppedCounter.next();
// undefined,第一次执行 yield 表达式的返回值
// { value: 2, done: false }
canBeStoppedCounter.next(true);
// true,第二次执行 yield 表达式的返回值
// { value: undefined, done: true }
复制代码
再来看一个连续传入值的例子:
const greet = (function* () {
console.log(yield);
console.log(yield);
console.log(yield);
return;
})();
greet.next(); // 执行第一个 yield表达式
greet.next('How'); // 第一个 yield 表达式的返回值是 "How",输出 "How"
greet.next('are'); // 第二个 yield 表达式的返回值是 "are",输出"are"
greet.next('you?'); // 第三个 yield 表达式的返回值是 "you?",输出 "you"
greet.next(); // { value: undefined, done: true }
复制代码
生成器函数外部能够向 throw
方法传递一个参数,这个参数会被 catch
语句捕获,若是不传递任何参数,catch
语句捕获到的将会是 undefined
,catch
语句捕获到以后会恢复生成器的执行,返回带有 IteratorResult
:
const caughtInsideCounter = (function* () {
let c = 0;
while (true) {
try {
yield ++c;
} catch (e) {
console.log(e);
}
}
})();
caughtInsideCounter.next(); // { value: 1, done: false}
caughtIndedeCounter.throw(new Error('An error occurred!'));
// 输出 An error occurred!
// { value: 2, done: false }
复制代码
须要注意的是若是生成器函数内部没有 catch
到,则会在外部 catch
到,若是外部也没有 catch
到,则会像全部未捕获的错误同样致使程序终止执行:
生成器的 return
方法会结束生成器,而且会返回一个 IteratorResult
,其中 done
是 true
,value
是向 return
方法传递的参数,若是不传递任何参数,value
将会是 undefined
:
const g = (function* () {
yield 1;
yield 2;
yield 3;
})();
g.next(); // { value: 1, done: false }
g.return("foo"); // { value: "foo", done: true }
g.next(); // { value: undefined, done: true }
复制代码
经过上面三个方法,使得生成器函数外部对生成器函数内部程序执行流程有了一个很是强的控制力。
生成器函数与异步操做结合是很是天然的表达:
const fetchUrl = (function* (url) {
const result = yield fetch(url);
console.log(result);
})('https://api.github.com/users/github');
const fetchPromise = fetchUrl.next().value;
fetchPromise
.then(response => response.json())
.then(jsonData => fetchUrl.next(jsonData));
// {login: "github", id: 9919, avatar_url: "https://avatars1.githubusercontent.com/u/9919?v=4", gravatar_id: "", url: "https://api.github.com/users/github", …}
复制代码
在上面的代码中,fetch
方法返回一个 Promise
对象 fetchPromise
,fetchPromise
经过一系列的解析以后会返回一个 JSON 格式的对象 jsonData
,将其经过 fetchUrl
的 next
方法传递给生成器函数中的 result
,而后打印出来。
从上面的过程能够看出,生成器配合 Promise
确实能够很简洁的进行异步操做,可是还不够,由于整个异步流程都是咱们手动编写的。当异步行为变的更加复杂起来以后(好比一个异步操做的队列),生成器的异步流程管理过程也将会变得难以编写和维护。
须要一种能自动执行异步任务的工具进行配合,生成器才能真正派上用场。实现这种工具一般有两种思路:
Promise
对象,将异步过程扁平化处理,基于这种思路的是 co 模块;下面来分别理解和实现。
thunk
函数的起源其实很早,而 thunkify
模块也做为异步操做的一种广泛解决方案,thunkify
的源码很是简洁,加上注释也才三十行左右,建议全部学习异步编程的开发者都去阅读一遍。
理解了 thunkify
的思想以后,能够将其删减为一个简化版本(只用于理解,不用于生产环境中):
const thunkify = fn => {
return (...args) => {
return callback => {
return Reflect.apply(fn, this, [...args, callback]);
};
};
};
复制代码
从上面的代码能够看出,thunkify
函数适用于回调函数是最后一个参数的异步函数,下面咱们构造一个符合该风格的异步函数便于咱们调试:
const asyncFoo = (id, callback) => {
console.log(`Waiting for ${id}...`)
return setTimeout(callback, 2000, `Hi, ${id}`)
};
复制代码
首先是基本使用:
const foo = thunkify(asyncFoo);
foo('Juston')(greetings => console.log(greetings));
// Waiting for Juston...
// ... 2s later ...
// Hi, Juston
复制代码
接下来咱们模拟实际需求,实现每隔 2s 输出一次结果。首先是构造生成器函数:
const genFunc = function* (callback) {
callback(yield foo('Carolanne'));
callback(yield foo('Madonna'));
callback(yield foo('Michale'));
};
复制代码
接下来实现一个自动执行生成器的辅助函数 runGenFunc
:
const runGenFunc = (genFunc, callback, ...args) => {
const g = genFunc(callback, ...args);
const seqRun = (data) => {
const result = g.next(data);
if (result.done) return;
result.value(data => seqRun(data));
}
seqRun();
};
复制代码
注意 g.next().value
是一个函数,而且接受一个回调函数做为参数,runGenFunc
经过第 7 行的代码实现了两个关键步骤:
yield
表达式的结果返回之生成器函数yield
表达式最后是调用 runGenFunc
而且将 genFunc
、须要用到的回调函数 callback
以及其余的生成器函数参数(这里的生成器函数只有一个回调函数做为参数)传入:
runGenFunc(genFunc, greetings => console.log(greetings));
// Waiting for Carolanne...
// ... 2s later ...
// Hi, Carolanne
// Waiting for Madonna...
// ... 2s later ...
// Hi, Madonna
// Waiting for Michale...
// ... 2s later ...
// Hi, Michale
复制代码
能够看到输出结果确实如指望的那样,每隔 2s 进行一次输出。
从上面的过程来看,使用 thunkify
模块进行异步流程的管理仍是不够方便,缘由在于咱们不得不本身引入一个辅助的 runGenFunc
函数来进行异步流程的自动执行。
co 模块能够帮咱们完成异步流程的自动执行工做。co
模块是基于 Promise
对象的。co
模块的源码一样很是简洁,也比较适合阅读。
co
模块的 API 只有两个:
co(fn*).then(val => )
co
方法接受一个生成器函数为惟一参数,而且返回一个 Promise
对象,基本使用方法以下:
const promise = co(function* () {
return yield Promise.resolve('Hello, co!');
})
promise
.then(val => console.log(val)) // Hello, co!
.catch((err) => console.error(err.stack));
复制代码
fn = co.wrap(fn*)
co.wrap
方法在 co
方法的基础上进行了进一步的包装,返回一个相似于 createPromise
的函数,它与 co
方法的区别就在与能够向内部的生成器函数传递参数,基本使用方法以下。
const createPromise = co.wrap(function* (val) {
return yield Promise.resolve(val);
});
createPromise('Hello, jkest!')
.then(val => console.log(val)) // Hello, jkest!
.catch((err) => console.error(err.stack));
复制代码
co
模块须要咱们将 yield
关键字后面的对象改造为一个 co
模块自定义的 yieldable 对象,一般能够认为是 Promise
对象或基于 Promise
对象的数据结构。
了解了 co
模块的使用方法后,不难写出基于 co
模块的自动执行流程。
只须要改造 asyncFoo
函数让其返回一个 yieldable
对象,在这里便是 Promise
对象:
const asyncFoo = (id) => {
return new Promise((resolve, reject) => {
console.log(`Waiting for ${id}...`);
if(!setTimeout(resolve, 2000, `Hi, ${id}`)) {
reject(new Error(id));
}
});
};
复制代码
而后就可使用 co
模块进行调用,因为须要向 genFunc
函数传入一个 callback
参数,因此必须使用 co.wrap
方法:
co.wrap(genFunc)(greetings => console.log(greetings));
复制代码
上述结果与指望一致。
其实 co
模块内部的实现方式与 thunkify 小节中的 runGenFunc
函数有殊途同归之处,都是使用递归函数反复去执行 yield
语句,知道生成器函数迭代结束,主要的区别就在于 co
模块是基于 Promise
实现的。
可能在实际工做中的大部分时候均可以使用外部模块去完成相应的功能,可是想理解实现原理或者不想引用外部模块,则深刻理解生成器的使用就很重要了。在下一篇文章[观察者模式在 Javascript 中的应用]中我会探究 RxJS 的实现原理,其中一样涉及到本文所所说起的迭代器模式。最后附上相关参考资料,以供感兴趣的读者继续学习。