【问题标题】:How to move multiple elements with requestAnimationFrame() without having to create a new function for each element如何使用 requestAnimationFrame() 移动多个元素,而无需为每个元素创建新函数
【发布时间】:2019-11-07 22:39:42
【问题描述】:

前言:这是我计划为各种网页重复使用和添加的内容。因此,我不想为此使用任何 JQuery,部分原因是我使用它的许多页面不需要其他任何东西(使我使用 JQuery 有点像用面粉填充 50 加仑桶,只是为了使用一茶匙面粉),部分原因是它是为资源稀少的移动浏览器设计的。

JQuery 效率低下似乎没什么大不了,特别是因为 its consolidated library 的大小不到 31MB,但根据 this page 的说法,对于 JavaScript 来说,它的 document.getElementById('foo'); 调用速度快了近 16 倍在 Chrome 中比 JQueries 等效:$('#foo');。此外,尽管$('#foo'); 在 Firefox 和 Safari 中比在 Chrome 中更快,document.getElementById('foo'); 在 Safari 中仍然快 10 倍,在 Firefox 中快 500 倍。不过,如果您的来源有相互矛盾的信息,请随时将其发布在 cmets 中并为任何已经做过的 cmets 点赞。

我有一组导航链接,我正试图以一定速度向上移动到一个位置,相对于它们与第一个导航链接的距离。为了帮助您了解我的意思,这是我的导航栏的动画(使用 JQuery slideUpslideDown 函数完成),因此您可以看到它是如何工作的(口吃):

如果有帮助,我还将包含导航栏的 HTML:

<nav>
    <ul id='nav'>
        <li><a href="index.php"><icon><img src="images/home-icon.png"></icon>Home</a></li>
        <li><a href="skillsets.php"><icon><img src="images/skills-icon.png"></icon>Skillsets</a></li>
        <li><a href="gallery.php"><icon><img src="images/gallery-icon.png"></icon>Gallery</a></li>
        <li><a href="about.php"><icon><img src="images/about-icon.png"></icon>About</a></li>
        <li><a href="contact.php" style='border-right:1px solid black;'><icon><img src="images/contact-icon.png"></icon>Contact</a></li>
    </ul>
</nav>

我的 JavaScript 文件有一个名为 prepareList() 的函数,它只是用来设置列表并折叠它(通过移动除第一个导航链接之外的所有链接)。为了防止 prepareList() 和它使用的任何变量在 HTML 加载之前被初始化,它们都包含在这个函数中:

document.addEventListener("DOMContentLoaded", function(event) {/*all the code*/});

我现在将包含我的 prepareList() JavaScript 函数,以及所有使用变量的初始化和值。我已经很好地评论了这一点,您可能仅从代码中的 cmets 就能理解它,但我会澄清我之后想要做什么:

//I will consolidate the first three objects into one href variable when I get the program working
var ul = document.getElementsByTagName("nav")[0].children[0],
    li = ul.children,
    href = new Array(li.length),
    is404 = true,
    isDown = false;

var itterator;
for(itterator = 0; itterator < li.length; itterator++)
    href[itterator] = li[itterator].children[0];

prepareList();

function prepareList() {
    //add a plus to the first nav link making it more obvious that it's the button to open the nav
    href[0].innerHTML = href[0].innerHTML + "<span> +</span>";
    //hide the remaining nav links with animations that move them up, at a speed,
    //and to a postion, relative to how far they are from the first nav link
    var lastTick = +new Date();
    /*This function will be called for each nav link so they move at the same time
    navElement is one of the li elements, number is the number (index) of the li element,
    and number is negative if the list is collapsing (moving up)*/
    var tick = function(navElement, number) {
        var navElementStyleTop = parseFloat(navElement.style.top) + number * navElement.clientHeight * (new Date() - lastTick) / 1000;
        //navElement.style.top = (navElementStyleTop + number * navElement.clientHeight * (new Date() - lastTick) / 1000) + "px"; //milliseconds
        navElement.style.top = navElementStyleTop + "px";
        lastTick = +new Date();

        //requestAnimationFrame(tick) will only loop tick if the animation can be performed: http://www.javascriptkit.com/javatutors/requestanimationframe.shtml
        //setTimeout(tick, 1000/15) will force the animation to be performed if it's been more than 1/15th of a second in order to maintain persistance of motion
        if(number < 0){
            if (navElementStyleTop > number * navElement.clientHeight)
                requestAnimationFrame(tick(navElement, number)) || setTimeout(tick, 1000/15);
            else //To prevent rounding errors from messing up the final position
                navElement.style.top = (number * navElement.clientHeight) + "px";
        }
        else{
            if (navElementStyleTop < 0)
                requestAnimationFrame(tick(navElement, number)) || setTimeout(tick, 1000/15);
            else //To prevent rounding errors from messing up the final position
                navElement.style.top = "0px";   
        }
    };
    /*The for loop is to make this work no matter the number of links in the nav bar
    (as the function executed before this may add a link to the nav bar)*/
    for(itterator = 1; itterator < li.length; itterator++){
        //necessary for top to work
        href[itterator].style.position = "relative";
        //give top a value so this doesn't rely on the browser defaulting it to 0
        href[itterator].style.top = "0px";
        tick(href[itterator], (-1) * itterator);
    }
}

