承诺回调返回承诺

Posted

技术标签:

【中文标题】承诺回调返回承诺【英文标题】:Promise callbacks returning promises 【发布时间】:2016-06-15 08:23:09 【问题描述】:

关于这两个伟大的来源:NZakas - Returning Promises in Promise Chains和MDN Promises,我想问一下:

每次我们从承诺履行处理程序返回一个值时,该值如何传递给从同一处理程序返回的新承诺?

例如,

let p1 = new Promise(function(resolve, reject) 
    resolve(42);
);

let p2 = new Promise(function(resolve, reject) 
    resolve(43);
);

let p3 = p1.then(function(value) 
    // first fulfillment handler
    console.log(value);     // 42
    return p2;
);

p3.then(function(value) 
    // second fulfillment handler
    console.log(value);     // 43
);

在这个例子中,p2 是一个承诺。 p3 也是源自 p1 的履行处理程序的承诺。但是p2 !== p3。相反,p2 以某种方式神奇地解析为43(如何?),然后将该值传递给p3 的履行处理程序。甚至这里的句子都令人困惑。

你能解释一下这里到底发生了什么吗?我完全对这个概念感到困惑。

【问题讨论】:

p2 somehow magically resolves to 43 (how?)“魔法”是你告诉它解析为 43 ...resolve(43); 是的,这正是magic of promises :-) 你明白当你返回一个普通值时它是如何工作的吗,比如p3 = p2.then(function(value) return value+1; ) @JaromandaX 魔法不在于 what p2 解析为 (43),而是它完全被解析,而不仅仅是传递给下一个函数仍然是一个承诺。 已解决的承诺与其已解决的值之间存在差异。 OP 期望 p3.then 收到 p2,这是一个解析为 43 的承诺。相反,它得到了字面值 43,根本没有任何承诺包装。 【参考方案1】:

假设在then()回调中抛出一个失败的结果承诺,并从then()回调返回一个成功的结果承诺。

let p2 = p1.then(() => 
  throw new Error('lol')
)
// p2 was rejected with Error('lol')

let p3 = p1.then(() => 
  return 42
)
// p3 was fulfilled with 42

但有时,即使在延续中,我们也不知道我们是否成功。我们需要更多时间。

return checkCache().then(cachedValue => 
  if (cachedValue) 
    return cachedValue
  

  // I want to do some async work here
)

但是,如果我在那里进行异步工作,那么 returnthrow 就太晚了,不是吗?

return checkCache().then(cachedValue => 
  if (cachedValue) 
    return cachedValue
  

  fetchData().then(fetchedValue => 
    // Doesn’t make sense: it’s too late to return from outer function by now.
    // What do we do?

    // return fetchedValue
  )
)

这就是为什么如果你不能解决另一个 Promise,Promise 就没有用处。

这并不意味着在您的示例中 p2变成 p3。它们是独立的 Promise 对象。但是,通过从产生p3then() 返回p2,您是在说“我希望p3 解析为p2 解析的任何内容,无论它成功还是失败”。 p>

至于如何发生这种情况,这是特定于实现的。在内部,您可以将then() 视为创建一个新的 Promise。实现将能够随时满足或拒绝它。通常情况下,它会在您返回时自动履行或拒绝:

// Warning: this is just an illustration
// and not a real implementation code.
// For example, it completely ignores
// the second then() argument for clarity,
// and completely ignores the Promises/A+
// requirement that continuations are
// run asynchronously.

then(callback) 
  // Save these so we can manipulate
  // the returned Promise when we are ready
  let resolve, reject

  // Imagine this._onFulfilled is an internal
  // queue of code to run after current Promise resolves.
  this._onFulfilled.push(() => 
    let result, error, succeeded
    try 
      // Call your callback!
      result = callback(this._result)
      succeeded = true
     catch (err) 
      error = err
      succeeded = false
    

    if (succeeded) 
      // If your callback returned a value,
      // fulfill the returned Promise to it
      resolve(result)
     else 
      // If your callback threw an error,
      // reject the returned Promise with it
      reject(error)
    
  )

  // then() returns a Promise
  return new Promise((_resolve, _reject) => 
    resolve = _resolve
    reject = _reject
  )

同样,这是非常伪代码,但展示了如何在 Promise 实现中实现 then() 背后的想法。

如果我们想要添加对 Promise 解析的支持,我们只需要修改代码以在你传递给 then()callback 返回一个 Promise 的情况下拥有一个特殊的分支:

    if (succeeded) 
      // If your callback returned a value,
      // resolve the returned Promise to it...
      if (typeof result.then === 'function') 
        // ...unless it is a Promise itself,
        // in which case we just pass our internal
        // resolve and reject to then() of that Promise
        result.then(resolve, reject)
       else 
        resolve(result)
      
     else 
      // If your callback threw an error,
      // reject the returned Promise with it
      reject(error)
    
  )

