Nodejs : promise的优雅异步

前言

Nodejs 异步操作是基于回调的,在日常的项目开发中有很多时候都会需要并行或者多个异步操作嵌套使用,这样就很容易产生“回调黑洞”。解决这种问题,promises无疑是一个好的选择,只不过promises是一个抽象的概念

promise是对异步编程的一种抽象。它是一个代理对象,代表一个必须进行异步处理的函数返回的值或抛出的异常。 – Kris Kowal on JSJ

promises

promises的核心方法是then(),我们从then()方法中获取原来回调产生的返回值,或者抛出的异常(拒绝执行的理由)。then()方法有两个可选的参数onFulfilled(执行)和onRejected(拒绝),主要思路:

var promise = doSomethingAync()
promise.then(onFulfilled, onRejected)

promise被执行时(异步处理内容已经完成)会调用onFulfilledonRejected 。两个函数中仅有一个会被触发,因为可能有一种结果的产生。

promises 和 callback

我们来看一个读取文件的异步 node callback:

readFile(function(err, data){
  if(err)  return console.error(err);
  console.log(data);
})

如果readFile()使用 promise,写法如下:

var promise = readFile();
promise.then(function(err, data){
  if(err)  return console.error(err);
  console.log(data);
})

then()中执行的是异步执行结束后需要执行的下一步内容。从上面我们似乎还感受不到 promise 带来了那些变化。但实际上 我们得到了一个代表本次异步操作得一个值(变量 promise),因此我们可以传递这个变量,我们不用关心该次异步操作是否已经结束,只有我们能使用 promise 这个变量得地方,我们都可以通过then()这个方法来获取刀异步操作所产生得返回值,额而且不用担心值会发生某种变化,因为 promise 只会被执行一次(约定只会被执行一次: 履行约定 和 拒绝履行约定)。

then当成对promise解包以得到异步操作结果(或异常)的函数对理解promise更有帮助,不要把它当成只是带两个callback(onFulfilledonRejected)的普通函数。详情请见此文<<指令式Callback,函数式Promise:对node.js的一声叹息>>

promise 嵌套

这里有一点要特别注意,then()方法返回得任然是 promise,也就是说后面可以链式接then(),例如:

var promise = readFile();
var promise2 = promise.then(readAnotherFile, consle.error);

这个promise表示的是 onFulfilledonRejected得返回结果,正如前面说的:promise 用来调用then()都可以借而回去到promise异步执行的返回值,且值不会发生变化,当嵌套发生时:

var promise = readFile();
var promise2 = promise.then(function(data){
  return readAnotherFile();    // if readFile is successful, let's readAnotherFile
}, function(err){
  console.error(err);    // if readFile was failed, let's log it but still readAnotherFile
});

promise2.then(doSomething, console.error);    //promise2 可以继续通过返回值执行下一个操作

因此可以有链式操作:

