Hold promise resolve / reject 函数参考等待用户输入

Hold promise resolve / reject function reference waiting on user input

我希望使用承诺来处理模态 window,这样当通过 await 语法调用模态 window 时,调用函数的同步执行是暂停,直到用户响应模态。下面的代码片段提取了问题的基本要素。虽然它起作用了,但我不确定这是否是一个 promise 反模式,或者如果 onclick 处理程序中出现错误,我是否会引入隐藏的复杂性。我能找到的最接近的问答 ( Resolve promise at a later time ) 并不能完全回答我的问题,因为答案似乎不适用于保留等待用户事件发生的承诺...

我的精简 Modal class 和示例执行包括以下关键元素...

<html><head>

<style>

#ModalArea {
  display: none;
}

#ModalArea.show {
  display: block;
}

</style>

<script>

class Modal {
  constructor() {

    this.parentNode = document.getElementById( 'ModalArea' );

    let okay = document.createElement( 'BUTTON' );
    okay.innerText = 'Okay';
    okay.onclick = ( event ) => {
      this.resolveFunction( 'Okay button clicked!' )
    };
    this.parentNode.appendChild( okay );
  
    let cancel = document.createElement( 'BUTTON' );
    cancel.innerText = 'Cancel';
    cancel.onclick = ( event ) => {
      this.rejectFunction( 'Cancel button clicked!' )
    };
    this.parentNode.appendChild( cancel );
    
    let cancelThrow = document.createElement( 'BUTTON' );
    cancelThrow.innerText = 'Cancel w/Throw';
    cancelThrow.onclick = ( event ) => {
      try {
        throw 'Thrown error!';
      } catch( err ) {
        this.rejectFunction( err );
      }
      this.rejectFunction( 'CancelThrow button clicked!' );
    };
    this.parentNode.appendChild( cancelThrow );
    
  }
  
  async show() {
    this.parentNode.classList.add( 'show' );
    
    // Critical code:
    //
    // Is this appropriate to stash away the resolve and reject functions
    // as attributes to a class object, to be used later?!
    //
    return new Promise( (resolve, reject) => {
      this.resolveFunction = resolve;
      this.rejectFunction = reject;
    });
  }

}

async function openModal() {

  // Create new modal buttons...
  let modal = new Modal();
  
  // Show the buttons, but wait for the promise to resolve...
  try {
    document.getElementById( 'Result' ).innerText += await modal.show();
  } catch( err ) {
    document.getElementById( 'Result' ).innerText += err;
  }
  
  // Now that the promise resolved, append more text to the result.
  document.getElementById( 'Result' ).innerText += ' Done!';
  
}

</script>

</head><body>


<button onclick='openModal()'>Open Modal</button>
<div id='ModalArea'></div>
<div id='Result'>Result: </div>
</body></html>

我处理 resolvereject 函数的方式是否存在缺陷?如果存在,是否有更好的设计模式来处理此用例?

编辑

根据 Roamer-1888 的指导,我得出了以下更清晰的延迟承诺实现...(请注意 Cancel w/Throw 的测试结果在控制台中显示 Uncaught (in Promise)错误,但处理按定义继续...)

<html><head>

<style>

#ModalArea {
  display: none;
}

#ModalArea.show {
  display: block;
}

</style>

<script>

class Modal {
  constructor() {

    this.parentNode = document.getElementById( 'ModalArea' );

    this.okay = document.createElement( 'BUTTON' );
    this.okay.innerText = 'Okay';
    this.parentNode.appendChild( this.okay );
  
    this.cancel = document.createElement( 'BUTTON' );
    this.cancel.innerText = 'Cancel';
    this.parentNode.appendChild( this.cancel );
    
    this.cancelThrow = document.createElement( 'BUTTON' );
    this.cancelThrow.innerText = 'Cancel w/Throw';
    this.parentNode.appendChild( this.cancelThrow );
    
  }
  
  async show() {
    this.parentNode.classList.add( 'show' );
    
    let modalPromise = new Promise( (resolve, reject) => {
      this.okay.onclick = (event) => {
        resolve( 'Okay' );
      };
      this.cancel.onclick = ( event ) => {
        reject( 'Cancel' );
      };
      this.cancelThrow.onclick = ( event ) => {
        try {
          throw new Error( 'Test of throwing an error!' );
        } catch ( err ) {
          console.log( 'Event caught error' );
          reject( err );
        }
      };
    });
    
    modalPromise.catch( e => {
      console.log( 'Promise catch fired!' );
    } );
    
    // Clear out the 'modal' buttons after the promise completes.
    modalPromise.finally( () => {
      this.parentNode.innerHTML = '';
    });

    return modalPromise;
  }

}

