关于javascript:如何将现有的回调API转换为promises?

How do I convert an existing callback API to promises?

我想使用承诺,但我有一个回调API,格式如下:

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

1
2
3
4
5
window.onload; // set to callback
...
window.onload = function() {

};

2。普通回调:

1
2
3
4
5
6
7
function request(onChangeHandler) {
    ...
}
request(function() {
    // change happened
    ...
});

三。节点样式回调("nodeback"):

1
2
3
4
5
6
function getStuff(dat, callback) {
    ...
}
getStuff("dataParam", function(err, data) {
    ...
})

4。带有节点样式回调的整个库:

1
2
3
4
5
6
7
8
API;
API.one(function(err, data) {
    API.two(function(err, data2) {
        API.three(function(err, data3) {
            ...
        });
    });
});

我如何在承诺中使用API,我如何"确定"它?


承诺是有状态的,它们开始时是待定的,可以解决:

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

承诺返回函数不应该抛出,而是应该返回拒绝。放弃承诺返回函数将强制您同时使用} catch {.catch。使用PromissifiedAPI的人不希望做出承诺。如果您不确定AsyncAPI在JS中是如何工作的,请先看看这个答案。

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

因此,创建承诺通常意味着指定它们何时结算——也就是说,当它们移动到完成或拒绝的阶段,以表明数据是可用的(并且可以通过.then访问)。

现代Promise实现支持Promise构造函数,如本机ES6承诺:

1
2
3
4
5
function load() {
    return new Promise(function(resolve, reject) {
        window.onload = resolve;
    });
}

然后,您可以这样使用由此产生的承诺:

1
2
3
load().then(function() {
    // Do things after onload
});

对于支持延迟的库(让我们在这里使用$Q作为示例,但稍后我们还会使用jquery):

1
2
3
4
5
function load() {
    var d = $q.defer();
    window.onload = function() { d.resolve(); };
    return d.promise;
}

或者使用类似API的jquery,钩住发生过一次的事件:

1
2
3
4
5
6
7
function done() {
    var d = $.Deferred();
    $("#myObject").once("click",function() {
        d.resolve();
    });
    return d.promise();
}

2。普通回调:

这些API相当常见,因为在JS中,回调很常见。让我们来看看拥有onSuccessonFail的常见情况:

1
function getUserData(userId, onLoad, onFail) {

现代Promise实现支持Promise构造函数,如本机ES6承诺:

1
2
3
4
5
function getUserDataAsync(userId) {
    return new Promise(function(resolve, reject) {
        getUserData(userId, resolve, reject);
    });
}

对于支持延迟的库(我们在这里使用jquery,但上面也使用了$Q):

1
2
3
4
5
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)表单的表达式,如下所示:

1
2
3
4
5
function getUserDataAsync(userId) {
    return $.Deferred(function(dfrd) {
        getUserData(userId, dfrd.resolve, dfrd.reject);
    }).promise();
}

注意:这里我们利用jquery deferred的resolvereject方法是"可分离的"这一事实,即它们绑定到jquery.deferred()的实例。并非所有libs都提供此功能。

三。节点样式回调("nodeback"):

节点样式回调(nodebacks)具有特定的格式,其中回调始终是最后一个参数,其第一个参数是错误。让我们先手动确认一个:

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

到:

1
2
3
4
5
6
7
8
function getStuffAsync(param) {
    return new Promise(function(resolve, reject) {
        getStuff(param, function(err, data) {
            if (err !== null) reject(err);
            else resolve(data);
        });
    });
}

使用deferred,您可以执行以下操作(让我们在本例中使用q,尽管q现在支持您应该喜欢的新语法):

1
2
3
4
5
6
7
8
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;  
}

一般来说,您不应该手动地过多地进行Promissify,大多数Promise库都考虑了节点以及节点8+中的本机Promissify,它们有一个内置的方法来Promissify nodeback。例如

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

4。带有节点样式回调的整个库:

这里没有黄金法则,你一个接一个地提出。但是,一些Promise实现允许您批量执行此操作,例如在Bluebird中,将nodeback API转换为Promise API非常简单:

1
Promise.promisifyAll(API);

或者在节点中使用本机承诺:

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

笔记:

  • 当然,当你在一个.then处理程序中时,你不需要先做决定。从.then处理程序返回承诺将以该承诺的价值解决或拒绝。从一个.then处理程序抛出也是一个很好的实践,并将拒绝承诺-这是著名的承诺抛出安全。
  • 在实际的onload情况下,应该使用addEventListener而不是onX


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

Promise的一个简单而基本的例子(用吻的方式):

纯javascript异步API代码:

1
2
3
4
5
6
7
8
9
function divisionAPI (number, divider, successCallback, errorCallback) {

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

    successCallback( number / divider )

}

