可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
ES6 Promises are great. So far it’s been pretty easy to adjust my thinking from
the callback idiom. I’ve found it naturally encourages more modular code, and of
course error handling is much clearer.
But a few times I’ve encountered flow situations that don’t seem(?) like they
can be readily translated from nodebacks to promises (and perhaps that’s just that, but maybe I’m just blind to the answers). Since promises are agnostic
about the next operation (or if there even is one), it seems pretty tough to use
Promises with APIs that don’t just take callbacks, but also return them.
The most common example that comes to mind is the ‘done’ callback. It shows up in things like database connections to signify ‘return connection to pool’ but I’ve seen it pop up in plenty of other places, too.
function getSomeStupidConnection(cb) {
var conn = /* ... */;
var iNeedToBeToldWhenIAmDone = function() { /* ... */ };
cb(conn, iNeedToBeToldWhenIAmDone);
}
getSomeStupidConnection(function(conn, done) {
/* ... */
conn.doLotsOfStuff(function(soMuchStuff) {
/* stuff! so much fun! */
/* okay conn go away I’m tired */
done();
});
});
Flow-reversal like this is obviously not something you want to have in your APIs
to start with, but it’s out there and you can’t really avoid it sometimes. With
callbacks, you can pass the ‘call later’ inner callback to the original ‘outer’
callback. It doesn’t exactly lead to a clean seperation of concerns, but at
least it’s quick and simple.
Is there a Promise-based approach suited to situations like this? A way to say,
‘here’s the resolve value -- but when the chain is complete, also do this’? I
suspect there’s nothing that perfectly matches what I just described because it
isn’t really possible to say a chain is ‘done’, but maybe I’m missing some
pattern that gets you close to that without making a mess...
Edit: Based on the feedback so far I've realized that there's simply no way to wrap such an API in true promises, because the promise you return will never be able to tell you anything about any subsequent chained promises that piggyback on it. But you can fake it. The twist is that the result is rather brittle; it must assume that the only then
which needs the connection object is the one which immediately follows. The consumer of the promise would need to understand that it’s a one-time-use connection, which isn’t otherwise obvious. Therefore I don't really recommend it in practice, but for the sake of curiosity here is a solution that hides the done
while behaving as (and ultimately becoming) a promise chain:
/* jshint node: true, esnext: true */
'use strict';
// Assume this comes from an external library. It returns a connection and a
// callback to signal that you are finished with the connection.
function getConnectionExternal(cb) {
let connection = 'Received connection.';
let done = () => console.log('Done was called.');
cb(null, connection, done);
}
// Our promisey wrapper for the above
function getConnection() {
let _done;
let promise = new Promise((resolve, reject) => {
getConnectionExternal((err, connection, done) => {
if (err) return reject(err);
_done = (val) => {
done();
return val;
};
resolve(connection);
});
});
let _then = promise.then.bind(promise);
promise.then = (handler) => _then(handler).then(_done, _done);
return promise;
}
// Test it out
getConnection()
.then(connection => {
console.log(connection);
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Finished using connection!');
resolve('This should be after connection closes.');
}, 200);
});
})
.then(msg => console.log(msg))
.catch(err => console.error(err));
Console prints:
- Received connection.
- Finished using connection!
- Done was called.
- This should be after connection closes.
回答1:
A way to say, ‘here’s the resolve value -- but when the chain is complete, also do this’?
No, native promises do not provide such a facility. I would go with a resource function that takes a promise-returning callback, the callback does everything (in a chain) that needs to be done while the connection is open. Instead of passing iNeedToBeTold
to the callback, the resource manager function observes the promise and does what needs to be done when it resolves.
function manageConnection(cb) {
return getSomeConnection(…) // get the connections asynchronously - via a promise of course
.then(function(conn) {
function whenDone() {
… // do what needs to be done
return result;
}
var result = cb(conn);
return result.then(whenDone, whenDone);
});
}
manageConnection(function(conn) {
return conn.doLotsOfStuff(soMuch)
.then(function(stuff) {
/* stuff! so much fun! */
});
}).then(…)
回答2:
Elaborating on Bergi's solution, this is called the disposer pattern. It exists in many forms in many languages - with
in Python, using
in C# and try(){
with resource in Java. Some languages handle resource in scopes this way natively through destructurs like C#.
The general idea is for a scope to encapsulate the life-time of a value. In your case a database connection. It's a lot tidier than having to call done
in a callback since it's much easier to forget to call done
which leaves an open connection and a resource leak. Synchronously it'd look like:
function scope(cb){
try{
var conn = getConnection(...);
return cb(conn);
} finally {
conn.release();
}
}
The promises version is not too different:
function conn(data){
var _connection;
return getConnection().then(function(connection){
_connection = connection; // keep a reference
return data(_connection); // pass it to the function
}).then(function(val){
// release and forward
_connection.release(); // if release is async - chain
return val;
}, function(err){
_connection.release();
throw err; // forward error
});
});
Which would use:
conn(function(db){
return db.query("SELECT * FROM ...");
}).then(function(result){ // handle result
// connection is released here
});
回答3:
The problem with a done() function is that people forget to call it, causing leaks.
I like Bergi's answer with the passed-in callback because it is clean, but it is not very "promise-y", and still open-endedly unsafe, e.g. if people chain in promises that never resolve the callback-promise, then it stalls and leaks.
This is a problem being discussed in browser APIs as well, and one pattern we're pondering is to return an
AutoClosingPromise
An AutoClosingPromise acts like a promise but does two things differently:
It "closes a ticket" (calls done) after executing its .then().
Additionally, when it subsumes another promise, if it sees another AutoClosingPromise returned from its .then(), then it forwards it out (passes that promise's ticket - a different ticket - out to the AutoClosingPromise it returned from its own .then() function).
The first part means that an API can return an AutoClosingPromise with a "ticket" that holds a resource open (like an open-count) and be assured that the ticket will be closed once the first .then() function returns.
The second part lets a caller make additional asynchronous calls on the API from within the immediate .then() function, letting the API keep the resource open for as long as tickets overlap in time.
A feature of this is that resources are not tendered across regular promises, only AutoClosing ones, avoiding risk of leaks. For example:
var lock = new ExampleLock();
lock.access("foo")
.then(() => lock.set("foo1"))
.then(() => lock.set("foo2"))
.then(() => lock.set("foo3"))
.then(() => {})
.then(() => lock.set("foo4"))
.catch(failed);
would extend the resource (the lock) to the first three, but not the fourth:
setting foo1 [LOCKED]
setting foo2 [LOCKED]
setting foo3 [LOCKED]
setting foo4 [UNLOCKED]
Here's the code:
function AutoClosingPromise(ticket, p) {
this.pending = true;
this.ticket = ticket;
var close = result => {
this.pending = false;
if (this.ticket) {
this.ticket.close();
if (result && result.handoffTicket && this.returnedThenPromise) {
// callback returned an AutoClosingPromise! Forward its ticket
this.returnedThenPromise.takeTicket(result.handoffTicket());
}
}
return result;
};
this.p = p.then(v => close(this.success && this.success(v)),
r => close(this.failure && this.failure(r)));
}
AutoClosingPromise.prototype = {
then: function(success, failure) {
if (this.pending && !this.success && !this.failure) {
this.success = success;
this.failure = failure;
this.returnedThenPromise = new AutoClosingPromise(null, this.p);
return this.returnedThenPromise;
} else {
return this.p.then(success, failure);
}
},
takeTicket: function(ticket) { this.ticket = ticket; },
handoffTicket: function() {
var ticket = this.ticket;
this.ticket = null;
return ticket;
}
};
and the fiddle: http://jsfiddle.net/jib1/w0ufvahL (needs a browser that understands es6 arrow functions, like Firefox, e.g. not Chrome).
Since the API controls all asynchronous calls that issue tickets, this should be fairly leak-proof. E.g. even if a caller ignores the promise returned from the API entirely, close is still called.
Note that this is a fairly new idea rather than a proven construct, so if you end up using it, please let me know how it works. ;-)
回答4:
I'm not 100% sure what you are getting at, but perhaps this is what you after? Essentially nested promises...
let connection = function(){
return new Promise(function(resolve, reject){
window.setTimeout(resolve("There"), 5000);
})
}
let connectionManager = function(){
return connection().then(function(value){
console.log("Hello");
return value;
});
}
connectionManager().then(function(value){
console.log(value);
});
Here it is on Babel REPL.
If you are trying to wrap existing asynchronous functionality with promises, this example here might be of help as well: http://www.2ality.com/2014/10/es6-promises-api.html#example%3A_promisifying_xmlhttprequest
Depending on what nesting requirements you need, you may want to resolve a promise with a promise object; feel free to comment if you need some clarification :)