JavaScript:为什么不能从外部变量更新循环变量

JavaScript: Why can't you update looping variable from external variable

提问人:Michael Johns 提问时间:8/12/2023 最后编辑:DanzigerMichael Johns 更新时间:8/14/2023 访问量:81

问:

我在玩,但我不明白为什么这行不通?从理论上讲,我以为会,但它只是进入了一个无休止的循环。不要运行它,它很可能会锁定您的计算机。

glb = "original";

async function wait() {
    await new Promise(resolve => setTimeout(resolve, 2000));
    glb = "updated";
}

function f() {
    wait();
    var glb2 = "original";
    while (glb2 == "original") {
        glb2 = glb;
        console.log("####  glb:" + glb);
    }
    console.log("#### DONE  glb:" + glb);
}

f();

为什么 glb2 从不更新?

谢谢!

JavaScript 循环 while 循环 计时 事件循环

评论

1赞 Kevin B 8/12/2023
因为只有循环执行代码,直到它所在的函数返回......在循环停止之前不会发生这种情况。
2赞 Tim Roberts 8/12/2023
右。 函数不是多线程的。由于是异步的,因此当应用空闲时,你的调用只会放在要执行的操作列表中。您的循环永远不会闲置。asyncwaitwait
0赞 phatfingers 8/14/2023
@TimRoberts关闭。这不是因为是异步的,而是因为其中包含一个,并且赋值遵循 .waitwaitawaitawait

答:

-3赞 Dev Ewerton 8/12/2023 #1

好吧,朋友,发生这种情况是因为“await()”函数返回一个承诺,它没有时间将变量更新为“updated”。为此,您需要使用保留字“await”使函数“f()”也异步等待函数“await()”的执行,如下所示:

async function f() {
    await wait();
    // rest of the function...
}

这样你就可以避免无限循环。

祝你好运。

评论

0赞 derpirscher 8/12/2023
但这违背了在一定时间内运行循环的最初目的。因为循环会在第一次迭代后中断......
0赞 Dev Ewerton 8/13/2023
在这种情况下,你需要重新制定你的朋友逻辑。
2赞 Danziger 8/13/2023 #2

JavaScript 的运行时是单线程的,并且基于运行到完成的事件循环,这意味着事件循环队列中的每个项目都需要在处理下一个项目之前完成(循环作为一个整体,而不是单个迭代,是该队列中的单个项目)。

来自 MDN

在处理任何其他消息之前,将完全处理每条消息。

在对程序进行推理时,这提供了一些不错的属性,包括每当函数运行时,它都不能被抢占,并且将在任何其他代码运行之前完全运行(并且可以修改函数操作的数据)。例如,这与 C 不同,如果一个函数在一个线程中运行,运行时系统可以在任何时候停止它以在另一个线程中运行一些其他代码。

此模型的缺点是,如果消息需要很长时间才能完成,则 Web 应用程序无法处理用户交互(如单击或滚动)。浏览器通过“脚本运行时间过长”对话框来缓解这种情况。一个好的做法是缩短消息处理时间,如果可能的话,将一条消息缩减为几条消息。

这意味着在代码中,循环会阻塞事件循环,从而阻止执行排队的任何其他操作(例如函数的 promise 解析)。whilewait

这也是 setTimeout 的延迟不能保证的原因之一(它们实际上可能需要更长的时间)。

这一系列的文章可能比 MDN 文档 IMO: JavaScript Visualized: ✨♻️ Event Loop 更能理解这一点

⏳ 固定时间的运行循环:

如果要运行循环一段时间,则可以采用同步(阻塞)和异步(非阻塞)方式。

您会注意到,在循环运行时,页面的响应能力以及在特定时间范围内能够运行的迭代次数都存在一些差异。

🛑 运行固定时间的循环 - 阻塞:

const msToRun = 2000 // 2 seconds

const t0 = performance.now() // or Date.now()

let iterations = 0

setTimeout(() => {
  console.log(`This won't be logged until the loop is over.`)
}, 0)

while ((performance.now() - t0) < msToRun) {
    ++iterations
}

