异步编程系列教程:git
你们若是能消化掉前面的知识,相信这一章的分析也确定是轻轻松松的。咱们这一章就来讲说,咱们以前一直高调提到的co
库。co
库,它用Generator和Promise相结合,完美提高了咱们异步编程的体验。咱们首先看看如何使用co
的,咱们仍旧以以前的读取Json文件的例子看看:github
// 注意readFile已是Promise化的异步API co(function* (){ var filename = yield readFile('hello3.txt', 'utf-8'); var json = yield readFile(filename, 'utf-8'); return JSON.parse(json).message; }).then(console.log, console.error);
你们看上面的代码,甚至是可使用同步的思惟,不用去理会回调什么鬼的。咱们readFile()
获得filename
,而后再次readFile()
获得json
,解析完json后输出就结束了,很是清爽。你们若是不相信的话,可使用原生的异步api尝试一下,fs.readFile()
像上面相互有依赖的,绝对恶心!编程
咱们能够看到,仅仅是在promise化的异步api前有个yield
标识符,就可使co
完美运做。上一篇咱们也假想过co
的内部是如何实现的,咱们再理(fu)顺(zhi)一次:json
next()
获得该异步的promise对象then()
中的resolve
对数据进行处理res
传入next(res)
,继续到下一次异步操做done: true
,结束遍历。若是不清楚咱们上面说过的Generator遍历器或promise对象的,能够先放一放这篇文章,从以前的几篇看起。api
co的源码包括注释和空行仅仅才240行,不能再精简!咱们抽出其中主要的代码来进行分析。数组
function co(gen) { var ctx = this; // context // return a promise return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.call(ctx); // 调用构造器来得到遍历器 if (!gen || typeof gen.next !== 'function') return resolve(gen); //...下面代码暂时省略... }) }
这里咱们须要关注的有两点:promise
co
内部的next(ret)
函数,它是整个遍历器自动运行的关键。function next(ret) { if (ret.done) return resolve(ret.value); var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); }
咱们能够看到,ret参数有done
和value
,那么ret确定就是遍历器每次next()
的结果。若是发现遍历器遍历结束的话,便直接return整个大Promise的resolve(ret.value)
方法结束遍历。对了,此遍历器的next()
和co的next()
在这里是不同的。固然你能够认为co将遍历器的next()
又封装了一遍方便源码使用。app
接着看,若是并无完成遍历。咱们就会对ret.value
调用toPromise()
,这里有知识点延伸,暂且先跳过,由于咱们 一个 promise化的异步操做就是返回promise的。不知道你们get到point没?我就透漏一点,当是数组或对象时,co
会识别并支持多异步的并行操做,先无论~~异步
咱们在保证咱们调用异步操做获得的value
是promise后,咱们就会调用value.then()
方法为promise的onFulfilled()
或onRejected()
进行回调的绑定。也就是说,这段时间程序都是在干其余和遍历器无关的事的。遍历器没有获得遍历器的next()
指令,就一直静静的等着。咱们能够想到,next()
指令,一定是放在了那两个回调函数(onFulfilled
,onRejected
)里。异步编程
promise化的异步API是先绑定了回调方法,而后等待异步完成后进行触发。因此咱们把遍历器继续遍历的next()
指令放在回调中,就能够达到回调返回数据后再调用遍历器next()
指令,遍历器才会继续下一个异步操做。
function onFulfilled(res) { var ret; try { ret = gen.next(res); // 遍历器进行遍历,ret是这次遍历项 } catch (e) { return reject(e); } next(ret); // ret.value is a promise }
咱们看到第四行,经过调用遍历器的next(res)
,再次启动遍历器获得新的遍历结果,再传入co
的next()
里,重复以前的操做,达到自动运行的效果。这里须要注意一个地方,咱们是经过向遍历器的next(res)
传入res
变量来实现将异步执行后的数据保存到遍历器里。
我相信我不可能说的很明白,让你们一会儿就知道关键重点是哪一个。我本身也是悟了很多时间的,最终发现那个可使思路清晰的就是Deferred
延迟对象。我在第二篇也有着重说过Deferred
延迟对象,它最重要的一点就是,它是用来延迟触发回调的。咱们先经过延迟对象的promise进行回调的绑定,而后在Node的异步操做的回调中触发promise绑定的函数,实现异步操做。固然这里也是如此,咱们是把遍历器的next()
指令延迟到回调时再触发。固然在co
源码里是直接使用了ES6的promise原生对象,咱们看不到deferred
的存在。
因此我很早前就说了,promise对理解co
相当重要。以前在promise上也花费了特别大的精力去理解,并分析原理。因此你们若是没有看以前的有关promise文章的,最好都回去看一看,绝对有好处!
分析完co
最关键的部分,接下来就是其余各类有用的源码分析。关于thunk
转化为promise
我就不说了,毕竟它也是被淘汰了的东西。那要说的东西其实就两个,一个是多异步并行,一个是将co-generator
转化为常规函数。咱们一个一个来说:
以前也有提到过,就是咱们须要对迭代对象的值进行toPromise()
操做。这个操做顾名思义,就是将全部须要yield的值,统统转化为promise对象。它的源码就是这样的,并不能看到实质的东西:
function toPromise(obj) { if (!obj) return obj; if (isPromise(obj)) return obj; if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); if ('function' == typeof obj) return thunkToPromise.call(this, obj); if (Array.isArray(obj)) return arrayToPromise.call(this, obj); if (isObject(obj)) return objectToPromise.call(this, obj); return obj; }
咱们还记得在co
的next()
函数里能够看到有一个注释是这样的:
'You may only yield a function, promise, generator, array, or object'
意思是,咱们不只仅只能够yield一个promise对象。function和promise咱们就不说了,重点就是在array和object上,它们都是经过递归调用toPromise()
来实现每个并行操做都是promise化的。
咱们先看看相对简单的array的源码:
function arrayToPromise(obj) { return Promise.all(obj.map(toPromise, this)); }
map是ES5的array的方法,这个相信也有人常用的。咱们将数组里的每一项的值,再进行一次toPromise
操做,而后获得所有都是promise对象的数组交给Promise.all
方法使用。这个方法在promise文章的第二篇也讲过它的实现,它会在全部异步都执行完后才会执行回调。最后resolve(res)
的res
是一个存有全部异步操做执行完后的值的数组。
Object就相对复杂些,不过原理依然是大同小异的,最后都是回归到一个promise数组而后使用Promise.all()
。使用Object的好处就是,异步操做的名字和值是能够对应起来的,来看看代码:
function objectToPromise(obj){ var results = new obj.constructor(); var keys = Object.keys(obj); // 获得的是一个存对象keys名字的数组 var promises = []; // 用于存放promise for (var i = 0; i < keys.length; i++) { var key = keys[i]; var promise = toPromise.call(this, obj[key]); if (promise && isPromise(promise)) defer(promise, key); else results[key] = obj[key]; } return Promise.all(promises).then(function () { return results; }); function defer(promise, key) { // predefine the key in the result results[key] = undefined; promises.push(promise.then(function (res) { results[key] = res; })); } }
第一个就是新建一个和传入的对象同样构造器的对象(这个写法太厉害了)。咱们先得到了对象的全部的keys属性名,而后根据keys,来获取到每个对象的属性值。同样是用toPromise()
让属性值——也就是并行操做promise化,固然非promise的值就会直接存到results这个对象里。若是是promise,就会执行内部定义的defer(promise, key)
函数。
因此理解defer函数是关键,咱们看到是在defer函数里,咱们才将当前的promise推入到promises数组里。而且每个promise都是绑定了一个resolve()
方法的,就是将结果保存到results
的对象中。最后咱们就获得一组都是promise的数组,经过Promise.all()
方法进行异步并行操做,这样每一个promise的结果都会保存到result对象相应的key里。而咱们须要进行数据操做的也就是那个对象里的数据。
这里强烈建议你们动手模拟实现一遍 objectToPromise。
co.wrap(*generatorFunc)
---
下一个颇有用的东西就是co.wrap()
,它容许咱们将co-generator
函数转化成常规函数,我以为这个仍是须要举例子来代表它的做用。假设咱们有多个异步的读取文件的操做,咱们用co来实现。
//读取文件1 co(function* (){ var filename = yield readFile('hello1.txt', 'utf-8'); return filename; }).then(console.log, console.error); //读取文件2 co(function* (){ var filename = yield readFile('hello2.txt', 'utf-8'); return filename; }).then(console.log, console.error);
天啊,我仿佛又回到了不会使用函数的年代,一个功能一段函数,不能复用。固然co.wrap()
就是帮你解决这个问题的。
var getFile = co.wrap(function* (file){ var filename = yield readFile(file, 'utf-8'); return filename; }); getFile('hello.txt').then(console.log); getFile('hello2.txt').then(console.log);
例子很简单,咱们能够将co-generator
里的变量抽取出来,造成一个常规的Promise函数(regular-function)。这样子就不管是复用性仍是代码结构都是优化了很多。
既然知道了怎么用,就该看看它内部如何实现的啦,毕竟这是一次源码分析。其实若是对函数柯里化(偏函数)比较了解,就会以为很是简单。
co.wrap = function (fn) { createPromise.__generatorFunction__ = fn; // 这个应该是像函数constructor的东西 return createPromise; function createPromise() { return co.call(this, fn.apply(this, arguments)); } };
就是一个偏函数,借助于高阶函数的特性,返回一个新函数createPromise()
,而后传给它的参数都会被导入到Generator函数中。