本文前部分会回顾一些与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
接口,一般有then
、catch
、done
、fail
等方法。
这些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
在实现了近似“同步”的编程写法的同时,也引入了很多相应的概念:
thunk
:仅有一个node风格的回调作为参数的函数。参考tj/node-thunkify。Promise、Generator都可以转换为一个thunk。yieldable
:可以被yield
的对象,包括thunk
、Promise
、Generator
、GeneratorFunction
,以及包含以上类型对象的Object和Array。
以上两个概念,都是co
独有的。ECMAScript均没有定义这两个东西。不得不说,co
的作者tj是非常天才的做到了co
既简单又好用的。
ES7来了
Javascript开发者已经习惯了标准制定者无休止的争论,但是随着互联网厂商的强力干涉(Google、Mozilla等),可以看到ES5、ES6的Draft进度是非常快的。尤其是Google通过其V8的垄断地位,已经提前把ES讨论中的一些概念给实现了,造成了比较大的影响(例如Promise、Proxy)。另一方面,也有很多开源的转义器(facebook/regenerator、google/traceur-compiler等等),能够将一些还未在JS引擎层面实现的语法,能够在运行时之前被翻译成现行JS引擎能够理解的代码。
好了,回到co
。四周之前,co
的贡献者之一jonathanong提交了一个PR。将co
的返回,从thunk
变成了Promise
对象。co
的原作者tj看了之后欣然的接受,事情就是这样。
co
虽好,有两点,在我们最开始使用co
的时候,就应该要有了解:
co
其实在滥用ES6的generator
co
引入的概念都不是ECMAScript的内容。换句话说,tj没有能力去影响ECMAScript的标准制定(也许是不感兴趣)。
第一点不管大家认同与否,势必造成一个新的东西来改善这种现状。就好像在ES5之前,大家会用闭包去控制属性的可读写一样,最终ES5的Object.defineProperty
最终给了一个更合适的方案。ES7已经给出了答案,相应的方案就是async
和await
。
假想一个原有的基于回调的方法:
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-extra
和co-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 function
。await
表达式的值,是一个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那也是极好的。 - 做开源还是得拥抱标准,特别是你不能影响标准的时候。例如,假设你现在要做开源的
module
和class
解决方案,你敢不兼容ES7里面的相应标准么? - 有能力的互联网团队,还是应该参与到标准的讨论,甚至制定。国内好像只有百度有相应的W3C团队。