console.log(`Loop run for ${ iterations } iterations.`)
body {
  height: 100vh;
  margin: 0;
}

body:hover {
  background: yellow;
}

🚦 运行“循环”固定时间 - 非阻塞 (setInterval):

const msToRun = 2000 // 2 seconds

const t0 = performance.now() // or Date.now()

let iterations = 0

setTimeout(() => {
  console.log(`This will be logged before the loop is over.`)
}, 0)

const intervalID = setInterval(() => {
  ++iterations
  
  if (performance.now() - t0 >= msToRun) {
    clearInterval(intervalID)
    
    console.log(`Loop run for ${ iterations } iterations.`)
  }
})
body {
  height: 100vh;
  margin: 0;
}

body:hover {
  background: yellow;
}

🚥 运行“循环”固定时间 - 非阻塞 (setTimeout):

const msToRun = 2000 // 2 seconds

const t0 = performance.now() // or Date.now()

let iterations = 0

setTimeout(() => {
  console.log(`This will be logged before the loop is over.`)
}, 0)

function runIteration() { 
  ++iterations
  
  if (performance.now() - t0 < msToRun) setTimeout(runIteration)
  else console.log(`Loop run for ${ iterations } iterations.`)
}

runIteration()
body {
  height: 100vh;
  margin: 0;
}

body:hover {
  background: yellow;
}

0赞 Heiko Theißen 8/13/2023 #3

如果希望循环在循环外部将全局变量设置为某个值时结束,则必须在循环中引入异步元素,以便外界有机会修改全局变量。以下示例使用一个函数,该函数在 100 毫秒后递归调用自身。 (也可以使用 0 毫秒的延迟,但必须涉及 a 或类似延迟。setTimeout

var a = "original";
setTimeout(function() {
  a = "updated";
}, 1000);

function loop() {
  console.log(a);
  if (a === "original") setTimeout(loop, 100);
}
loop();

请注意,将递归替换为

if (a === "original") Promise.resolve().then(loop);

不够“异步”,无法允许在两者之间完成。setTimeout(..., 1000)

评论

1赞 Danziger 8/14/2023
那是因为这是一个宏观任务,而承诺是一个微任务。执行时,一个新的微任务被添加到微任务队列中,其中的任务将继续执行,直到它为空,并允许事件循环重新开始并选择新的宏任务。setTimeoutPromise.resolve().then(loop)
0赞 phatfingers 8/14/2023 #4

正如几个答案中所描述的,问题的本质是 JavaScript 是单线程的,异步代码块,例如 Promise 引用的代码或 之后的所有代码,一旦前一个进程完成,就会排队等待单独运行,在这种情况下,您的父进程永远不会完成。then()await

为了更加清晰起见,我将稍微重写您的代码以提供两种不同的方案,其中第一种方案运行为完成,而第二种方案则无休止地循环。我已经删除了计时器,因为它无关紧要。

async function wait1() {
    new Promise(resolve => {});
    glb = "updated1";
}

async function wait2() {
    await new Promise(resolve => {});
    glb = "updated2";
}

function f(ver) {
    if (ver==1) {
        wait1();
    } else {
        wait2();
    }
    var glb2 = "original";
    while (glb2 == "original") {
        glb2 = glb;
        console.log("####  glb:" + glb, "ver:" + ver);
    }
    console.log("#### DONE  glb:" + glb, "ver:" + ver);
}

let glb = "original";
f(1);
glb="original";
f(2);

Note that the difference between and is that awaits its Promise before assigning a new value to glb. It doesn't really matter that either or function is declared . An function is still immediately run to completion-- it just converts its return value to a . What gets you, in this case, is that all code following your , including your assignment of , is suspended and your function immediately returns an unfulfilled . That suspended code must wait in line behind the next available process, which happens to be the continuation of your function.wait1wait2wait2wait1wait2asyncasyncPromiseawaitglb = "updated";Promisef

To sum up, because your assignment followed an , it is suspended and not run until your function completes, which never happens.glb = "updated";awaitf()