你能异步创建数千个DOM元素吗?

7
我正在构建一个表情选择器,最具挑战性的任务是为每个表情创建约1500个DOM元素,这会导致页面在500-700毫秒内无响应/卡顿。

我已经进行了调试,并且似乎创造DOM元素是阻塞其余JS执行的原因:

function load_emojis(arr){
  var $emojis = [];
  # arr.length = 1500
  _.each(arr, function(){
    $emojis.push($('<span/>').append($('<img/>')));
  });

  $('.emojis').html($emojis);
}

有没有一种方法可以异步执行整个程序,或在另一个线程中执行,以便它不会阻止后续的JS执行?

我尝试将其放入setTimeout中,但似乎仍在同一线程中执行,从而仍然阻止JS执行。


1
你能否在服务器端进行创建吗? - Hopeless
为什么你不对存储在arr中的值进行任何操作?这段代码似乎不完整... - trincot
我不确定jQuery是否在幕后执行此操作,但我希望使用文档片段会比每次添加新表情符号时强制进行DOM回流更加高效。 - zero298
你想让表情符号一次性全部显示出来还是逐个呈现?你尝试过将它们分页,只显示可见的表情符号簇,并在用户向下滚动表情符号列表时加载更多吗? - zero298
@Darkrum 这是一个简化的代码,只是为了展示一个想法(根据我的测试,设置属性不会影响性能) - CodeOverload
显示剩余3条评论
5个回答

5

JavaScript不是线程化的,它像大多数UI库一样,在单个线程中使用事件循环执行所有操作;异步行为可能在后台线程中执行工作(对JS程序员来说是看不见的;他们不会显式地管理或甚至看到线程),但结果始终传递到单个前台线程进行处理。

元素渲染实际上要等到你的JS代码完成,并且控制返回到事件循环时才会完成;如果你正在渲染太多东西,延迟发生在浏览器需要绘制它时,这时你无法做太多事情来帮助。最多你可以通过显式创建元素而不是传递一堆文本进行解析来减少解析开销,但即便如此,你的选择也是有限的。

