怎麼promisify(promise ⇄ callback)

zzszzs發表於2019-04-02
相信大家都知道Node 8提供了兩個工具函式util.promisify, util.callbackify用於在回撥函式和promise之間做方便的切換, 今天我們講下他們簡單的實現。因為到了瀏覽器你就不能直接用node的函式啦,當然npm有一些包可以使用。

回顧用法

下面Node提供的例子,簡單易懂

const util = require('util');
const fs = require('fs');

// stat方法用來檢視檔案資訊的,定義:fs.stat(path, callback), 原用法如下
// fs.stat('.', (err, stats) => {
//     if (err) { // handle error }
//     // 處理取到的資訊
// })

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

方便起見,我們假設前端能直接用fs.stat方法,在不能用util.promisify的情況下我們怎麼把他轉化成promise?

具體例子具體實現

中心思想無非就是callback出結果後把相應的結果/錯誤用Promise.resolve/reject丟擲去:

const stat = path => new Promise((resolve, reject) => {
    fs.stat(path, (err, info) => {
        if (err) {
            reject(err);
        } else {
            resolve(info);
        }
    });
});

// 使用1
stat('.').then(info =>{}).catch(err => {});
// 使用2 async語法
const useFunc = async () => {
    try {
        const info = await stat('.');
        return info;
    } catch(err) {
        console.error('報錯啦', err);
    }
}
useFunc();複製程式碼

通用實現

為了以後重用,我們自己實現一個promisify,他接收函式是標準的node寫法:

fn(...args, (err, value) => {}) {}

const promisify = fnWithStandardCallback => (...args) => new Promise((resolve, reject) => {
    fnWithStandardCallback(...args, (err, info) => {
        if (err) {
            reject(err);
        } else {
            resolve(info);
        }
    });
});

// 使用
const convertedStat = promisify(fs.stat);複製程式碼

為了有些童鞋可能看不清楚太多的箭頭函式,附上更清楚的版本:

const promisify = fnWithStandardCallback => {
    return (...args) => {
        return new Promise((resolve, reject) => {
            fnWithStandardCallback(...args, (err, info) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(info);
                }
            });
        });
    };
};複製程式碼

Node對另一種callback的處理

除了上面標準的callback外層函式寫法外,另一種寫法如下也比較常見:

fn(...args, onSuccess, onError) {}

onSuccess/onError都是 fn(value) {}樣子的回撥函式,這種寫法沒有把成功/失敗放在一個callback裡而是分開處理。

Node對非標準的情況處理是:新增了一個Symbol叫做util.promisify.custom,然後你在呼叫promisify方法前自定義你的返回邏輯。本質上就是上面我們具體例子具體實現的版本:

const { promisify } = require('util');

const isTruthy = (value, onSuccess, onError) => {
    if (value) {
        onSuccess(value);
    } else {
        onError(value);
    }
};

isTruthy[promisify.custom] = value => new Promise((resove, reject) => {
    isTruthy(value, () => {
        resolve(value);
    }, () => {
        reject(value);
    });
});

promisify(isTruthy)(true).then(() => {});複製程式碼

改動我們的版本

const CUSTOM = Symbol('mypromisify.custom');

const promisify = fnWithStandardCallback => {
    if (fnWithStandardCallback[CUSTOM]) {
        // 有自定義?直接返回你自定義的版本
        return fnWithStandardCallback[CUSTOM];
    }

    return (...args) => {
        return new Promise((resolve, reject) => {
            fnWithStandardCallback(...args, (err, info) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(info);
                }
            });
        });
    };
};

promise.custom = CUSTOM; // 方便使用者呼叫,不然沒人記得那個symbol是啥複製程式碼

把promise轉成callback形式

這種轉換幾乎不會用到,我們簡單講下實現:promise.then/catch完之後,再呼叫使用者提供的callback函式即可

const callbackify = (fnThatReturnsPromise) => (...args) => {
    // 按照約定,最後一個引數為回撥
    const callback = args[args.length - 1];
    // 真正傳給原方法的引數
    const otherArgs = args.slice(0, -1);

    fnThatReturnsPromise(...otherArgs).then(info => {
        callback(null, info);
    }).catch(err => {
        callback(err);
    });
}複製程式碼


個人而言,我是贊成直接把程式碼改成promise形式的,而不是對已有的callback加上這個中間層,因為其實改動的成本差不多。但總有各種各樣的情況,比如你的回撥函式已經有很多地方使用了,牽一髮而動全身,這時這個中間層還是比較有用的。


引用:

medium.com/trabe/under…

github.com/nodejs/node…


相關文章