Como faço para converter uma API de retorno de chamada existente em promessas?
Quero trabalhar com promessas, mas tenho uma API de retorno de chamada em um formato como:
1. Carregamento de DOM ou outro evento único:
window.onload; // set to callback
...
window.onload = function() {
};
2. Retorno de chamada simples:
function request(onChangeHandler) {
...
}
request(function() {
// change happened
...
});
3. Retorno de chamada de estilo de nó ("nodeback"):
function getStuff(dat, callback) {
...
}
getStuff("dataParam", function(err, data) {
...
})
4. Uma biblioteca inteira com callbacks de estilo de nó:
API;
API.one(function(err, data) {
API.two(function(err, data2) {
API.three(function(err, data3) {
...
});
});
});
Como faço para trabalhar com a API em promessas, como faço para "prometê-la"?
Respostas
As promessas têm estado, começam como pendentes e podem ser estabelecidas para:
- cumprido, o que significa que o cálculo foi concluído com êxito.
- rejeitado, o que significa que o cálculo falhou.
As funções de retorno de promessa nunca devem ser lançadas , mas sim rejeições. Jogar de uma função de retorno de promessa forçará você a usar tanto a } catch {
quanto a .catch
. Pessoas que usam APIs prometidas não esperam promessas. Se você não tem certeza de como as APIs assíncronas funcionam em JS - consulte esta resposta primeiro.
1. Carregamento de DOM ou outro evento único:
Portanto, criar promessas geralmente significa especificar quando elas são estabelecidas - ou seja, quando passam para a fase cumprida ou rejeitada para indicar que os dados estão disponíveis (e podem ser acessados com .then
).
Com implementações de promessa modernas que suportam o Promise
construtor como promessas ES6 nativas:
function load() {
return new Promise(function(resolve, reject) {
window.onload = resolve;
});
}
Você então usaria a promessa resultante da seguinte forma:
load().then(function() {
// Do things after onload
});
Com bibliotecas que suportam adiado (vamos usar $ q para este exemplo aqui, mas também usaremos jQuery mais tarde):
function load() {
var d = $q.defer();
window.onload = function() { d.resolve(); };
return d.promise;
}
Ou com um jQuery como API, enganchando em um evento acontecendo uma vez:
function done() {
var d = $.Deferred();
$("#myObject").once("click",function() {
d.resolve();
});
return d.promise();
}
2. Retorno de chamada simples:
Essas APIs são bastante comuns, pois bem ... retornos de chamada são comuns em JS. Vejamos o caso comum de ter onSuccess
e onFail
:
function getUserData(userId, onLoad, onFail) { …
Com implementações de promessa modernas que suportam o Promise
construtor como promessas ES6 nativas:
function getUserDataAsync(userId) {
return new Promise(function(resolve, reject) {
getUserData(userId, resolve, reject);
});
}
Com bibliotecas que suportam adiado (vamos usar jQuery para este exemplo aqui, mas também usamos $ q acima):
function getUserDataAsync(userId) {
var d = $.Deferred();
getUserData(userId, function(res){ d.resolve(res); }, function(err){ d.reject(err); });
return d.promise();
}
O jQuery também oferece um $.Deferred(fn)
formulário, que tem a vantagem de nos permitir escrever uma expressão que emula muito de perto a new Promise(fn)
forma, da seguinte maneira:
function getUserDataAsync(userId) {
return $.Deferred(function(dfrd) {
getUserData(userId, dfrd.resolve, dfrd.reject);
}).promise();
}
Nota: Aqui nós exploramos o fato de que métodos resolve
e adiados de um jQuery reject
são "destacáveis"; ie. eles estão vinculados à instância de um jQuery.Deferred (). Nem todas as bibliotecas oferecem esse recurso.
3. Retorno de chamada de estilo de nó ("nodeback"):
Callbacks de estilo de nó (nodebacks) têm um formato particular onde os callbacks são sempre o último argumento e seu primeiro parâmetro é um erro. Vamos primeiro prometê-lo manualmente:
getStuff("dataParam", function(err, data) { …
Para:
function getStuffAsync(param) {
return new Promise(function(resolve, reject) {
getStuff(param, function(err, data) {
if (err !== null) reject(err);
else resolve(data);
});
});
}
Com adiados, você pode fazer o seguinte (vamos usar Q para este exemplo, embora Q agora suporte a nova sintaxe que você deve preferir ):
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;
}
Em geral, você não deve prometer muito as coisas manualmente, a maioria das bibliotecas de promessa que foram projetadas com o Node em mente, bem como as promessas nativas no Node 8+, têm um método embutido para prometer nodebacks. Por exemplo
var getStuffAsync = Promise.promisify(getStuff); // Bluebird
var getStuffAsync = Q.denodeify(getStuff); // Q
var getStuffAsync = util.promisify(getStuff); // Native promises, node only
4. Uma biblioteca inteira com callbacks de estilo de nó:
Não há regra de ouro aqui, você os promete um por um. No entanto, algumas implementações de promessa permitem que você faça isso em massa, por exemplo no Bluebird, a conversão de uma API nodeback em uma API de promessa é tão simples quanto:
Promise.promisifyAll(API);
Ou com promessas nativas no 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}), {});
Notas:
- É claro que, quando você está em um
.then
manipulador, não precisa prometer coisas. Retornar uma promessa de um.then
manipulador resolverá ou rejeitará o valor dessa promessa. Arremessar de um.then
manipulador também é uma boa prática e rejeitará a promessa - essa é a famosa segurança de arremesso de promessa. - Em um
onload
caso real , você deve usar emaddEventListener
vez deonX
.
Hoje, posso usar o Promise
in Node.js
como um método simples de Javascript.
Um exemplo simples e básico para Promise
(com o jeito KISS ):
Código de API simples de JavaScript assíncrono:
function divisionAPI (number, divider, successCallback, errorCallback) {
if (divider == 0) {
return errorCallback( new Error("Division by zero") )
}
successCallback( number / divider )
}
Promise
Código da API Javascript Async:
function divisionAPI (number, divider) {
return new Promise(function (fulfilled, rejected) {
if (divider == 0) {
return rejected( new Error("Division by zero") )
}
fulfilled( number / divider )
})
}
(Recomendo visitar esta linda fonte )
Também Promise
pode ser usado junto async\await
com o ES7
para fazer o fluxo do programa esperar por um fullfiled
resultado como o seguinte:
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
Outro uso com o mesmo código usando o .then()
método
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
Também pode ser usado em qualquer plataforma que é baseado em Node.js como react-native
.
Bônus : um método híbrido
(presume-se que o método de retorno de chamada tenha dois parâmetros como erro e resultado)
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 )
})
}
O método acima pode responder ao resultado para callback à moda antiga e usos de promessa.
Espero que isto ajude.
Antes de converter uma função como promessa em Node.JS
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)
})
Depois de convertê-lo
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)
})
Caso você precise atender a vários pedidos
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)
});
Não creio que a window.onload
sugestão do @Benjamin funcione o tempo todo, pois não detecta se é chamado após o carregamento. Já fui mordido por muitas vezes. Aqui está uma versão que sempre deve funcionar:
function promiseDOMready() {
return new Promise(function(resolve) {
if (document.readyState === "complete") return resolve();
document.addEventListener("DOMContentLoaded", resolve);
});
}
promiseDOMready().then(initOnLoad);
O Node.js 8.0.0 inclui uma nova util.promisify()
API que permite que APIs de estilo de retorno de chamada Node.js padrão sejam agrupadas em uma função que retorna uma Promessa. Um exemplo de uso de util.promisify()
é mostrado abaixo.
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
readFile('/some/file')
.then((data) => { /** ... **/ })
.catch((err) => { /** ... **/ });
No candidato a lançamento do Node.js 8.0.0, há um novo utilitário util.promisify
(escrevi sobre util.promisify ), que encapsula a capacidade de prometer qualquer função.
Não é muito diferente das abordagens sugeridas nas outras respostas, mas tem a vantagem de ser um método central e não requer dependências adicionais.
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
Então você tem um readFile
método que retorna um nativo Promise
.
readFile('./notes.txt')
.then(txt => console.log(txt))
.catch(...);
Você pode usar promessas nativas de JavaScript com Node JS.
Link do código do My 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
Com o velho e simples javaScript vanilla, aqui está uma solução para prometir um callback de API.
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);
});
A biblioteca Q de kriskowal inclui funções de retorno para promessa. Um método como este:
obj.prototype.dosomething(params, cb) {
...blah blah...
cb(error, results);
}
pode ser convertido com Q.ninvoke
Q.ninvoke(obj,"dosomething",params).
then(function(results) {
});
Quando você tem algumas funções que recebem um retorno de chamada e deseja que elas retornem uma promessa, você pode usar esta função para fazer a conversão.
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;
}
}
No nó v7.6 + que possui promessas integradas e assíncronas:
// 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;
Como usar:
let readdir = require('fs').readdir;
let promisify = require('./promisify');
let readdirP = promisify(readdir);
async function myAsyncFn(path) {
let entries = await readdirP(path);
return entries;
}
No Node.js 8, você pode promisificar métodos de objeto em tempo real usando este módulo npm:
https://www.npmjs.com/package/doasync
Ele usa util.promisify e Proxies para que seus objetos permaneçam inalterados. A memorização também é feita com o uso de WeakMaps). aqui estão alguns exemplos:
Com objetos:
const fs = require('fs');
const doAsync = require('doasync');
doAsync(fs).readFile('package.json', 'utf8')
.then(result => {
console.dir(JSON.parse(result), {colors: true});
});
Com funções:
doAsync(request)('http://www.google.com')
.then(({body}) => {
console.log(body);
// ...
});
Você pode até usar nativo call
e apply
vincular algum contexto:
doAsync(myFunc).apply(context, params)
.then(result => { /*...*/ });
Você pode usar Promise nativa no ES6, por exemplo, lidar com setTimeout:
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
});
}
Neste exemplo, a Promessa não tem razão para falhar, então reject()
nunca é chamada.
A função de estilo de retorno de chamada é sempre assim (quase todas as funções em node.js são deste estilo):
//fs.readdir(path[, options], callback)
fs.readdir('mypath',(err,files)=>console.log(files))
Este estilo possui o mesmo recurso:
a função de retorno de chamada é passada pelo último argumento.
a função de retorno de chamada sempre aceita o objeto de erro como seu primeiro argumento.
Então, você pode escrever uma função para converter uma função com este estilo, como este:
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
)
)
)
Para mais conciso, o exemplo acima usou ramda.js. Ramda.js é uma excelente biblioteca para programação funcional. No código acima, usamos aplicar (como javascript function.prototype.apply
) e anexar (como javascript function.prototype.push
). Então, podemos converter a função de estilo de retorno de chamada em função de estilo de promessa agora:
const {readdir} = require('fs')
const readdirP = toPromise(readdir)
readdir(Path)
.then(
(files) => console.log(files),
(err) => console.log(err)
)
A função toPromise e checkErr é de propriedade da biblioteca berserk , é uma bifurcação de biblioteca de programação funcional por ramda.js (criada por mim).
Espero que esta resposta seja útil para você.
Você pode fazer algo assim
// @flow
const toPromise = (f: (any) => void) => {
return new Promise<any>((resolve, reject) => {
try {
f((result) => {
resolve(result)
})
} catch (e) {
reject(e)
}
})
}
export default toPromise
Então use
async loadData() {
const friends = await toPromise(FriendsManager.loadFriends)
console.log(friends)
}
es6-promisify
converte funções baseadas em callback em funções baseadas em Promise.
const promisify = require('es6-promisify');
const promisedFn = promisify(callbackedFn, args);
Minha versão promissora de uma callback
função é a P
função:
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))
A P
função requer que a assinatura de retorno de chamada seja callback(error,result)
.
Abaixo está a implementação de como uma função (API de retorno de chamada) pode ser convertida em uma promessa.
function promisify(functionToExec) {
return function() {
var array = Object.values(arguments);
return new Promise((resolve, reject) => {
array.push(resolve)
try {
functionToExec.apply(null, array);
} catch (error) {
reject(error)
}
})
}
}
// USE SCENARIO
function apiFunction (path, callback) { // Not a promise
// Logic
}
var promisedFunction = promisify(apiFunction);
promisedFunction('path').then(()=>{
// Receive the result here (callback)
})
// Or use it with await like this
let result = await promisedFunction('path');
Do futuro 😄
Uma função genérica simples que normalmente uso.
const promisify = (fn, ...args) => {
return new Promise((resolve, reject) => {
fn(...args, (err, data) => {
if (err) {
return reject(err);
}
resolve(data);
});
});
};
Como usá-lo
promisify(fn, arg1, arg2)
Você provavelmente não está olhando para esta resposta, mas isso ajudará a entender o funcionamento interno dos utilitários disponíveis