将递归函数转换为异步 CPS 实现 (javascript)

IT技术 javascript jquery dom asynchronous callback
2021-03-05 12:30:20

这是我的功能。

    function duplicate_step_through_highlighted (element_jq, target_jq, char_cb) {
        console.log( element_jq);
        var contents = element_jq.contents();
        for (var i = 0 ; i < contents.length; ++i) {
            // if text node, step
            if (contents[i].nodeType === 3) {
                // insert empty text node
                var new_tn = document.createTextNode('');
                target_jq.append(new_tn);

                // iterate it 
                var text = contents[i].nodeValue;
                for (var j = 0; j < text.length; j++) {
                    char_cb(text[j],new_tn);
                    new_tn.nodeValue += text[j];
                    // *** I want an async delay here *** 
                }
            } else { // type should be 1: element
                // target_jq gets a duplicate element inserted, copying attrs
                var new_elem = $(contents[i].cloneNode(false)).appendTo(target_jq);
                duplicate_step_through_highlighted($(contents[i]),$(new_elem),char_cb);

                // then a recursive call is performed on the newly created element as target_jq
                // and the existing one as element_jq. char_cb is passed in
            }
        }
    }

我正在做的是通过一次重建一个字符来重建一个 HTML 元素。这样做有一个很好的理由,我想要它被“输入”的视觉效果。

所以现在没有延迟,所以我的元素会立即复制。我已经检查过结果是否一致,但我越来越清楚,我可能需要完全重新编写功能,以便我能够在插入每个字符后进行异步延迟。

我是否需要重新编写它并有一个堆栈来跟踪我在元素中的位置?

3个回答

您可能想看看我最近的回答这个较旧的回答Demo),了解如何实现这种效果。


提示:不要将元素克隆到新元素中,只需隐藏它们并使它们逐个显示。

此外,除了原生 DOM 元素之外,根本不处理 jQuery 实例可能更容易。所以是的,重写可能会做 :-) 而且我认为它也确实需要一个堆栈。

function animate(elements, callback) {
/* get: array with hidden elements to be displayes, callback function */
    var i = 0;
    (function iterate() {
        if (i < elements.length) {
            elements[i].style.display = "block"; // show
            animateNode(elements[i], iterate); 
            i++;
        } else if (callback)
            callback();
    })();
    function animateNode(element, callback) {
        var pieces = [];
        if (element.nodeType==1) {
            while (element.hasChildNodes())
                pieces.push(element.removeChild(element.firstChild));
            setTimeout(function childStep() {
                if (pieces.length) {
                    animateNode(pieces[0], childStep); 
                    element.appendChild(pieces.shift());
                } else
                    callback();
            }, 1000/60);
        } else if (element.nodeType==3) {
            pieces = element.data.match(/.{0,2}/g); // 2: Number of chars per frame
            element.data = "";
            (function addText(){
                element.data += pieces.shift();
                setTimeout(pieces.length
                    ? addText
                    : callback,
                  1000/60);
            })();
        }
    }
}

animate($("#foo").children());

jsfiddle.net 上的演示

怎么运行的:

  • addText函数向当前文本节点添加一些字符,并为自身设置超时 - 动画!如果一切都完成了,它会调用该callback函数。
  • childStep在子节点上运行动画,并将自身作为回调传递,直到没有子节点剩下 - 然后调用该callback函数。
  • 两者一起,animateNode递归地遍历节点树并按顺序为文本节点设置动画。
  • iterate函数animateNode通过将自身作为回调传递给所有输入元素调用(在取消隐藏它们之后)。在所有输入元素完成后,它调用callback作为第二个参数给定的外部元素animate
