Javascript 当一个同步函数突然需要一个异步值时,是否要避免连锁重构?

Javascript 当一个同步函数突然需要一个异步值时,是否要避免连锁重构?,javascript,callback,Javascript,Callback,在使用javascript时,我经常遇到这样的情况:以前的同步代码突然需要一个值,而该值只能异步获得 例如,我正在编写一个TamperMonkey脚本,其中有一个函数可以对从location.hash解析的字符串进行操作。现在,我想通过使用GM_getTab(callback)接口更改代码,以便在选项卡中跨URL更改启用持久性 由于我需要以不变的顺序执行两个函数,因此会产生连锁反应,因为我需要等待值,然后突然将调用堆栈中的几个函数重构为async函数,直到到达一个点,不再需要保证顺序 然而,更重

在使用javascript时,我经常遇到这样的情况:以前的同步代码突然需要一个值,而该值只能异步获得

例如,我正在编写一个TamperMonkey脚本,其中有一个函数可以对从
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;