async 函数会在开始时正确更新迭代器,但不会在结束时正确更新迭代器

async function does update iterator correctly at the begining but not at the end

在下面的代码中,我使用 CSS DOM,这可能计算量很大。这显然是访问 :after 选择器所必需的。 renderItem 方法将向 DOM 添加一个新项目(包括它在元素之后),这就是我使用 async 函数和 await 的原因 return 在 loadFromStorage.

的每次迭代中

然而,await 似乎无法正常工作,或者 renderItem 函数内部发生了一些奇怪的事情。 n 迭代器在函数的开头正确更新(项目正确呈现到屏幕上,第一个 console.debug 以正确的顺序打印正确的值),但在底部,第二个打印值,始终是最后一次迭代值(在我的例子中是 4,​​因为我试图从本地存储中呈现 4 个项目)并且 getCSSRule 方法得到一个错误的数字。

let books = []
let n = 0

const renderItem = async (entry, direction = 1) => {
  const li = document.createElement('li')
  const ul = document.querySelector('ul')
  li.classList.add('item')
  n += 1
  console.debug(`iter: ${n}`)
  li.id = (`item${n}`)
  await addCSSRule(`#item${n}:after`)
  li.innerText = entry.slice(0, entry.length - 13)
  if (direction === 1)
    ul.appendChild(li)
  else
    ul.insertBefore(li, ul.firstChild)
  console.debug(`iter: ${n}`)
  const s = await getCSSRule(`#item${n}::after`).catch(() => {
    console.debug(`Failed to find ':after' selector of 'item${n}'`)
    return false
  })
  s.style.content = "\""+ entry.slice(entry.length - 13, entry.length) +"\""
  return true
}

const loadFromStorage = () => {
  books = localStorage.getItem('books').split('//')
  books.forEach(async (entry) => {
    await renderItem(entry)
  })
}

...

控制台结果(考虑localStorage.getItem('books').split('//') returns 4项):

iter: 1
iter: 2
iter: 3
iter: 4
iter: 4 // Printed x4

我也一直在尝试将这个 renderItem 方法传递给 Promise 对象内部的 await,这给了我相同的结果。此外,当我在函数末尾更新 n 迭代器时,同样的事情发生了,但在它的开头。

如果我使用的某些术语在 JavaScript 的上下文中不正确,我很抱歉,我已经很多年没有使用这种语言了,目前我正在努力学习。

这里的关键问题是您将一个异步函数传递给 forEach,因此即使您 await 在它的内部,forEach 也不会等待函数本身。为了说明这里的事件顺序,假设您有 4 本书 A、B、C、D。您的执行看起来像这样。

  • renderItem(A)
  • n += 1(n 现在是 1)
  • console.log(n)(日志 1)
  • await addCSSRule(`#item:after`)(这是一个真正的异步事件,因此这释放了事件循环来处理其他事情,即 forEach 中的下一个元素)
  • renderItem(B)
  • n += 1 (2)
  • console.log(n)(日志 2)
  • ...
  • renderItem(C) ... n += 1 (3) ... await addCSSRule
  • renderItem(D) ... n += 1 (4) ... await addCSSRule

然后每当 addCSSRule 调用 resolve n 将永远是 4 无论你在哪个调用。

解决方案

使用 for await...of 循环代替 Array.prototype.forEach

for await (const entry of books) {
    await renderItem(entry);
}

或者传统的for循环,修改renderItemn为参数

for (let i = 0; i < books.length; i++) {
    renderItem(books[i], i+1);
    // we don't need to await in this case, and we add 1 to i so that the 'n' value is 1-indexed to match your current behaviour.
}

我更喜欢后一种选择,因为它是避免可变全局状态(您的 n 变量)的最佳实践 - 因为它会导致混乱的控制流和问题,就像您遇到的那样。

另一种选择是在 renderItem 内递增后将局部变量设置为 n 的值,这样在该函数运行期间该值不会改变,但是对我来说似乎是一个非常 hacky 的解决方法。