等等,什么?你说我需要一个堆栈,但你的实现不需要一个。这段代码似乎工作得很好,但我似乎无法理解它......
2021-04-20 12:30:20
我知道了。感谢你这样做,Bergi,我研究这段代码已经有一段时间了,它帮助我更好地理解了函数式编程。对于延续传递风格,我总是在如何最大程度地简化它(使用更少的函数)方面苦苦挣扎,因为它完全令人困惑。但是经过足够长的思考之后,很明显迭代必须转换为递归(这甚至可能没有必要)。我很高兴接近可以轻松地将同步代码转换为 CPS 的地步,这为我打开了许多大门。
2021-04-21 12:30:20
其实我并没有转换你的代码 :-) 我只是看了一下它的作用,然后想了如何异步去做,然后从头开始写解决方案。
2021-04-23 12:30:20
你能告诉我过程,你经历了产生这种代码?你用什么样的规则把我开始的代码转换成这个?我想我已经接近理解它为什么以及如何工作了,但我仍然不确定在等待超时触发时代码在哪里阻塞。这似乎是必要的,但我不清楚这些部分在哪里。谢谢
2021-05-07 12:30:20
这是一个调用堆栈:-) 此外,该解决方案构建了一个要附加的片段堆栈,这些片段要么是递归动画的子节点,要么是逐帧添加的文本片段。
2021-05-15 12:30:20

这是我的解决方案,它是一种更高效、更清洁、更快捷的方法:

var start = 0; //Makes sure you start from the very beggining of the paragraph.
var text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras viverra sem dolor, nec tempor purus luctus vitae. Nulla massa metus, iaculis et orci euismod, faucibus fringilla metus. Sed pellentesque in libero nec.'; //Your text
var speed = 14; //Of course you can choose your own speed, 0 = instant, the more you add the slower it gets.
function typeWriter() {
  if (start < text.length) {
    document.querySelector('.demo').innerHTML += text.charAt(start);
    start++;
  }
  setTimeout(typeWriter, speed);
}
<body onload="typeWriter();">

<p class="demo"></p>

</body>

我制作了一个简单的脚本在我的网站上使用,它可能会帮助那些希望达到这种效果的人。

这是Github 上的 repo链接,解释如下:

class Typer {

    constructor(typingSpeed, content, output) {

        this.typingSpeed = typingSpeed;
        // Parses a NodeList to a series of chained promises
        this.parseHtml(Array.from(content), output);
    };

    makePromise(node, output) {

        if (node.nodeType == 1) // element 
        {
            // When a new html tag is detected, append it to the document
            return new Promise((resolve) => {
                var tag = $(node.outerHTML.replace(node.innerHTML, ""));
                tag.appendTo(output);
                resolve(tag);
            });

        } else if (node.nodeType == 3) // text
        {
            // When text is detected, create a promise that appends a character
            // and sleeps for a while before adding the next one, and so on...
            return this.type(node, output, 0);
        } else {
            console.warn("Unknown node type");
        }
    }

    parseHtml(nodes, output) {
        return nodes.reduce((previous, current) => previous
            .then(() => this.makePromise(current, output)
                .then((output) => this.parseHtml(Array.from(current.childNodes), output))), Promise.resolve());
    }

    type(node, output, textPosition) {
        var textIncrement = textPosition + 1;

        var substring = node.data.substring(textPosition, textIncrement);

        if (substring !== "") {
            return new Promise(resolve => setTimeout(resolve, this.typingSpeed))
                .then(() => output.append(substring))
                .then(() => this.type(node, output, textIncrement));
        }

        return Promise.resolve(output);
    }
}
我的最后一个答案被删除了,也许是因为它只有一个链接?这是一个更完整的版本
2021-04-23 12:30:20
不用担心。JS 一直能够实现甜蜜的异步控制流,但直到最近它才以一种可访问的方式实际呈现......传递延续并处理所有闭包太费脑筋而且容易出错!未来是光明的,这是肯定的!
2021-04-29 12:30:20
是的,我的评论也随之而来。你有没有想过使用发电机?我认为生成器是表达这种异步行为的自然方式
2021-05-03 12:30:20
老实说,我以前没有听说过他们。在谷歌上搜索了一下之后,他们似乎可以极大地简化这段代码!我肯定会仔细研究它,感谢您的洞察力:)
2021-05-08 12:30:20