【问题标题】:Can you write this without using a Deferred?你能在不使用 Deferred 的情况下写这个吗?
【发布时间】:2020-08-05 01:54:53
【问题描述】:

我在下面编写了一些使用 Promise 的代码,我能找到的最简单的编写方法是使用 Deferred 对象而不是通常的 Promise 执行器函数,因为我需要从执行器外部解析 Promise。我想知道是否有一个基于 Promise 执行器函数的公认设计模式来解决这样的问题,它不使用类似延迟的解决方案?是否可以在无需从 Promise Executor 外部解决 Promise 的情况下完成?

这里是详细信息。

我有一个项目使用一组工作线程和想要不时使用工作线程的代码的各个部分。为了管理它,我创建了一个简单的WorkerList 类,它保留了可用工作线程的列表。当有人想要使用一个时,他们会调用get() 并返回一个解析为工作线程的承诺。如果工作线程立即可用,则承诺立即解决。如果所有工作线程都在使用中(因此可用工作线程列表为空),则承诺不会解析,直到稍后通过add(worker) 方法将一个返回可用列表。

这个 WorkerList 类只有两个方法,add(worker)get()。你get() 一个工人,当你完成它时,你add(worker) 它回来了。当您add(worker) 回来时,该类会检查是否有任何任务在等待可用的 Worker。如果有,它会通过可用的 Worker 解决他们的承诺。解决其他人的承诺就是使用 Deferred 的地方。

这是WorkerList的代码:

class WorkerList {
    constructor() {
        this.workers = [];
        this.deferredQueue = [];
    }
    add(worker) {
        this.workers.push(worker);

        // if someone is waiting for a worker,
        // pull the oldest worker out of the list and
        // give it to the oldest deferred that is waiting
        while (this.deferredQueue.length && this.workers.length) {
            let d = this.deferredQueue.shift();
            d.resolve(this.workers.shift());
        }
    }
    // if there's a worker, get one immediately
    // if not, return a promise that resolves with a worker
    //    when next one is available
    get() {
        if (this.workers.length) {
            return Promise.resolve(this.workers.shift());
        } else {
            let d = new Deferred();
            this.deferredQueue.push(d);
            return d.promise;
        }
    }
}

还有,这里是延迟的实现:

function Deferred() {
    if (!(this instanceof Deferred)) {
        return new Deferred();
    }
    const p = this.promise = new Promise((resolve, reject) => {
        this.resolve = resolve;
        this.reject = reject;
    });
    this.then = p.then.bind(p);
    this.catch = p.catch.bind(p);
    if (p.finally) {
        this.finally = p.finally.bind(p);
    }
}

【问题讨论】:

  • 对不起,如果这看起来很愚蠢 - 我正在等待咖啡开始。我读对了吗 - Deferred 基本上是一个无参数的 Promise 用于解决/拒绝它在执行人之外?所以,问题是“有没有一种标准的方法可以从执行者外部解决 Promise”?
  • @VLAZ - 我更感兴趣的是是否有不同的方法来实现这个接口,而不必从执行程序外部解决承诺。我看到人们抛弃了延迟的概念,我想知道是否有一个很好的替代方案来替代使用类似延迟的解决方案。
  • 啊,我明白了。我不确定有没有。除非大脑现在让我失望,否则如果它必须在 executor 内部,这似乎是鸡和蛋的问题,因为你需要知道何时解决,但你不知道何时创建 Promise。也许可以使用不同的构造。类似于 Future/Task 的东西,它代表一个异步 操作 而不是一个值。但我不太确定最好的方法是什么。
  • @VLAZ - 我想象从EventEmitter 派生并让.add() 发出承诺执行者正在监听的东西,但我不确定如何保持整个系统的先进先出,因为每次等待promise 会监听同一个事件,它很快看起来比这里的要复杂。
  • @grodzi - 如果没有人在这两种方法之外弄乱实例变量,则不需要 while 循环。但是,我确实想象了一个可以预填充实例数据的构造函数。所以,我认为while 循环实际上只是一个简单的if 的代码,所以我这样说。

标签: javascript node.js promise deferred


【解决方案1】:

这是一种解决方案,它不会在 Promise executor 函数之外的任何地方公开 Promise 解析器函数。

在我对我自己关于基于事件的解决方案的问题的评论之后,这就是我想出的。它使用触发事件和事件侦听器在 promise 执行器函数中触发操作。

class WorkerList extends EventEmitter {
    constructor() {
        this.workers = [];
    }
    add(worker) {
        this.workers.push(worker);
        // notify listeners that there's a new worker in town
        this.emit('workerAdded');
    }
    // if there's a worker, get one immediately
    // if not, return a promise that resolves with a worker
    //    when next one is available
    get() {
        if (this.workers.length) {
            return Promise.resolve(this.workers.shift());
        } else {
            return new Promise(resolve => {
                const onAdded = () => {
                    if (this.workers.length) {
                        this.off('workerAdded', onAdded);
                        resolve(this.workers.shift());
                    }
                }

                this.on('workerAdded', onAdded);
            });
        }
    }
}