readFile()
    .then(readAnotherFile)
    .then(doSomething
    .then(doSomeEles)
    .then(..)
promises 错误处理

除了return,我们可以像java种一样使用throwtry/catch来捕获、抛出异常。如:

try{
  doThing();
  doAnotherThing();
}catch{
  console.error(err);
}

上面 doThing()doAnotherTing()执行时如果抛出异常或者错误,会被捕获到打印出错误日志,异步代码中可以这么使用:

doAsync()
  .then(doAnotherAsync)
  .then(null, console.error);

如果doAnotherAsync()没有成功,其promise会被拒绝执行,因此处理脸上的下一个then()中的onRejected会被调用(即,在上面代码中,doAnotherAsync中的异常或者错误会在下一个then()的 console.error中打印出来)。跟 try/catch 一样, doAnotherAsync() 根本就不会被调用。这相比于 callback 要好了很多,但是promise远比这样还要好,任何被抛出的异常,无论显式的还是隐士的,then()的回调中也会处理

doThisAsync()
  .then(function (data) {
    data.foo.baz = 'bar' // throws a ReferenceError as foo is not defined
  })
  .then(null, console.error)

上例中抛出的ReferenceError会被处理链中下一个onRejected捕获。相当漂亮!当然,这对显式抛出的异常也有效:

oThisAsync()
  .then(function (data) {
    if (!data.baz) throw new Error('Expected baz to be there')
  })
  .then(null, console.error)
Q 更容易的promise 返回

安装:
npm install q --save
node 的核心异步函数不会返回promise; 它们采用了callback 的方式。 使用Q可以很容易的让其返回promise, 如:

var fs_readFile = Q.denodify(fs.readFile)
var promise = fs_readFile('myfile.txt')
promise.then(console.log, console.error)

fs.readFile是布恩那个返回promise的,我们通过Q对其进行封装后,就可以返回 promise了

Q 提供了一些辅助函数,可以将Node和其他环境适配为promise可用的。请参见 READMEAPI documentation 了解详情。

Q 创建原始的 promise

通过Q.defer可以手动创建promise(基本上就是Q.denodify来封装)。比如将fs.readFile封装成promise的:

function fs_readFile (file, encoding) {
  var deferred = Q.defer()
  fs.readFile(file, encoding, function (err, data) {
    if (err) deferred.reject(err) // rejects the promise with `er` as the reason
    else deferred.resolve(data) // fulfills the promise with `data` as the value
  })
  return deferred.promise // the promise is returned
}
fs_readFile('myfile.txt').then(console.log, console.error)
同时支持 callback 和 promise 两种风格

可以通过Q将函数封装成callback和promise 两种风格的返回:

function fs_readFile (file, encoding, callback) {
  var deferred = Q.defer()
  fs.readFile(function (err, data) {
    if (err) deferred.reject(err)   // rejects the promise with `er` as the reason
    else deferred.resolve(data)   // fulfills the promise with `data` as the value
  })
  return deferred.promise.nodeify(callback)   // the promise is returned
}

如果提供了callback,当promise被拒或被解决时,会用标准Node的callback 风格的(err, result) 参数来进行调用:

fs_readFile('myfile.txt', 'utf8', function (er, data) {
  // ...
})
使用promise 来进行并行操作

Q 对并行操作提供了Q.all来提供并行操作多个异步操作的方法。Q.all完成时,onFulfilled只会有一个参数(一个包含多个结果的数组,有几个异步并行操作就有几个结果),任何一个操作失败,Q.all的promise会被拒绝履约。例如:

var allPromise = Q.all([ fs_readFile('file1.txt'), fs_readFile('file2.txt') ])
allPromise.then(console.log, console.error)

不得不强调一下,promise在模仿函数。函数只有一个返回值。当传给Q.all两个成功完成的promises时,调用onFulfilled只会有一个参数(一个包含两个结果的数组)。你可能会对此感到吃惊;然而跟同步保持一致是promise的一个重要保证。如果你想把结果展开成多个参数,可以用Q.spread。

使用 promise 来同时进行 http.get 和http.post 请求,并将两个请求的结果合并返回给 client

首先我们可以编写一个使用Q来封装的http请求的文件,文件名:http_tools.js

var http = require('http');
var Q = require('q');

/**
 * 使用Q promise 封装 http.get
 * @param op
 */
exports.my_get = function (op) {
    var def = Q.defer();
    http.get(op, function (res) {
        res.setEncoding('utf8');
        res.on('data', function (chunk) {
            def.resolve(chunk);
        });
        res.on('error', function (err) {
            def.reject(err);
        });
    });
    return def.promise;
};

/**
 * 使用Q promise 封装 http.request
 * @param op POST请求参数
 * @param post_data POST提交的数据
 */
exports.my_post = function (op, post_data) {
    var def = Q.defer();
    var req = http.request(op, function(res) {
        res.setEncoding('utf8');
        res.on('data', function (chunk) {
            def.resolve(chunk);
        });
        res.on('error', function (err) {
            def.reject(err);
        });
    });
    // write data to POST request body
    req.write(post_data + "\n");
    req.end();
    return def.promise;
};

我们通过Q来自行封装 my_getmy_post两个方法,使其可以返回promise,route文件如下:

var express = require('express');
var router = express.Router();
var http = require('../tools/http_tools');      //引入我们上面的方法文件

var PAGE_SIZE = 6;

router.get('/batch_material/:type/:page', function (req, res, next) {
    // client 端请求参数
    var page = req.params.page;
    var type = req.params.type;

    // post 请求推送的数据
    var post_data = JSON.stringify({
        "type": type,
        "offset": page * PAGE_SIZE,
        "count": PAGE_SIZE
    });

    // post 请求设置参数参数
    var post_op = {
        hostname: conf.wxserver.host,
        port: conf.wxserver.port,
        path: '/material/batchget_material',
        method: 'POST'
    };

    // get 请求参数
    var get_op = {
        hostname: conf.wxserver.host,
        port: conf.wxserver.port,
        path: '/menu/selfmenuinfo',
        method: 'GET'
    };

    var allPromise = Q.all( [http.my_post(post_op, post_data),  http.my_get(get_op)] );
    allPromise.spread(function (cb1, cb2) {
        console.log("post result --:", cb1);   // post 请求返回的结果
        console.log("get result --:", cb2);    // get 请求返回的结果
    });
});

module.exports = router;

在上面的代码中,并行执行 一个 get 和一个 post 的网络请求,同时 post还需要向server端推送数据,结果我们通过Q.spread来分别返回, 如果使用Q.then()则返回数组,如:

allPromise.then(function (cb) {
        console.log("post result --:", cb[0]);   // post 请求返回的结果
        console.log("get result --:", cb[1]);    // get 请求返回的结果
    });

结尾

以上就是对promise使用的一些小的记录,知识的汲取离不开网友大神的无私贡献,在这里感谢各位大大们的无私风险,尤其是这篇来自图灵社区的文章:
在Node.js 中用 Q 实现Promise – Callbacks之外的另一种选择
以及
原文:Promises in Node.js with Q – An Alternative to Callbacks

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • 本文适用的读者 本文写给有一定Promise使用经验的人,如果你还没有使用过Promise,这篇文章可能不适合你,...
    HZ充电大喵阅读 7,293评论 6 19
  • 00、前言Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区...
    夜幕小草阅读 2,127评论 0 12
  • Promiese 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,语法上说,Pr...
    雨飞飞雨阅读 3,348评论 0 19
  • Promise的含义:   Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和...
    呼呼哥阅读 2,161评论 0 16
  • 前言 如今的互联网是 互联网+ 的时代,是全民互联网时代,曾经特定的网民人群已经是代表全公民的群体了,那么互联网面...
    活这么大就没饱过阅读 488评论 0 0