为什么我的输入单词的函数会产生乱码输出?

Why does my function that type words out produce garbled output?

在下面的代码中,我希望它获取所有 .auto-type 元素中的单词作为输入,然后开始自动逐个字符地输入它们。但是,我的代码不起作用,但它也不会在控制台中产生任何错误。我错过了什么?

window.onload = () => {
  let elements = document.getElementsByClassName("auto-type");
  for (element of elements) {
    const text = element.innerHTML.toString();
    element.innerHTML = "";
    let charIndex = 0;
    const typing = () => {
      element.innerHTML += text.charAt(charIndex);
      if (charIndex < text.length) {
        setTimeout(typing, 100);
        charIndex++;
      }
    };
    typing();
  }
};
body {
  background-color: black;
  color: lime;
  font-family: consolas, monospace;
}
<span class="auto-type code">hello!</span>
<br />
<span class="auto-type code">some text</span>
<br />
<div class="auto-type">some other text</div>
<span class="auto-type code">here is a better text</span>

该代码有效,但因为它是异步的,它恰好试图同时全部输入。它没有意义的另一个原因是因为它还试图将其键入 same 元素。

调查问题

呈现后,您会在页面的 HTML 中看到:

<span class="auto-type code">h</span>
<br>
<span class="auto-type code">s</span>
<br>
<div class="auto-type">s</div>
<span class="auto-type code">heooelmmrleeeo   !toietsxh tear  bteetxtter text</span>

关注最后一行,您可以看到每条消息都已键入到最后一个元素中。通过用数字替换原始消息中的每个字符,可以使这一点更加明显:

<span class="auto-type code">111111</span>
<br />
<span class="auto-type code">222222222</span>
<br />
<div class="auto-type">333333333333333</div>
<span class="auto-type code">444444444444444444444</span>

呈现:

<span class="auto-type code">1</span>
<br>
<span class="auto-type code">2</span>
<br>
<div class="auto-type">3</div>
<span class="auto-type code">412341234123412341234234234234343434343434444444</span>

所以最后一行是这样构建的:

heooelmmrleeeo   !toietsxh tear  bteetxtter text
 │││└ the 'e' from "here" in the fourth element
 ││└ the 'o' from "some" in the third element
 │└ the 'o' from "some" in the second element
 └ the 'e' from "hello" in the first element

发生这种情况的原因是因为这个 for-loop,其中变量 element 的范围不是 for-loop,这意味着它被每个 typing 共享函数,并将引用 elements:

中的最后一个元素
for (element of elements) { // <- this element variable is a global!
    /* ... */
}

您可以通过简单地用 letconst 重新定义 element 变量来解决您的主要问题:

for (const element of elements) { // <- this element variable is now scoped to this for-loop
    /* ... */
}

这个改动的效果可以看下面StackSnippet:

window.onload = () => {
  let elements = document.getElementsByClassName("auto-type");
  for (const element of elements) {
    const text = element.innerHTML.toString();
    element.innerHTML = "";
    let charIndex = 0;
    const typing = () => {
      element.innerHTML += text.charAt(charIndex);
      if (charIndex < text.length) {
        setTimeout(typing, 100);
        charIndex++;
      }
    };
    typing();
  }
};
body {
  background-color: black;
  color: lime;
  font-family: consolas, monospace;
}
<span class="auto-type code">hello!</span>
<br />
<span class="auto-type code">some text</span>
<br />
<div class="auto-type">some other text</div>
<span class="auto-type code">here is a better text</span>

其他问题

由于 JavaScript 的性质,加载页面和所有依赖项可能需要一段时间。这意味着您的消息在页面加载时对用户可见,这可能不是您想要的。所以你应该用 CSS 隐藏消息,然后在你准备好时显示它们。

.auto-type {
  visibility: hidden; /* like display:none; but still consumes space on the screen */
}

然后当您清空文本时,再次使元素可见:

const text = element.innerHTML.toString();
element.innerHTML = "";
element.style.visibility = "visible";

更正代码

您需要修改“输入”功能,以便在输入上一条消息之前不触发。我们可以通过将每个“键入”函数更新为 return 一个 Promise,然后将这些 Promise 链接在一起来做到这一点。

/**
 * Types text into the given element, returning a Promise
 * that resolves when all the text has been typed out.
 */
function typeTextInto(element, text, typeDelayMs = 100) {
    return new Promise(resolve => {
        let charIndex = 0;
        const typeNextChar = () => {
            element.innerHTML += text.charAt(charIndex);
            if (charIndex < text.length) {
                setTimeout(typeNextChar, typeDelayMs);
                charIndex++;
            } else {
                resolve(); // finished typing
            }
        };
        typeNextChar(); // start typing
    });
}

window.onload = () => {
    const elements = document.getElementsByClassName("auto-type");
    const elementTextPairs = [];
    
    // pass 1: empty out the contents
    for (element of elements) {
        const text = element.innerHTML.toString();
        element.innerHTML = "";
        element.style.visibility = "visible";
        elementTextPairs.push({ element, text });
    }

    // pass 2: type text into each element
    let chain = Promise.resolve(); // <- empty Promise to start the chain
    for (elementTextPair of elementTextPairs) {
        const { element, text } = elementTextPair; // unwrap pair
        chain = chain // <- add onto the chain
            .then(() => typeTextInto(element, text));
    }

    // basic error handling
    chain.catch(err => console.error("failed to type all messages:", err));
};

这可以使用 async/await 进一步清理,并将输入逻辑拆分到它自己的函数中。这显示在下面的工作 StackSnippet 中:

/**
 * Types text into the given element, returning a Promise
 * that resolves when all the text has been typed out.
 */
function typeTextInto(element, text, typeDelayMs = 100) {
    return new Promise(resolve => {
        let charIndex = 0;
        const typeNextChar = () => {
            element.innerHTML += text.charAt(charIndex);
            if (charIndex < text.length) {
                setTimeout(typeNextChar, typeDelayMs);
                charIndex++;
            } else {
                resolve(); // finished typing
            }
        };
        typeNextChar(); // start typing
    });
}

window.onload = async () => {
    try {
        const elements = document.getElementsByClassName("auto-type");
        const elementTextPairs = [];
    
        // pass 1: empty out the contents
        for (element of elements) {
            const text = element.innerHTML.toString();
            element.innerHTML = "";
            element.style.visibility = "visible";
            elementTextPairs.push({ element, text });
        }

        // pass 2: type text into each element
        for (elementTextPair of elementTextPairs) {
            const { element, text } = elementTextPair; // unwrap pair
            await typeTextInto(element, text); // await in a for-loop makes the asynchronous work run one-after-another
        }
    } catch (err) {
        // basic error handling
        console.error("failed to type all messages:", err);
    }
};
body {
  background-color: black;
  color: lime;
  font-family: consolas, monospace;
}

.auto-type {
  visibility: hidden;
}
<span class="auto-type code">hello!</span>
<br />
<span class="auto-type code">some text</span>
<br />
<div class="auto-type">some other text</div>
<span class="auto-type code">here is a better text</span>