【问题标题】:ES6 Promise patterns for exotic control flows外来控制流的 ES6 Promise 模式
【发布时间】:2015-05-04 18:12:06
【问题描述】:

ES6 Promise 很棒。到目前为止,调整我的想法很容易 回调成语。我发现它自然会鼓励更多的模块化代码,并且 当然错误处理更加清晰。

但有几次我遇到的心流情况看起来(?)不像他们 可以很容易地从 nodebacks 转换为 Promise(也许就是这样,但也许我只是对答案视而不见)。因为承诺是不可知的 关于下一个操作(或者即使有一个),它似乎很难使用 Promise 的 API 不仅接受回调,而且 return 它们。

想到的最常见的例子是“完成”回调。它出现在诸如数据库连接之类的东西中,表示“将连接返回到池”,但我也看到它出现在很多其他地方。

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();
    });
});

这样的流程反转显然不是您希望在您的 API 中拥有的东西 首先,但它就在那里,有时你无法真正避免它。和 回调,您可以将“稍后调用”内部回调传递给原始“外部” 打回来。它并不完全导致关注点的完全分离,但在 至少它又快又简单。

是否有适合这种情况的基于 Promise 的方法?一种说法, “这是解析值——但是当链完成时,也要这样做”?一世 怀疑没有什么与我刚才描述的完全匹配,因为它 真的不可能说一个链条已经“完成”,但也许我错过了一些 模式可以让你接近它而不会弄得一团糟......


编辑:根据目前的反馈,我意识到根本没有办法将这样的 API 包装在真正的 Promise 中,因为您返回的 Promise 将永远无法告诉您任何有关任何后续链式 Promise 的信息它。但是你可以伪造它。扭曲的是结果相当脆弱。它必须假定唯一需要连接对象的then 是紧随其后的那个。承诺的消费者需要了解它是一次性使用的连接,否则这并不明显。因此,我在实践中并不真正推荐它,但为了好奇,这里有一个解决方案,它隐藏done,同时表现为(并最终成为)承诺链:

