在 Promise 返回函数中找到第一个成功

Finding first success amongst Promise returning functions

给定一些函数,返回承诺:

function foo(arg) {
  return new Promise(function(resolve, reject) {
    if (stuff(arg)) {
      resolve('result from foo');
    } else {
      resolve(null);
    }
  });
);

// ... maybe more of these functions ...

function bar(arg) {
  return new Promise(function(resolve, reject) {
    if (otherStuff(arg)) {
      resolve('result from bar');
    } else {
      resolve(null);
    }
  });
);

我们如何以串行方式遍历函数,在第一个函数返回非空值后短路?

[
  foo,
  // ...
  bar
].firstWithArg('some arg')
  .then(function(result) {
    // result: 'result from ___', or `null`
  });

本质上,所需的行为是:

new Promise(function(resolve, reject){
  foo('some-arg')
    .then(function(result) {
      if (result) {
        resolve(result);
      } else {

        // ...

          bar('some-arg')
            .then(function(result) {
              if (result) {
                resolve(result);
              } else {
                resolve(null); // no functions left
              }
            })
      }
    });
});

Promise.race() 不能使用,因为函数不能全部触发。它们必须连续执行,在第一次成功后停止。

我认为没有为此预先构建的东西。您无需太多工作即可创建自己的。假设你有一个函数数组,在调用时 return 承诺。然后,您可以遍历该数组并在获得喜欢的结果时停止。当序列中的 promise 被拒绝时,不清楚你想做什么 - 这个实现会继续到下一个函数,但你可以为这种情况编写你想要的任何行为:

function iterateUntilGood(list, args) {
    var cntr = 0;

    return new Promise(function(resolve, reject) {
        function next() {
            if (list.length > cntr) {
                list[cntr++].apply(null, args).then(function(result) {
                    // check the result here
                    if (some condition) {
                        resolve(result);
                    } else {
                        next();
                    }
                }, next);
            } else {
                reject("No function succeeded");
            }
        }
        next();
    });
}

// usage
iterateUntilGood([fn1, fn2, fn3, fn4], [arg1, arg2]).then(function(result) {
    // got result here
}, function(err) {
    // handle error here
});

工作演示:https://jsfiddle.net/jfriend00/fwr03f7q/

您说过您的第一个问题实际上只是为第二个问题做准备,这才是真正的问题。

所以我认为你的问题是:你如何执行一系列 return 连续承诺的函数,当第一个函数以非 null 值解析时短路?

我可能不会,我会使用 reject 而不是 resolve(null) (但在评论中你已经澄清你想要 resolve(null),我明白你的意思;我在下面介绍):

function foo(arg) {
  return new Promise(function(resolve, reject) {
    if (stuff(arg)) {
      resolve('result from foo');
    } else {
      reject();          // <=== Note
    }
  });
}

// ... maybe more of these functions ...

function bar(arg) {
  return new Promise(function(resolve, reject) {
    if (otherStuff(arg)) {
      resolve('result from bar');
    } else {
      reject();          // <=== Note
    }
  });
}

然后您使用 catch 处理拒绝,直到您得到解决方案:

foo("a")
  .catch(() => bar(1))
  .catch(() => foo("b"))
  .catch(() => bar(2))
  .catch(() => foo("c"))
  .catch(() => bar(3))
  .then(value => {
    console.log("Done", value);
  });

function otherStuff(arg) {
  return arg == 2;
}

function stuff(arg) {
  return arg == "c";
}

function foo(arg) {
  console.log("foo:", arg);
  return new Promise(function(resolve, reject) {
    if (stuff(arg)) {
      console.log("foo:", arg, "resolving");
      resolve('result from foo');
    } else {
      console.log("foo:", arg, "rejecting");
      reject(); // <=== Note
    }
  });
}

// ... maybe more of these functions ...

function bar(arg) {
  console.log("bar:", arg);
  return new Promise(function(resolve, reject) {
    if (otherStuff(arg)) {
      console.log("bar:", arg, "resolving");
      resolve('result from bar');
    } else {
      console.log("bar:", arg, "rejecting");
      reject(); // <=== Note
    }
  });
}

foo("a")
  .catch(() => bar(1))
  .catch(() => foo("b"))
  .catch(() => bar(2))
  .catch(() => foo("c"))
  .catch(() => bar(3))
  .then(value => {
    console.log("Done", value);
  });

之所以可行,是因为解析会绕过 catch 处理程序,因此永远不会调用后续函数。

如果你有一组函数要调用,有一个成语:Array#reduce:

let functions = [
  () => foo("a"),
  () => bar(1),
  () => foo("b"),
  () => bar(2),
  () => foo("c"),
  () => bar(3)
];

functions.reduce((p, fn) => p.catch(fn), Promise.reject())
  .then(value => {
    console.log("Done", value);
  });

