Javascript 当一个同步函数突然需要一个异步值时,是否要避免连锁重构?
在使用javascript时,我经常遇到这样的情况:以前的同步代码突然需要一个值,而该值只能异步获得 例如,我正在编写一个TamperMonkey脚本,其中有一个函数可以对从Javascript 当一个同步函数突然需要一个异步值时,是否要避免连锁重构?,javascript,callback,Javascript,Callback,在使用javascript时,我经常遇到这样的情况:以前的同步代码突然需要一个值,而该值只能异步获得 例如,我正在编写一个TamperMonkey脚本,其中有一个函数可以对从location.hash解析的字符串进行操作。现在,我想通过使用GM_getTab(callback)接口更改代码,以便在选项卡中跨URL更改启用持久性 由于我需要以不变的顺序执行两个函数,因此会产生连锁反应,因为我需要等待值,然后突然将调用堆栈中的几个函数重构为async函数,直到到达一个点,不再需要保证顺序 然而,更重
location.hash
解析的字符串进行操作。现在,我想通过使用GM_getTab(callback)
接口更改代码,以便在选项卡中跨URL更改启用持久性
由于我需要以不变的顺序执行两个函数,因此会产生连锁反应,因为我需要等待值,然后突然将调用堆栈中的几个函数重构为async
函数,直到到达一个点,不再需要保证顺序
然而,更重要的是,当等待被遗忘时,需要显式地wait
ed的承诺可能会导致意外行为:例如if(condition())
可能突然总是计算为true
,而'Hello'+getUserName()
可能突然导致Hello[对象承诺]
有没有办法避免这种“重构地狱”
简化示例
随后,我给出了一个非常简单的例子:在调用堆栈中需要一个wait
,同时需要保留执行顺序,导致重构一直到事件回调updateFromHash
// -- Before
function updateFromHash(){
updateTextFromHash();
updateTitleFromText();
}
function updateTextFromHash(){
DISPLAY_AREA.innerText = getTextFromHash();
}
// -- After
async function updateFromHash(){
await updateTextFromHash();
updateTitleFromText();
}
async function updateTextFromHash(){
DISPLAY_AREA.innerText = getTextFromHash()
|| await new Promise((accept,reject)=>GM_getTab(accept));
}
在本例中,它相对简单,但我以前看到异步性在调用堆栈中的位置越来越高,并在错过wait
时导致意外行为。我见过的最糟糕的情况是,我突然需要“DEBUG”标志来依赖异步存储的用户设置
时间限制
正如@DavidSampson所指出的,在这个例子中,如果函数不依赖于可变的全局状态,可能会更好
然而,在实践中,代码是在时间限制下编写的,通常是由其他人编写的,然后您需要一个“小更改”——但是如果这个小更改涉及到以前同步函数中的异步数据,则最好将重构工作减到最少。解决方案需要现在就开始工作,清理设计问题可能要等到下一次项目会议
在给出的示例中,重构是可行的,因为它是一个小型的私有脚本。它最终只是用来说明问题,但在商业项目场景下,在给定的项目范围内清理代码可能不可行。这里有一些通用做法,可以最大限度地减少问题的影响 有一件事是,首先尝试编写不依赖于以特定顺序发生的代码——正如您在这里看到的,这可能会导致很多麻烦
async function updateFromHash(){
await updateTextFromHash();
updateTitleFromText();
}
在这里,您可以在某个地方更新全局变量中的一些文本(稍后将对此进行详细介绍),然后调用一个函数来查看该变量,并在此基础上更新其他变量。显然,如果其中一个尚未完成,另一个将无法正常工作
但是,如果取而代之的是,您在一个地方检索异步数据,然后将update
调用和它们所需的数据作为函数参数进行调度,会怎么样?此外,还可以使用。然后
链接来处理异步数据,而无需使函数异步
async getHash(){
return new Promise((accept,reject)=>GM_getTab(accept))
}
function setText(text){
DISPLAY_AREA.innerText = text;
}
function setTitle(text){
// make some modifications to the 'text' variable
TITLE.innerText = myModifiedText // or whatever
}
function updateFromHash(){
getHash()
.then(text => {
setText(text);
setTitle(text);
// You could also call setTitle first, since they aren't dependent on each other
});
}
您可以做的另一个改进是,从广义上讲,在可能的情况下保持函数的纯粹性通常是一个好主意——它们应该修改的唯一内容是您作为参数传入的内容,您期望它们产生的唯一效果是它们返回的内容。由于各种原因,杂质必须存在,但请尽量将它们放在代码的边线上
所以在你的例子中,你有一个函数修改一个全局变量,然后另一个函数可能会去查看这个变量,并根据这个信息修改另一个变量。这会隐式地将这两个变量绑定在一起,除非不经意的观察者不清楚为什么会出现这种情况,因此这会使跟踪bug变得更加困难。一种处理情况的方法是:
function createTitleFromText(text){
// modify the text passed in to get the title you want
return myModifiedText;
// this function is pure
}
function updateContent(text){
// This is now the ONLY function that modifies state
// It also has nothing to do with *how* the text is retrieved
TITLE_EL.innerText = createTitleFromText(text);
DISPLAY_AREA.innerText = text;
}
async function getTextFromHash(){
return new Promise((accept,reject)=>GM_getTab(accept))
}
// Then, somewhere else in your code
updateContent(await getTextFromHash());
这也可能是一个好主意,洒在一些面向对象的这一点,使它更清楚什么拥有什么,并处理一些订单为您
class Content {
constructor(textEl, titleEl){
this.textEl = textEl;
this.titleEl = titleEl;
}
static createTitleFromText(text){
//make modifications
return myTitle;
}
update(text){
this.textEl.innerText = text;
this.titleEl.innerText = Content.createTitleFromText(text);
}
}
let myContent = new Content(DISPLAY_AREA, TITLE_EL);
// Later
myContent.update(await getTextFromHash());
没有一个正确或错误的方法,但这些是一些你可以玩的想法
至于阻止异步性冒泡,使用。那么链接可能是最好的选择。以你最初的例子:
// -- After
function updateFromHash(){
updateTextFromHash()
.then(updateTitleFromText);
}
async function updateTextFromHash(){
DISPLAY_AREA.innerText = getTextFromHash()
|| await new Promise((accept,reject)=>GM_getTab(accept));
}
现在,updateFromHash
可以保持同步,但请记住,updateFromHash
完成并不意味着UpdateTextFromText
完成,甚至也不意味着updateTextFromHash
完成。因此,您需要尽可能地让异步性冒泡起来,以处理任何有序的效果
不幸的是,由于JS引擎的单线程特性,无法同步地等待
,如果同步函数正在等待某个任务完成,那么其他任何任务都无法运行
不过,在特定情况下,您可能能够同步复制该功能,但这也将涉及大量重构
例如,您可以定义一个属性DISPLAY\u AREA.isValid=true
,然后在updateTextFromHash
DISPLAY_AREA.isValid = false;
DISPLAY_AREA.innerText = getTextFromHash()
|| await new Promise((accept,reject)=>GM_getTab(accept));
DISPLAY_AREA.isValid = true;
然后,在任何需要显示的代码区域.innerText
,首先检查该数据是否有效,如果无效,然后在一段时间内设置超时,然后再次检查
或者,您也可以定义一个显示区。queuedActions=[]
,然后您的其他函数可以检查DIS的有效性
DISPLAY_AREA.isValid = false;
DISPLAY_AREA.innerText = getTextFromHash()
|| await new Promise((accept,reject)=>GM_getTab(accept));
for(var cb of DISPLAY_AREA.queuedActions){
cb()
}
DISPLAY_AREA.isValid = true;