Jak przekonwertować istniejący interfejs API wywołania zwrotnego na obietnice?
Chcę pracować z obietnicami, ale mam API wywołania zwrotnego w formacie:
1. Ładowanie DOM lub inne jednorazowe zdarzenie:
window.onload; // set to callback
...
window.onload = function() {
};
2. Zwykłe wywołanie zwrotne:
function request(onChangeHandler) {
...
}
request(function() {
// change happened
...
});
3. Wywołanie zwrotne w stylu węzła („nodeback”):
function getStuff(dat, callback) {
...
}
getStuff("dataParam", function(err, data) {
...
})
4. Cała biblioteka z wywołaniami zwrotnymi w stylu węzłów:
API;
API.one(function(err, data) {
API.two(function(err, data2) {
API.three(function(err, data3) {
...
});
});
});
Jak pracować z API w obietnicach, jak to „obiecać”?
Odpowiedzi
Obietnice mają status, zaczynają się jako oczekujące i mogą zostać ustalone na:
- spełnione, co oznacza, że obliczenia zakończyły się pomyślnie.
- odrzucone, co oznacza, że obliczenia nie powiodły się.
Obietnica, że funkcje zwracające nigdy nie powinny zwracać, zamiast tego powinny zwracać odrzucenia. Wyrzucenie z funkcji zwracającej obietnicę zmusi cię do użycia zarówno a, jak } catch {
i a .catch
. Osoby korzystające z obiecanych interfejsów API nie spodziewają się rzucania obietnic. Jeśli nie masz pewności, jak działają asynchroniczne interfejsy API w JS - najpierw zapoznaj się z tą odpowiedzią .
1. Ładowanie DOM lub inne jednorazowe zdarzenie:
Tak więc tworzenie obietnic ogólnie oznacza określenie, kiedy się rozliczają - to znaczy, kiedy przechodzą do fazy wypełnionej lub odrzuconej, aby wskazać, że dane są dostępne (i można uzyskać do nich dostęp .then
).
Dzięki nowoczesnym implementacjom obietnic, które obsługują Promise
konstruktora, jak natywne obietnice ES6:
function load() {
return new Promise(function(resolve, reject) {
window.onload = resolve;
});
}
Następnie wykorzystasz wynikową obietnicę w następujący sposób:
load().then(function() {
// Do things after onload
});
Z bibliotekami, które obsługują deferred (użyjmy $ q w tym przykładzie tutaj, ale użyjemy również jQuery później):
function load() {
var d = $q.defer();
window.onload = function() { d.resolve(); };
return d.promise;
}
Lub za pomocą jQuery, takiego jak API, przechwytywanie zdarzenia, które ma miejsce raz:
function done() {
var d = $.Deferred();
$("#myObject").once("click",function() {
d.resolve();
});
return d.promise();
}
2. Zwykłe wywołanie zwrotne:
Te interfejsy API są dość powszechne, ponieważ cóż… wywołania zwrotne są powszechne w JS. Spójrzmy na typowy przypadek posiadania onSuccess
i onFail
:
function getUserData(userId, onLoad, onFail) { …
Dzięki nowoczesnym implementacjom obietnic, które obsługują Promise
konstruktora, jak natywne obietnice ES6:
function getUserDataAsync(userId) {
return new Promise(function(resolve, reject) {
getUserData(userId, resolve, reject);
});
}
Z bibliotekami, które obsługują deferred (użyjmy jQuery w tym przykładzie tutaj, ale użyliśmy również $ q powyżej):
function getUserDataAsync(userId) {
var d = $.Deferred();
getUserData(userId, function(res){ d.resolve(res); }, function(err){ d.reject(err); });
return d.promise();
}
jQuery oferuje również $.Deferred(fn)
formularz, który ma tę zaletę, że pozwala nam napisać wyrażenie, które bardzo dokładnie naśladuje new Promise(fn)
formularz, w następujący sposób:
function getUserDataAsync(userId) {
return $.Deferred(function(dfrd) {
getUserData(userId, dfrd.resolve, dfrd.reject);
}).promise();
}
Uwaga: tutaj wykorzystujemy fakt, że jQuery odroczone resolve
i reject
metody są „odłączalne”; to znaczy. są one powiązane z wystąpieniem jQuery.Deferred (). Nie wszystkie biblioteki oferują tę funkcję.
3. Wywołanie zwrotne w stylu węzła („nodeback”):
Wywołania zwrotne w stylu węzła (nodeback) mają określony format, w którym wywołania zwrotne są zawsze ostatnim argumentem, a ich pierwszym parametrem jest błąd. Najpierw obiecajmy ręcznie:
getStuff("dataParam", function(err, data) { …
Do:
function getStuffAsync(param) {
return new Promise(function(resolve, reject) {
getStuff(param, function(err, data) {
if (err !== null) reject(err);
else resolve(data);
});
});
}
Z odroczonymi możesz wykonać następujące czynności (użyjmy Q w tym przykładzie, chociaż Q obsługuje teraz nową składnię, którą powinieneś preferować ):
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;
}
Ogólnie rzecz biorąc, nie powinieneś zbytnio obiecywać rzeczy ręcznie, większość obiecujących bibliotek zaprojektowanych z myślą o Node, a także natywnych obietnic w Node 8+ ma wbudowaną metodę obiecywania nodebacków. Na przykład
var getStuffAsync = Promise.promisify(getStuff); // Bluebird
var getStuffAsync = Q.denodeify(getStuff); // Q
var getStuffAsync = util.promisify(getStuff); // Native promises, node only
4. Cała biblioteka z wywołaniami zwrotnymi w stylu węzłów:
Nie ma tu złotej zasady, obiecujesz im jeden po drugim. Jednak niektóre obiecujące implementacje pozwalają robić to zbiorczo, na przykład w Bluebird, konwersja nodeback API do obiecującego API jest tak prosta, jak:
Promise.promisifyAll(API);
Lub z natywnymi obietnicami w 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}), {});
Uwagi:
- Oczywiście, gdy jesteś w
.then
treserze, nie musisz niczego obiecywać. Zwrócenie obietnicy od.then
opiekuna rozstrzygnie lub odrzuci obietnicę o wartości. Rzucanie przez przewodnika.then
jest również dobrą praktyką i odrzuca obietnicę - to słynne obietnice dotyczące bezpieczeństwa rzutu. - W rzeczywistym
onload
przypadku, należy użyćaddEventListener
zamiastonX
.
Dziś mogę używać Promise
w Node.js
jako zwykłej metody Javascript.
Prosty i podstawowy przykład, aby Promise
(ze sposobem KISS ):
Zwykły JavaScript asynchroniczny kod API:
function divisionAPI (number, divider, successCallback, errorCallback) {
if (divider == 0) {
return errorCallback( new Error("Division by zero") )
}
successCallback( number / divider )
}
Promise
Kod JavaScript Async API:
function divisionAPI (number, divider) {
return new Promise(function (fulfilled, rejected) {
if (divider == 0) {
return rejected( new Error("Division by zero") )
}
fulfilled( number / divider )
})
}
(Polecam odwiedzenie tego pięknego źródła )
Również Promise
można stosować łączne async\await
się ES7
, aby czekać przepływu programu na fullfiled
skutek tak:
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
Inne użycie z tym samym kodem przy użyciu .then()
metody
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
może być również używany na dowolnej platformie, która jest oparta na node.js podobnego react-native
.
Bonus : metoda hybrydowa
(zakłada się, że metoda wywołania zwrotnego ma dwa parametry jako błąd i wynik)
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 )
})
}
Powyższa metoda może odpowiedzieć na wynik dla staromodnych połączeń zwrotnych i obietnic.
Mam nadzieję że to pomoże.
Przed konwersją funkcji zgodnie z obietnicą w 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)
})
Po konwersji
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)
})
W przypadku, gdy musisz obsłużyć wiele żądań
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)
});
Nie sądzę, aby window.onload
sugestia @Benjamina działała cały czas, ponieważ nie wykrywa, czy jest wywoływana po załadowaniu. Wiele razy mnie to ugryzło. Oto wersja, która zawsze powinna działać:
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 zawiera nowy util.promisify()
interfejs API, który pozwala standardowym funkcjom wywołań zwrotnych Node.js zawrzeć funkcję, która zwraca Obietnicę. Przykładowe użycie util.promisify()
jest pokazane poniżej.
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
readFile('/some/file')
.then((data) => { /** ... **/ })
.catch((err) => { /** ... **/ });
Zobacz Ulepszona obsługa obietnic
W wersji Kandydat do wydania dla Node.js 8.0.0 jest nowe narzędzie util.promisify
(pisałem o util.promisify ), które zawiera zdolność obiecywania dowolnej funkcji.
Nie różni się zbytnio od podejść sugerowanych w innych odpowiedziach, ale ma tę zaletę, że jest metodą podstawową i nie wymaga dodatkowych zależności.
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
Następnie masz readFile
metodę, która zwraca wartość natywną Promise
.
readFile('./notes.txt')
.then(txt => console.log(txt))
.catch(...);
Możesz używać natywnych obietnic JavaScript w Node JS.
Link 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
W przypadku zwykłego, starego javaScript, oto rozwiązanie obiecujące wywołanie zwrotne interfejsu 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);
});
Biblioteka Q autorstwa kriskowala zawiera funkcje zwrotne na obietnicę. Taka metoda:
obj.prototype.dosomething(params, cb) {
...blah blah...
cb(error, results);
}
można przekonwertować za pomocą Q.ninvoke
Q.ninvoke(obj,"dosomething",params).
then(function(results) {
});
Jeśli masz kilka funkcji, które odbierają wywołanie zwrotne i chcesz, aby zamiast tego zwracały obietnicę, możesz użyć tej funkcji do wykonania konwersji.
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;
}
}
W węźle v7.6 +, który ma wbudowane obietnice i asynchroniczne:
// 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;
Jak używać:
let readdir = require('fs').readdir;
let promisify = require('./promisify');
let readdirP = promisify(readdir);
async function myAsyncFn(path) {
let entries = await readdirP(path);
return entries;
}
W Node.js 8 możesz obiecywać metody obiektów w locie za pomocą tego modułu npm:
https://www.npmjs.com/package/doasync
Używa util.promisify i serwerów proxy, dzięki czemu obiekty pozostają niezmienione. Memoizacja odbywa się również za pomocą WeakMaps). Oto kilka przykładów:
Z obiektami:
const fs = require('fs');
const doAsync = require('doasync');
doAsync(fs).readFile('package.json', 'utf8')
.then(result => {
console.dir(JSON.parse(result), {colors: true});
});
Z funkcjami:
doAsync(request)('http://www.google.com')
.then(({body}) => {
console.log(body);
// ...
});
Możesz nawet użyć natywnego call
i apply
powiązać jakiś kontekst:
doAsync(myFunc).apply(context, params)
.then(result => { /*...*/ });
Możesz użyć natywnej Promise w ES6, na przykład w przypadku 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
});
}
W tym przykładzie Obietnica nie ma powodu do niepowodzenia, więc reject()
nigdy nie jest nazywana.
Funkcja stylu wywołania zwrotnego zawsze wygląda tak (prawie wszystkie funkcje w node.js mają ten styl):
//fs.readdir(path[, options], callback)
fs.readdir('mypath',(err,files)=>console.log(files))
Ten styl ma tę samą funkcję:
funkcja zwrotna jest przekazywana przez ostatni argument.
funkcja zwrotna zawsze akceptuje obiekt błędu jako pierwszy argument.
Możesz więc napisać funkcję do konwersji funkcji w tym stylu w następujący sposób:
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
)
)
)
Aby uzyskać bardziej zwięzły, powyższy przykład użył ramda.js. Ramda.js to doskonała biblioteka do programowania funkcjonalnego. W powyższym kodzie użyliśmy go zastosuj (jak javascript function.prototype.apply
) i dołącz (jak javascript function.prototype.push
). Możemy więc teraz przekonwertować funkcję stylu wywołania zwrotnego na funkcję obiecującą styl:
const {readdir} = require('fs')
const readdirP = toPromise(readdir)
readdir(Path)
.then(
(files) => console.log(files),
(err) => console.log(err)
)
toPromise i checkErr funkcja jest właścicielem przez berserk biblioteki, to funkcjonalna biblioteka programistyczna widelec przez ramda.js (tworzenie przeze mnie).
Mam nadzieję, że ta odpowiedź jest dla Ciebie przydatna.
Możesz zrobić coś takiego
// @flow
const toPromise = (f: (any) => void) => {
return new Promise<any>((resolve, reject) => {
try {
f((result) => {
resolve(result)
})
} catch (e) {
reject(e)
}
})
}
export default toPromise
Następnie użyj go
async loadData() {
const friends = await toPromise(FriendsManager.loadFriends)
console.log(friends)
}
es6-promisify
konwertuje funkcje oparte na wywołaniach zwrotnych na funkcje oparte na Promise.
const promisify = require('es6-promisify');
const promisedFn = promisify(callbackedFn, args);
Moja wersja callback
funkcji promisify to P
funkcja:
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
Funkcja wymaga, aby podpis zwrotna musi być callback(error,result)
.
Poniżej znajduje się implementacja, w jaki sposób funkcję (API zwrotne) można przekształcić w obietnicę.
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');
Z przyszłości 😄
Prosta funkcja ogólna, której zwykle używam.
const promisify = (fn, ...args) => {
return new Promise((resolve, reject) => {
fn(...args, (err, data) => {
if (err) {
return reject(err);
}
resolve(data);
});
});
};
Jak tego użyć
promisify(fn, arg1, arg2)
Prawdopodobnie nie szukasz tej odpowiedzi, ale pomoże to zrozumieć wewnętrzne działanie dostępnych narzędzi