/* 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));

控制台打印:

  • 已收到连接。
  • 使用连接完成!
  • 已调用完成。
  • 这应该是在连接关闭之后。

【问题讨论】:

  • 不适用于 ES6-Promises,但更强大的库通常会提供 .finally()-methods,这正是您所需要的。也可以看看Bluebird manages resources.
  • 喜欢你的方法名:)

标签: callback promise ecmascript-6 es6-promise


【解决方案1】:

详细说明 Bergi 的解决方案,这称为处置器模式。它以多种语言以多种形式存在 - Python 中的 with、C# 中的 using 和 Java 中的带有资源的 try(){。一些语言通过 C# 之类的析构函数以这种方式原生地处理作用域中的资源。

一般的想法是使用一个范围来封装一个值的生命周期。在您的情况下是数据库连接。这比必须在回调中调用done 更简洁,因为忘记调用done 会容易得多,这会留下打开的连接和资源泄漏。同步它看起来像:

function scope(cb){
     try{ 
         var conn = getConnection(...);
         return cb(conn);
     } finally {
         conn.release();
     }
}

promises 版本差别不大:

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
    });
});

哪个会使用:

conn(function(db){
    return db.query("SELECT * FROM ...");
}).then(function(result){ // handle result
    // connection is released here
});

【讨论】:

  • 谢谢,这样呈现非常优雅。
【解决方案2】:

一种说法,“这是解析值 - 但是当链完成时,也这样做”?

不,原生 Promise 不提供这样的功能。我会使用一个资源函数,它接受一个返回承诺的回调,回调会在连接打开时完成所有需要完成的事情(在链中)。资源管理器函数不是将iNeedToBeTold 传递给回调,而是观察承诺并在它解决时执行需要做的事情。

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(…)

【讨论】:

  • 有趣,贝尔吉。这可能是不使用专门库的最简洁的方法——承诺除了一个回调来管理奇怪的情况之外的所有内容。
  • Bergi,您可能对我对问题所做的编辑感兴趣。您的回答是正确的,但我确实成功地设计了一种(可能有用的)方法来提供一个承诺 API,该 API 可以在后台处理完成的回调。
  • 嗯,我觉得这很可疑。当然,您总是可以将 manageConnection 隐藏在一个函数中,该函数使用 then 方法返回一个对象,该方法完成所有工作,但是当有人查看链。这是一个巧妙的小技巧,但我永远不会使用它。顺便说一句,handler 失败时不会调用您的_done
  • 是的,这就是为什么我将其归为好奇/不推荐的原因。我修复了你提到的 _done 位,但它当然仍然取决于太多的“正确”。
  • 也称为“disposer模式”,类似于c#中的“using”,Python中的“with”或Java中的try with resource。
【解决方案3】:

done() 函数的问题是人们忘记调用它,导致泄漏。

我喜欢 Bergi 对传入回调的回答,因为它很干净,但它不是很“promise-y”,而且仍然是开放式不安全的,例如如果人们将永远不会解决回调承诺的承诺链接起来,那么它就会停止并泄漏。

这也是浏览器 API 中讨论的一个问题,我们正在考虑的一种模式是返回一个

AutoClosingPromise

AutoClosingPromise 的作用类似于 Promise,但有两件不同的事情:

  1. 它“关闭一张票”(调用完成)执行其 .then() 之后。

  2. 1234563自己的 .then() 函数)。

第一部分意味着 API 可以返回带有“票证”的 AutoClosingPromise,该票证保持资源打开(如打开计数),并确保一旦第一个 .then() 函数返回,票证将被关闭。

第二部分让调用者在即时 .then() 函数中对 API 进行额外的异步调用,让 API 保持资源打开,只要票证在时间上重叠。

这样做的一个特点是资源不会在常规承诺中提供,只有 AutoClosing 承诺,避免了泄漏的风险。例如:

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);

将资源(锁)扩展到前三个,但不是第四个:

setting foo1 [LOCKED]
setting foo2 [LOCKED]
setting foo3 [LOCKED]
setting foo4 [UNLOCKED]

代码如下:

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;
  }
};

和小提琴:http://jsfiddle.net/jib1/w0ufvahL(需要理解 es6 箭头功能的浏览器,例如 Firefox,例如不是 Chrome)。

由于 API 控制所有发出票证的异步调用,这应该是相当防泄漏的。例如。即使调用者完全忽略了从 API 返回的承诺,仍然会调用 close。

请注意,这是一个相当新的想法,而不是经过验证的构造,因此如果您最终使用它,请告诉我它是如何工作的。 ;-)

【讨论】:

  • 哦,我不认为我的方法不是 promise-y - 它是一个非常声明性的包装器,就像 try-finally - 但我可能有偏见 :-) 我不认为有什么好处应对永远未决的承诺但明确超时的方法 - 您的 AutoClosingPromise 似乎也无法处理它们。我不喜欢该模式的其他一些事情:它违反了多次或延迟then 调用的承诺合同,而且我不确定它对于具有不同票证的多个获取的资源的组合情况如何。
  • AutoClosingPromise 通过不在普通 Promise 中保持资源打开来防止永远挂起的 Promise 保持资源打开状态。资源仅在 同步 .then() 回调以及在该回调内发起的任何新的异步调用 API 上保持打开状态。因此,API 永远不会等待任何超出其控制范围的异步操作,并且客户端不会导致泄漏。
  • 关于promise-contract,这不是一般的promise,而是更严格的约定。扩展类以支持多个 .then() 调用似乎是可行的,但我会将其留给需要该功能的人。至于延迟的.then(),它并没有违反它们,而是故意不为它们打开资源,因为那样会泄漏。您不希望资源永远保持打开状态,以防有人稍后添加 .then() 。 ;-)
  • 哦,我开始明白了 :-) 我认为我可以通过从then 回调返回一个new AutoClosingPromise(null, Promise.never) 来玩它,但这只会得到我自己的票 从未关闭。我仍然不喜欢当票证想要支持保持资源打开时需要自己实现计数器,但我想这很容易解决。一个非常有趣的想法,在所有回调执行后立即调用close()。我想我会在我自己的 Promise 库中实现这个模式,+1!
  • 另一个问题是它们的作曲不是那么容易。例如,Promise.all([openFile("toread"), openFile("towrite")]).then([read, write] => read.pipe(write); )。如何实现all 以使其同时保留两个资源,而不仅仅是稍后解析的资源?
【解决方案4】:

我不能 100% 确定您的目标是什么,但也许这就是您所追求的?本质上是嵌套的 Promise...

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.

如果您尝试使用 Promise 包装现有的异步功能,这里的示例也可能会有所帮助:http://www.2ality.com/2014/10/es6-promises-api.html#example%3A_promisifying_xmlhttprequest

根据你需要的嵌套需求,你可能想用一个promise对象来解析一个promise;如果您需要澄清,请随时发表评论:)

【讨论】:

  • 谢谢,但这不是我所指的。这是嵌套的,因为由于 connectionManager 的内容,最后一行没有显示隐含在链中的“then”。但据我所知,除了由拒绝引起的短路外,这个链条仍然是一个完全线性的流程。诚然,这可能是没有明智的方法与承诺。
  • @Semicolon 嗯,是的,我仍然不确定您在使用哪种工作流程:S 我相信您已经探索了大多数选项,但如果它是多个选项你正在等待的承诺需要在链的不同点被调用,你可能想看看使用Promise.all() 并将未解决的承诺对象传递到链中。或者,如果您有想要承诺的特定工作流程,那也会有所帮助:)
  • 等等,Andrew,我认为你在 Promise.all 中找到了一些非常聪明的东西。我将对此进行测试——我觉得可以通过使用 Promise.all 仅使用原生 Promise 方法来实现此行为。
  • 那是一厢情愿的红鲱鱼 - 我没有考虑“链”只是一个抽象概念的事实,如果层隐藏“完成”的非承诺行为-needing API 返回一个真正的承诺,它根本无法知道附加到该承诺的 future 承诺,句号。
猜你喜欢
  • 2016-03-20
  • 1970-01-01
  • 2017-04-30
  • 2016-05-31
  • 2016-12-03
  • 1970-01-01
  • 2016-12-03
  • 1970-01-01
  • 2012-09-03
相关资源
最近更新 更多