React中的Virtual DOM
什么是虚拟DOM
本质上是 Javascript 对象,是对DOM更加轻量级的描述。
起源
React是由facebook开发,其中的Virtual DOM自然也离不开facebook的影子。
字符串拼接时代 - 2004
动态页面需要根据用户的操作不断的对页面进行渲染,这就需要后端的支持,此时,大家还都是在用PHP的字符串拼接开发网站。
$str = '<ul>'; foreach ($talks as $talk) { $str += '<li>' . $talk->name . '</li>'; } $str += '</ul>';
代码不优雅,容易出现XSS攻击,较差的用户体验都是这种方法存在的问题。
XHP 时代 - 2010
到了 2010 年,为了更加高效的编码,同时也避免转义 HTML 标签的错误,Facebook 开发了 XHP 。XHP 是对 PHP 的语法拓展,它允许开发者直接在 PHP 中使用 HTML 标签,而不再使用字符串。
$content = <ul />; foreach ($talks as $talk) { $content->appendChild(<li>{$talk->name}</li>); }
JSX - 2013
到了 2013 年,前端工程师 Jordan Walke 向他的经理提出了一个大胆的想法:把 XHP 的拓展功能迁移到 JS 中。首要任务是需要一个拓展来让 JS 支持 XML 语法,该拓展称为 JSX。因为当时由于 Node.js 在 Facebook 已经有很多实践,所以很快就实现了 JSX。
const content = ( <TalkList> { talks.map(talk => <Talk talk={talk} />)} </TalkList> );
React - Now
在这个时候,就有另外一个很棘手的问题,那就是在进行更新的时候,需要去操作 DOM,传统 DOM API 细节太多,操作复杂,所以就很容易出现 Bug,而且代码难以维护。
然后就想到了 PHP 时代的更新机制,每当有数据改变时,只需要跳到一个由 PHP 全新渲染的新页面即可。
从开发者的角度来看的话,这种方式开发应用是非常简单的,因为它不需要担心变更,且界面上用户数据改变时所有内容都是同步的。
为此 React 提出了一个新的思想,即始终整体“刷新”页面
当发生前后状态变化时,React 会自动更新 UI,让我们从复杂的 UI 操作中解放出来,使我们只需关于状态以及最终 UI 长什么样。
局部刷新:
// 下面是伪代码 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> }
整体刷新虽然让我们忽略了操作DOM的细节,但是却带来了性能差的问题。
Diff
有人便想到了使用版本控制的思想解决上述问题,先对比前后变化的差异,再进行刷新。
Virtual 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 。
那么现在的过程就是这样:
维护一个使用 JS 对象表示的 Virtual DOM,与真实 DOM 一一对应 对前后两个 Virtual DOM 做 diff ,生成变更(Mutation) 把变更应用于真实 DOM,生成最新的真实 DOM
可以看出,因为要把变更应用到真实 DOM 上,所以还是避免不了要直接操作 DOM ,但是 React 的 diff 算法会把 DOM 改动次数降到最低。
总结
传统前端的编程方式是命令式的,直接操纵 DOM,告诉浏览器该怎么干。这样的问题就是,大量的代码被用于操作 DOM 元素,且代码可读性差,可维护性低。
React 的出现,将命令式变成了声明式,摒弃了直接操作 DOM 的细节,只关注数据的变动,DOM 操作由框架来完成,从而大幅度提升了代码的可读性和可维护性。
在初期我们可以看到,数据的变动导致整个页面的刷新,这种效率很低,因为可能是局部的数据变化,但是要刷新整个页面,造成了不必要的开销。
所以就有了 Diff 过程,将数据变动前后的 DOM 结构先进行比较,找出两者的不同处,然后再对不同之处进行更新渲染。
但是由于整个 DOM 结构又太大,所以采用了更轻量级的对 DOM 的描述—虚拟 DOM。
不过需要注意的是,虚拟 DOM 和 Diff 算法的出现是为了解决由命令式编程转变为声明式编程、数据驱动后所带来的性能问题的。换句话说,直接操作 DOM 的性能并不会低于虚拟 DOM 和 Diff 算法,甚至还会优于。