如何将现有的回调API转换为Promises?

864

我想使用Promise,但我手头有一个回调API的格式,如下:

1. DOM加载或其他一次性事件:

window.onload; // set to callback
...
window.onload = function() {

};

2. 普通回调函数:

function request(onChangeHandler) {
    ...
}
request(function() {
    // change happened
    ...
});

3. 节点风格回调函数("nodeback"):

function getStuff(dat, callback) {
    ...
}
getStuff("dataParam", function(err, data) {
    ...
})

4. 一个具有节点样式回调的完整库:

API;
API.one(function(err, data) {
    API.two(function(err, data2) {
        API.three(function(err, data3) {
            ...
        });
    });
});

我如何在promises中使用API,如何将其“promisify”?


我发布了自己的答案,但是欢迎更多针对特定库或更多情况下如何实现此操作的回答和编辑。 - Benjamin Gruenbaum
@Bergi 这是一个有趣的想法,我尝试提供一个通用的答案,使用了两种常见的方法(Promise构造函数和延迟对象)。我在回答中提供了这两种替代方案。我同意阅读手册可以解决此问题,但我们在这里和错误跟踪器中经常遇到这个问题,因此我认为有必要提供一个“规范问题” - 我认为阅读手册可以解决JS标签中大约50%的问题:D 如果您可以在答案或编辑中贡献有趣的见解,将不胜感激。 - Benjamin Gruenbaum
创建一个 new Promise 是否会增加任何显著的开销?我想将所有同步的 Noje.js 函数包装在 Promise 中,以从我的 Node 应用程序中删除所有同步代码,但这是最佳实践吗?换句话说,接受静态参数(例如字符串)并返回计算结果的函数,我应该将其包装在 Promise 中吗?...我在某个地方读到过,在 Nodejs 中不应该有任何同步代码。 - Ronnie Royston
1
@RonRoyston不,将同步调用包装在Promise中并不是一个好主意——只有可能执行I/O的异步调用可以这样做。 - Benjamin Gruenbaum
24个回答

860

承诺有状态,它们开始时是挂起状态,可以解决为:

  • 已实现意味着计算成功完成。
  • 已拒绝意味着计算失败。

返回承诺的函数不应该抛出异常,而应该返回拒绝。从返回承诺的函数中抛出异常将强制您同时使用 catch {} .catch。使用承诺化API的人们不希望承诺抛出异常。如果您不确定JS中的异步API如何工作,请首先查看此答案

1. DOM加载或其他一次性事件:

因此,创建承诺通常意味着指定它们何时解决 - 这意味着何时移动到实现或拒绝阶段以表示数据可用(并且可以使用.then访问)。

借助现代支持Promise构造函数的承诺实现,例如原生ES6承诺:

function load() {
    return new Promise(function(resolve, reject) {
        window.onload = resolve;
    });
}
您可以像这样使用得到的 Promise:
load().then(function() {
    // Do things after onload
});

有支持延迟执行的库(这里我们用 $q 作为例子,但稍后我们也会使用 jQuery):

function load() {
    var d = $q.defer();
    window.onload = function() { d.resolve(); };
    return d.promise;
}

或者使用类似于jQuery的API,针对事件只发生一次时进行挂钩:

function done() {
    var d = $.Deferred();
    $("#myObject").once("click",function() {
        d.resolve();
    });
    return d.promise();
}

2. 普通回调函数:

这些API在JS中非常常见,因为回调函数也很常见。让我们看一个常见的情况,即具有onSuccessonFail的场景:

