审视co和展望ES7
发布在逼格编程杂谈2014年11月17日view:8643
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

本文前部分会回顾一些与co相关的技术方案,所以不了解co的同学也可以继续阅读。

周末看到co已经正式发布了4.0。twitter和weibo上都是普天同庆的节奏。不过看到API如此变化后,还有多少人能愉快的度过本周。

如果大家还没有抽空去了解新版的co,这里简单的让你了(xia)解(niao)一下。

co(function* () {
  var result = yield Promise.resolve(true);
  return result;
}).then(function (value) {
  console.log(value);
}, function (err) {
  console.error(err.stack);
});

对,大家没有看错。co现在不在返回一个thunk,而是返回一个Promise(或Promise-like)对象。

也有情提示大家在没有更改自己的代码之前,请反复检查自己的package.json,确保没有自动升级到4.0.0。

对于co这样的基础库进行如此大的改变,也让我十分好奇。周末抽空去了解了一下这前因后果,且让我放一些马后炮。

Javascript异步编程之殇

从作业调度的角度来讲,Javascript是绝对的屌丝。什么thread、fiber、coroutine、proc,统统木有。但得益于近些年V8的和ECMAScript标准的发展,Javascript也慢慢有一些特色的异步编程解决方案。

co这些第三方库的出现,就是为了解决异步编程这个永恒的难题。

在ES5时代,就有大把的Promise/Deffered方案:

一个简单的使用rsvp.js进行异步编程的例子(来自rsvp的说明文档)。我们构造一个getJSON函数,来异步获取JSON数据。

