文章首发自我的博客javascript
最近我发现不少面试题里面都有「如何理解虚拟 DOM」这个题,我以为这个题应该没有想象中那么好答,由于不少人没有真正理解虚拟 DOM 它的价值所在,我这篇从虚拟 DOM 的诞生过程来引出它的价值以及历史地位,帮助你深刻的理解它。php
本质上是 JavaScript 对象,这个对象就是更加轻量级的对 DOM 的描述。前端
对,就是这么简单!java
就是一个复杂一点的对象而已,没什么好说的,重点是为何要有这个东西,以及有了这个描述有什么好处才是咱们今天要介绍的内容。react
再谈为何要用虚拟 DOM 以前,先来聊一聊 React 是怎么诞生的,毕竟在了解历史背景,再去思考他的诞生,就知道是必然会出现的。git
再查了不少关于 React 的历史相关的文章,这篇文章我感受比较值得令我信服:React 是怎样炼成的。面试
众所周知,Facebook 是 PHP 大户,因此 React 最开始的灵感就来至于 PHP。算法
在 2004 年这个时候,你们都还在用 PHP 的字符串拼接来开发网站:编程
$str = '<ul>';
foreach ($talks as $talk) {
$str += '<li>' . $talk->name . '</li>';
}
$str += '</ul>';
复制代码
这种方式代码写出来很差看不说,还容易形成 XSS 等安全问题。小程序
应对方法是对用户的任何输入都进行转义(Escape)。可是若是对字符串进行屡次转义,那么反转义的次数也必须是相同的,不然会没法获得原内容。若是又不当心把 HTML 标签(Markup)给转义了,那么 HTML 标签会直接显示给用户,从而致使不好的用户体验。
到了 2010 年,为了更加高效的编码,同时也避免转义 HTML 标签的错误,Facebook 开发了 XHP 。XHP 是对 PHP 的语法拓展,它容许开发者直接在 PHP 中使用 HTML 标签,而再也不使用字符串。
$content = <ul />;
foreach ($talks as $talk) {
$content->appendChild(<li>{$talk->name}</li>);
}
复制代码
这样的话,全部的 HTML 标签都使用不一样于 PHP 的语法,咱们能够轻易的分辨哪些须要转义哪些不须要转义。
不久的后来,Facebook 的工程师又发现他们还能够建立自定义标签,并且经过组合自定义标签有助于构建大型应用。
到了 2013 年,前端工程师 Jordan Walke 向他的经理提出了一个大胆的想法:把 XHP 的拓展功能迁移到 JS 中。首要任务是须要一个拓展来让 JS 支持 XML 语法,该拓展称为 JSX。由于当时因为 Node.js 在 Facebook 已经有不少实践,因此很快就实现了 JSX。
能够猜测一下为何要迁移到 js 中,我猜测应该是先后端分离致使的。
const content = (
<TalkList> { talks.map(talk => <Talk talk={talk} />)} </TalkList> ); 复制代码
在这个时候,就有另一个很棘手的问题,那就是在进行更新的时候,须要去操做 DOM,传统 DOM API 细节太多,操做复杂,因此就很容易出现 Bug,并且代码难以维护。
而后就想到了 PHP 时代的更新机制,每当有数据改变时,只须要跳到一个由 PHP 全新渲染的新页面便可。
从开发者的角度来看的话,这种方式开发应用是很是简单的,由于它不须要担忧变动,且界面上用户数据改变时全部内容都是同步的。
为此 React 提出了一个新的思想,即始终总体“刷新”页面
当发生先后状态变化时,React 会自动更新 UI,让咱们从复杂的 UI 操做中解放出来,使咱们只需关于状态以及最终 UI 长什么样。
下面看看局部刷新和总体刷新的区别。
图片来自于极客时间王沛老师的《React进阶与实战》
局部刷新:
// 下面是伪代码
var ul = find(ul) // 先找到 ul
ul.append(`<li>${message3}</li>`) //而后再将message3插到最后
// 想一想若是是不插到最后一个,而是插到中间的第n个
var ul = find(ul) // 先找到 ul
var preli = find(li(n-1)) // 再找到 n-1 的一个 li
preli.next(`<li>${message3}</li>`) // 再插入到 n-1 个的后面
复制代码
总体刷新:
UI = f(messages) // 总体刷新 3 条消息,只须要调用 f 函数
// 这个是在初始渲染的时候就定义好的,更新的时候不用去管
function f(messages) {
return <ul> {messages.map(message => <li>{ message }</li>)} </ul>
}
复制代码
这个时候,我只须要关系个人状态(数据是什么),以及 UI 长什么样(布局),再也不须要关系操做细节。
这种方式虽然简单粗暴,可是很明显的缺点,就是很慢。
另外还有一个问题就是这样没法包含节点的状态。好比它会失去当前聚焦的元素和光标,以及文本选择和页面滚动位置,这些都是页面的当前状态。
为了解决上面说的问题,对于没有改变的 DOM 节点,让它保持原样不动,仅仅建立并替换变动过的 DOM 节点。这种方式实现了 DOM 节点复用(Reuse)。
至此,只要可以识别出哪些节点改变了,那么就能够实现对 DOM 的更新。因而问题就转化为如何比对两个 DOM 的差别。
说道对比差别,可能很容易想到版本控制(git)。
DOM 是树形结构,因此 diff 算法必须是针对树形结构的。目前已知的完整树形结构 diff 算法复杂度为 O(n^3) 。
可是时间复杂度 O(n^3) 过高了,因此Facebook工程师考虑到组件的特殊状况,而后将复杂度下降到了 O(n)。
附:详细的 diff 理解:难以想象的 react diff 。
前面说到,React 其实实现了对 DOM 节点的版本控制。
作过 JS 应用优化的人可能都知道,DOM 是复杂的,对它的操做(尤为是查询和建立)是很是慢很是耗费资源的。看下面的例子,仅建立一个空白的 div,其实例属性就达到 231 个。
// Chrome v63
const div = document.createElement('div');
let m = 0;
for (let k in div) {
m++;
}
console.log(m); // 231
复制代码
对于 DOM 这么多属性,其实大部分属性对于作 Diff 是没有任何用处的,因此若是用更轻量级的 JS 对象来代替复杂的 DOM 节点,而后把对 DOM 的 diff 操做转移到 JS 对象,就能够避免大量对 DOM 的查询操做。这个更轻量级的 JS 对象就称为 Virtual DOM 。
那么如今的过程就是这样:
能够看出,由于要把变动应用到真实 DOM 上,因此仍是避免不了要直接操做 DOM ,可是 React 的 diff 算法会把 DOM 改动次数降到最低。
剩下的历史就不谈了,已经引出这篇文章的重点:虚拟 DOM。详细的历史可见:React 是怎样炼成的,文中历史部份内容不少摘抄与此。
传统前端的编程方式是命令式的,直接操纵DOM,告诉浏览器该怎么干。这样的问题就是,大量的代码被用于操做 DOM 元素,且代码可读性差,可维护性低。
React 的出现,将命令式变成了声明式,摒弃了直接操做 DOM 的细节,只关注数据的变更,DOM 操做由框架来完成,从而大幅度提高了代码的可读性和可维护性。
在初期咱们能够看到,数据的变更致使整个页面的刷新,这种效率很低,由于多是局部的数据变化,可是要刷新整个页面,形成了没必要要的开销。
因此就有了 Diff 过程,将数据变更先后的 DOM 结构先进行比较,找出二者的不一样处,而后再对不一样之处进行更新渲染。
可是因为整个 DOM 结构又太大,因此采用了更轻量级的对 DOM 的描述—虚拟 DOM。
不过须要注意的是,虚拟 DOM 和 Diff 算法的出现是为了解决由命令式编程转变为声明式编程、数据驱动后所带来的性能问题的。换句话说,直接操做 DOM 的性能并不会低于虚拟 DOM 和 Diff 算法,甚至还会优于。
这么说的缘由是由于 Diff 算法的比较过程,比较是为了找出不一样从而有的放矢的更新页面。可是比较也是要消耗性能的。而直接操做 DOM 就是有的放矢,咱们知道该更新什么不应更新什么,因此不须要有比较的过程。因此直接操做 DOM 效率可能更高。
React 厉害的地方并非说它比 DOM 快,而是说无论你数据怎么变化,我均可以以最小的代价来进行更新 DOM。 方法就是我在内存里面用新的数据刷新一个虚拟 DOM 树,而后新旧 DOM 进行比较,找出差别,再更新到 DOM 树上。
框架的意义在于为你掩盖底层的 DOM 操做,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架能够比纯手动的优化 DOM 操做更快,由于框架的 DOM 操做层须要应对任何上层 API 可能产生的操做,它的实现必须是普适的。
若是你想了解更多的虚拟 DOM 与性能的关系,请看下面公众号里面的两篇文章和那个知乎话题,会让你对虚拟 DOM 又更深层次的理解。
另外再提一个点,不少人会把 Diff 、数据更新、提高性能等概念绑定起来,可是你想一想这个问题:React 因为只触发更新,而不能知道精确变化的数据,因此须要 diff 来找出差别而后 patch 差别队列。Vue 采用数据劫持的手段能够精准拿到变化的数据,为何还要用虚拟DOM?
要想回答上面那个问题,真的不要仅仅觉得虚拟 DOM 或者 React 是来解决性能问题的,好处可还有不少呢。下面我总结了一些虚拟 DOM 好做用。
既然虚拟 DOM 有这么多做用,那么上面的问题,Vue 采用虚拟 DOM 的缘由是什么呢?
Vue 2.0 引入 vdom 的主要缘由是 vdom 把渲染过程抽象化了,从而使得组件的抽象能力也获得提高,而且能够适配 DOM 之外的渲染目标。 来自尤大文章:Vue 的理念问题
本文在介绍虚拟 DOM 并无像其余文章同样去解释它的实现以及相关的 Diff 算法,关于 Diff 算法能够看这篇 虚拟 DOM 究竟是什么?文中介绍了不少库的 diff 算法,可见其实 React 的 diff 算法并不算太快。
而是经过历史来得出他的价值体现,从历史怎么看大牛们是怎么一步一步的去解决问题,从历史中看为何别人能作出这么伟大的东西,而咱们不能?
每一个伟大的产品都会有很是多的背景支持,都是一步一步发展而来的。
另外洗清了一个错误观念:不少人认为虚拟 DOM 最大的优点是 diff 算法,减小 JavaScript 操做真实 DOM 的带来的性能消耗。
虽然这一个虚拟 DOM 带来的一个优点,但并非所有。虚拟 DOM 最大的优点在于抽象了本来的渲染过程,实现了跨平台的能力,而不只仅局限于浏览器的 DOM,能够是安卓和 IOS 的原生组件,能够是近期很火热的小程序,也能够是各类 GUI。
最后但愿你们多思考,跟随者浪潮站在浪潮之巅。
我是桃翁,一个爱思考的前端er,想了解关于更多的前端相关的,请关注个人公号:「前端桃园」,若是想加入交流群关注公众号后回复「微信」拉你进群