In the code bellow I work with CSS DOM, which may be computation heavy. This is apparently necessary to access the :after
selector. renderItem
method will add a new item to the DOM (including it's after element) and this is the reason why I have used the async
function and await
for it's return in each iteration inside of loadFromStorage
.
However, the await
seems to not work correctly, or something weird happens inside of renderItem
function. The n
iterator is updated correctly at the beginning of the function (items are correctly rendered to the screen and the first console.debug
prints a correct value in a correct order), but the at the bottom, the second printed value, is always the last iteration value (which is 4 in my case, as I am trying to render 4 items from the local storage) and getCSSRule
method is getting a wrong number.
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)
})
}
...
Console result (considering localStorage.getItem('books').split('//')
returns 4 items):
iter: 1
iter: 2
iter: 3
iter: 4
iter: 4 // Printed x4
I been also trying to pass this renderItem
method to await
inside of a Promise
object, which give me the same result. Also when I update the n
iterator at the end of function the same thing happens, but at the beginning of it.
I am sorry if some terminology I have used is not correct in the context of JavaScript, I been not using this language for many years and currently I am trying to catch on.
CodePudding user response:
The key problem here is that you're passing an async function to forEach
, so even though you're await
ing inside the body of it, forEach
will not wait for the function itself. To illustrate the order of events here, say you have 4 books A, B, C, D. Your execution will look something like this.
renderItem(A)
n = 1
(n is now 1)console.log(n)
(logs 1)await addCSSRule(`#item${1}:after`)
(This is a truly asynchronous event, and so this frees up the event loop to work on other things, namely the next elements in theforEach
)renderItem(B)
n = 1
(2)console.log(n)
(logs 2)- ...
renderItem(C)
...n = 1
(3) ...await addCSSRule
renderItem(D)
...n = 1
(4) ...await addCSSRule
And then whenever the addCSSRule
calls resolve n
will always be 4
no matter which call you're in.
Solution
Use a for await...of
loop instead of Array.prototype.forEach
.
for await (const entry of books) {
await renderItem(entry);
}
Or a traditional for loop, and modify renderItem
to take n
as an argument
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.
}
I would prefer the latter option as it's best practice to avoid mutable global state (your n
variable) - as it can lead to confusing control flow and issues just like the one you're having.
One other option is to set a local variable to the value of n
after incrementing it inside renderItem
, so that for the duration of that function the value won't change, but that seems like a very hacky workaround to me.