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

推荐阅读更多精彩内容

  • This document is intended to explain how promises work an...
    JasonFF阅读 892评论 0 4
  • 本文适用的读者 本文写给有一定Promise使用经验的人,如果你还没有使用过Promise,这篇文章可能不适合你,...
    HZ充电大喵阅读 3,552评论 5 19
  • 00、前言Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区...
    夜幕小草阅读 792评论 0 11
  • Promiese 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,语法上说,Pr...
    雨飞飞雨阅读 1,390评论 2 18
  • Promise的含义:   Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和...
    呼呼哥阅读 1,006评论 0 15