function otherStuff(arg) {
  return arg == 2;
}

function stuff(arg) {
  return arg == "c";
}

function foo(arg) {
  console.log("foo:", arg);
  return new Promise(function(resolve, reject) {
    if (stuff(arg)) {
      console.log("foo:", arg, "resolving");
      resolve('result from foo');
    } else {
      console.log("foo:", arg, "rejecting");
      reject(); // <=== Note
    }
  });
}

// ... maybe more of these functions ...

function bar(arg) {
  console.log("bar:", arg);
  return new Promise(function(resolve, reject) {
    if (otherStuff(arg)) {
      console.log("bar:", arg, "resolving");
      resolve('result from bar');
    } else {
      console.log("bar:", arg, "rejecting");
      reject(); // <=== Note
    }
  });
}

let functions = [
  () => foo("a"),
  () => bar(1),
  () => foo("b"),
  () => bar(2),
  () => foo("c"),
  () => bar(3)
];

functions.reduce((p, fn) => p.catch(fn), Promise.reject())
  .then(value => {
    console.log("Done", value);
  });

您可能知道,Array#reduce 对于 "reducing" 数组到一个值很有用,例如简单的求和:

[1, 2, 3].reduce((sum, value) => sum + value, 0); // 6

在上面,对于 "sum" 等效项,我们从一个被拒绝的承诺开始,并使用 catch 创建承诺链。调用 reduce 的结果是 catch.

的最后承诺

但是,如果你想用resolve(null)代替,你用类似的方式使用then

foo("a")
  .then(result => result ? result : bar(1))
  .then(result => result ? result : foo("b"))
  .then(result => result ? result : bar(2))
  .then(result => result ? result : foo("d"))
  .then(result => result ? result : bar(3))
  .then(value => {
    console.log("Done", value);
  });

function otherStuff(arg) {
  return arg == 2;
}

function stuff(arg) {
  return arg == "c";
}

function foo(arg) {
  console.log("foo:", arg);
  return new Promise(function(resolve, reject) {
    if (stuff(arg)) {
      console.log("foo:", arg, "resolving");
      resolve('result from foo');
    } else {
      console.log("foo:", arg, "resolving null");
      resolve(null);
    }
  });
}

// ... maybe more of these functions ...

function bar(arg) {
  console.log("bar:", arg);
  return new Promise(function(resolve, reject) {
    if (otherStuff(arg)) {
      console.log("bar:", arg, "resolving");
      resolve('result from bar');
    } else {
      console.log("bar:", arg, "resolving null");
      resolve(null);
    }
  });
}

foo("a")
  .then(result => result ? result : bar(1))
  .then(result => result ? result : foo("b"))
  .then(result => result ? result : bar(2))
  .then(result => result ? result : foo("d"))
  .then(result => result ? result : bar(3))
  .then(value => {
    console.log("Done", value);
  });

或者用数组:

let functions = [
  () => foo("a"),
  () => bar(1),
  () => foo("b"),
  () => bar(2),
  () => foo("c"),
  () => bar(3)
];

functions.reduce((p, fn) => p.then(result => result ? result : fn()), Promise.resolve(null))
  .then(value => {
    console.log("Done", value);
  });

function otherStuff(arg) {
  return arg == 2;
}

function stuff(arg) {
  return arg == "c";
}

function foo(arg) {
  console.log("foo:", arg);
  return new Promise(function(resolve, reject) {
    if (stuff(arg)) {
      console.log("foo:", arg, "resolving");
      resolve('result from foo');
    } else {
      console.log("foo:", arg, "resolving null");
      resolve(null);
    }
  });
}

// ... maybe more of these functions ...

function bar(arg) {
  console.log("bar:", arg);
  return new Promise(function(resolve, reject) {
    if (otherStuff(arg)) {
      console.log("bar:", arg, "resolving");
      resolve('result from bar');
    } else {
      console.log("bar:", arg, "resolving null");
      resolve(null);
    }
  });
}

let functions = [
  () => foo("a"),
  () => bar(1),
  () => foo("b"),
  () => bar(2),
  () => foo("c"),
  () => bar(3)
];
    
functions.reduce((p, fn) => p.then(result => result ? result : fn()), Promise.resolve(null))
  .then(value => {
    console.log("Done", value);
  });

这是有效的,因为如果我们得到一个真值(或者你可以使用 result => result !== null ? result : nextCall()),我们 return 会沿着链条产生结果,这意味着 then return 是具有该值的已解决承诺;但是如果我们得到一个虚假的值,我们会调用下一个函数和 return 它的承诺。

如您所见,这有点冗长,这就是为什么 promise 在解决和拒绝之间有这种区别的部分原因。

感谢@T.J.Crowder 和@jfriend00 的回答。

长话短说:

