前言
在文章 把书看薄-深入浅出nodejs-异步控制 中,我们探讨了几种传统的异步控制解决方案,随着ES6的普及,有了新的异步解决方案 - generator。
基础
Iterator
Iterator即迭代器。是ES6规范中的一种新的数据结构。Iterator对象有一个属性方法 next ,每次调用next的时候返回一个状态:{done: false/true, value: xxx}
,当状态完成之后,将done置为true。
generator
generator会自动返回一个Iterator对象,其语法表示为 function *
,当generator函数执行之后,可以对返回的Iterator进行迭代,即不断的调用next方法,直到状态转换完毕。
yield
yield必须存在于generator函数体里面,当generator返回的Iterator对象调用next方法时,正常的执行流遇到yield关键字时候就会停止,退出函数,返回状态为false,返回值赋值为yield的返回值,同时保存现场。下次调用next方法的时候会恢复现场,从退出的语句开始继续执行。如果遇到return语句或者正常函数执行完毕,返回状态为true,返回值对应return的值。
yield*
yield后面可以跟一个Iterator对象,写作 yield *xxxIter。此时会形成一个类似嵌套的效果,调用next方法时会进入xxxIter,直到把xxxIter中的状态全部走完。
next
Iterator对象具有next方法,每次调用时返回可返回一个状态。同时在调用next方法时可以传递一个参数到next方法中,传递的参数将会被赋值给 yield 的返回值(默认yield 语句是没有返回值的),此处如此设计就是为了让next方法的调用方可以和generator内部通信,解决异步问题的关键就在于这个特性,我们后面会详细说明。
示例代码
1 | //test为generator函数 |
利用generator解决异步问题
从上面的基础我们了解到,generator函数可以实现执行到yield之后保存当前运行环境并退出,直到下一次next方法继续执行。这里我们似乎可以看到利用点,利用两次 next 方法调用之间来执行异步函数,并且在异步执行完之后调用下一次next方法。
场景:
1 | //回调嵌套写法 |
我们预想可以通过generator变成同步的书写方式:
1 | function* work() { |
思路分解
- 1.想要上述代码正常执行,第一步肯定是对work初始化,获取Iterator:
1
var workIter = work();
2.利用generator的特点,我们开始执行next,第一步应该是执行get url1,同时代码退出,直到url1返回,调用下一次next。
要实现url1返回之后才能调用下一次next,所以上一次next返回来的数据应该是一个异步方法。这样我们就可以在其callback中放置下一次的next,同时可以将第一次的返回url2通过next的参数传递进generator函数内部,在执行下一次next时使用。
1 | var request1 = workIter.next(); |
整合方法
从上面的思路可以看出,虽然generator work 方法写起来是同步的,但是执行其内部next的方法需要处理多次回调,层层嵌套,不可能手动去触发next方法直到结束,所以我们需要一个方法来自动触发next方法,直到结束。
1 | function geHelper(worker) { |
同时,我们需要封装request方法,让其返回一个异步fn。
1 | function request(url) { |
现在可以使用上述的简单封装方法来完成需求了:
1 | geHelper(function*() { |
co
上述封装还是太过简陋,业内早有成熟的方案:co
co大致思路和我们的一样,不过其将返回结果promise化,更符合未来异步的潮流。简单使用方法:
1 | co(function* () { |
co的generator函数中,yield后面可以兼容的几种写法为:
- promise,即yield返回一个promise,co可在内部自动控制在then方法里面置入下一次next方法。
- Thunks, 即我们上述request类似函数,返回一个只有一个参数即为callback的异步函数。
- array,可以保护多个promise/thunks
- Generators,即function*,嵌套generator