javascript - 保持承诺解决/拒绝功能参考等待用户输入
问题描述
我希望使用 promise 来处理模态窗口,这样当通过await
语法调用模态窗口时,调用函数的同步执行将暂停,直到用户响应模态窗口。下面的代码片段提取了问题的基本要素。虽然它起作用了,但我不确定这是否是一个承诺反模式,或者我是否引入了隐藏的复杂性,应该在onclick
处理程序中抛出错误。我能找到的最接近的问答(Resolve promise at later time)并不能完全回答我的问题,因为答案似乎不适用于保留等待用户事件发生的承诺......
我的精简Modal
课程和示例执行包括以下关键元素......
- 类
Modal
构造模态 DOM 元素,并将它们附加到 HTML 文档。 - 类
Modal
有一个名为的方法show
,它显示模式(在这个简化的示例中,三个按钮)并设置一个承诺。然后,promise的resolve
和reject
函数被保存为Modal
实例属性,特别是resolveFunction
和rejectFunction
。 - 只有当用户点击 Okay、Cancel 或 CancelThrow 时,promise 才会被解决或拒绝。
- function
openModal
是设置并显示 modal 的函数,然后暂停等待由 modalshow()
方法创建的 promise 的解析。
<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>
我处理resolve
andreject
函数的方式是否存在缺陷,如果是,是否有更好的设计模式来处理这个用例?
编辑
根据 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
解决方案
您看到意外(不直观)行为的原因在于代码的这一部分:
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 的执行顺序,但后来我意识到值得写一篇长文来解释清楚所以我只强调以下部分:
case_a
through主体中的每一行代码在case_c
调用时都会在同步作业中执行。- 这包括调用
.then
,.catch
和.finally
,这意味着将处理程序回调附加到 Promise 是同步操作。 - 还有
d.resolve
andd.reject
,这意味着承诺的结算可以同步发生,在这个例子中确实如此。
- 这包括调用
- JS 中的异步作业只能以回调函数的形式表达。在承诺的情况下:
- 所有处理程序回调都是异步作业。
- 作为同步作业的唯一与承诺相关的回调是executor 回调
new Promise(executorCallback)
。
- 最后很明显,异步作业总是等待同步作业完成。异步作业发生在同步作业之后的单独一轮执行中,它们不会交织在一起。
考虑到上述规则,让我们回顾一个新的例子。
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
是一个同步作业。
其次,即使我们已经d.proimise
在点被拒绝了4
,我们仍然能够c2
在点附加处理程序回调6
并成功捕获错误,这要归功于规则 2.1。
所有处理程序回调都是异步作业
因此,我们不会立即得到一个uncaught error
after 点4
,拒绝同步发生,但被拒绝的错误是异步抛出的。
而且因为同步代码优先于异步代码,我们有足够的时间附加c2
处理程序来捕获错误。
推荐阅读
- mysql - 如何复制最后插入的行并将其粘贴到 SQL 中的其他表中?
- c# - C# 连接到 Rest 服务以检索信息
- c# - 从外部库 .NET Framework WEB API 访问控制器的问题
- javascript - 如何为每个对象实例创建一个新变量
- javascript - CSS动画。过渡适用于所有其他时间,但不是第一次
- ios - 使用 AVPlayerLooper 时“因内存问题而终止”
- android - Android:我可以使用我的实体类使用 DISTINCT 查询房间数据库吗
- c++ - C++ 将同构包装类型的元组转换为原始类型的元组
- salesforce - 销售队伍创建潜在客户休息 API
- docker - 带有 docker 容器、反向代理中的 nginx 和 https 的 PhpStorm 调试器