让我再次澄清一下,这不是一个实际的 Promise 实现,并且存在很大的漏洞和不兼容性。但是,它应该让您直观地了解 Promise 库如何实现对 Promise 的解析。在你对这个想法感到满意之后,我建议你看看实际的 Promise 实现如何handle this。

【讨论】:

多么棒的解释啊!感谢您分享您的见解! 非常好的答案 - 我认为它通过不实现缓存行为(这会很小)留下了一个小差距,并且还有很多其他不准确之处,但以制作的名义绝对没问题概念平易近人。 @BenjaminGruenbaum 如果您可以改进它而又不会使其过于复杂,请随意! 值得注意的是,传递给then的每个函数都应该具有a -> Promise b的类型,即返回另一个Promise。但是,决定 then 在传递普通函数 a -> b 时隐式进行包装。【参考方案2】:

基本上p3return - 另一个承诺:p2。这意味着p2 的结果将作为参数传递给下一个then 回调,在这种情况下它解析为43

每当您使用关键字return 时,您都会将结果作为参数传递给下一个then 的回调。

let p3 = p1.then(function(value) 
    // first fulfillment handler
    console.log(value);     // 42
    return p2;
);

你的代码:

p3.then(function(value) 
    // second fulfillment handler
    console.log(value);     // 43
);

等于:

p1.then(function(resultOfP1) 
    // resultOfP1 === 42
    return p2; // // Returning a promise ( that might resolve to 43 or fail )
)
.then(function(resultOfP2) 
    console.log(resultOfP2) // '43'
);

顺便说一句,我注意到您使用的是 ES6 语法,您可以通过使用粗箭头语法来获得更轻松的语法:

p1.then(resultOfP1 => p2) // the `return` is implied since it's a one-liner
.then(resultOfP2 => console.log(resultOfP2)); 

【讨论】:

非常感谢,我会一直处理这些问题的!【参考方案3】:

在这个例子中,p2 是一个承诺。 p3 也是源自 p1 的履行处理程序的承诺。但是 p2 !== p3。相反,p2 以某种方式神奇地解析为 43(如何?),然后将该值传递给 p3 的履行处理程序。甚至这里的句子都令人困惑。

这是如何工作的简化版本(仅伪代码)

function resolve(value)
    if(isPromise(value))
        value.then(resolve, reject);
    else
        //dispatch the value to the listener
    

整个事情要复杂得多,因为你必须小心,承诺是否已经解决,还有一些事情。

【讨论】:

【参考方案4】:

我将尝试回答问题 “为什么 then 回调可以返回 Promises 自己” 更规范。换个角度,我将Promises 与不那么复杂和容易混淆的容器类型——Arrays 进行比较。

Promise 是一个未来值的容器。 Array 是任意数量值的容器。

我们不能将普通函数应用于容器类型:

const sqr = x => x * x;
const xs = [1,2,3];
const p = Promise.resolve(3);

sqr(xs); // fails
sqr(p); // fails

我们需要一种机制将它们提升到特定容器的上下文中:

xs.map(sqr); // [1,4,9]
p.then(sqr); // Promise [[PromiseValue]]: 9

但是当提供的函数本身返回一个相同类型的容器时会发生什么?

const sqra = x => [x * x];
const sqrp = x => Promise.resolve(x * x);
const xs = [1,2,3];
const p = Promise.resolve(3);

xs.map(sqra); // [[1],[4],[9]]
p.then(sqrp); // Promise [[PromiseValue]]: 9

sqra 的行为符合预期。它只是返回一个具有正确值的嵌套容器。不过这显然不是很有用。

但是如何解释sqrp 的结果呢?如果我们按照自己的逻辑,它必须是 Promise [[PromiseValue]]: Promise [[PromiseValue]]: 9 之类的东西 - 但事实并非如此。那么这里发生了什么魔法呢?

要重建机制,我们只需要稍微调整一下map 方法即可:

const flatten = f => x => f(x)[0];
const sqra = x => [x * x];
const sqrp = x => Promise.resolve(x * x);
const xs = [1,2,3];

xs.map(flatten(sqra))

flatten 只接受一个函数和一个值,将函数应用于该值并解包结果,从而将嵌套数组结构减少了一层。

简单地说,Promises 上下文中的then 相当于mapArrays 上下文中的flatten 组合。这种行为非常重要。 我们不仅可以将普通函数应用于Promise,还可以将函数本身返回Promise

事实上,这是函数式编程的领域。 Promisemonad 的特定实现,thenbind/chain,返回 Promise 的函数是一元函数。当您了解 Promise API 时,您基本上了解所有 monad。

【讨论】:

以上是关于承诺回调返回承诺的主要内容,如果未能解决你的问题,请参考以下文章

在承诺回调中发送数组响应,但响应为空白

如何创建从回调函数返回承诺的函数[重复]

Mongoose:我如何避免回调地狱,同时允许对不返回承诺的 mongoose 方法进行存根?

如何编写测试用例来覆盖承诺链中所有嵌套的“then”回调

Node.js 从函数返回一个承诺

承诺回调参数来自哪里