【问题标题】:JS - chain async methods in sequence w/o callback or modificationJS - 按顺序链接异步方法,无需回调或修改
【发布时间】:2016-12-26 01:08:16
【问题描述】:

我正在尝试向原型添加一个“默认回调”,如果没有提供回调函数(以 promise 的形式)分配给异步方法。

目标是让类的异步方法链同步运行

Item.async1().async2()....asyncN()

请注意,异步函数本身需要回调,但它们不会作为函数调用中的参数传递(这告诉我,当回调查找失败时,类需要默认行为)

规范声明我不能直接修改原型方法的行为或副作用。我可以添加原型方法。我们无法了解这些原型方法是如何实现的。

TLDR:在不修改原型方法的情况下,如何链接 N 个异步方法并确保它们按顺序运行?

顺便说一句:如果我想实现承诺的版本,承诺有问题的原型方法会很有帮助,但看起来我们受限于原始函数调用

【问题讨论】:

  • 仅供参考,通常的 promisify 操作会创建一个返回 promise 的新函数/方法——它们不会修改现有的。这似乎是允许的。但是,promise 不会按照您所说的链接方式进行链接,因此虽然 promisification 可能对实现内部有用,但无论如何它不会直接给您Item.async1().async2()....asyncN()

标签: javascript node.js asynchronous promise


【解决方案1】:

如果.async1().async2() 已经提供并且需要回调并且不允许修改它们,则无法实现Item.async1().async2()....asyncN()。您正在调用的方法并非以这种方式工作,如果您不允许更改它们,除了用可以按您想要的方式工作的方法替换这些方法之外,您无能为力。

如果您可以创建具有自己名称的新方法并在内部使用原始方法,那么就可以做到。如何做到这一点的一种模型是 jQuery 动画。它们允许您执行以下操作:

$("#progress").slideDown(300).delay(1000).slideUp(300);

这些异步操作中的每一个都将链接在一起。 jQuery 通过执行以下操作来实现:

  1. 每个方法都返回原始对象,因此对象上的任何方法的链接都将起作用。
  2. 如果异步操作已在运行,则每个被调用的新异步方法都会与该方法的参数一起进入队列(在对象上)。
  3. 每个方法的底层实现都可以使用带有传统回调(或承诺)的异步操作。操作完成后,它会检查队列以查看是否有更多操作要运行,如果有,则从队列中开始下一个操作并将其从队列中删除。
  4. 如果在另一个异步操作运行时调用了新的异步操作,它们会再次被添加到队列的末尾。每个对象(或您想要序列化的任何一组项目)都有自己的队列。

因此,如果预期回调的原始异步方法是 .async1().async2(),您可以创建 .async1Chain().async2Chain(),这样您就可以像这样工作:

Item.async1Chain().async2Chain()....asyncNChain()

在内部,.async1Chain() 将使用本地回调调用 .async1(),并且该回调将被配置为检查队列以运行下一个排队操作(如果有的话)。

这只是解决问题的一种方法。可能还有其他人。

【讨论】:

  • “每个方法都返回原始对象,因此链接将起作用。”这不一定是真的,老实说,这不是一个好主意。
  • 知道了。不允许修改原始原型方法似乎是一个不必要的困难约束。
  • 另外,这完全没有必要——你不需要实现你自己的队列,你已经有了 Promise——你只需要使用对象原型的 Promisified 版本子类化 Promise,然后让对象返回那些承诺并完成它。 15 人能做的事,不用 100 行写出来。
  • @BenjaminGruenbaum - 在特定的 jQuery 示例中,我正在描述它确实返回原始对象,并且它非常适合它正在做的事情,所以我不确定你的反对意见是什么。如果您想仅使用 Promise 提出答案,那么请务必在您自己的答案中向我们展示如何做到这一点。在我一直在工作的世界中,将其他代码创建承诺的子类化承诺,你不一定控制代码已经一团糟。如果您知道这不是问题的世界,请告诉我们如何做。
  • 在 jQuery 动画中,重点在于它指向同一个 DOM 节点——这显然不是 OP 所做的。
【解决方案2】:

好吧,我不打算回答 - 但我 was challenged

很容易利用 Promise 的内置功能来免费获得这种排队。以下是这种转换的工作方式:

  • 我们将回调 API 转换为带有 Promise 子类的新对象上的 Promise。
  • 我们将所有承诺的方法添加到子类本身 - 所以它链接。
    • 我们告诉子类执行then 中的所有方法,因此它们将排队,因为then 是promise 的排队机制。

注意:我在这里写的 promisifypromisifyAll 方法 - 你应该抓住 NPM - 许多好的和快速的用法都需要一个 Promise 构造函数。

首先,我们需要一个方法,converts a callback API to promises:

// F is a promise subclass
function promisify(fn) { // take a function
    return function(...args) {  // return a new one with promises
      return new F((resolve, reject) => { // that returns a promise
         // that calls the original function and resolves the promise
         fn.call(this, ...args, (err, data) => err ? reject(err) : resolve(data));
      });
    };
  } 

现在,让我们承诺整个对象:

  function promisifyAll(obj) {
    const o = {};
    for(const prop in obj) {
      if(!(obj[prop].call)) continue; // only functions
      o[prop] = promisify(obj[prop]).bind(obj);
    }
    return o;
  }

到目前为止,还没有什么新鲜事,很多 NPM 库都这样做了——现在为了神奇的 Promise——让我们创建一个在 then 中的原始对象上执行函数的方法:

