如果你觉得虚拟 DOM 很快,那么这篇文章可能就是你所缺少的
我经常听到有人在群里,或者在社区里说的一个很严重的错误,那就是说 React 的 Virtual Dom 是以快出名的,比原生 DOM 快多了,啥啥啥的,每次都一两句话说不清楚,所以下次有谁再说 React 是以快出名的,你就把这篇文章丢给他,下面进入正题。
在过去的几年里,你一直在跟踪 JavaScript 社区的发展,你至少听说过 Virtual DOM(React,Vue.js 2,Riot.js,Angular 2等等)。他们承诺(或者更确切地说,他们的宣传)更快的渲染界面,特别是更新,减少麻烦。你很快的上手了使用虚拟DOM的应用程序,这很好。几个月后,您的应用程序现在变得越来越复杂,你可能从用户交互到屏幕更新只需要一两秒钟的更新。你可能会想,这东西很神奇,应该会比 jQuery 快,但是实际上不是这个样子的。
虽然我同意虚拟 DOM 为我们提供了很多便利,但我将解释为什么我认为根据定义,更快的渲染和更快的更新是不正确的。要付出代价,其利益并不是大多数人想象或至少希望的。
要阅读本文,您需要熟悉DOM。理想情况下,您至少可以使用 DOM API。如果你只使用 DOM API 构建东西,你可能不需要这篇文章,但我仍然希望你阅读它并在评论中留下一点评语。
渲染和更新
让我们来看看手动执行 DOM 节点的创建和更新的鸟瞰图。这对于理解虚拟DOM如何工作以及它解决了哪些问题非常重要。
在谈论 JavaScript Web 应用程序时,用户界面的更改通过 DOM 操作发生。这个过程分为两个阶段:
- JS 部分:定义 JavaScript 世界中的变化
- DOM 部分:使用 DOM API 函数和属性执行更改
性能是根据整个过程的速度来衡量的,但了解每部分的速度也很重要,以便了解要优化的内容。
有两种方法可以创建和更新DOM树的各个部分。
①字符串方式创建
使用字符串既快速又简单,但在更新方面并不是非常精细。对于字符串,JS部分是它如此之快的原因。您可以在几毫秒内创建一段代表5000个节点的HTML。这是一个例子:
const userList = document.getElementById("user-list"); // JS 部分 const html = users.map(function (user) { return ` <div id="${user.id}" class=”user”> <h2 class="header">${user.firstName} ${user.lastName}</h2> <p class="email"><a href=”mailto:${user.email}”>EMAIL</a></p> <p class="avg-grade">Average grade: ${user.avgGrade}</p> <p class="enrolled">Enrolled: ${user.enrolled}</p> </div> ` }).join(""); // DOM 部分 userList.innerHTML = html;
我提到使用这种方法时存在局限性。请考虑以下示例:
const search = document.getElementById("search"); search.innerHTML = `<input class="search" type="text" value="foo">`; // Change value to "bar"? search.innerHTML = `<input class="search" type="text" value="bar">`;
虽然看起来上面的内容很简单,但它实际上并不起作用。当我们运行上面的代码时,原始<input>
元素被替换而不是更新,例如,如果用户有焦点的字段,他们将失去焦点。
②使用 DOM 对象
创建和更新 DOM 树的另一种方法是使用 DOM 对象。就你必须编写的代码而言,这种方法非常冗长,而且总体来说它也慢得多。
让我们使用这个方法重写用户列表示例:
const userList = document.getElementById(“user-list”); // JS part const = document.createDocumentFragment(); users.forEach(function(user){ const div = document.createElement(“div”); div.id = user.id; div.className =“user”; const header = document.createElement(“h2”); h2 .className =“header”; h2.appendChild( document.createTextNode(`$ {user.firstName} $ {user.lastName}`) ); // .... frag.appendChild(div); }); // DOM部分 userList.innerHTML =“”; userList.appendChild(FRAG);
这看起来不太好,但它仍然是创建DOM节点的有效方法。它还有一个优点,即我们能够将它与第三方库(如D3)混合使用,以执行 HTML 字符串不易处理的事情。在真正的优势,虽然是执行粒度更新现有的树时:
const search = document.getElementById(“search”); search.innerHTML =`<input class ="search" type ="text"value ="foo">`; //将值更改为“bar”? search.querySelector("input")。value ="bar";
这次我们结合快速方便的字符串 HTML 方法来创建初始 UI,然后我们使用 DOM 操作方法来更新 value 属性。不像我们第一次这样做,<input>
现在没有被替换,所以它不会像第一个例子那样引起 UX 故障。
进入虚拟DOM
让我们回到输入示例的第一个版本:
const search = document.getElementById("search"); search.innerHTML = `<input class="search" type="text" value="foo">`; // Change value to "bar"? search.innerHTML = `<input class="search" type="text" value="bar">`;
如果我们参数化值部分,它将如下所示:
const search = document.getElementById("search"); const renderInput = function (value) { search.innerHTML = `<input class="search" type="text" value="${value}">`; }; renderInput("foo"); // Change value to "bar"? renderInput("bar");
好吧,新 renderInput() 功能肯定看起来很酷,但我们已经知道这不是好方法。
如果我们有一些骚操作可以让我们继续使用类似的东西,但同时弄清楚我们想要做什么并做正确的事情呢?第二次 renderInput() 被调用,我们只更新 value 属性,所以只更新该属性而不是重新渲染整个属性<input>
?
我们说过创建和更新 DOM 树的整个过程分为两个阶段。使用虚拟 DOM,DOM 阶段应该尽可能高效,代价是在 JS 阶段完成的额外工作。这项额外的工作会做 diff(不要以为 js 计算就不花费代价),因此它的另一个名称将是开销。根据定义,虚拟 DOM 比精心设计的手动更新慢,但它为我们提供了一个更方便的 API 来创建 UI。
虚拟DOM比精心设计的手动更新慢。
为什么有些开发人员认为Virtual DOM更快
在虚拟DOM(尤其是React)的早期,传播了一个神话,即虚拟 DOM 使 DOM 快速更新。正如我们在前面的章节中看到的那样,这在技术上是不可行的。DOM 更新就是它们的原因,并且没有任何魔法可以使它更快:它必须在浏览器的本机代码中进行优化。
可以看到 React 主页里面没有提到性能,而是开发人员的便利性。
React 的基本思维模式是每次有变动就整个重新渲染整个应用。如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。很多人都没有意识到,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作… 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。
您仍然可以看到比较各种虚拟 DOM 实现的基准测试,并且一些措辞会误导新开发人员认为虚拟 DOM 是当今事实上的标准,并且不值得对其他技术进行基准测试。然而,有一些基准可以将它与其他技术进行比较,例如 Aerotwist 的 React +性能文章,它描绘了虚拟 DOM 在宏观方案中所处位置的更真实的画面。
我们得到了什么?这值得么?
虚拟DOM最终是一种执行 DOM 更新的循环方式。但是,它打开了通向有趣架构的大门,例如将视图视为状态函数,或者编写和组合视图组件。虚拟 DOM 带来了很多好东西,尽管疯狂的性能水平不是其中之一。您可以将其视为 Python 或 PHP 中的编码与 C 中的编码之间的差异。我们以性能为代价获得更多的开发人员工具。换句话说,这是一种权衡。
另一方面,开发人员的时间丢失也是一些实现方面的事情。虚拟 DOM 试图弄清楚它需要执行哪些更改的部分是由人类实现的,因此它并不总是万无一失。有时你必须介入。在某些情况下,无法进行干预。对于绝对性能至关重要的事情,它甚至可能不是一种选择。
衡量您的表现并根据硬数据来决定。
最重要的是,虚拟DOM只是您可以使用的工具之一。衡量您的表现并根据硬数据来决定。数据绑定仍然非常可行,我们已经看到您也可以手动完成所有操作。它绝对不是万能的,因此没有必要与虚拟DOM结合。
结论
React 厉害的地方并不是说它比 DOM 快,而是说不管你数据怎么变化,我都可以以最小的代价来进行更新 DOM。 方法就是我在内存里面用心的数据刷新一个虚拟 DOM 树,然后新旧 DOM 进行比较,找出差异,再更新到 DOM 树上。
这就是所谓的 diff 算法,虽然说 diff 算法号称算法复杂度 O(n) 可以得到最小操作结果,但实际上 DOM 树很大的时候,遍历两棵树进行各种对比还是有性能损耗的,特别是我在顶层 setState 一个简单的数据,你就要整棵树 walk 一遍,而真实中我可以一句 jQuery 就搞定,所以就有了 shouldComponentUpdate
这种东西。
框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。
针对每一个点,都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。