const arg = 'some common arg';
const functions = [
  arg => new Promise((resolve, reject) => {
    /* Does some work, then calls:
     *   resolve(something) if success
     *   resolve(null)      if failure
     *   reject(error)      if error
     */
  })
]

functions.reduce(
  (prev, fn) => prev.then(res => res ? res : fn(arg)),
  Promise.resolve(null) // base case
) // returns promise which honours same contract as functions
  //   (resolves with something or null, or rejects with error)

Objective:遍历返回 Promises 的函数,直到我们成功 resolve 有一个值,然后我们短路。我们不想 Promise.race,而是 运行 串行函数。

有关完整的工作示例,请参阅此代码段:

/* Define functions which return Promises honouring the following contract:
 *   switch (state) {
 *     case success:
 *       resolve(result);
 *       break;
 *     case failure:
 *       resolve(null);
 *       break;
 *     case error:
 *       reject(error);
 *   }
 */
const functions = [
  arg => new Promise((resolve) => {
    console.log('checking a against', arg);
    if (arg === 'a') {
      resolve('A');
    } else {
      resolve();
    }
  }),
  arg => new Promise((resolve) => {
    console.log('checking b against', arg);
    if (arg === 'b') {
      resolve('B');
    } else {
      resolve();
    }
  }),
  // Intentionally omit handling 'c'
  arg => new Promise((resolve, reject) => {
    console.log('checking d against', arg);
    if (arg === 'd') {
      console.log('simulating error');
      reject(new Error('D'));
    } else {
      resolve();
    }
  }),
  arg => new Promise((resolve) => {
    console.log('checking e against', arg);
    if (arg === 'e') {
      resolve('E');
    } else {
      resolve();
    }
  })
];

/* Successively call functions with given arg until we resolve a value,
 * after which we short-circuit.
 */
function delegate(arg) {
  console.log('\nDELEGATING for', arg);

  functions.reduce(
      // Note that this null comparison always happens N times,
      // where N is the number of functions
      // (unless one of the functions rejects)
      (p, fn) => p.then(r => r ? r : fn(arg)),
      Promise.resolve(null)
    ).then(value => {
      console.log('Done:', value);
    })
    .catch(error => {
      console.log('Error:', error);
    });
}

// Run sample input through the delegate function
['a', 'b', 'c', 'd', 'e'].forEach(function(e, i) {
  setTimeout(delegate.bind(null, e), i * 100); // delay, for printing
});

我使用建议的 reject(null) 策略实现了相同的示例,而不是 resolve(null),以表示不应停止函数迭代的非错误失败:

/* Define functions which return Promises honouring the following contract:
 *   switch (state) {
 *     case success:
 *       resolve(result);
 *       break;
 *     case failure:
 *       reject(null); // << NOTE
 *       break;
 *     case error:
 *       reject(error);
 *   }
 */
const functions = [
  arg => new Promise((resolve, reject) => {
    console.log('checking a against', arg);
    if (arg === 'a') {
      resolve('A');
    } else {
      reject();
    }
  }),
  arg => new Promise((resolve, reject) => {
    console.log('checking b against', arg);
    if (arg === 'b') {
      resolve('B');
    } else {
      reject();
    }
  }),
  // Intentionally omit handling 'c'
  arg => new Promise((resolve, reject) => {
    console.log('checking d against', arg);
    if (arg === 'd') {
      console.log('simulating error');
      reject(new Error('D'));
    } else {
      reject();
    }
  }),
  arg => new Promise((resolve, reject) => {
    console.log('checking e against', arg);
    if (arg === 'e') {
      resolve('E');
    } else {
      reject();
    }
  })
];
/* Successively call functions with given arg until we resolve a value,
 * after which we short-circuit.
 */
function delegate(arg) {
  console.log('\nDELEGATING for', arg);

  functions.reduce(
      // Check for error, or just rejection without value.
      // Note that this check happens N-1 times,
      // where N is the number of functions until one resolves
      (p, fn) => p.catch(e => e ? Promise.reject(e) : fn(arg)),
      Promise.reject()
    ).then(value => {
      console.log('Done:', value);
    })
    .catch(error => {
      console.log('Error:', error);
    });
}

['a', 'b', 'c', 'd', 'e'].forEach(function(e, i) {
  setTimeout(delegate.bind(null, e), i * 100); // delay, for printing
});

请注意,如果您阅读了@T.J.Crowder 的优秀示例,我们必须在 catch 函数中添加错误检查:

(p, fn) => p.catch(e => e ? Promise.reject(e) : fn(arg))

如果我们刚刚

(p, fn) => p.catch(() => fn(arg))

(p, fn) => p.catch(fn.bind(null, arg))

我们会默默地吸收错误(reject(error)),然后继续,就好像我们只是遇到了一个非错误的失败。

考虑到这一点,我们最终通过使用 reject(null) 风格得到了更清晰、更高效(平均而言)的代码。