function whenReadyAll(obj) {
    const obj2 = {}; // create a new object
    for(const prop in obj) { // for each original object
       obj2[prop] = function(...args) { 
         // return a function that does the same thing in a `then`
         return this.then(() => obj[prop](...args));
       };
    }
    return obj2;
  }

现在,让我们总结一下

function liquidate(obj) {
  const promised = promisifyAll(obj); // convert the object to a promise API
  class F extends Promise {} // create a promise subclass
  Object.assign(F.prototype, whenReadyAll(promised)); // add the API to it
  return promised; // return it
  // previous code here
}

就是这样,如果我们希望示例是自包含的(同样,promis 和 promisifyAll 通常由库提供):

function liquidate(obj) {
  const promised = promisifyAll(obj);
  class F extends Promise {}
  Object.assign(F.prototype, whenReadyAll(promised)); // add the API  
  return promised;
  function whenReadyAll(obj) {
    const obj2 = {};
    for(const prop in obj) {
       obj2[prop] = function(...args) { 
         return this.then(() => obj[prop](...args));
       };
    }
    return obj2;
  }
  function promisifyAll(obj) {
    const o = {};
    for(const prop in obj) {
      if(!(obj[prop].call)) continue; // only functions
      o[prop] = promisify(obj[prop]).bind(obj);
    }
    return o;
  }
  function promisify(fn) {
    return function(...args) { 
      return new F((resolve, reject) => {
         fn.call(this, ...args, (err, data) => err ? reject(err) : resolve(data));
      });
    };
  } 
}

或者使用一个可以承诺的库:

function liquidate(obj) { // 14 LoC
  class F extends Promise {} 
  const promised = promisifyAll(obj, F); // F is the promise impl
  Object.assign(F.prototype, whenReadyAll(promised)); // add the API  
  return promised;
  function whenReadyAll(obj) {
    const obj2 = {};
    for(const prop in obj) {
       obj2[prop] = function(...args) { 
         return this.then(() => obj[prop](...args));
       };
    }
    return obj2;
  }
}

没有演示的答案是什么:

var o = {  // object with a delay callback method
  delay(cb) { 
    console.log("delay"); 
    setTimeout(() => cb(null), 1000); 
  }
};
var o2 = liquidate(o); // let's liquidate it
// and we even get `then` for free, so we can verify this works
var p = o2.delay().then(x => console.log("First Delay!")).
                   delay().
                   then(x => console.log("Second Delay!"));

// logs delay, then First Delay! after a second, 
// then delay and then Second Delay! after a second

将此复制粘贴到您友好的邻居控制台,然后自己查看:)


为了证明这保留了原始对象的状态(如果需要,可以很容易地修改它),让我们添加一个 i 变量并延迟递增它,看看事情是否有效:

var o = {  // object with a delay callback method
  delay(cb) { 
    console.log("delay", this.i++); 
    setTimeout(() => cb(null), 1000); 
  },
  i: 0
};
var o2 = liquidate(o); // let's liquidate it
// and we even get `then` for free, so we can verify this works
var p = o2.delay().then(x => console.log("First Delay!")).
                   delay().
                   then(x => console.log("Second Delay!", o.i));
//logs:
// delay 0
// First Delay!
// delay 1
// Second Delay! 2

【讨论】:

  • 为什么要在liquidate() 中复制两个对象?而且,您不是在假设甚至可以通过这种方式成功复制对象吗?而且,由于它是可能被任何调用的方法改变的副本,你不是假设没有其他人对原始对象有有意义的引用并且它再也不会被使用了吗?这里的想法很酷,但我认为你没有正确地陈述你对实现的所有假设或限制。
  • 例如,如果其他人有对传递给liquidate(obj) 的对象的引用,并希望此场景中涉及的所有未来方法调用都对原始对象进行操作,这样做会这样做吗?在我看来并非如此。您已经制作了一个副本,现在只在该副本上进行操作。
  • 实现的目的是可读的 - 不是高性能的 - 并且外部 promisify 可能是一个更好的主意 - 这就是为什么两个对象克隆 - 都引用原始对象。我制作了一个副本,然后通过闭包将其所有方法绑定到原始对象 - 我本可以做得更好 - 但它会使实现复杂化。
  • 我将添加一个带状态的示例
  • 仅供参考,Bluebird Promise 可以这样子类化吗?在我的脑海中,我认为出于性能考虑,这样做存在一些限制。还是我记错了?
【解决方案3】:

我建议您为此使用一个库,我自己制作了一个库,它不仅允许连续链接,还允许您使用循环和 ifElse 结构。

https://github.com/Raising/PromiseChain

(请注意,在此示例中我们没有使用父对象,这会大量清理代码) 内部作用域保存每个继续提供的名称的结果

var internalScope = {}; //i'll use scope

new PromiseChain(internalScope )  
    .continue(function(internalScope){ return async1();},"firstResult")  
    .continue(function(internalScope){ return async2();},"secondResult")  
    .continue(function(internalScope){ return async3();},"thridResult")  
.end();

或者,如果所有函数都属于同一个对象并且只需要范围作为参数,您可以这样做

new PromiseChain(internalScope,yourObject)  // this is important if you use the 'this' keyword inside the functions, it works as a .bind(yourObject) for every function
    .continue(yourObject.async1,"firstResult")  
    .continue(yourObject.async2,"secondResult")  
    .continue(yourObject.async3,"thridResult")  
.end();

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-03-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-18
    • 2016-12-26
    • 2023-01-25
    • 2013-01-15
    相关资源
    最近更新 更多