昨天上午,群友 @小陈 在群里发了几个自称隔壁转发的面试题,其中有一道题目我感觉比较有意思。

在一个页面上,以追加方式追加 1 万个 div,每个 div 里显示一个数字,依次为 1、2、3 直到 10000,但不是一下子全显示出来,动态的从 1 依次显示到 10000,你会怎么做,请说明你的思路或代码片段。

刚看到这道题,我以为是要手写实现一个虚拟 DOM 和虚拟滚动(React 太入魔了),但这难度有亿点点高啊!@玩水 大佬很快给出了自己的答案:requestAnimationFramecreateDocumentFragment

前者是浏览器完成渲染一次后的回调函数,执行一次后即失效。

MDN:你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 window.requestAnimationFrame()

后者则是我完全没认识过的一个 API,我把它理解成了一个“中间件”(就是中介,类似暂存区的东西),而实际上的“中间件”应该不是这样的意思。

我突然想到 React 有一个 Fragment(或者是 <>)标签,它们应该是同一种东西。这种节点的特点就是,你可以一次性将多个子元素插入,当你把多个存放了多个子元素的 Fragment 分别插入到一个父节点里,实际上父节点里只会出现 Fragment 的所有子节点,其本身并非是一个“真实”存在的节点。

如果你没看懂我上面那句话的意思,不妨看看这段代码里数组之间的关系,你就绝对明白了:

// Before
const a = [1, 2, 3, 4, 5];
const b = [6, 7, 8, 9, 10];
const dom = [];

// After
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // createDocumentFragment
[[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]] // createElement

回过头来看,这道题本身也是存在一些“文字游戏”的,我起初并没有使用 Fragment,单纯的用 requestAnimationFrame 实现了「动态依次显示到 1000」的效果(肉眼可见,速度比较慢)。

let i = 0;

function frame() {
  if(i < 1000){
    const div = document.createElement('span');
    div.innerHTML = i;
    document.body.appendChild(div);
    i++;

    // 重点,自己调用自己实现刷新
    window.requestAnimationFrame(frame);
  }
}

window.requestAnimationFrame(frame);

我让 @小陈 发出了自己的题解,大致意思就是使用 createDocumentFragment 先提前创建 100 个节点,再插入到 DOM 里面触发 requestAnimationFrame 函数,实现一次执行插入 100 条数据的效果,而这也是符合「不是一下子全显示出来,依次显示」这个说法的。

至于为什么不要“一下子全显示出来”,主要原因还是浏览器本身的渲染机制吧。如果你用一个真实 DOM 来不断执行 appendChild 实现添加节点,就会不断触发浏览器的重新渲染,肯定会造成卡顿和性能浪费。

实际上,现代浏览器也对这种代码做了优化,遇到这种频繁操作最终会合并成一个操作去执行,实际执行上的差异依旧存在,但会小很多。我只能说,牛逼!面试题果然就是面试题啊。

这个时候就需要不在视图上显示的 DOM 来实现这种功能,前面也提到了 createElement 也能实现,只是节点上的差异,这样插入元素到视图里,就既流畅又能同时在子节点创建时就自带注册事件等功能了。

在 JQ 时代还有一个很常见的一个操作就是遍历生成字符串,最后用 innerHTML 进行修改,但这种办法只能再用 getElementsByClassName 一类的办法重新捕获节点,再注册事件了。