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用法
    原文作者:宛丘之上兮
    原文地址: https://www.jianshu.com/p/139ef018281d
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