async function openModal() {

  // Create new modal buttons...
  let modal = new Modal();
  document.getElementById( 'Result' ).innerHTML =  'Result: ';
  
  // Show the buttons, but wait for the promise to resolve...
  try {
    document.getElementById( 'Result' ).innerText += await modal.show();
  } catch( err ) {
    document.getElementById( 'Result' ).innerText += err;
  }
  
  // Now that the promise resolved, append more text to the result.
  document.getElementById( 'Result' ).innerText += ' Done!';  
}

</script>

</head><body>


<button onclick='openModal()'>Open Modal</button>
<div id='ModalArea'></div>
<div id='Result'></div>
</body></html>

不过似乎还是有些不对劲。在选择Cancel w/Throw时,添加了一个承诺catch,错误会通过modalPromise.catch传播,但是控制台仍然记录以下错误:

Uncaught (in promise) Error: Test of throwing an error! at HTMLButtonElement.cancelThrow.onclick

FWIW ...在研究了 Promises ( https://javascript.info/async-await#error-handling and https://javascript.info/promise-error-handling ) 的错误处理和大量实验之后,我得出结论(可能是错误的) deferred Promise reject 将导致 Uncaught (in promise) 错误。

  • 也就是说,javascript 错误,尽管在延迟的 Promise 的执行中被捕获和处理,以 Promise reject 结束,将显示为 Uncaught (in promise) 错误控制台(即使错误被包装在延迟的 Promise 和创建 Promise 的调用函数中的 try..catch 中!)。
  • 但是,如果 javascript 错误在延迟的 Promise 的执行中被捕获和处理,并以 Promise resolve 结束,那么 no Uncaught (in promise) 错误。

以下代码遍历了 resolving/rejecting 延迟 Promise 的变体。 (需要打开控制台才能查看处理顺序和 Uncaught (in promise) 错误。)

  • 没有错误(在延迟的 Promise 中)以 Promise 结束 resolve
  • 没有错误以 Promise reject 结束。 (触发未捕获的错误)
  • 捕获错误,以 Promise 结束 resolve
  • 一个捕获的错误,以 Promise reject 结束。 (触发未捕获的错误)

请注意,即使最初的 openModal() 调用也使用了 Promise catch 函数,除了用 try..catch 包装之外,但这仍然不会捕获 reject.

<html><head>

<style>
#ModalArea { display: none; }
#ModalArea.show { display: block; }
</style>

<script>

class Modal {
  constructor() {
    this.parentNode = document.getElementById( 'ModalArea' );

    this.okay = document.createElement( 'BUTTON' );
    this.okay.innerText = 'Okay - Promise RESOLVE';
    this.parentNode.appendChild( this.okay );
  
    this.cancel = document.createElement( 'BUTTON' );
    this.cancel.innerText = 'Cancel - Promise REJECT';
    this.parentNode.appendChild( this.cancel );
    
    this.cancelThrow1 = document.createElement( 'BUTTON' );
    this.cancelThrow1.innerText = 'Cancel w/Throw - Promise RESOLVE';
    this.parentNode.appendChild( this.cancelThrow1 );
    
    this.cancelThrow2 = document.createElement( 'BUTTON' );
    this.cancelThrow2.innerText = 'Cancel w/Throw - Promise REJECT';
    this.parentNode.appendChild( this.cancelThrow2 );
  }
  
  async show() {
    this.parentNode.classList.add( 'show' );
    
    let modalPromise = new Promise( (resolve, reject) => {
      this.okay.onclick = (event) => {
        resolve( 'Okay via Promise RESOLVE' );
      };
      this.cancel.onclick = ( event ) => {
        reject( 'Cancel via Promise REJECT' );
      };
      this.cancelThrow1.onclick = ( event ) => {
        try {
          throw new Error( 'Throw /catch error concluding with Promise RESOLVE' );
        } catch ( err ) {
          console.log( 'Cancel w/Throw via Promise RESOLVE' );
          resolve( err );
        }
      };
      this.cancelThrow2.onclick = ( event ) => {
        try {
          throw new Error( 'Throw /catch error concluding with Promise resolve' );
        } catch ( err ) {
          console.log( 'Cancel w/Throw via Promise REJECT' );
          reject( err );
        }
      };
    });
    
    modalPromise.catch( e => {
      console.log( 'Promise CATCH fired!' );
    } );
    
    // Clear out the 'modal' buttons after the promise completes.
    modalPromise.finally( () => {
      console.log( 'Promise FINALLY fired!' );
      this.parentNode.innerHTML = '';
    });

    return modalPromise;
  }

}

async function openModal() {

  // Create new modal buttons...
  let modal = new Modal();
  document.getElementById( 'Result' ).innerHTML =  'Result: ';
  
  // Show the buttons, but wait for the promise to resolve...
  try {
    document.getElementById( 'Result' ).innerText += await modal.show();
  } catch( err ) {
    document.getElementById( 'Result' ).innerText += err;
  }
  
  // Now that the promise resolved, append more text to the result.
  document.getElementById( 'Result' ).innerText += ' - Done!';  
}

</script>

</head><body>


<button onclick="
try {
  openModal()
  .then( x => console.log( 'openModal().THEN fired!' ) )
  .catch( x => console.log( 'openModal().CATCH fired!' ) );
} catch( err ) {
  console.log( [ 'openModal() TRY/CATCH fired!', err ] );
}
">Open Modal</button>
<div id='ModalArea'></div>
<div id='Result'></div>
</body></html>

所以,我得出结论(再次,可能是错误的)延迟 Promise 只能以 resolve 结束,如果希望避免 Uncaught (in promise)错误。另一个明显的选择是合并一个 unhandledrejection 事件处理程序,但如果可以避免 Promise reject...

,这似乎只会增加不必要的复杂性

编辑:@hackape 的回答指出了我的缺陷

总之,我不是把then()finally()当成分支的原诺,他们自己return一个诺!考虑到这一点,可以采用@hackape 的示例并对其进行调整,以尽可能以松鼠的方式进一步保留和传递这些分支承诺,同时避免 Uncaught (in promise) 错误。

// Modified version of @hackape's example
function defer() {
  let resolve
  let reject
  const promise = new Promise((rsv, rjt) => {
    resolve = rsv
    reject = rjt
  })

  return {
    promise,
    resolve,
    reject,
  }
}

// 1 uncaught
function case_a( promiseB ) {
  const d = defer()
  d.promise.catch(e => console.log('[catch(a1)]', e))
  d.promise.finally(e => console.log('[finally(a1)]', e)) // uncaught
  d.reject('apple')
  
  // Let's get squirrely.  Create and return the
  // promiseB finally() promise, which is then passed
  // to case_c, which will handle the associated catch!
  let pbFinally = promiseB.promise.finally(e => console.log('[finally(b1)]', e))
  return pbFinally
}

// all caught!
function case_b() {
  const d = defer()
  d.promise.catch(e => console.log('[catch(b1)]', e))
  d.reject('banana')
  return d
}

// 2 uncaught
function case_c( pb ) {
  const d = defer()
  d.promise.catch(e => console.log('[catch(c1)]', e))
  d.promise.finally(e => console.log('[finally(c1)]', e)).catch(e => console.log('[catch(c2)]', e))
  d.promise.finally(e => console.log('[finally(c2)]', e)) // uncaught
  d.promise.finally(e => console.log('[finally(c3)]', e)) // uncaught
  
  // Catch associated with case_b finally(), which prevents
  // the case_b finally() promise from throwing an
  // 'Uncaught (in promise)' error!
  pb.catch(e => console.log('[catch(b2)]', e))
  
  d.reject('cherry')
}

function test() {
  let promiseB = case_b()
  let promiseBFinally = case_a( promiseB )
  case_c( promiseBFinally )
}

test()

您看到意外(不直观)行为的原因在于您的这部分代码:

async show() {
    // ...

    modalPromise.catch(e => {
      console.log( 'Promise CATCH fired!' );
    });

    modalPromise.finally(() => {
      console.log( 'Promise FINALLY fired!' );
      this.parentNode.innerHTML = '';
    });

    return modalPromise;
}

如果你把上面的改成下面的,错误处理行为就正常了:

async show() {
    // ...

    return modalPromise.catch(e => {
      console.log( 'Promise CATCH fired!' );
    }).finally(() => {
      console.log( 'Promise FINALLY fired!' );
      this.parentNode.innerHTML = '';
    });
}

每个承诺 API 调用,.then.catch.finally,都会产生一个新的递减承诺。所以它实际上形成了一个树结构,每次 API 调用它都会产生一个新的分支。

要求是,每个分支 应该附加一个错误处理程序,否则会抛出uncaught error

(:由于链式承诺中错误的传播性质,您不必将错误处理程序附加到分支上的每个节点,只需将其应用到下游某处相对于错误来源。)

回到你的案例。你写它的方式,实际上分支成两个后代,而 child_two 分支没有错误处理程序,因此抛出未捕获的错误。

const ancestor = new Promise(...)
const child_one = ancestor.catch(fn)
const child_two = ancestor.finally(fn)
return ancestor

经验法则 处理 promise 中的错误时?不要分支,将它们链接起来,保持线性。

诚然,这是一个令人困惑的行为。我把下面的片段放在一起来展示案例。您需要打开浏览器控制台才能看到未捕获的错误。

function defer() {
  let resolve
  let reject
  const promise = new Promise((rsv, rjt) => {
    resolve = rsv
    reject = rjt
  })

  return {
    promise,
    resolve,
    reject,
  }
}

// 1 uncaught
function case_a() {
  const d = defer()
  d.promise.catch(e => console.log('[catch(a1)]', e))
  d.promise.finally(e => console.log('[finally(a1)]', e)) // uncaught
  d.reject('apple')
}

// all caught!
function case_b() {
  const d = defer()
  d.promise.catch(e => console.log('[catch(b1)]', e))
  d.promise.finally(e => console.log('[finally(b1)]', e)).catch(e => console.log('[catch(b2)]', e))
  d.reject('banana')
}

// 2 uncaught
function case_c() {
  const d = defer()
  d.promise.catch(e => console.log('[catch(c1)]', e))
  d.promise.finally(e => console.log('[finally(c1)]', e)).catch(e => console.log('[catch(c2)]', e))
  d.promise.finally(e => console.log('[finally(c2)]', e)) // uncaught
  d.promise.finally(e => console.log('[finally(c3)]', e)) // uncaught
  d.reject('cherry')
}

function test() {
  case_a()
  case_b()
  case_c()
}

test()


响应 OP 编辑​​的后续更新。

我想简单地解释一下promise的执行顺序,但后来我意识到值得写一篇长篇文章来解释清楚所以我只强调以下部分:

  1. case_acase_c 主体中的每一行代码,在调用时都在同步作业中执行。
    1. 这包括调用 .then.catch.finally,这意味着 attachment 承诺的处理程序回调是同步操作。
    2. 还有 d.resolved.reject,这意味着承诺的 结算 可以同步发生,在这个例子中确实如此。
  2. JS中的异步作业只能以回调函数的形式表现。在承诺的情况下:
    1. 所有 处理程序回调 都是异步作业。
    2. 作为同步作业的唯一与承诺相关的回调是 执行器回调 new Promise(executorCallback).
  3. 最后,显而易见的是,异步作业总是等待同步作业完成。异步作业发生在同步作业之后的单独执行轮中,它们不会交织。

记住上面的规则,让我们回顾一个新的例子。

function defer() {
  const d = {};
  const executorCallback = (resolve, reject) => {
    Object.assign(d, { resolve, reject });
  };
  d.promise = new Promise(executorCallback);
  return d;
}

function new_case() {
  // 1. inside `defer()` the `executorCallback` is called sync'ly
  const d = defer();

  // 2. `f1` handler callback is attached to `branch_1` sync'ly
  const f1 = () => console.log("finally branch_1");
  const branch_1 = d.promise.finally(f1);

  // 3. `c1` handler callback is attached to `branch_1` sync'ly
  const c1 = (e) => console.log("catch branch_1", e);
  branch_1.catch(c1);

  // 4. ancestor promise `d.promise` is settled to `rejected` state,
  d.reject("I_dont_wanna_get_caught");

  // CODE BELOW IS EQUIVALENT TO YOUR PASSING-AROUND PROMISE_B
  // what really matters is just execution order, not location of code

  // 5. `f2` handler callback is attached to `branch_2` sync'ly
  const f2 = () => console.log("finally branch_2");
  const branch_2 = d.promise.finally(f2);

  // 6. `c2` handler callback is attached to `branch_2` sync'ly
  const c2 = (e) => console.log("catch branch_2", e);
  branch_2.catch(c2);
}

new_case()

规则 1. sync 函数体中的所有代码都是同步调用的,所以标有符号的代码行都按数字顺序执行。

我想强调点 46

// 4.
d.reject("I_dont_wanna_get_caught");

// 6.
branch_2.catch(c2);

首先,点 4 可以看作是 executorCallback 的延续。仔细想想,d.reject只是在executorCallback外面吊起的一个变量,现在我们只是从外面扣动扳机。记住规则 2.2,executorCallback 是一个同步作业。

其次,即使我们已经在点 4 处拒绝了 d.proimise,我们仍然能够在点 6 处附加 c2 处理程序回调并成功由于规则 2.1 捕获错误。

all handler callbacks are async jobs

因此我们不会在点 4 之后立即得到 uncaught error,拒绝是同步发生的,但是被拒绝的错误是异步抛出的。

并且由于同步代码优先于异步代码,我们有足够的时间附加 c2 处理程序来捕获错误。