callback hell

(图片来源于互联网)

回调地狱

相信每一个JS程序员都曾被或者正在被回调地狱所折磨,特别是写过Nodejs代码的程序员。

 

JS的后续传递风格(回调)是这门语言的优点也是这门语言的缺点,优点之一是我们可以很轻易的写出异步执行的代码,而缺点也是由异步引起的,当太多的异步步骤需要一步一步执行,或者一个函数里有太多的异步操作,这时候就会产生大量嵌套的回调,使代码嵌套太深而难以阅读和维护,即所谓的回调地狱。

解决方案

随着JS这门语言的发展,出现了很多处理回调地狱的解决方案。

具名函数

如最基本的,使用具名函数并保持代码层级不要太深

Promise

进阶一级的使用Promise或者链式Promise,但是还是需要不少的回调,虽然没有了嵌套

Anync

使用async等辅助库,代价是需要引入额外的库,而且代码上也不够直观

Generator

现在,ES6来了,ES6带来了新一代解决回调地狱的神器——Generator,如果你不知道Generator是什么,可以看我之前写的ES6 Generator介绍

Generator本意上应该是一种方便按照某种规则生成元素的迭代器,不过鉴于其特殊的语法和运行原理,可以通过某种神奇的方式写出同步化的异步代码,从而避免回调,使代码更易阅读。

前文介绍过生成器的运行原理和yield、yield*、next等的用法,那么怎么用生成器写出异步执行的同步代码呢?

CO

串行的“同步代码”

首先看看下面的代码

串行化执行异步函数

我知道你有疑问,co是个什么鬼?co是一个基于Generator(生成器)的异步流控制器,可以完美实现写出非阻塞的“同步代码”的目的。不解释更多…直接上github,TJ大神的作品。 源代码算上大把注释也才200多行,为了帮助你理解,根据co的思想,我写了个最简单最初级的co

结合yield和next的用法(如果你还不清楚,强烈推荐先看ES6 Generator介绍),不难理解(才怪,文字上不好解释,希望读者细心体会)为什么可以写出同步的代码,而且确实是异步执行的。

并行的“同步代码”

并行化执行异步函数

普通写法

使用co的写法

不能简单得更多了。 友情提示:请使用官方版co,前文的简单版co不支持并行。 co不仅支持数组,还支持对象

这种方式也是并行执行的。

CO API

简单看看co的最新API,已改成内部使用Promise的方式,不过思想上还是一样的。

co(fn*).then( val => )

Returns a promise that resolves a generator, generator function, or any function that returns a generator.

var fn = co.wrap(fn*)

 

co.wrap的使用场景

要想写出“同步代码”,必须要有一个Generator Function,可是当前JS的很多API接受的callback都是普通函数,如数组的forEach、map、reduce等,更广泛的Nodejs本身的API的callback绝大多数都是普通函数,这时候co.wrap就起作用了

再来看一个处理事件的

可以看到,有了co.wrap,可以将生成器函数转换成普通函数,从而可以用在任何以普通函数作为callback的地方,继续方便的写“同步代码”。

谁在用co

co的优点让人无法抗拒,而基于co的第三方库更是多得受不了。 将co用到极致的是TJ的另一个作品Koa…不解释更多了,谁用谁知道,上手CO最快的方法就是写一个基于Koa的服务端程序。

原创文章转载请注明:

转载自AlloyTeam:http://www.alloyteam.com/2015/04/solve-callback-hell-with-generator/

  1. iJavascript 2016 年 3 月 30 日

    看不懂co代码的都是不理解闭包的

    • 最亚伦 2016 年 10 月 9 日

      跟闭包没啥关系~ 主要还是 js 的一些细节处理。

  2. 陈雪 2015 年 12 月 18 日

    行,有意义,我顶 谢谢您了

  3. 陈雪 2015 年 12 月 18 日

    参考参考,我认为很好,大家说说

  4. 李太新 2015 年 10 月 12 日

    Promise 中 asyncFun1()具体指代什么 ? 怎么建立promise 函数0 0 [泪]

    • TAT.云中飞扬 2015 年 12 月 16 日

      就是一个返回Promise的函数,最简单就像这样
      function asyncFun1() {
      return new Promise((resolve, reject) => {
      resolve();
      });
      }

  5. Chris 2015 年 6 月 14 日

    function co(genFun) {
    // 通过调用生成器函数得到一个生成器
    var gen = genFun();
    return function(fn) {
    next();
    function next(err, res) {
    if(err) return fn(err);
    // 将res传给next,作为上一个yield的返回值
    var ret = gen.next(res);
    // 如果函数还没迭代玩,就继续迭代
    if(!ret.done) return ret.value(next);
    // 返回函数最后的值
    fn && fn(null, res);
    }
    }
    }

    co简易版真的是这样写的吗?看的不是太明白,return返回的不是个匿名函数的指针么?怎么能继续执行下去?fn是回调函数吗?

  6. storm 2015 年 5 月 22 日

    完整的代码应该是这样子吧

    function co(genFunc) {
    var gen = genFunc();
    return function(fn) {
    next();
    function next(res, err) {
    if (err) return fn(err);
    var ret = gen.next(res);
    if (!ret.done) return ret.value.then(next);
    fn && fn(null, err);
    }
    };
    }
    co(function*() {
    var a = yield f_a();
    var b = yield f_b(a);
    var c = yield f_c(b);
    })();
    function f_a() {
    return Promise.resolve(1);
    }
    function f_b(a) {
    return Promise.resolve(a + 3);
    }
    function f_c(b) {
    return Promise.resolve(b + 6);
    }

  7. TAT.老教授

    TAT.weber 2015 年 4 月 29 日

    盯着co的简易版理解了好久才看懂,原来是卡在不知道 asyncFun1 这几个函数的返回值上

发表评论