tag 存放 DOM 元素的标签名
props 存放标签内的所有属性
children 存放标签内嵌子元素
由于 DOM 本身就以树形结构展示,所以使用 JavaScript 对象类型就能很简单的表示。用 JavaScript 对象来表示 DOM 的好处有两点:1. 提升性能;2. 跨平台;具体优势参照如下。
虚拟 DOM 带来的优势:
提升性能:原生 DOM 因为浏览器厂商需要实现众多的规范(各种 HTML5 属性、DOM事件),即使创建一个空的 div 也要付出昂贵的代价(JS 操作真实 DOM 元素会带来巨大的性能消耗),而虚拟 DOM 则可通过 diff 算法比对新旧 Virtual DOM Tree (JavaScript 原生对象),找到需要变更的 DOM 节点,然后仅在真实 DOM 上对改动节点及其子节点进行更新操作,而不是更新整个视图,从而减少 JavaScript 操作真实 DOM 的带来的性能消耗。
跨平台:抽象渲染过程,实现跨平台的能力。(真实 DOM 局限于浏览器,而表示为 JavaScript 对象,就可以应用于安卓 / IOS等不同平台,甚至是小程序)
很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI。
-- 摘自 虚拟 DOM 到底是什么?
HTML 代码 -> Virtual DOM
h 函数是实现 HTML 代码向 Virtual DOM 转换的关键。主流的虚拟 DOM 库(snabbdom、virtual-dom),通常都有一个 h 函数。React 是通过 babel 将 jsx 转换为 h 函数渲染的形式,而 Vue 是使用 vue-loader 将模版转为 h 函数渲染的形式(也可以通过 babel-plugin-transform-vue-jsx 插件在 vue 中使用 jsx,本质还是转换为 h 函数渲染形式)。
关于子节点的对比,可以说是 diff 算法中,变动最多的部分,因为前面的部分,各个库对比的方向基本一致,而关于子节点的对比,各个仓库都在前者基础上不断得进行改进。
为什么需要改进子节点的对比方式? 如果我们直接按照深度优先遍历的方式,一个个去对比子节点,子节点的顺序发生改变,那么就会导致 diff 算法认为所有子节点都需要进行 replace,重新将所有子节点的虚拟 DOM 转换成真实 DOM,这种操作是十分消耗性能的。但是,如果我们能够找到新旧虚拟 DOM 对应的位置,然后进行移动,那么就能够尽量减少 DOM 的操作。
参考:
什么是 Virtual DOM ?
与其将 “Virtual DOM” 视为一种技术,不如说它是一种模式,是一种编程概念。在这个概念里, UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。
概念
Virtual DOM 本质就是一个普通的 JavaScript 对象,包含了 tag、props、children 三个属性:
由于 DOM 本身就以树形结构展示,所以使用 JavaScript 对象类型就能很简单的表示。用 JavaScript 对象来表示 DOM 的好处有两点:1. 提升性能;2. 跨平台;具体优势参照如下。
虚拟 DOM 带来的优势:
HTML 代码 -> Virtual DOM
h 函数是实现 HTML 代码向 Virtual DOM 转换的关键。主流的虚拟 DOM 库(snabbdom、virtual-dom),通常都有一个 h 函数。React 是通过 babel 将 jsx 转换为 h 函数渲染的形式,而 Vue 是使用 vue-loader 将模版转为 h 函数渲染的形式(也可以通过 babel-plugin-transform-vue-jsx 插件在 vue 中使用 jsx,本质还是转换为 h 函数渲染形式)。
h 函数
h 函数接受三个参数,对应位置分别为 DOM 元素的标签名 tag、属性 props、子节点 children,最终返回一个虚拟 DOM 的对象。以 React JSX 为例,babel 会将 jsx 转换为以 h 函数实现的形式,最终返回 Virtual DOM 对象:
渲染 Virtual DOM
前面提到,虚拟 DOM 一大优势在于它可以跨平台,这也就意味着,不同平台上,渲染虚拟 DOM 的方式也有所不同。以浏览器为例,虚拟 DOM 会被渲染为真实 DOM,然后被浏览器解析。
以浏览器环境为例,渲染 Virtual DOM 的流程如下:
将虚拟 DOM 渲染成真实 DOM 后,只需要插入到对应的根节点即可:
JSX -> 虚拟DOM -> 真实DOM
diff 算法
本质:对比新旧 Virtual DOM 对象的差异,将改动的部分更新到视图上。 代码实现:实现一个 diff 函数,接收新旧 Virtual DOM 作为参数,并将改动部分以某种方式渲染到视图上。
虚拟 DOM 库对 diff 算法的实现
不同虚拟 DOM 库对于 diff 算法有着自己不同的实现: 最开始出现的是 virtual-dom 这个库,通过深度优先搜索与 in-order tree 来实现高效的 diff 。 然后是 cito.js 采用两端同时进行比较的算法,将 diff 速度拉高到几个层次。 紧随其后的是 kivi.js,在 cito.js 的基础上提出两项优化方案,使用 key 实现移动追踪以及基于 key 的最长自增子序列算法应用(算法复杂度 为O(n^2))。但这样的 diff 算法太过复杂了,于是后来者 snabbdom 将 kivi.js 进行简化,去掉编辑长度矩离算法,调整两端比较算法。速度略有损失,但可读性大大提高。 再之后,就是著名的 vue2.0 把 sanbbdom 整个库整合掉了。
VDOM 对比规则
子节点对比
关于子节点的对比,可以说是 diff 算法中,变动最多的部分,因为前面的部分,各个库对比的方向基本一致,而关于子节点的对比,各个仓库都在前者基础上不断得进行改进。 为什么需要改进子节点的对比方式? 如果我们直接按照深度优先遍历的方式,一个个去对比子节点,子节点的顺序发生改变,那么就会导致 diff 算法认为所有子节点都需要进行 replace,重新将所有子节点的虚拟 DOM 转换成真实 DOM,这种操作是十分消耗性能的。但是,如果我们能够找到新旧虚拟 DOM 对应的位置,然后进行移动,那么就能够尽量减少 DOM 的操作。
virtual-dom 对子节点添加 key 值,其中 key 在当前子节点集合中必须是唯一标识的,通过 key 值的对比,来判断子节点是否进行了移动。通过 key 值对比子节点是否移动的模式,被各个库沿用,这也就是为什么主流的视图库中,子节点如果缺失 key 值,会有 warning 的原因。 cito.js 再此基础上改动了对子节点对比的算法,引入了两端对比,将 diff 算法的速度提升了几个量级。 kivi 的 diff 算法在 cito 的基础上,引入了最长增长子序列,通过子序列找到最小的 DOM 操作数。
为什么不用 index 作为 key ?
参考:轻松理解为什么不用Index作为key
具体例子参考文章,总结如下: 数组遍历时产生的 index 满足子节点 key 唯一标识的要求,当遍历的数组不发生更改时(增 / 删),看不出 index 作为 key 所带来的问题。一旦数组发生了更改,数组遍历时就会重新对新的子节点赋予 index,新 VDOM 中的子节点对应的 key 被重新赋值,导致 新旧 VDOM 中相同 key 的子节点对应的值不同。致命的问题在于,若数组改动发生在第一个子节点,无论是删除还是增加,都会让 diff 算法将后续值不相同的一连串子节点识别为需要重新渲染的目标,增大了开销。