张子阳的博客

首页 读书 技术 店铺 关于
张子阳的博客 首页 读书 技术 店铺 关于

ES6中的Generator函数

2018-3-6 作者: 张子阳 分类: Web前端

之前在React项目中,遇到异步请求,都是通过redux-thunk来处理,但使用这种方式,action就变得不那么纯净了。当前新的趋势是使用redux-saga来处理side effects(副效应)。在redux-saga中,重度使用了generator函数的概念,这篇文章先就Generator函数做一个小结。

创建Generator函数

与普通函数的声明不同,Generator函数需要在function关键字后面加星号*。

function* generator(){ console.log("a") }

执行Generator函数并不会运行函数体,而是返回一个迭代器iterator对象。

let iterator = generator(); // 并不会输出a

运行Generator函数

如果想要运行generator函数,则需要在迭代器上执行next()方法。

function* generator(){ console.log("a") return "a"; } let iterator = generator(); let state = iterator.next(); // 输出a console.log(state); // 输出 [object Object] { done: true, value: "a" }

next()方法返回了一个对象,该对象有两个参数,done参数表明了generator函数是否执行完毕,value则为函数的返回值。

使用yield关键字

目前看上去Generator函数好像并没有什么用,实际上,它可以结合yield关键字,从而实现函数的分段执行。

function* generator(){ console.log("a1") yield "a2"; console.log("b1") yield "b2"; console.log("c1") return "c2"; } let iterator = generator(); let state = iterator.next(); // 输出a1 console.log(state); // 输出 [object Object] { done: false, value: "a2" } state = iterator.next(); // 输出b1 console.log(state); // 输出 [object Object] { done: false, value: "b2" } state = iterator.next(); // 输出b3 console.log(state); // 输出 [object Object] { done: true, value: "b3" } state = iterator.next(); // 无输出 console.log(state); // 输出 [object Object] { done: true, value: undefined }

执行上面的代码,可以看到,函数每运行到yield的位置就暂停了,直到下一次执行next()。

向Generator函数进行传值

从上面的例子,可以看到,通过使用yield和return,可以获取Generator函数每段执行的返回值。那么如何向函数中传入值?可以通过函数的参数和next()方法。

function* generator(p){ console.log("1: " + p) p = yield "X"; console.log("2: " + p) p = yield "Y"; console.log("3: " + p) } let iterator = generator("A"); let state = iterator.next("B"); // 输出 1: A console.log(state); // 输出 [object Object] { done: false, value: "X" } state = iterator.next("C"); // 输出 2: C console.log(state); // 输出 [object Object] { done: false, value: "Y" } state = iterator.next("D"); // 输出 3: D console.log(state); // 输出 [object Object] { done: true, value: undefined }

这里的规则是:第x调用next()方法时传入的参数,是第x-1次调用yield的返回值。当x=1,也就是第1次调用next()方法时,因为此时还从来没有调用过yield,因此输入参数会被丢弃(如上栗例中没有输出B)。此时,如果要传入参数,则应使用generator函数的输入参数。

使用for...of 遍历迭代器

function* generator() { console.log("1"); yield "A"; console.log("2"); yield "B"; console.log("3"); return "C"; }; let iterator = generator(); for (let value of iterator) { console.log(value); }

上面代码的输出是:

"1" "A" "2" "B" "3"

注意到并没有输出C,因为这个遍历只对yield有效。既然已经可以利用yiled获得函数任意执行阶段的返回值,所以建议generator函数中不要再使用return,这样可以统一访问方式。将原先需要return的返回值,放到最后一个yield即可。

串联多个Generator函数

可以通过yield* 串联Generator函数。

function* gen1() { yield "A"; yield* gen2(); }; function* gen2(){ yield "B"; yield* gen3(); } function* gen3(){ yield "C"; } let iterator = gen1(); for (let value of iterator) { console.log(value); // 输出: A B C }

使用Generator修改回调代码

和Promise一样,Generator函数也可以将异步请求中的层层回调,改写成串行化的方式。将add()函数改写成了异步执行的方式,如下例所示:

const add = (x, y, advancer) => { setTimeout(() => { let sum = x+y; advancer(sum); }, 200); }; const curry = (method, ...args) => { var value = (advancer) => { args.push(advancer); return method.apply({}, args); } return value; }; const controller = (generator) => { const iterator = generator(); const advancer = (response) => { var state; state = iterator.next(response); if (!state.done) { state.value(advancer); } } advancer(); }; function* generator(){ var value = curry(add, 1, 2); const sum1 = yield value; const sum2 = yield curry(add, sum1, 3); const sum3 = yield curry(add, sum2, 4); console.log(sum1, sum2, sum3); } controller(generator);

上面代码的核心在于,add方法的回调函数并非直接写死,而是改成参数,由advancer递归传入,这样使得只要state.done为false,就会依次串行执行。

总结

这篇文章简要地介绍了Generator函数及其作用,对于想要熟悉Redux/Saga的同学来说,算是一个前导的知识点。

感谢阅读,希望这篇文章能给你带来帮助!