VirtualDOM原理

VirtualDOM原理

Tags
Published
Author

Virtual DOM 原理

Virtual DOM是什么?

官方定义:Virtual DOM是一种编程理念(数据驱动视图),将UI虚拟的保持到内存中,并且通过某些库渲染成真实的dom,这个过程又叫做协调。
通俗来讲,就是对HTML节点进行抽象,用js对象的形式表示HTML

Virtual DOM VS 原生DOM

DOM操作是很慢的,其元素非常庞大,页面的性能问题鲜有由JS引起的,大部分都是由DOM操作引起的 ### 原生DOM更新
DOM API 调用更新UI

Virtual DOM更新

以React为例 1. 每次render都会产生一份新的React DOM 2. Virtual DOM要对新旧React DOM进行比较,也就是常说的diff算法,从而确定在旧DOM的基础上进行多少变更 3. 确定最优的变更策略之后调用DOM API更新

Virtual DOM 数据结构

虽然一个DOM节点有N个属性,但是我们平时在操作DOM时,只需要以下3个基本信息:
  • 标签名: tagName
  • HTML属性: attribute
  • 子节点: children
对于其他属性我们并不关心,于是,我们可以对一段DOM进行以下抽象:
<li class="item" <a>link</a> </li> { tag:'li', props:{ class:['item'] }, children:{ tag:'a', props:{}, children:'link' } }
可以看到,一段DOM片段被抽象成了JS对象,对应着节点的tagName,attribute和children。也就是说,React组件中,render方法返回的是一个JS对象。
class List extends React.Component { ...... //render方法返回的是JS对象 render() { return ( <li className="item"> <a>link</a> </li> ); }}
既然React中得到的是JS对象而不是DOM,那么对象是在哪里转换成真实的DOM呢?答案就在ReactDOM.render方法中:
//将得到的虚拟DOM转化为真实DOMReactDOM.render(<List/>, $container);
FaceBook将React拆分成了两个库,一个是React的核心,另一个是针对React运行平台的渲染库,这里运行平台是浏览器,所以渲染库就是ReactDOM。
这样做的好处在哪?
  1. 性能提升:将对DOM的操作提升到了对JS对象的操作,单纯操作JS对象的性能肯定会优于操作庞大的DOM对象的性能。
  1. 抽象扩展性更强,不再仅限于渲染DOM:React的设计思想是UI = F(State,props),在传统的前端开发思想中,这个UI可能就是DOM,现在React在开发者和DOM之间抽象了一层,这一层抽象可以被设计得十分强大:对于开发者可以有着相同的API,但是另一边不仅仅可以对接DOM,还可能时Native或者是服务端渲染(这也是React Native实现的基础),所以这个UI的概念就很自然地被扩大了。核心功能与渲染职责的拆分使得React变成了一个平台无关的UI库。
notion image
虚拟DOM中间层

Virtual DOM Diff

如果说虚拟DOM是React的核心,那么diff算法就是虚拟DOM的核心
通常情况下,对DOM结构进行修改,我们可以用jquery直接操作,现在有了虚拟DOM,事情反而变得复杂起来了。因为当你对虚拟DOM进行修改时,按理来说,React需要将你修改的虚拟DOM映射回真实的DOM上面去。但是问题是,React根本不知道你修改了哪个地方,那么现在办法有两个:
  1. 按照新的虚拟DOM结构,重新生成一个整个真实DOM。
  1. 将新、旧两个虚拟DOM进行对比,找出不同的地方,更新到真实DOM。
很显然第一个办法的效率是最低的(但是足够简单粗暴),React采用的是第二种方法,这个方法就是diff算法,顾名思义diff算法就是对比两个虚拟DOM差异的算法
diff算法发生在setState方法里面:
/* * setStete到底做了什么: * 1. 将新,旧state进行对比合并 * 2. 生成一个新的虚拟DOM * 3. 将新,旧虚拟DOM进行对比,记录差异 * 4. patch:将差异更新到真实DOM */ handler() { this.setState(); }
也就是说,设计一个diff算法主要有两个要点: - 如何比较两个JS对象树 - 如何记录对象之间的差异
由于diff操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n^3),其中n是树中元素的数量。
而一个文档的DOM结构有上百个节点是很正常的情况,如果在React中使用了该算法,那么展示1000个元素所需要执行的计算量将在十亿的量级范围。所以React团队进行了优化,预设三个限制: > - 只对同级元素进行diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。 > - 两个不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。 > - 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分(开发者可以通过设置 key 来表示哪些子节点能够保持稳定)
// 更新前<div> <p key="p-node">ka</p> <h3 key="h-node">song</h3></div>// 更新后<div> <h3 key="h-node">song</h3> <p key="p-node">ka</p></div>
如果没有key,React会认为div的第一个子节点由p变为h3,第二个子节点由h3变为p。这符合限制2的设定,会销毁并新建
但是当我们用key指明了节点前后对应关系后,React知道key === “p-node”的p在更新后还存在,所以DOM节点可以复用,只是需要交换下顺序。