Promisejavascript异步API代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function divisionAPI (number, divider) {

    return new Promise(function (fulfilled, rejected) {

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

        fulfilled( number / divider )

     })

}

(我建议您访问这个美丽的来源)

也可以将PromiseES7中的async\await一起使用,使程序流等待fullfiled的结果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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()方法对同一代码的另一种用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

奖励:混合方法(假定回调方法有两个参数作为错误和结果)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 )

     })

}

上述方法可以对旧时尚回调的结果做出响应,并保证使用效果。

希望这有帮助。


在node.js中将函数转换为promise之前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
})

转换后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
})

如果您需要处理多个请求

1
2
3
4
5
6
7
8
9
10
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)
});

我认为@Benjamin提出的window.onload建议不会一直有效,因为它无法检测负载后是否调用它。我被那个咬了很多次。以下是一个应该始终有效的版本:

1
2
3
4
5
6
7
function promiseDOMready() {
    return new Promise(function(resolve) {
        if (document.readyState ==="complete") return resolve();
        document.addEventListener("DOMContentLoaded", resolve);
    });
}
promiseDOMready().then(initOnLoad);


在node.js 8.0.0的候选版本中,有一个新的实用程序,util.promisify(我写过关于util.promisify的文章),它封装了实现任何功能的能力。

它与其他答案中建议的方法没有太大的不同,但具有核心方法的优势,并且不需要额外的依赖性。

1
2
3
4
const fs = require('fs');
const util = require('util');

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

然后是返回本机PromisereadFile方法。

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


node.js 8.0.0包括一个新的util.promisify()API,它允许标准node.js回调样式的API包装在一个返回承诺的函数中。下面显示了util.promisify()的一个示例用法。

1
2
3
4
5
6
7
8
const fs = require('fs');
const util = require('util');

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

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

看到对承诺的改进支持


您可以在节点JS中使用JavaScript本机承诺。

我的云9代码链接:https://ide.c9.io/adx2803/native-promises-in-node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 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

Kriskowal的Q库包含了对Promise函数的回调。这样的方法:

1
2
3
4
obj.prototype.dosomething(params, cb) {
  ...blah blah...
  cb(error, results);
}

可以用q.ninvoke转换

1
2
3
Q.ninvoke(obj,"dosomething",params).
then(function(results) {
});


在内置承诺和异步的节点V7.6+下:

1
2
3
4
5
6
7
8
9
10
// promisify.js
let promisify = fn => (...args) =>
    new Promise((resolve, reject) =>
        fn(...args, (err, result) => {
            if (err) return reject(err);
            return resolve(result);
        })
    );

module.exports = promisify;

如何使用:

1
2
3
4
5
6
7
8
let readdir = require('fs').readdir;
let promisify = require('./promisify');
let readdirP = promisify(readdir);

async function myAsyncFn(path) {
    let entries = await readdirP(path);
    return entries;
}

在node.js 8中,您可以使用此NPM模块即时提示对象方法:

https://www.npmjs.com/package/doasync

它使用了util.promisify和代理,以便您的对象保持不变。记忆化也可以通过weakmaps来完成。以下是一些例子:

对象:

1
2
3
4
5
6
7
const fs = require('fs');
const doAsync = require('doasync');

doAsync(fs).readFile('package.json', 'utf8')
  .then(result => {
    console.dir(JSON.parse(result), {colors: true});
  });

具有功能:

1
2
3
4
5
doAsync(request)('http://www.google.com')
  .then(({body}) => {
    console.log(body);
    // ...
  });

您甚至可以使用本机callapply来绑定一些上下文:

1
2
doAsync(myFunc).apply(context, params)
  .then(result => { /*...*/ });

对于普通的老版本普通的JavaScript,这里有一个解决方案来支持API回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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);
});


当您有几个函数接受回调,并且希望它们返回一个承诺时,您可以使用此函数进行转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function callbackToPromise(func){

    return function(){

        // change this to use what ever promise lib you are using
        // In this case i'm using angular $q that I exposed on a util module

        var defered = util.$q.defer();

        var cb = (val) => {
            defered.resolve(val);
        }

        var args = Array.prototype.slice.call(arguments);
        args.push(cb);    
        func.apply(this, args);

        return defered.promise;
    }
}

回调样式函数总是这样(node.js中的几乎所有函数都是这种样式):

1
2
//fs.readdir(path[, options], callback)
fs.readdir('mypath',(err,files)=>console.log(files))