var getJSON = function(url) {
  var promise = new RSVP.Promise(function(resolve, reject){
    var client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

    function handler() {
      if (this.readyState === this.DONE) {
        if (this.status === 200) { resolve(this.response); }
        else { reject(this); }
      }
    };
  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
  // continue
}, function(error) {
  // handle errors
});

Promise对象拥有一个标准的thenable接口,一般有thencatchdonefail等方法。

这些Promise库,大多遵循一个Promise/A+的标准。这个标准的组织就是开源社区的一些志同道合的大侠们。

node 0.11.x之后,合并了支持generator的V8内核,支持了yield语句。

要注意,generator是最初用于解决迭代器的问题:

function* argumentsGenerator() {
  for (let i = 0; i < arguments.length; i += 1) {
    yield arguments[i];
  }
}

var argumentsIterator = argumentsGenerator('a', 'b', 'c');

// Prints "a b c"
console.log(
    argumentsIterator.next().value,
    argumentsIterator.next().value,
    argumentsIterator.next().value
);

但是有一个特性。yield语句执行之后,该函数的执行将会暂停(suspend),直到generator被调用next方法。

这个特性自然可以用作异步流程控制:

var fs = require("fs");
spawn(function*(resume) {
  var content = yield fs.readFile(__filename, resume);
  var list = yield fs.readdir(__dirname, resume);
  return [content, list];
})(function(e, res) {
  console.log(e,res);
});

spawn的实现非常简单:

var slice = Array.prototype.slice.call.bind(Array.prototype.slice);

var spawn = function(gen) {//`gen` is a generator function
  return function(callback) {
    var args, iterator, next, ctx, done;
    ctx = this;
    args = slice(arguments);

    next = function(e) {
      if(e) {//throw up or send to callback
        return callback ? callback(e) : iterator.throw(e);
      }
      var ret = iterator.next(slice(arguments, 1));
      if(ret.done && callback) {//run callback is needed
        callback(null, ret.value);
      }
    };

    resume = function(e) {
      next.apply(ctx, arguments);
    };

    args.unshift(resume);
    iterator = gen.apply(this, args);
    next();//kickoff
  };
};

co本质上和这个spawn一模一样。老版本的co

var read = function(file) {
    return function(callbakc) {
        fs.readFile(file, callback);
    };
};

co(function *() {
    var content = yield read("target.html");
    //TOOD: handle with `content`
});

co在实现了近似“同步”的编程写法的同时,也引入了很多相应的概念:

  1. thunk:仅有一个node风格的回调作为参数的函数。参考tj/node-thunkify。Promise、Generator都可以转换为一个thunk。
  2. yieldable:可以被yield的对象,包括thunkPromiseGeneratorGeneratorFunction,以及包含以上类型对象的Object和Array。

以上两个概念,都是co独有的。ECMAScript均没有定义这两个东西。不得不说,co的作者tj是非常天才的做到了co既简单又好用的。

ES7来了

Javascript开发者已经习惯了标准制定者无休止的争论,但是随着互联网厂商的强力干涉(Google、Mozilla等),可以看到ES5、ES6的Draft进度是非常快的。尤其是Google通过其V8的垄断地位,已经提前把ES讨论中的一些概念给实现了,造成了比较大的影响(例如Promise、Proxy)。另一方面,也有很多开源的转义器(facebook/regeneratorgoogle/traceur-compiler等等),能够将一些还未在JS引擎层面实现的语法,能够在运行时之前被翻译成现行JS引擎能够理解的代码。

好了,回到co。四周之前,co的贡献者之一jonathanong提交了一个PR。将co的返回,从thunk变成了Promise对象。co的原作者tj看了之后欣然的接受,事情就是这样。

co虽好,有两点,在我们最开始使用co的时候,就应该要有了解:

  1. co其实在滥用ES6的generator
  2. co引入的概念都不是ECMAScript的内容。换句话说,tj没有能力去影响ECMAScript的标准制定(也许是不感兴趣)。

第一点不管大家认同与否,势必造成一个新的东西来改善这种现状。就好像在ES5之前,大家会用闭包去控制属性的可读写一样,最终ES5的Object.defineProperty最终给了一个更合适的方案。ES7已经给出了答案,相应的方案就是asyncawait

假想一个原有的基于回调的方法:

var Foo = function() {};

Foo.prototype.bar = function(callback) { //async function
    //fake async execution
    setTimeout(callback, 1000);
};

我们用用co进行改造:

var co = require("co");

var Foo = function() {};

//backup
Foo.prototype._bar = Foo.prototype.bar;

//co-style API
Foo.prototype.bar = co(function*() {
    yield this._bar;
});

或者这样:

Foo.prototype.bar = function*() {
    yield this._bar;
};

第一种通常是作为wrapper,将传统的基于callback的方案转换为co风格的异步风格。通常会有两个包,例如fs-extraco-fs-extra

而第二种,就干脆面向co编程,不支持传统的callback形式。现在这种包的形式有一些,笔者不清楚是不是有流行的趋势。但是假若真流行了,可谓是一种灾难。因为这些借口,脱离了co的上下文,是完全没有办法正常工作的。

那么,关于第二点,再多的讨论也只不过是八卦。ECMAScript的大神们,用什么,不用什么,都可以说出道理。不妨,从ES7的角度来看看,到底ES7给的方案,比co有哪些好处。

对比,之前的文件读取的例子:

function read(file) {
    var deffered = new Deffered();
    fs.readFile(file, function(e, content) {
        if(e) {
            deffered.errback(e);
        } else {
            deffered.callback(content);
        }
    });
    return deffered;
}

async function job(file) {
    await read(file);
};

job(file).then(function(content) {
    console.log(content);
});

async标记的函数支持await表达式。包含await表达式的的函数是一个deferred functionawait表达式的值,是一个awaited object。当该表达式的值被评估(evaluate)之后,函数的执行就被暂停(suspend)。只有当deffered对象执行了回调(callback或者errback)后,函数才会继续。

也就是说,把co函数内yield替换为await就好啦。最重要的是,你不需要用co包裹你的函数啦。

更复杂的async函数例子:https://github.com/lukehoban/ecmascript-asyncawait#await-and-parallelism

上面提到的优势可能每个人看到的都不同,也许有些人根部不觉得这是优势。但ES7最终就会带来这些东西,最终版的形式可能稍有不同,但一定会有。

也许co会作为一种兼容方案(对于不支持async/await的平台)继续存在。

其他的一些想法

  • 对于“新”特性,应用之前应该多了解其原理
  • 对于co这种使用频率高的库,应该主动watch,了解他们的变化和出现的已知bug。顺手能改几个bug那也是极好的。
  • 做开源还是得拥抱标准,特别是你不能影响标准的时候。例如,假设你现在要做开源的moduleclass解决方案,你敢不兼容ES7里面的相应标准么?
  • 有能力的互联网团队,还是应该参与到标准的讨论,甚至制定。国内好像只有百度有相应的W3C团队。
评论
发表评论
暂无评论
WRITTEN BY
ELFVision
养猫的互联网绅士
TA的新浪微博
PUBLISHED IN
逼格编程杂谈

各种杂记,你懂的。

友情链接 大搜车前端团队博客
我的收藏