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,我如何"确定"它?
承诺是有状态的,它们开始时是待定的,可以解决:
- 完成意味着计算成功完成。
- 拒绝意味着计算失败。
承诺返回函数不应该抛出,而是应该返回拒绝。放弃承诺返回函数将强制您同时使用
因此,创建承诺通常意味着指定它们何时结算——也就是说,当它们移动到完成或拒绝的阶段,以表明数据是可用的(并且可以通过
现代Promise实现支持
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中,回调很常见。让我们来看看拥有
1 | function getUserData(userId, onLoad, onFail) { … |
现代Promise实现支持
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还提供了一个
1 2 3 4 5 | function getUserDataAsync(userId) { return $.Deferred(function(dfrd) { getUserData(userId, dfrd.resolve, dfrd.reject); }).promise(); } |
注意:这里我们利用jquery deferred的
节点样式回调(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 。
今天,我可以在
纯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 ) } |
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 ) }) } |
(我建议您访问这个美丽的来源)
也可以将
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 |
使用
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) }) |
奖励:混合方法(假定回调方法有两个参数作为错误和结果)
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提出的
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的候选版本中,有一个新的实用程序,
它与其他答案中建议的方法没有太大的不同,但具有核心方法的优势,并且不需要额外的依赖性。
1 2 3 4 | const fs = require('fs'); const util = require('util'); const readFile = util.promisify(fs.readFile); |
然后是返回本机
1 2 3 | readFile('./notes.txt') .then(txt => console.log(txt)) .catch(...); |
node.js 8.0.0包括一个新的
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); // ... }); |
您甚至可以使用本机
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
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 }); } |
在这个例子中,承诺没有理由失败,所以没有人打电话给
1 2 3 | const promisify = require('es6-promisify'); const promisedFn = promisify(callbackedFn, args); |
参考:https://www.npmjs.com/package/es6-promisify
我推荐的
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)) |
好像晚了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