util.promisify
是在
node.js 8.x
版本中新增的一個工具,用于将老式的
Error first callback
轉換為
Promise
對象,讓老項目改造變得更為輕松。
在官方推出這個工具之前,民間已經有很多類似的工具了,比如es6-promisify、thenify、bluebird.promisify。
以及很多其他優秀的工具,都是實作了這樣的功能,幫助我們在處理老項目的時候,不必費神将各種代碼使用
Promise
再重新實作一遍。
工具實作的大緻思路
首先要解釋一下這種工具大緻的實作思路,因為在
Node
中異步回調有一個約定:
Error first
,也就是說回調函數中的第一個參數一定要是
Error
對象,其餘參數才是正确時的資料。
知道了這樣的規律以後,工具就很好實作了,在比對到第一個參數有值的情況下,觸發
reject
,其餘情況觸發
resolve
,一個簡單的示例代碼:
function util (func) {
return (...arg) => new Promise((resolve, reject) => {
func(...arg, (err, arg) => {
if (err) reject(err)
else resolve(arg)
})
})
}
- 調用工具函數傳回一個匿名函數,匿名函數接收原函數的參數。
- 匿名函數被調用後根據這些參數來調用真實的函數,同時拼接一個用來處理結果的
。callback
- 檢測到
有值,觸發err
,其他情況觸發reject
resolve
resolve 隻能傳入一個參數,是以
callback
中沒有必要使用
...arg
擷取所有的傳回值
正常的使用方式
拿一個官方文檔中的示例
const { promisify } = require('util')
const fs = require('fs')
const statAsync = promisify(fs.stat)
statAsync('.').then(stats => {
// 拿到了正确的資料
}, err => {
// 出現了異常
})
以及因為是
Promise
,我們可以使用
await
來進一步簡化代碼:
const { promisify } = require('util')
const fs = require('fs')
const statAsync = promisify(fs.stat)
// 假設在 async 函數中
try {
const stats = await statAsync('.')
// 拿到正确結果
} catch (e) {
// 出現異常
}
用法與其他工具并沒有太大的差別,我們可以很輕易的将回調轉換為
Promise
,然後應用于新的項目中。
自定義的 Promise 化
有那麼一些場景,是不能夠直接使用
promisify
來進行轉換的,有大概這麼兩種情況:
- 沒有遵循
約定的回調函數Error first callback
- 傳回多個參數的回調函數
首先是第一個,如果沒有遵循我們的約定,很可能導緻
reject
的誤判,得不到正确的回報。
而第二項呢,則是因為
Promise.resolve
隻能接收一個參數,多餘的參數會被忽略。
是以為了實作正确的結果,我們可能需要手動實作對應的
Promise
函數,但是自己實作了以後并不能夠確定使用方不會針對你的函數調用
promisify
是以,
util.promisify
還提供了一個
Symbol
類型的
key
,
util.promisify.custom
Symbol
類型的大家應該都有了解,是一個唯一的值,這裡是
util.prosimify
用來指定自定義的
Promise
化的結果的,使用方式如下:
const { promisify } = require('util')
// 比如我們有一個對象,提供了一個傳回多個參數的回調版本的函數
const obj = {
getData (callback) {
callback(null, 'Niko', 18) // 傳回兩個參數,姓名和年齡
}
}
// 這時使用promisify肯定是不行的
// 因為Promise.resolve隻接收一個參數,是以我們隻會得到 Niko
promisify(obj.getData)().then(console.log) // Niko
// 是以我們需要使用 promisify.custom 來自定義處理方式
obj.getData[promisify.custom] = async () => ({ name: 'Niko', age: 18 })
// 當然了,這是一個曲線救國的方式,無論如何 Promise 不會傳回多個參數過來的
promisify(obj.getData)().then(console.log) // { name: 'Niko', age: 18 }
關于
Promise
為什麼不能
resolve
多個值,我有一個大膽的想法,一個沒有經過考證,強行解釋的理由:如果能
resolve
多個值,你讓
async
函數怎麼
return
(當個樂子看這句話就好,不要當真)
不過應該确實跟
return
有關,因為
Promise
是可以鍊式調用的,每個
Promise
中執行
then
以後都會将其傳回值作為一個新的
Promise
對象
resolve
的值,在
JavaScript
中并沒有辦法
return
多個參數,是以即便第一個
Promise
可以傳回多個參數,隻要經過
return
的處理就會丢失
在使用上就是很簡單的針對可能會被調用
promisify
的函數上添加
promisify.custom
對應的處理即可。
當後續代碼調用
promisify
時就會進行判斷:
- 如果目标函數存在
屬性,則會判斷其類型:promisify.custom
- 如果不是一個可執行的函數,抛出異常
- 如果是可執行的函數,則直接傳回其對應的函數
- 如果目标函數不存在對應的屬性,按照
的約定生成對應的處理函數然後傳回Error first callback
添加了這個
custom
屬性以後,就不用再擔心使用方針對你的函數調用
promisify
了。
而且可以驗證,指派給
custom
的函數與
promisify
傳回的函數位址是一處:
obj.getData[promisify.custom] = async () => ({ name: 'Niko', age: 18 })
// 上邊的指派為 async 函數也可以改為普通函數,隻要保證這個普通函數會傳回 Promise 執行個體即可
// 這兩種方式與上邊的 async 都是完全相等的
obj.getData[promisify.custom] = () => Promise.resolve({ name: 'Niko', age: 18 })
obj.getData[promisify.custom] = () => new Promise(resolve({ name: 'Niko', age: 18 }))
console.log(obj.getData[promisify.custom] === promisify(obj.getData)) // true
一些内置的 custom 處理
在一些内置包中,也能夠找到
promisify.custom
的蹤迹,比如說最常用的
child_process.exec
就内置了
promisify.custom
的處理:
const { exec } = require('child_process')
const { promisify } = require('util')
console.log(typeof exec[promisify.custom]) // function
因為就像前邊示例中所提到的曲線救國的方案,官方的做法也是将函數簽名中的參數名作為
key
,将其所有參數存放到一個
Object
對象中進行傳回,比如
child_process.exec
的傳回值抛開
error
以外會包含兩個,
stdout
和
stderr
,一個是指令執行後的正确輸出,一個是指令執行後的錯誤輸出:
promisify(exec)('ls').then(console.log)
// -> { stdout: 'XXX', stderr: '' }
或者我們故意輸入一些錯誤的指令,當然了,這個隻能在
catch
子產品下才能夠捕捉到,一般指令正常執行
stderr
都會是一個空字元串:
promisify(exec)('lss').then(console.log, console.error)
// -> { ..., stdout: '', stderr: 'lss: command not found' }
包括像
setTimeout
、
setImmediate
也都實作了對應的
promisify.custom
之前為了實作
sleep
的操作,還手動使用
Promise
封裝了
setTimeout
:
const sleep = promisify(setTimeout)
console.log(new Date())
await sleep(1000)
console.log(new Date())
内置的 promisify 轉換後函數
如果你的
Node
版本使用
10.x
以上的,還可以從很多内置的子產品中找到類似
.promises
的子子產品,這裡邊包含了該子產品中常用的回調函數的
Promise
版本(都是
async
函數),無需再手動進行
promisify
轉換了。
而且我本人覺得這是一個很好的指引方向,因為之前的工具實作,有的選擇直接覆寫原有函數,有的則是在原有函數名後邊增加
Async
進行區分,官方的這種在子產品中單獨引入一個子子產品,在裡邊實作
Promise
版本的函數,其實這個在使用上是很友善的,就拿
fs
子產品進行舉例:
// 之前引入一些 fs 相關的 API 是這樣做的
const { readFile, stat } = require('fs')
// 而現在可以很簡單的改為
const { readFile, stat } = require('fs').promises
// 或者
const { promises: { readFile, stat } } = require('fs')
後邊要做的就是将調用
promisify
相關的代碼删掉即可,對于其他使用
API
的代碼來講,這個改動是無感覺的。
是以如果你的
node
版本夠高的話,可以在使用内置子產品之前先去翻看文檔,有沒有對應的
promises
支援,如果有實作的話,就可以直接使用。
promisify 的一些注意事項
- 一定要符合
的約定Error first callback
- 不能傳回多個參數
- 注意進行轉換的函數是否包含
的引用this
前兩個問題,使用前邊提到的
promisify.custom
都可以解決掉。
但是第三項可能會在某些情況下被我們所忽視,這并不是
promisify
獨有的問題,就一個很簡單的例子:
const obj = {
name: 'Niko',
getName () {
return this.name
}
}
obj.getName() // Niko
const func = obj.getName
func() // undefined
類似的,如果我們在進行
Promise
轉換的時候,也是類似這樣的操作,那麼可能會導緻生成後的函數
this
指向出現問題。
修複這樣的問題有兩種途徑:
- 使用箭頭函數,也是推薦的做法
- 在調用
之前使用promisify
綁定對應的bind
this
不過這樣的問題也是建立在
promisify
轉換後的函數被指派給其他變量的情況下會發生。
如果是類似這樣的代碼,那麼完全不必擔心
this
指向的問題:
const obj = {
name: 'Niko',
getName (callback) {
callback(null, this.name)
}
}
// 這樣的操作是不需要擔心 this 指向問題的
obj.XXX = promisify(obj.getName)
// 如果指派給了其他變量,那麼這裡就需要注意 this 的指向了
const func = promisify(obj.getName) // 錯誤的 this
小結
個人認為
Promise
作為當代
javaScript
異步程式設計中最核心的一部分,了解如何将老舊代碼轉換為
Promise
是一件很有意思的事兒。
而我去了解官方的這個工具,原因是在搜尋
Redis
相關的
Promise
版本時看到了這個readme:
This package is no longer maintained. node_redis now includes support for promises in core, so this is no longer needed.
然後跳到了
node_redis
裡邊的實作方案,裡邊提到了
util.promisify
,遂抓過來研究了一下,感覺還挺有意思,總結了下分享給大家。
參考資料
- util.promisify
- child_process.exec
- fs.promises