保持承诺解析/拒绝函数引用等待用户输入



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

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

  • Modal构造模式 DOM 元素,并将它们追加到 HTML 文档中。
  • Modal有一个名为show的方法,它显示模态(在此简化示例中为三个按钮)并设置一个承诺。 然后,承诺的resolvereject函数作为Modal实例属性保存,特别是resolveFunctionrejectFunction
  • 仅当用户点击"确定"、"取消"或"取消抛出"时,承诺才会得到解决或拒绝。
  • 函数openModal是设置并显示模态,然后暂停等待由模态show()方法创建的承诺的解决的函数。

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

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

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

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

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

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

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

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的编辑。

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

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

我想强调第4点和6

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

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

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

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

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

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

以下代码演练了解析/拒绝延期承诺的变体。(控制台需要打开才能查看处理顺序和Uncaught (in promise)错误。

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

请注意,即使是原始的 openModal() 调用也使用了 Promisecatch函数,除了用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>

因此,我得出的结论是(再次,可能是错误的),如果希望避免Uncaught (in promise)错误,则只能以resolve结束延期承诺。 另一个明显的选择是合并一个unhandledrejection事件处理程序,但如果可以避免 Promisereject,这似乎会增加不必要的复杂性......

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

简而言之,我并没有把then()finally()当作原始承诺的分支,他们自己回报了一个承诺! 考虑到这一点,人们可以以@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()

最新更新