【问题标题】:Converting a recursive function into an asynchronous CPS implementation (javascript)将递归函数转换为异步 CPS 实现 (javascript)
【发布时间】:2012-07-26 08:40:32
【问题描述】:

这是我的功能。

    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 元素。这样做是有充分理由的,我想要“输入”它的视觉效果。

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

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

【问题讨论】:

  • 将元素导出为字符串然后处理该字符串而不是使用真实元素不是更容易吗?
  • 字符串太多 dom 转换,如果我这样做的话。它可能不会太慢​​,但如果我能提供帮助,我不会编写低效的代码。我想看到元素动态获得新字母。我实际上打算在插入的字母上做动画。

标签: javascript jquery dom asynchronous callback


【解决方案1】:

您可能想看看我最近的answerthis older one (Demo),了解如何实现这样的效果。


提示:不要将元素克隆到新元素中,只需隐藏它们并让它们部分显示。

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

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());

Demo at jsfiddle.net

它是如何工作的:

  • addText 函数将一些字符添加到当前文本节点,并为自身设置超时 - 动画!如果一切都完成了,它会调用callback 函数。
  • childStep 在子节点上运行动画,并将自身作为回调传递,直到没有子节点 - 然后调用 callback 函数。
  • 两者一起,animateNode 递归地在节点树上运行并按顺序为文本节点设置动画。
  • iterate 函数在所有输入元素上调用animateNode(取消隐藏它们之后),方法是将自身作为回调传递。完成所有输入元素后,它会调用外部callback,它作为animate 的第二个参数给出。

【讨论】:

  • 等等,什么?你说我需要一个堆栈,但你的实现不需要一个。这段代码似乎工作得很好,但我似乎无法理解它......
  • 这是一个调用堆栈 :-) 此外,此解决方案构建了一堆要附加的片段,这些片段要么是递归动画的子节点,要么是逐帧添加的文本 sn-ps。
  • 您能告诉我您为生成此代码所经历的过程吗?你使用什么样的规则将我开始的代码转换成这个?我想我已经接近理解它为什么以及如何工作,但我仍然不确定在等待超时触发时代码块在哪里。这似乎是必要的,但我不清楚这些部分在哪里。谢谢
  • 其实我并没有转换你的代码 :-) 我只是看看它做了什么,然后想着怎么异步做,然后从头写了解决方案。
  • 我明白了。感谢您这样做,Bergi,我研究这段代码已经有一段时间了,它帮助我更好地理解了函数式编程。对于延续传递风格,由于纯粹的混乱,我一直在努力解决如何最大程度地简化它(使用更少的函数)。但是在考虑了足够长的时间之后,很明显迭代必须简单地转换为递归(甚至可能没有必要)。我很高兴能够轻松地将同步代码转换为 CPS,这为我打开了很多大门。
【解决方案2】:

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

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>

【讨论】:

    【解决方案3】:

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

    这是一个link 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);
        }
    }
    

    【讨论】:

    • 我上一个答案被删除了,可能是因为它只有一个链接?这是一个更完整的版本
    • 是的,我的评论也随之而来。您是否考虑过使用生成器?我认为生成器是表达这种异步行为的自然方式
    • 说实话,我以前没有听说过它们。在谷歌搜索了一下之后,他们似乎可以极大地简化这段代码!我肯定会仔细研究一下,感谢您的洞察力:)
    • 不用担心。 JS 一直能够 提供甜蜜的异步控制流,但直到最近它才真正以一种可访问的方式呈现......传递延续和处理所有闭包太费脑筋并且容易出错!未来是光明的,这是肯定的!
    猜你喜欢
    • 1970-01-01
    • 2016-01-24
    • 2018-06-05
    • 2016-01-06
    • 1970-01-01
    • 2018-02-24
    • 2023-03-11
    • 1970-01-01
    • 2018-12-30
    相关资源
    最近更新 更多