首页 > 解决方案 > 保持承诺解决/拒绝功能参考等待用户输入

问题描述

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

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

<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>

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

编辑

根据 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>

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

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

标签: javascripteventspromisemodal-dialog

解决方案


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

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 = '';
    });
}

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

要求是,每个分支†</sup> 都应该附加一个错误处理程序,否则uncaught error将被抛出。

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

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

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

处理承诺错误时的经验法则?不要分支,链接它们,保持线性。

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

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_athrough主体中的每一行代码在case_c调用时都会在同步作业中执行。
    1. 这包括调用.then,.catch.finally,这意味着将处理程序回调附加到 Promise 是同步操作。
    2. 还有d.resolveand d.reject,这意味着承诺的结算可以同步发生,在这个例子中确实如此。
  2. JS 中的异步作业只能以回调函数的形式表达。在承诺的情况下:
    1. 所有处理程序回调都是异步作业。
    2. 作为同步作业的唯一与承诺相关的回调是executor 回调 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. 同步函数主体中的所有代码都是同步调用的,因此项目符号行的代码都按照它们的数字顺序执行。

我想强调一点46

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

// 6.
branch_2.catch(c2);

首先,点4可以看作是 的延续executorCallback。如果你想一想,它d.reject只是一个被提升到外面的变量executorCallback,现在我们只是从外面扣动扳机。记住规则 2.2,executorCallback是一个同步作业。

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

所有处理程序回调都是异步作业

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

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


推荐阅读