可能有所帮助的事情取决于浏览器、月相等因素,包括:

  1. 创建并填充一个完全不属于DOM的单个父元素,然后在填充完成后将该单个父元素添加到DOM中(可以避免维护DOM结构所涉及的工作)
  2. 创建一个重复添加的结构模板,然后使用 cloneNode(True) 复制该模板,并在之后填充小的差异;这避免了在实际上是重复多次使用相同节点树的情况下解析X节点树所涉及的工作,只需进行一些属性调整即可。
  3. 如果图像大于视口,则仅插入/渲染最初可见的元素,随着用户滚动,将其他元素添加到后台(可能还会主动添加它们,但数量足够小,并且在它们之间具有足够的 setTimeout/setInterval 窗口,以便任何给定的插入不会花费太长时间,UI保持响应)。
  4. 对于“许多图像的数组”来说,一个重要的方法是停止使用1500+个图像,而是使用单个单片式图像。然后你可以:

    a. 通过CSS重复使用给定的固定偏移量和大小的单片式图像(图像被解压缩和渲染一次,并且该图像的视图在不同的偏移量上重复映射,或者...

    b. 使用 map/area标签 仅插入图像一次,但使每个图像部分的点击行为不同(将DOM布局工作减少到单个图像;所有其他DOM元素必须存在于树中,但不需要被渲染)


非常感谢您的想法!请忽略我之前的评论。我现在明白了您所说的一个带有地图/区域的图像,那可能会起作用(虽然我仍然希望在悬停在表情符号上时有一些悬停效果)。我猜我会找到一个解决方法。 - CodeOverload

2

这是一个将工作分成块的函数:

function load_emojis(arr){
    var chunk = 20;
    $('.emojis').html(''); // clear before start, just to be sure
    (function loop(i) {
        if (i >= arr.length) return; // all done
        var $emojis = [];
        $.each(arr.slice(i, i+chunk), function(){
            $emojis.push($('<span/>').append($('<img/>')));
        });
        $('.emojis').append($emojis);
        setTimeout(loop.bind(null, i+chunk));
    })(0);
}

这将为您的数组中的每20个项目执行一次 setTimeout

显然,完成总时间会更长,但是在许多小暂停期间仍可以发生其他JS和用户事件。

我省略了 setTimeout 的第二个参数,因为默认值(0)足以让出事件队列中的其他任务。

此外,我觉得您使用 html() 有点奇怪,因为文档允许参数为字符串或函数,但您提供了一个jQuery元素数组...在这种情况下,应使用 append() 函数。

根据需要调整 chunk 大小,以找到理想大小。它可能比只有20个更大。


这是一个非常有趣的想法 - 我会尝试并回复你。 - CodeOverload
这就是我本来会回答的内容。但是我会移除数组,直接将其附加到DOM中。 - Darkrum
不需要分块。 - Darkrum
@Darkum,分块允许减少setTimeout步骤的数量,这可能很有趣,因为setTimeout引入了最小延迟4ms,这将对1500个项目的完成时间造成最短6秒的影响,这可能是可以接受的,也可能不可接受。 - trincot
@trincot 很有趣,我不知道 setTimeout 中会添加最小延迟,我一直以为如果没有传递参数,它就会立即在排队的事件之后插入此事件。(也许我把它和一个特定于 node.js 的函数混淆了) 但是从那个链接中看来,postMessage 似乎可以添加零延迟的功能。 - Darkrum

1

我的第一个解决方案不起作用,它只是推迟了问题。 你最好的选择是大大优化这些函数,以使其真正快速执行。

function load_emojis(arr) {
  var emojis = new Array(arr.length);
  var emojisDiv = document.querySelector('.emojis');
  for (var i = 0; i < arr.length; i++) {
    emojis[i] = '<span><img src="" alt=""/></span>';
  }
  emojisDiv.innerHTML = emojis.join('');
}

无法工作:这个问题有一个好的解决方案,但它不是跨浏览器的,只能在Chrome、Firefox和Opera中使用:使用window.requestIdleCallback。

function load_emojis(arr) {
  window.requestIdleCallback(function() {
     var $emojis = [];
     # arr.length = 1500
     _.each(arr, function(){
       $emojis.push($('<span/>').append($('<img/>')));
     });

     $('.emojis').html($emojis);
  });
}

请查看https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback

这里有一个polyfill:https://github.com/PixelsCommander/requestIdleCallback-polyfill


有趣的是,我已经尝试在整个函数上使用它,但没有任何区别。不过我还是会继续阅读它。 - CodeOverload
只要工作没有回到事件循环,等待空闲还是不等待都无所谓;只要你的JS在运行时没有让出控制权,浏览器就会无响应。因此,如果你在时间X处处于空闲状态,你的代码将在时间X+1分派,而用户在时间X+2尝试点击、滚动等操作时,他们的操作将等待直到你的代码返回控制权给事件循环,即使这个过程一直持续到时间X+1000。 - ShadowRanger
@Gatsbill 使用 HTML 字符串而非对象可以显著减少代码该部分的执行时间,然而当您附加最终字符串时,它需要相同的时间(我猜测 JS 解析 HTML 字符串,并且以我们已经使用的方式创建对象),换句话说,整个函数仍需要相同的时间来执行。 - CodeOverload
@Ryan,移除jQuery方法可以提高函数的速度,字符串拼接要比jQuery创建方法快,但我猜这还不够。你应该看一下trincot的想法。 - Gatsbill

0

Javascript是单线程的,所以简短的回答是不行。因此,setTimeout将在同一线程中执行函数,但会允许您的代码继续运行,直到超时完成(此时它将被阻塞)。有关事件循环的更多信息,请参见MDN的优秀解释

话虽如此,浏览器实现了Web Worker API,允许您启动单独的进程来并发运行JS代码。因此,您可以在浏览器中实现某种程度的多线程。不幸的是,工作进程没有与主进程相同的API访问权限,并且工作进程无法直接呈现到DOM,正如这篇文章所解释的那样。


0

只有一个线程可以执行DOM操作。您可以使用WebWorkers在单独的线程中执行非DOM操作,但显然这不起作用,因为您正在添加到DOM。

您一次执行1501个DOM操作,每个DOM操作都非常昂贵。您可以轻松将其减少到1个DOM操作:

function load_emojis(arr){
  var $emojis = [];
  # arr.length = 1500
  _.each(arr, function(){
    $emojis.push('<span><img src="..."/></span>');
  });

  $('.emojis').html($emojis.join(''));
}

这应该比单个简单的DOM操作慢一个数量级,但比慢1500倍要好得多。在将其添加到DOM之前,应该放入src,因为您不能再轻松地访问单个图像了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接