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