此样式具有相同的功能:

  • 回调函数由最后一个参数传递。

  • 回调函数总是接受错误对象作为第一个参数。

  • 因此,您可以编写一个函数来转换具有如下样式的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    const R =require('ramda')

    /**
     * A convenient function for handle error in callback function.
     * Accept two function res(resolve) and rej(reject) ,
     * return a wrap function that accept a list arguments,
     * the first argument as error, if error is null,
     * the res function will call,else the rej function.
     * @param {function} res the function which will call when no error throw
     * @param {function} rej the function which will call when  error occur
     * @return {function} return a function that accept a list arguments,
     * the first argument as error, if error is null, the res function
     * will call,else the rej function
     **/

    const checkErr = (res, rej) => (err, ...data) => R.ifElse(
        R.propEq('err', null),
        R.compose(
            res,
            R.prop('data')
        ),
        R.compose(
            rej,
            R.prop('err')
        )
    )({err, data})

    /**
     * wrap the callback style function to Promise style function,
     * the callback style function must restrict by convention:
     * 1. the function must put the callback function where the last of arguments,
     * such as (arg1,arg2,arg3,arg...,callback)
     * 2. the callback function must call as callback(err,arg1,arg2,arg...)
     * @param {function} fun the callback style function to transform
     * @return {function} return the new function that will return a Promise,
     * while the origin function throw a error, the Promise will be Promise.reject(error),
     * while the origin function work fine, the Promise will be Promise.resolve(args: array),
     * the args is which callback function accept
     * */

     const toPromise = (fun) => (...args) => new Promise(
        (res, rej) => R.apply(
            fun,
            R.append(
                checkErr(res, rej),
                args
            )
        )
    )

    为了更简洁,上面的示例使用了ramda.js。ramda.js是一个优秀的函数式编程库。在上面的代码中,我们使用了apply(如javascript function.prototype.apply)和append(如javascript function.prototype.push)。因此,我们现在可以将回调样式函数转换为Promise样式函数:

    1
    2
    3
    4
    5
    6
    7
    const {readdir} = require('fs')
    const readdirP = toPromise(readdir)
    readdir(Path)
        .then(
            (files) => console.log(files),
            (err) => console.log(err)
        )

    TopPromise和Checkerr函数由Berserk库拥有,它是Ramda.js(由我创建)的一个函数式编程库分支。

    希望这个答案对你有用。


    你可以这样做

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // @flow

    const toPromise = (f: (any) => void) => {
      return new Promise((resolve, reject) => {
        try {
          f((result) => {
            resolve(result)
          })
        } catch (e) {
          reject(e)
        }
      })
    }

    export default toPromise

    然后使用它

    1
    2
    3
    4
    5
    async loadData() {
      const friends = await toPromise(FriendsManager.loadFriends)

      console.log(friends)
    }


    您可以在ES6中使用本机承诺,例如处理设置超时:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    enqueue(data) {

        const queue = this;
        // returns the Promise
        return new Promise(function (resolve, reject) {
            setTimeout(()=> {
                    queue.source.push(data);
                    resolve(queue); //call native resolve when finish
                }
                , 10); // resolve() will be called in 10 ms
        });

    }

    在这个例子中,承诺没有理由失败,所以没有人打电话给reject()


    es6-promisify将基于回调的函数转换为基于承诺的函数。

    1
    2
    3
    const promisify = require('es6-promisify');

    const promisedFn = promisify(callbackedFn, args);

    参考:https://www.npmjs.com/package/es6-promisify


    我推荐的callback函数版本是P函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    var P = function() {
      var self = this;
      var method = arguments[0];
      var params = Array.prototype.slice.call(arguments, 1);
      return new Promise((resolve, reject) => {
        if (method && typeof(method) == 'function') {
          params.push(function(err, state) {
            if (!err) return resolve(state)
            else return reject(err);
          });
          method.apply(self, params);
        } else return reject(new Error('not a function'));
      });
    }
    var callback = function(par, callback) {
      var rnd = Math.floor(Math.random() * 2) + 1;
      return rnd > 1 ? callback(null, par) : callback(new Error("trap"));
    }

    callback("callback", (err, state) => err ? console.error(err) : console.log(state))
    callback("callback", (err, state) => err ? console.error(err) : console.log(state))
    callback("callback", (err, state) => err ? console.error(err) : console.log(state))
    callback("callback", (err, state) => err ? console.error(err) : console.log(state))

    P(callback,"promise").then(v => console.log(v)).catch(e => console.error(e))
    P(callback,"promise").then(v => console.log(v)).catch(e => console.error(e))
    P(callback,"promise").then(v => console.log(v)).catch(e => console.error(e))
    P(callback,"promise").then(v => console.log(v)).catch(e => console.error(e))

    P函数要求回调签名必须是callback(error,result)


    好像晚了5年,但我想在这里发布我的promesify版本,它从回调API获取函数并将其转化为承诺

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const promesify = fn => {
      return (...params) => ({
        then: cbThen => ({
          catch: cbCatch => {
            fn(...params, cbThen, cbCatch);
          }
        })
      });
    };

    请看下面这个非常简单的版本:https://gist.github.com/jdtoregrosas/aeee96dd07558a5d18db1f02f31e21a