我最初担心维护 FIFO 排序,以便第一个调用 get() 的人获得下一个可用的工作人员。但是,因为 eventListeners 是按照添加的顺序调用的,所以我认为这实际上会实现 FIFO 顺序。如果有多个对get() 的调用,他们都会收到关于workerAdded 的通知,但是在第一个处理消息并带走工人之后,其他人会发现没有工人留给他们,因此他们的听众将保持连接状态等待未来的workerAdded 消息,当他们有工作人员时(当他们的听众排在第一位时)。

我认为我不一定比显示的其他选项更喜欢这个,但它是一种替代方法,不使用 Deferreds,甚至不将 resolve 处理程序暴露在 executor 函数之外。


正如建议的那样,这也可以在 eventEmitter 是实例变量而不是基类的情况下完成:

class WorkerList {
    constructor() {
        this.workers = [];
        this.emitter = new EventEmitter();
    }
    add(worker) {
        this.workers.push(worker);
        // notify listeners that there's a new worker in town
        this.emitter.emit('workerAdded');
    }
    // if there's a worker, get one immediately
    // if not, return a promise that resolves with a worker
    //    when next one is available
    get() {
        if (this.workers.length) {
            return Promise.resolve(this.workers.shift());
        } else {
            return new Promise(resolve => {
                const onAdded = () => {
                    if (this.workers.length) {
                        this.emitter.off('workerAdded', onAdded);
                        resolve(this.workers.shift());
                    }
                }

                this.emitter.on('workerAdded', onAdded);
            });
        }
    }
}

【讨论】:

  • 这很酷,但我怀疑WorkerList 本身成为一个 EventEmitter 有多大意义,因为所有事件都在其内部工作中发生。看来你可以通过包含一个 EventEmitter 而不是继承一个 EventEmitter 来达到同样的效果。
  • @JLRishe - 我同意,无论哪种方式都可以。如果现在或将来没有外部用途,那么它可能只是一个实例变量(尽管这确实需要更冗长的代码才能使其以这种方式工作)。
  • @JLRishe - 我添加了您的建议作为选项。
【解决方案2】:

也许下面只是一个穷人处理延迟的方法,并没有真正解决问题的关键,但是你可以保留一个解析器函数队列,而不是延迟队列。

与您的方法相比,这会节省少量代码,并避免显式使用 Deferreds。

我不知道是否有一个既定的模式,但这本身似乎是一种用于维护异步对象池的可重用模式,因此与其将其命名为 WorkerList,不如将​​其命名为 AsyncPool ,然后在您的WorkerList 中将其组合为可重复使用的部分:

class AsyncPool {
    constructor() {
        this.entries = [];
        this.resolverQueue = [];
    }
    add(entry) {
        console.log(`adding ${entry}`);
        this.entries.push(entry);

        // if someone is waiting for an entry,
        // pull the oldest one out of the list and
        // give it to the oldest resolver that is waiting
        while (this.resolverQueue.length && this.entries .length) {
            let r = this.resolverQueue.shift();
            r(this.entries.shift());
        }
    }
    // if there's an entry, get one immediately
    // if not, return a promise that resolves with an entry
    //    when next one is available
    get() {
        return new Promise((r) => 
            this.entries.length
                ? r(this.entries.shift())
                : this.resolverQueue.push(r)
        );
    }
}


let pool = new AsyncPool();

pool.add('Doc');
pool.add('Grumpy');
pool.get().then(console.log);
pool.get().then(console.log);
pool.get().then(console.log);
pool.get().then(console.log);

// add more entries later
setTimeout(() => pool.add('Sneezy'), 1000);
setTimeout(() => pool.add('Sleepy'), 2000);

【讨论】:

  • 这很聪明。如果您还想拒绝承诺(想象一下外部超时或工作人员的问题),它就不是那么灵活了,但它确实满足了我所描述的需求。我不得不说这个this.waiterQueue.shift()(this.entries.shift()); 让人挠了一阵子,试图弄清楚这是在做什么。我现在明白了,但不是很明显。
  • @jfriend00 是的,我想我在那里做得过火了。我已经把它改回了你之前的两个单独的行。
  • 这样会好很多。 this.waiterQueue 也可以是 this.resolverQueue
  • @jfriend00 有道理。我也会改变它。但是,是的,我同意,如果你想要一种方法来违背承诺,那么 Deferreds 似乎是你最好的选择,因为它们将解决和拒绝功能打包在一起。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-03-24
  • 2020-07-08
  • 2012-09-19
  • 1970-01-01
  • 1970-01-01
  • 2014-08-13
  • 2013-08-08
相关资源
最近更新 更多