如果你没有看到我不能怪你的问题,那就是requestAnimationFrame(tick(navElement, number))。为了不抱怨递归,它必须以这种方式执行:requestAnimationFrame(tick)。根据this StackOverFlow answerrequestAnimationFrame是异步的,意思是“主线程不等待requestAnimFrame调用返回,之前”继续函数。

问题是我必须至少将number 传递给函数,以便函数知道它正在使用哪个元素。我可以看到使函数不需要传入任何参数的唯一方法是每次都在 for 循环中对其进行初始化。我真的不喜欢那个解决方案,因为我希望能够重用这个功能,以免浪费周期(可能还有很多内存)在第一个之后为每个导航链接重新创建它。所以,我想知道是否有办法将number 传递到所有的刻度函数中,同时保持requestAnimationFrame 的异步特性。

如果您有比 JQuery 的 slideUpslideDown 函数更高效的更好方法,我会全力以赴。也许有人在网上某个地方有一个效率更高的 JavaScript 版本,但我还没有找到。

如果您需要更多信息,或者我有任何拼写错误,请随时在 cmets 中告诉我。

【问题讨论】:

    标签: javascript performance


    【解决方案1】:

    通过将我试图从 tick() 每次调用自身时传递给自身的变量转换为外部变量,我很快就解决了这个问题。不过,如果我的回答就这么简单,我就做不到了。但是,有一些优化对我在移动浏览器上的使用产生了巨大的影响。我做的第一个也是最小的优化是从导航中删除多余的元素:

    <nav>
        <a href="index.php"><icon><img src="images/home-icon.png"></icon><span>Home</span></a>
        <a href="skillsets.php"><icon><img src="images/skills-icon.png"></icon><span>Skillsets</span></a>
        <a href="gallery.php"><icon><img src="images/gallery-icon.png"></icon><span>Gallery</span></a>
        <a href="about.php"><icon><img src="images/about-icon.png"></icon><span>About</span></a>
        <a href="contact.php"><icon><img src="images/contact-icon.png"></icon><span>Contact</span></a>
    </nav>
    

    应用于导航栏中最后一个链接的样式属性已通过将其应用于nav a:last-child 类而移至外部 CSS 文件。应该注意的是,这阻止了我将每个链接中的图像和文本锚定到链接底部。这是必要的,因为它看起来好像每个导航链接实际上都在向上滑动,而不是从底部被切断。为了解决这个问题,我实际上将每个导航链接向上移动。这要求我在我的 CSS 文件中设置 nav a 类以具有属性 position:absolute。我发现这比更改除第一个导航链接之外的所有链接的高度要快得多,因为每个导航链接中的图像和文本都锚定在它们的底部。我相信这是因为我必须使用动态 FLEX CSS 属性来进行锚定。

    以这种方式为每个导航链接提供一个绝对位置会使它们都具有相同的位置,并且每个导航链接都显示在前一个导航链接的顶部。这与我想要发生的相反,所以在 JavaScript 中我将它们向下移动并给它们中的每一个比另一个更高的 z-index,使最后一个导航链接没有这样的属性,以便有一个 z-index 0:

    var href = document.getElementsByTagName("nav")[0].children;
    //Place the first link on top of the second link, on top of the third link, etcetera
    for(var itterator = 0, hrefLengthMinusOne = href.length - 1; itterator < hrefLengthMinusOne; itterator++)
        href[itterator].style.zIndex = hrefLengthMinusOne - itterator;
    //Move the links down instantaneously (to be slid up later to help the user understand the nav bar is dynamic)
    for(var itterator = 1, hrefLength = href.length, linkHeight = href[0].clientHeight; itterator < hrefLength; itterator++)
        href[itterator].style.top = itterator * linkHeight;
    

    需要注意的是,如果需要将所有这些链接显示在 z-index 大于 0 的某物之上,则只需将父类 nav 的 z-index 设置为在高于该元素的 z-index 的引用 CSS 文件中。

    我对tick函数的改动如下:

    /*Used to determine the height each link needs to get up to when moving down the menu
    (created because referencing navHeight is a LOT faster than referencing
    href[0].clientHeight 1000 times every time the menu needs to be slid down)*/
    var navHeight = href[0].clientHeight;
    
    /*Put the first links span text into a variable so that we don't have to slice() off the last character every time to add on a '+' or '-' sign.
    It should be noted that navSpanText is not an HTML element (so changing it will not change the HTML span text).
    Because of that, & because href[0].children[1].textContent calls more than two child elements. navSpan is an HTML element,
    referencing navSpanText is over 60^2 times faster than href[0].children[1].textContent.
    but because it's only one call, it's still orders of magnitude faster than href[0].children[1].textContent*/
    var navSpan = href[0].children[1];
    var navSpanText = navSpan.textContent + " ";
    var navSpanTextLength = navSpanText.length - 1;
    //add a plus to the first nav link making it more obvious that it's the button to open the nav
    navSpan.textContent = navSpanText + "+";
    /*Create a function (& appropriate variables) to hide the remaining nav links with animations that move them up,
    at a speed, and to a postion, relative to how far they are from the first nav link.
    These functions are created locally as closures so as not to make global functions that recreate these closures every time.*/
    var currentTick;
    var lastTick = Date.now(); //System time before tick changed the DOM
    var hrefFactor = -navHeight; //navHeight when the menu needs to be slid down & -navHeight when the menu needs to be slid up
    var hrefPosition = navHeight; //Used to hold the new position of the first link so as not to have to grab it from the DOM every time
    var itterator; //Used in for loops to itterate through the nav links
    var tick = function() {
        //console.log("lastTick: " + lastTick + ", hrefPosition: " + hrefPosition + ", hrefFactor: " + hrefFactor);
        currentTick = Date.now();
        hrefPosition += (currentTick - lastTick) * hrefFactor / 400;
        lastTick = currentTick;
        /* The for loop is to make this work no matter the number of links in the nav bar
        (as the function executed before this may add a link to the nav bar)*/
        for(itterator = href.length - 1; itterator > 0; itterator--)
            href[itterator].style.top = hrefPosition * itterator;
    
        //requestAnimationFrame(tickUp) will only loop tickUp if the animation can be performed: http://www.javascriptkit.com/javatutors/requestanimationframe.shtml
        if ((hrefFactor > 0 && hrefPosition < navHeight) || (hrefFactor < 0 && hrefPosition > 0))
            requestAnimationFrame(tick);
        else if (hrefFactor > 0) //To prevent rounding errors from messing up the final positions
            for(itterator = href.length - 1; itterator > 0; itterator--)
                href[itterator].style.top = navHeight * itterator;
        else //To prevent rounding errors from messing up the final postions
            for(itterator = href.length - 1; itterator > 0; itterator--)
                href[itterator].style.removeProperty("top");
    };
    //Actually slide the nav menu up
    tick();
    
    //Add an event listener to make the menu slide up & down by tapping on the first link
    href[0].addEventListener('click', function(event){
        event.preventDefault();
        if(hrefFactor < 0){
            lastTick = Date.now();
            hrefPosition = 0;
            hrefFactor = navHeight;
            tick();
            //replace the '+' with a '-' making it more obvious that it's now the button to close the nav
            navSpan.textContent = navSpanText + "-";
        }else{
            lastTick = Date.now();
            hrefPosition = navHeight;
            hrefFactor = -navHeight;
            tick();
            //replace the '-' with a '+' making it more obvious that it's now the button to open the nav
            navSpan.textContent = navSpanText + "+";
        }
    });
    

    现在已经很多了,cmets 应该解释其中的大部分内容,但有两件重要的事情是我不再使用 new Date()setTimeout(tick, 1000/15),因为 Date.now() 的速度是 @987654331 的两倍@(当 tick 调用它近 1000 次时,这真的很重要)虽然 setTimeout(tick, 1000/15) 可能会在某些处理器非常慢的计算机上停止口吃,但它也会干扰页面的其他部分,这对我来说很重要,因为这不是我拥有的唯一一段 JavaScript。

    我还使用hrefFactor 是否大于或小于零来查看最终的hrefPosition 应该是&gt;=navHeight 还是&lt;=0。这比使用两种不同的功能打开和关闭菜单需要更多的周期,但我认为节省内存是值得的。应该注意的是,我单独更改每个导航链接的位置会大大减慢滴答的每次迭代速度,甚至比使用new Date() 而不是Date.now() 更慢,但是不可能分配不同的位置每个导航链接同时进行。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-12-08
      • 1970-01-01
      • 1970-01-01
      • 2019-01-07
      • 1970-01-01
      • 2016-05-06
      • 2018-09-11
      相关资源
      最近更新 更多