异步编程之co——源码分析
异步编程系列教程:
- (翻译)异步编程之Promise(1)——初见魅力
- 异步编程之Promise(2):探究原理
- 异步编程之Promise(3):拓展进阶
- 异步编程之Generator(1)——领略魅力
- 异步编程之Generator(2)——剖析特性
- 异步编程之co——源码分析
如何使用co
大家如果能消化掉前面的知识,相信这一章的分析也肯定是轻轻松松的。我们这一章就来说说,我们之前一直高调提到的co
库。co
库,它用Generator和Promise相结合,完美提升了我们异步编程的体验。我们首先看看如何使用co
的,我们仍旧以之前的读取Json文件的例子看看:
// 注意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)一次:
- 我们调用遍历器的
next()
得到该异步的promise对象 - 在promise对象的
then()
中的resolve
对数据进行处理 - 把处理后的数据作为参数
res
传入next(res)
,继续到下一次异步操作 - 重复2,3步骤。直到迭代器的
done: true
,结束遍历。
如果不清楚我们上面说过的Generator遍历器或promise对象的,可以先放一放这篇文章,从之前的几篇看起。
进入co的世界
获得遍历器
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);
//...下面代码暂时省略...
})
}
这里我们需要关注的有两点:
- co函数最终返回的是一个Promise。
- 第6行代码,我们可以看到gen变量一开始就已经自身调用了。也就是gen从构造器变成了遍历器。
遍历器开始遍历
我们首先看看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()
又封装了一遍方便源码使用。
接着看,如果并没有完成遍历。我们就会对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其他的内容
分析完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
我们先看看相对简单的array的源码:
function arrayToPromise(obj) {
return Promise.all(obj.map(toPromise, this));
}
map是ES5的array的方法,这个相信也有人经常使用的。我们将数组里的每一项的值,再进行一次toPromise
操作,然后得到全部都是promise对象的数组交给Promise.all
方法使用。这个方法在promise文章的第二篇也讲过它的实现,它会在所有异步都执行完后才会执行回调。最后resolve(res)
的res
是一个存有所有异步操作执行完后的值的数组。
对象Object
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函数中。
异步编程之co——源码分析的更多相关文章
- 并发编程之 Exchanger 源码分析
前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...
- Java并发编程之ThreadLocal源码分析
## 1 一句话概括ThreadLocal<font face="微软雅黑" size=4> 什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象 ...
- 并发编程之 Semaphore 源码分析
前言 并发 JUC 包提供了很多工具类,比如之前说的 CountDownLatch,CyclicBarrier ,今天说说这个 Semaphore--信号量,关于他的使用请查看往期文章并发编程之 线程 ...
- 并发编程之 CyclicBarrier 源码分析
前言 在之前的介绍 CountDownLatch 的文章中,CountDown 可以实现多个线程协调,在所有指定线程完成后,主线程才执行任务. 但是,CountDownLatch 有个缺陷,这点 JD ...
- Java并发编程之ReentrantLock源码分析
ReentrantLock介绍 从JDK1.5之前,我们都是使用synchronized关键字来对代码块加锁,在JDK1.5引入了ReentrantLock锁.synchronized关键字性能比Re ...
- 并发编程之 Condition 源码分析
前言 Condition 是 Lock 的伴侣,至于如何使用,我们之前也写了一些文章来说,例如 使用 ReentrantLock 和 Condition 实现一个阻塞队列,并发编程之 Java 三把锁 ...
- 并发编程之 CountDown 源码分析
前言 Doug Lea 大神在 JUC 包中为我们准备了大量的多线程工具,其中包括 CountDownLatch ,名为倒计时门栓,好像不太好理解.不过,今天的文章之后,我们就彻底理解了. 如何使用? ...
- Java并发编程之AbstractQueuedSynchronizer源码分析
为什么要说AbstractQueuedSynchronizer呢? 因为AbstractQueuedSynchronizer是JUC并发包中锁的底层支持,AbstractQueuedSynchroni ...
- 并发编程之ThreadLocal源码分析
当访问共享的可变数据时,通常需要使用同步.一种避免同步的方式就是不共享数据,仅在单线程内部访问数据,就不需要同步.该技术称之为线程封闭. 当数据封装到线程内部,即使该数据不是线程安全的,也会实现自动线 ...
随机推荐
- Python IDLE 的使用与调试
Python IDLE 是Python 安装包自带的集成开发环境.IDLE集成了Python 解释器.编辑器与调试器.适用于初学者了解Python 语法知识.1.使用 Python IDLE 编辑Py ...
- DevOps实践
云计算 [ 01 ]GitLab+Rancher实践DevOps[转载] [ 02 ]Kubernetes CentOS 7.4搭建Kubernetes 1.8.5集群 kubeadm搭建kubern ...
- CSS Div固定在网页顶部、底部、左侧、右侧
Div固定在网页顶部 .header { width:100%; height:80px; background-color:#FAFAFA; position:fixed; top:; left:; ...
- CentOS 7.4中firewall防火墙详解和配置以及切换为iptables防火墙
转载:https://blog.csdn.net/xlgen157387/article/details/52672988 一.firewall介绍 CentOS 7中防火墙是一个非常的强大的功能,在 ...
- Java并发--lock锁详解
在上一篇文章中我们讲到了如何使用关键字synchronized来实现同步访问.本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方 ...
- test20190308
测试 晚上考试,是 \(SCOI\ 2016\ Day\ 2\) 的题目. 妖怪 由于之前在洛谷上用三分水过去了,就很 \(naive\) 地打了一个三分就跑了.获得 \(10\) 分好成绩. 记 \ ...
- 查询避免Unknown column ‘xxx’ in ‘where clause’
但凡写过sql语句的人估计都曾经碰到过类似于Unknown column ‘xxx’ in ‘where clause’的问题. 单从字面理解,我们很容易得出列名不存在的结论,但是,很多时候起始并不是 ...
- 【angularJS】Controller控制器
1. 定义 控制器(Controller)在AngularJS中作用是增强视图(View),AngularJS控制器是一个构造方法,用来向视图(View)中添加额外功能. ng-controller指 ...
- Django基于Form之登录和注册
1.创建Forms文件,内容略多,大家将就着看,不懂请留言 #!/usr/bin/env python # -*- coding: utf8 -*- #__Author: "Skiler H ...
- 转载——关于bp神经网络
一.BP神经网络的概念 BP神经网络是一种多层的前馈神经网络,其主要的特点是:信号是前向传播的,而误差是反向传播的.具体来说,对于如下的只含一个隐层的神经网络模型: (三层BP神经网络模型) ...