NodeJs 畅谈异步
来源:互联网 发布:淘宝客服晚班在家上班 编辑:程序博客网 时间:2024/06/10 21:28
我们都知道NodeJs是基于事件回调处理事件的,那我们经常会有这样的疑问,比如我们会使用db或者redis或者fs等模块去存储我们的信息,当我们需要实时返回一个数据并作出处理的时候,异步的通知显得不是那么人性化了,此时我们需要将异步变成同步,在此过程中我们有这样几个知识点要掌握以下,包括JavaScript中的生成器generartor函数,配合它的是yield关键字的暂停,thunk和promise的使用,以及co模块和框架thunkify的配合使用。
生成器Generartor
生成器函数是写成:function* functionName(){} 的形式,其本质也是一个函数,所以它具备普通函数所具有的所有特性。除此之外,它还具有以下有用特性:
1. 执行生成器函数后返回一个生成器(Generator),且生成器具有throw()方法,可手动抛出一个异常,也常被用于判断是否是生成器;
2. 在生成器函数内部可以使用yield(或者yield*),函数执行到yield的时候都会暂停执行,并返回yield的右值(函数上下文,如变量的绑定等信息会保留),通过生成器的next()方法会返回一个对象,含当前yield右边表达式的值(value属性),以及generator函数是否已经执行完(done属性)等的信息。每次执行next()方法,都会从上次执行的yield的地方往下,直到遇到下一个yield并返回包含相关执行信息的对象后暂停,然后等待下一个next()的执行;
3. 生成器的next()方法返回的是包含yield右边表达式值及是否执行完毕信息的对象;而next()方法中的参数是上一个暂停处yield的返回值。var a = yield ... 该参数的值将赋值给a变量。
实例1:
function test(){return 'b';}function* func(){var a = yield 'a';console.log('gen:',a);// gen: undefined var b = yield test(); console.log('gen:',b);// gen: undefined} var func1 = func(); var a = func1.next(); console.log('next:', a);// next: { value: 'a', done: false } var b = func1.next(); console.log('next:', b);// next: { value: 'b', done: false } var c = func1.next(); console.log('next:', c);// next: { value: undefined, done: true }
根据上面说过的第3条执行准则:“生成器的next()方法返回的是包含yield右边表达式值及是否执行完毕信息的对象;而next()方法的参数是上一个暂停处yield的返回值”,因为我们没有往生成器的next()中传入任何值,所以:var a = yield ‘a’;中a的值为undefined。
那我们可以将例子稍微修改下:
function test(){return 'b';}function* func(){var a = yield 'a';console.log('gen:',a);// gen:1var b = yield test();console.log('gen:',b);// gen:2}var func2 = func();var a = func2.next();console.log('next:', a);// next: { value: 'a', done: false }var b = func2.next(1);console.log('next:', b);// next: { value: 'b', done: false }var c = func2.next(2);console.log('next:', c);// next: { value: undefined, done: true }
关于yield
普通yield实例:
function* outer() {yield 'begin';yield inner();yield 'end';}function* inner() {yield 'inner';}var it = outer(), v;v = it.next().value;console.log(v); // -> 输出:beginv = it.next().value;console.log(v); // -> 输出:{}console.log(v.toString()); // -> 输出:[object Generator]v = it.next().value;console.log(v); // -> 输出:end
代理yield实例:
function* outer() {yield 'begin';/** 这行等价于 yield 'inner';就是把inner里面的代码替换过来。同时获得的rt刚好就是inner的返回值*/var rt = yield* inner();console.log(rt); // -> 输出:return from inneryield 'end';}function* inner() {yield 'inner';return 'return from inner';}var it = outer(), v;v = it.next().value;console.log(v); // -> 输出:beginv = it.next().value;console.log(v); // -> 输出:innerv = it.next().value;console.log(v); // -> 输出:end
根据文档的描述,yield* 后面接受一个 iterable object 作为参数,然后去迭代(iterate)这个迭代器(iterable object),同时 yield* 本身这个表达式的值就是迭代器迭代完成时(done: true)的返回值。调用 generator function 会返回一个 generator object,这个对象本身也是一种 iterable object,所以,我们可以使用 yield* generator_function() 这种写法。
啊,好绕。在我实际的使用过程中,yield* 一般用来在一个 generator 函数里“执行”另一个 generator 函数,并可以取得其返回值。
关于yield和co
co(function* () {// yield promisevar a = yield Promise.resolve(1);console.log(a); // -> 输出:1// yield thunkvar b = yield later(10);console.log(b); // -> 输出:10// yield generator functionvar c = yield fn;console.log(c); // -> 输出:fn_1// yield generatorvar d = yield fn(5);console.log(d); // -> 输出:fn_5// yield arrayvar e = yield [Promise.resolve('a'),later('b'),fn,fn(5)];console.log(e); // -> 输出:['a', 'b', 'fn_1', 'fn_5']// yield objectvar f = yield {'a': Promise.resolve('a'),'b': later('b'),'c': fn,'d': fn(5)};console.log(f); // -> 输出:{a: 'a', b: 'b', c: 'fn_1', d: 'fn_5'}function* fn(n) {n = n || 1;var a = yield later(n);return 'fn_' + a;}function later(n, t) {t = t || 1000;return function(done) {setTimeout(function() { done(null, n); }, t);};}}).catch(function(e) {console.error(e);});
通过上面的代码,我们看清了一个事实,那就是co模块中yield后面的只能是promise、generator、thunk函数等,并且他只是同步的,在之前我们讨论过,如果我们想要这句代码 var a = yield 'value' 的a变量赋值,那么我们需要在调用next的时候传入参数值,但是在co模块中,他会将promise的通知值(也就是resolve)、thunk函数的灰调函数的值赋值给a变量。
promise函数
Promise对象是CommonJS工作组提出的一种规范,目的是为异步操作提供统一接口。
那么,什么是Promises?
首先,它是一个对象,也就是说与其他JavaScript对象的用法,没有什么两样;其次,它起到代理作用(proxy),充当异步操作与回调函数之间的中介。它使得异步操作具备同步操作的接口,使得程序具备正常的同步运行的流程,回调函数不必再一层层嵌套。
简单说,它的思想是,每一个异步任务立刻返回一个Promise对象,由于是立刻返回,所以可以采用同步操作的流程。这个Promises对象有一个then方法,允许指定回调函数,在异步任务完成后调用。
比如,异步操作f1返回一个Promise对象,它的回调函数f2写法如下。(new Promise(f1)).then(f2);
// 传统写法step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // ... }); }); });});// Promises的写法(new Promise(step1)) .then(step2) .then(step3) .then(step4);
从上面代码可以看到,采用Promises接口以后,程序流程变得非常清楚,十分易读。
注意,为了便于理解,上面代码的Promise对象的生成格式,做了简化,真正的语法请参照下文。
总的来说,传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promises规范就是为了解决这个问题而提出的,目标是使用正常的程序流程(同步),来处理异步操作。它先返回一个Promise对象,后面的操作以同步的方式,寄存在这个对象上面。等到异步操作有了结果,再执行前期寄放在它上面的其他操作。
Promises原本只是社区提出的一个构想,一些外部函数库率先实现了这个功能。ECMAScript 6将其写入语言标准,因此目前JavaScript语言原生支持Promise对象。
关于Promise接口,前面说过,Promise接口的基本思想是,异步任务返回一个Promise对象。他只有三种状态pending(未完成)、resolved(已完成)、rejected(失败),那么当它返回时,只有成功和失败两个概念了,执行成功Promise对象传回一个值,状态变为resolved,异步操作失败,Promise对象抛出一个错误,状态变为rejected。
Promise对象使用then方法添加回调函数。then方法可以接受两个回调函数,第一个是异步操作成功时(变为resolved状态)时的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(可以省略)。一旦状态改变,就调用相应的回调函数。
po.then(console.log, console.error);上面代码中,Promise对象po使用then方法绑定两个回调函数:操作成功时的回调函数console.log,操作失败时的回调函数console.error(可以省略)。这两个函数都接受异步操作传回的值作为参数。当然了,then可支持链式编程。
上面代码中,po的状态一旦变为resolved,就依次调用后面每一个then指定的回调函数,每一步都必须等到前一步完成,才会执行。最后一个then方法的回调函数console.log和console.error,用法上有一点重要的区别。console.log只显示回调函数step3的返回值,而console.error可以显示step1、step2、step3之中任意一个发生的错误。也就是说,假定step1操作失败,抛出一个错误,这时step2和step3都不会再执行了(因为它们是操作成功的回调函数,而不是操作失败的回调函数)。Promises对象开始寻找,接下来第一个操作失败时的回调函数,在上面代码中是console.error。这就是说,Promises对象的错误有传递性。
ES6提供了原生的Promise构造函数,用来生成Promise实例。
var promise = new Promise(function(resolve, reject) { // 异步操作的代码 if (/* 异步操作成功 */){ resolve(value); } else { reject(error); }});这里我们可能有点和then的用法冲突了,其实没有,下面我们给出一个ajax配合使用promise的用法:
function search(term) { var url = 'http://example.com/search?q=' + term; var xhr = new XMLHttpRequest(); var result; var p = new Promise(function (resolve, reject) { xhr.open('GET', url, true); xhr.onload = function (e) { if (this.status === 200) { result = JSON.parse(this.responseText); resolve(result); } }; xhr.onerror = function (e) { reject(e); }; xhr.send(); }); return p;}search("paramValue").then(console.log, console.error);
此时很明白了吧。。。。。。
那我们说了这么多,我们也想promise配合co模块使用一次,将其异步变成同步的使用方法就可以了,实例如下:
var fs = require('fs');var readFile = function (fileName){ return new Promise(function (resolve, reject){ fs.readFile(fileName, function(error, data){ if (error) reject(error); resolve(data); }); });};var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString());};到此,明白了吧,这个配合co模块实现了同步的操作了吧
关于promise的更多详细讲解,参照 http://javascript.ruanyifeng.com/advanced/promise.html官方,http://www.ruanyifeng.com/blog/2015/05/co.html阮一峰
这其中有几点要注意的也就是,首先promise是一个将异步金字塔管理成为then的调用,他会立即返回一个状态,当其中一个执行成功后,then去执行下一个异步回调函数,请注意它的三个状态,其中一个api Promise.resolve(value),这个api只是的立刻马上返回一个value给return使用。thunk函数
co模块可以将我们的生成器自动的去执行,而不需要我们手动进行next执行,但是在co的应用中,为了能像写同步代码那样书写异步代码,比较多的使用方式是使用thunk函数(但不是唯一方式,还可以是:Promise)。比如读取文件内容的一步函数fs.readFile()方法,转化为thunk函数的方式如下:
function readFile(path, encoding){ return function(cb){ fs.readFile(path, encoding, cb); };}
thunk函数具备以下两个要素:
1. 有且只有一个参数是callback的函数;
2. callback的第一个参数是error。
其实去看看这两点,在node中的回调函数都是可以写成thunk函数去执行的。使用thunk函数,同时结合co我们就可以像写同步代码那样来写书写异步代码,先来个例子感受下:
var co = require('co'),fs = require('fs'),Promise = require('es6-promise').Promise;function readFile(path, encoding){return function(cb){ // thunk函数fs.readFile(path, encoding, cb);};}co(function* (){// 外面不可见,但在co内部其实已经转化成了promise.then().then()..链式调用的形式var a = yield readFile('a.txt', {encoding: 'utf8'});console.log(a); // avar b = yield readFile('b.txt', {encoding: 'utf8'});console.log(b); // bvar c = yield readFile('c.txt', {encoding: 'utf8'});console.log(c); // creturn yield Promise.resolve(a+b+c);}).then(function(val){console.log(val); // abc}).catch(function(error){console.log(error);});其实,对于每次都去自己书写一个thunk函数还是比较麻烦的,有一个框架thunkify可以帮我们轻松实现,修改后的代码如下:
var co = require('co'), thunkify = require('thunkify'), fs = require('fs'), Promise = require('es6-promise').Promise; var readFile = thunkify(fs.readFile); co(function* (){// 外面不可见,但在co内部其实已经转化成了promise.then().then()..链式调用的形式 var a = yield readFile('a.txt', {encoding: 'utf8'}); console.log(a); // a var b = yield readFile('b.txt', {encoding: 'utf8'}); console.log(b); // b var c = yield readFile('c.txt', {encoding: 'utf8'}); console.log(c); // c return yield Promise.resolve(a+b+c);}).then(function(val){ console.log(val); // abc}).catch(function(error){ console.log(error);});
区分好thunkify的使用,它可以包含一个异步的具有回调函数的thunk函数,提供给co模块使用。
那好,我们最后来一个NodeJs异步中的总结,首先因为生成器和yield可以使程序暂停执行,但是并不具有将异步函数的返回值返回给var a变量的能力,那么我们加入了co这个模块来使用,co可以将yield后面的无论什么回调异步的返回值return给变量a使用,但是有一个问题,yield后面如果有多个或者更多的异步怎么办,我们就使用thunk和promise将所有的异步串行执行起来,最后加入co模块,完美!再次推荐使用thunk,因为我们大多是情况下是需要一个异步函数的返回值,并且有thunkify框架的支持,而promise也是有一些异样的东西,比如,有时候你在拿去它的返回值的时候,不太容易,为什么呢,then后如果写了console.log,因为上一次的值传入了log中,log并没与传出所以失去了结果,比如代码好像是这样的
var promise = new Promise(function(resolve, reject) { if (true){ //成功时,将2作为返回值,传入下一个then的参数中,如果没有下一个then,会将值村吃在promise._result中 resolve(2); } else { reject(error); }});var result = promise._result;
- NodeJs 畅谈异步
- nodejs 异步
- nodejs异步
- 畅谈
- Nodejs 异步框架async
- nodejs 异步优化
- Nodejs 异步 I/O
- nodejs-异步I/O
- nodejs异步控制
- nodejs异步编程
- nodejs异步实践
- nodejs 异步编程 async
- Nodejs异步,回调
- nodejs的异步调用
- NodeJS -- 异步I/O
- nodejs-mysql异步(2)
- NodeJs学习 -- 异步机制
- nodejs异步IO的实现
- perl中的字符编码
- 【Android】操作Camera对象采集图像
- ORACLE建表空间及用户
- 安卓面试:activity
- unity5.0新功能-布料、动画系统
- NodeJs 畅谈异步
- E:安桌层及文件系统层的PRINTf输出原理
- HDU5875-Function(RMQ + 二分)
- iOS 使用fopen返回null
- Majority Element
- iOS 动画Animation-3: CATransform3D 特效详解
- 网页上随机验证码的生成:
- 基于Alcatraz安装CocoaPods报错
- A/D驱动程序