ES6之Promise 与 Node.js 8新特性之util.promisify()

2017年五月底Node.js 8正式发布,带来了 很多新特性 。本文讨论下util.promisify()这个方法。

Promise

介绍promisify之前,首先来看下Promise这个API,因为util.promisify()这个方法就是把原来的异步回调方法改成支持 Promise 的方法并返回一个Promise实例。ES2015(ES6)加入了 Promise,可以直接使用。Promise没有新的语法元素,即使在不支持原生Promise的环境里也可以使用,比如 Q 或者 Bluebird,甚至 jQuery ,在小程序里有效。

ES2017 增加了 await/async 语法,但请注意, await 后面必须跟Promise实例才能实现异步。

由于历史原因,js中存在大量异步回调,回调层数多的话就形成了“地狱回调”,不仅代码丑陋,也很难维护。针对这种现象,开发社区总结出来一套名为 Promise/A+ 的解决方案。大体上来说,这套方案通过使用 “Promise 回调实例”包裹原先的回调函数,可以将原先复杂的嵌套展开、铺平,从而降低开发和维护的难度和成本1。下面来自文献1的代码非常简单清晰、一目了然:

new Promise( (resolve, reject) => { // 构建一个 Promise 实例
  someAsyncFunction( (err, result) => { // 调用原来的异步函数
    if (err) { // 发生错误,进入错误处理模式
      return reject(err);
    }
    resolve(result); // 一切正常,进入队列的下一环节
  });
})
  .then( result => { // 下一环节
    return doSomething(result);
  })
  .then( result2 => { // 又下一环节
    return doSomething2(result2);
  })
  ... // 各种中间环节
  .catch( err => { // 错误处理
    console.log(err);
  });

ES6规定,Promise对象是一个构造函数,用来生成Promise实例。下面代码创造了一个Promise实例。

var promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

假设一个事件eventFor2Seconds操作耗时2秒,那么我们可以使用Promise这样写:

function eventFor2Seconds(x) {//eventFor2Seconds方法返回一个Promise实例代表一个异步操作
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}
async function doSomething() {
  var x = await eventFor2Seconds(10);//需要使用await实现异步
  console.log(x); // 2秒后打印 10
}
doSomething();

Promise有以下几个特点2

  1. Promise对象有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)且对象的状态不受外界影响。只有异步操作的结果,可以决定哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
  2. 一旦状态生成,就不会再变,任何时候都得到这个状态。Promise对象只有两种可能的状态改变方式:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
  3. Promise所有API都返回当前实例(就是builder设计模式),因此可以采用连续的then/catch链式操作来写回调。
  4. resolve方法会使之后的连续then执行(不写then也没事),reject方法会使之后的catch执行(如果不写catch会出现异常,因此catch必须写)。
  5. 可以在then中return出数据,并且这个数据会以参数的形式传入下一个then。
  6. Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

Promise对象提供统一的接口,包括resolve,reject,then,catch,all,race,使得异步流程控制更加方便。

Promise.all()
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。var p = Promise.all([p1, p2, p3]);all()接受数组作为参数。p1,p2,p3都是Promise的实例对象,p要变成Resolved状态需要p1,p2,p3状态都是Resolved,如果p1,p2,p3至少有一个状态是Rejected,p的状态就变成Rejected(很像&&符号链接)

Promise.race()
var p = Promise.race( [p1,p2,p3] )上面代码中,只要p1、p2、p3之中有一个实例首先改变状态,p的状态就跟着改变。那个首先改变的Promise 实例的返回值,就传递给p的回调函数(很像||符号操作)

var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 500, "one"); 
});
var p2 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 100, "two"); 
});
Promise.race([p1, p2]).then(function(value) {
  console.log(value); // "two"
  // Both resolve, but p2 is faster
});

Promise resolve()
resolve可以将将现有对象转为Promise对象。

Promise.resolve('fa')
// 等价于
new Promise(resolve => resolve('fa'))

Promise reject()
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。

Promise.reject('foo')
// 等价于
new Promise(reject => reject('foo'))


util.promisify

Promise的用法差不多就这些了,下面来看下util.promisify的用法。我的理解是util.promisify只是返回一个Promise实例来方便异步操作。比如要延迟一段时间执行代码,我们可以这样:

let { promisify } = require('util')
const sleep = promisify(setTimeout)
async function fuc() {
  console.log('before')
  await sleep(2000)
  console.log('after')
}
fuc()

上面的代码结合while(1)死循环可以实现一个简单的定时器功能。

大家都知道nodejs的fs库有读文件的API,结合util.promisify使用链式操作代替地狱回调:

const util = require('util');
const fs = require('fs');
const stat = util.promisify(fs.stat);
stat('.')
 .then((stats) => {
  // Do something with `stats`
 })
 .catch((error) => {
  // Handle the error.
 });

在绑定的函数的参数列表中的最后会多出一个参数,这个参数是函数而且包含两个参数为 (err, result),前面是可能的错误,后面是正常的结果。这个多出来的参数是promisify在绑定的时候强制添加的作为默认的回调函数,这个默认的回调函数源码如下:

(err, ...values) => {
  if (err) {
    promiseReject(promise, err);
  } else if (argumentNames !== undefined && values.length > 1) {
    const obj = {};
    for (var i = 0; i < argumentNames.length; i++)
      obj[argumentNames[i]] = values[i];
    promiseResolve(promise, obj);
  } else {
    promiseResolve(promise, values[0]);
  }
}

然后再看一个例子:

function paramObj(params, callback) {//假设业务需求这个函数需要传入一个对象参数params才是正常的,否则就表示异常
  console.log('params', params)
  console.log('callback', callback)
  if (typeof params != 'object') {
    params(JSON.stringify({ "code": "1", "msg": "params null" }), null)//抛出异常,原因是params null
  } else {
    callback(null, params)//promisify会添加一个自己风格的类型为function的参数
  }
}

async function test() {//async会返回一个Promise实例
  var data = await promisify(paramObj).bind(paramObj)()//不传入一个对象的参数的话会抛出异常
  console.log('inner data: ', data)
  return data
}
test()
  .then(data => {
    console.log('outer data:', data)
  })
  .catch(err => {
    console.log('outer err:', err)
  })



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

推荐阅读更多精彩内容

  • Promiese 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,语法上说,Pr...
    雨飞飞雨阅读 3,352评论 0 19
  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,703评论 1 56
  • Promise含义 Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更强大。所谓Pr...
    oWSQo阅读 1,084评论 0 4
  • 1. Promise 的含义 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个...
    ROBIN2015阅读 486评论 0 0
  • https://leetcode.com/problems/path-sum-iii/#/description ...
    Zihowe阅读 378评论 0 0