function getUserData(userId, onLoad, onFail) { …

通过支持Promise构造函数的现代Promise实现,例如原生ES6 Promise:

function getUserDataAsync(userId) {
    return new Promise(function(resolve, reject) {
        getUserData(userId, resolve, reject);
    });
}

使用支持延迟的库(这里我们以jQuery为例,但上面也用了$q):

function getUserDataAsync(userId) {
    var d = $.Deferred();
    getUserData(userId, function(res){ d.resolve(res); }, function(err){ d.reject(err); });
    return d.promise();
}

jQuery还提供了一个$.Deferred(fn)形式,它的优点在于允许我们编写一个表达式,非常接近于new Promise(fn)形式,如下所示:

jQuery也提供了$.Deferred(fn)形式,这种形式的优势在于可以使我们编写一个表达式,非常接近于new Promise(fn)形式,具体请参考以下代码:

function getUserDataAsync(userId) {
    return $.Deferred(function(dfrd) {
        getUserData(userId, dfrd.resolve, dfrd.reject);
    }).promise();
}

注意:这里我们利用了jQuery deferred的resolvereject方法是“可分离”的事实;即它们绑定在一个jQuery.Deferred()实例上。并非所有库都提供此功能。

3. Node风格回调("nodeback"):

Node风格回调(nodebacks)具有特定的格式,其中回调始终是最后一个参数,其第一个参数是错误。让我们首先手动将其promisify:

getStuff("dataParam", function(err, data) { …

收件人:

function getStuffAsync(param) {
    return new Promise(function(resolve, reject) {
        getStuff(param, function(err, data) {
            if (err !== null) reject(err);
            else resolve(data);
        });
    });
}

使用deferreds,您可以执行以下操作(让我们在此示例中使用Q,尽管Q现在支持新语法 您应该优先考虑):

function getStuffAsync(param) {
    var d = Q.defer();
    getStuff(param, function(err, data) {
        if (err !== null) d.reject(err);
        else d.resolve(data);
    });
    return d.promise;   
}

通常情况下,您不应该手动过度promisify一些东西,大多数Promise库都是为Node设计的,并且在Node 8+中具有内置的方法用于promisifying回调函数。例如

var getStuffAsync = Promise.promisify(getStuff); // Bluebird
var getStuffAsync = Q.denodeify(getStuff); // Q
var getStuffAsync = util.promisify(getStuff); // Native promises, node only

4. 一个带有Node风格回调的完整库:

这里没有黄金法则,您需要逐个将它们转换为Promise。但是,一些Promise实现允许您批量执行此操作,例如在Bluebird中,将nodeback API转换为promise API就像这样简单:

Promise.promisifyAll(API);

或者在Node中使用原生承诺

const { promisify } = require('util');
const promiseAPI = Object.entries(API).map(([key, v]) => ({key, fn: promisify(v)}))
                         .reduce((o, p) => Object.assign(o, {[p.key]: p.fn}), {});

注:

  • 当您在 .then 处理程序中时,您不需要将事物进行 Promisify。从 .then 处理程序返回一个 promise 将使用该 promise 的值解析或拒绝。从 .then 处理程序抛出异常也是一种良好的实践,将拒绝 promise - 这就是著名的 promise throw safety。
  • 在实际的 onload 情况下,您应该使用 addEventListener 而不是 onX

@Roamer-1888,它被拒绝了,因为我没有及时看到并接受它。就其价值而言,我认为这个添加并不是太相关,尽管有用。 - Benjamin Gruenbaum
2
Benjamin,无论resolve()reject()是否被编写成可重用的形式,我认为我的建议编辑是相关的,因为它提供了一个jQuery示例,即$.Deferred(fn)形式,否则缺少这种形式。如果只包含一个jQuery示例,则建议应该是这种形式,而不是var d = $.Deferred();等形式,因为人们应该被鼓励使用常被忽视的$.Deferred(fn)形式,在这样的答案中,它使jQuery更加与使用Revealing Constructor Pattern的库相媲美。 - Roamer-1888
嘿,为了百分之百的公平,我不知道jQuery允许你执行$.Deferred(fn),如果您在接下来的15分钟内编辑此内容,而不是现有的示例,我相信我可以尽快批准它 :) - Benjamin Gruenbaum
我有一个关于嵌套回调函数中的第三个问题。为什么我不能简单地调用 reject/resolve?在我的情况下,似乎我必须返回它们。 - user1164937
7
这是一个很好的回答。你可能希望通过提及 util.promisify 来更新它,从8.0.0发布候选版开始,Node.js 将在其核心库中添加这个功能。 它的工作方式与 Bluebird 的 Promise.promisify 没有太大的区别,但有一个优点,就是不需要额外的依赖,如果你只想使用本地的 Promise。 我写了一篇关于 util.promisify 的博客文章,供任何想深入了解这个主题的人阅读。 - Bruno
显示剩余20条评论

66

今天,我可以在 Node.js 中使用 Promise 作为普通的 JavaScript 方法。

一个简单和基本的例子来展示使用 Promise(采用 KISS 方式):

普通 JavaScript 异步 API 代码:

function divisionAPI (number, divider, successCallback, errorCallback) {

    if (divider == 0) {
        return errorCallback( new Error("Division by zero") )
    }

    successCallback( number / divider )

}

Promise是JavaScript异步API的代码:

function divisionAPI (number, divider) {

    return new Promise(function (fulfilled, rejected) {

        if (divider == 0) {
            return rejected( new Error("Division by zero") )
        }

        fulfilled( number / divider )

     })

}

(我建议访问这个优美的来源

Promise也可以与ES7中的async\await一起使用,以使程序流等待fullfiled结果,如下所示:

function getName () {

    return new Promise(function (fulfilled, rejected) {

        var name = "John Doe";

        // wait 3000 milliseconds before calling fulfilled() method
        setTimeout ( 
            function() {
                fulfilled( name )
            }, 
            3000
        )

    })

}


async function foo () {

    var name = await getName(); // awaits for a fulfilled result!

    console.log(name); // the console writes "John Doe" after 3000 milliseconds

}


foo() // calling the foo() method to run the code

使用 .then() 方法的相同代码的另一种用法

function getName () {

    return new Promise(function (fulfilled, rejected) {

        var name = "John Doe";

        // wait 3000 milliseconds before calling fulfilled() method
        setTimeout ( 
            function() {
                fulfilled( name )
            }, 
            3000
        )

    })

}


// the console writes "John Doe" after 3000 milliseconds
getName().then(function(name){ console.log(name) })

Promise 也可以在基于 Node.js 平台的任何平台上使用,例如 react-native

Bonus:一种混合方法
(假定回调方法有两个参数,分别为错误和结果)

function divisionAPI (number, divider, callback) {

    return new Promise(function (fulfilled, rejected) {

        if (divider == 0) {
            let error = new Error("Division by zero")
            callback && callback( error )
            return rejected( error )
        }

        let result = number / divider
        callback && callback( null, result )
        fulfilled( result )

     })

}

上述方法可以响应旧式回调和 Promise 用法的结果。

希望这有所帮助。


3
似乎这些内容没有展示如何转换为 Promise。 - Dmitri Zaitsev

41

在将Node.JS中的函数转换为Promise之前

var request = require('request'); //http wrapped module

function requestWrapper(url, callback) {
    request.get(url, function (err, response) {
      if (err) {
        callback(err);
      }else{
        callback(null, response);             
      }      
    })
}


requestWrapper(url, function (err, response) {
    console.log(err, response)
})

转换后

var request = require('request');

function requestWrapper(url) {
  return new Promise(function (resolve, reject) { //returning promise
    request.get(url, function (err, response) {
      if (err) {
        reject(err); //promise reject
      }else{
        resolve(response); //promise resolve
      }
    })
  })
}


requestWrapper('http://localhost:8080/promise_request/1').then(function(response){
    console.log(response) //resolve callback(success)
}).catch(function(error){
    console.log(error) //reject callback(failure)
})

如果您需要处理多个请求

var allRequests = [];
allRequests.push(requestWrapper('http://localhost:8080/promise_request/1')) 
allRequests.push(requestWrapper('http://localhost:8080/promise_request/2'))
allRequests.push(requestWrapper('http://localhost:8080/promise_request/5'))    

Promise.all(allRequests).then(function (results) {
  console.log(results);//result will be array which contains each promise response
}).catch(function (err) {
  console.log(err)
});

25

我不认为@Benjamin提出的window.onload建议总是有效的,因为它无法检测它是否在加载后被调用。我已经多次受到此类问题的困扰。以下版本应始终有效:

function promiseDOMready() {
    return new Promise(function(resolve) {
        if (document.readyState === "complete") return resolve();
        document.addEventListener("DOMContentLoaded", resolve);
    });
}
promiseDOMready().then(initOnLoad);

1
“已完成”分支不应该使用setTimeout(resolve, 0)(或者如果可用,使用setImmediate)来确保它被异步调用吗? - Alnitak
6
@Alnitak 调用 resolve 同步执行是可以的。无论 resolve 是否同步调用,Promise 的 then 处理程序都被框架保证异步调用。 - Jeff Bowman

25

我通常使用的一个简单的通用函数。

const promisify = (fn, ...args) => {
  return new Promise((resolve, reject) => {
    fn(...args, (err, data) => {
      if (err) {
        return reject(err);
      }
      resolve(data);
    });
  });
};

如何使用它

  • promisify函数接受一个带有回调函数的函数:
   const cb = (result) => `The result is ${result}`;

   const sum = (a, b, cb) => {
    const result = a + b;
    cb(result); // passing args to the callback function
   }


  // using the util
  promise = promisify(sum, 3, 1, cb);
  promise.then(x => console.log(x)) // 4

你可能不是在寻找这个答案,但这将有助于理解可用工具的内部运作。


我正在尝试使用这个,但是如果我调用 promisify(fn, arg1, arg2).then(() => { alert("Done!"); });,警报永远不会触发。你认为这会起作用吗? - Philip Stratford
1
感谢Philip Stratford提出的问题。promisify函数用于将带有回调函数的函数转换为Promise对象。我会更新我的答案来解释这个问题。 - Josiah Nyarega
我很乐意听取关于这个解决方案的任何建议,抄送@Philip Stratford。谢谢。 - Josiah Nyarega
const promisify = (fn, ...args) => { return new Promise((resolve, reject) => { fn.apply(null, args.concat((err, result) => { if (err) reject(err); else resolve(result); })); }); };const cb = (result) => result;const sum = (a, b, cb, t) => { t('rr', cb(a + b)) }promise = promisify(sum, 3, 1, cb); promise.then(x => console.log(x)).catch((err) => console.log(err)) // 4 - Amarkant Kumar

25

Node.js 8.0.0 包含一个新的util.promisify() API,它允许将标准的 Node.js 回调风格 API 包装在返回 Promise 的函数中。下面是 util.promisify() 的一个示例用法。

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

const readFile = util.promisify(fs.readFile);

readFile('/some/file')
  .then((data) => { /* ... */ })
  .catch((err) => { /* ... */ });

查看Promises的改进支持


2
已经有两个回答描述了这个问题,为什么要发第三个呢? - Benjamin Gruenbaum
2
只是因为该版本的Node现在已发布,并且我已经报告了“官方”的功能描述和链接。 - Gian Marco
1
@BenjaminGruenbaum 我点赞了这个回答,因为它更简洁有效。排在前面的那个回答有太多其他的东西,导致答案被淹没了。 - Lucio Mollinedo

15
在Node.js 8.0.0的发布候选版中,有一个新的实用程序util.promisify(我曾经写过util.promisify),它封装了将任何函数转换为Promise的能力。
它与其他答案中提出的方法并没有太大区别,但它具有作为核心方法和不需要额外依赖项的优点。
const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

那么你就有一个返回本地 PromisereadFile 方法。

readFile('./notes.txt')
  .then(txt => console.log(txt))
  .catch(...);

1
嘿,我(OP)实际上两次建议使用util.promisify(一次是在2014年写这个问题时,另一次是几个月前 - 我作为Node的核心成员推动了它,并且是我们在Node中当前版本)。由于它尚未公开发布 - 我还没有将其添加到此答案中。我们非常感谢使用反馈,并了解一些陷阱,以便为发布提供更好的文档 :) - Benjamin Gruenbaum
1
此外,您可能希望在博客文章中讨论使用util.promisify的自定义标志进行Promise化。 - Benjamin Gruenbaum
@BenjaminGruenbaum 你是指使用 util.promisify.custom 符号可以覆盖 util.promisify 的结果这一事实吗?说实话,这是一个故意的失误,因为我还没有找到一个有用的用例。也许你可以给我一些建议? - Bruno
1
当考虑到像 fs.exists 这样的 API 或者不遵循 Node 约定的 API 时 - 使用 bluebird 的 Promise.promisify 将出现错误,但是使用 util.promisify 将得到正确结果。 - Benjamin Gruenbaum

8
您可以在Node JS中使用JavaScript本地promises。
我的Cloud 9代码链接:https://ide.c9.io/adx2803/native-promises-in-node
/**
* Created by dixit-lab on 20/6/16.
*/

var express = require('express');
var request = require('request');   //Simplified HTTP request client.


var app = express();

function promisify(url) {
    return new Promise(function (resolve, reject) {
        request.get(url, function (error, response, body) {
            if (!error && response.statusCode == 200) {
                resolve(body);
            }
            else {
                reject(error);
            }
        })
    });
}

//get all the albums of a user who have posted post 100
app.get('/listAlbums', function (req, res) {
    //get the post with post id 100
    promisify('http://jsonplaceholder.typicode.com/posts/100').then(function (result) {
        var obj = JSON.parse(result);
        return promisify('http://jsonplaceholder.typicode.com/users/' + obj.userId + '/albums')
    })
    .catch(function (e) {
        console.log(e);
    })
    .then(function (result) {
        res.end(result);
    })
})

var server = app.listen(8081, function () {
    var host = server.address().address
    var port = server.address().port

    console.log("Example app listening at http://%s:%s", host, port)
})

//run webservice on browser : http://localhost:8081/listAlbums

8

通过普通的JavaScript,这里有一个解决方案可以将api回调函数封装成promises。

function get(url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open('get', url);
        xhr.addEventListener('readystatechange', function () {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    console.log('successful ... should call callback ... ');
                    callback(null, JSON.parse(xhr.responseText));
                } else {
                    console.log('error ... callback with error data ... ');
                    callback(xhr, null);
                }
            }
        });
        xhr.send();
    }

/**
     * @function promisify: convert api based callbacks to promises
     * @description takes in a factory function and promisifies it
     * @params {function} input function to promisify
     * @params {array} an array of inputs to the function to be promisified
     * @return {function} promisified function
     * */
    function promisify(fn) {
        return function () {
            var args = Array.prototype.slice.call(arguments);
            return new Promise(function(resolve, reject) {
                fn.apply(null, args.concat(function (err, result) {
                    if (err) reject(err);
                    else resolve(result);
                }));
            });
        }
    }

var get_promisified = promisify(get);
var promise = get_promisified('some_url');
promise.then(function (data) {
        // corresponds to the resolve function
        console.log('successful operation: ', data);
}, function (error) {
        console.log(error);
});

7

kriskowal的Q库包含回调转Promise函数。 像这样的一个方法:

obj.prototype.dosomething(params, cb) {
  ...blah blah...
  cb(error, results);
}

可以使用Q.ninvoke进行转换。
Q.ninvoke(obj,"dosomething",params).
then(function(results) {
});

1
规范答案已经提到了 Q.denodeify。我们需要强调库助手吗? - Bergi
3
我发现这很有用,因为在Q中进行Promise化的谷歌搜索会导向这里。 - Ed Sykes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接