Virtual DOM 是一种编程理念。UI 信息被特定语言描述并保存到内存中,再通过特定的库,例如 reactDOM 与真实的 DOM 同步信息。这一过程成为 协调 (Reconciliation)。
Virtual DOM 反映到实际的数据结构上,就是每一个 React 的 fiber node
// UI 组件描述
const Span = (props) => <span></span>
// 实际的 Fiber node structure
{
stateNode: new htmlSpanElement,
type: "span",
alternate: null,
key: null,
updateQueue: null,
memoizedState: null,
pendingProps: {},
memoizedProps: {},
tag: 1,
effectTag: 0,
nextEffect: null
}
这一抽离结构有点像 React 版本的 AST 抽象语法树。
在 Virtual DOM -> Real DOM 之间的转换过程中,需要高效率的算法来支撑。由于某个时刻调用 React render() 方法生成的 React 元素组成的树,与下一次 state 或 props 变化时调用同一个 render 返回的树是不一样的,React 需要根据这两个不同的树来决定如何高效地让最新的 Virtual DOM 反应到真实 DOM 中。
Diffing 算法就是解决如何更有效率地更新 UI 的关键。
React 采取了一个复杂度为 O(n) 的比较策略,这个策略有两个假设
如果为不同类型,React 将会把原有的树拆卸并重新建立新的树。例如 <div> -> <span>。
在根节点以下的组件也会被卸载,它们的状态会被销毁。例如:
<div>
<Counter />
</div>
<span>
<Counter />
</span>
当对比两个同类型的 React 元素时,React 会保留 DOM 节点,仅对比以及更新有变化的属性
<div className="before" title="stuff" />
<div className="after" title="stuff" />
通过对比两个元素,React 得知 className 变化,所以只需要更新 DOM 对应元素上的 class。
当处理完当前节点时,React 将会对子节点进行递归。
当一个 React 组件需要更新时(例如 props 有变化),组件实例保持不变,实例中的 state 能在不同渲染时保持一致。React 将更新该组件实例的 props 以保持与最新的元素的一致。并调用 该实例的原型 上的函数 getDerivedStateFromProps(官方文档是 componentWillReceiveProps 和 componentWillUpdate,但这将会被弃用)。
下一步是调用该实例的 render 方法,diffing 算法将在之前的结果和最新的结果中进行递归。
在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表,当发现两个子元素有差异时,将生成一个「变种(mutation)」。
例如在子元素列表末尾新增元素时,更变开销比较小。比如:
// before
<ul>
<li>first</li>
<li>second</li>
</ul>
// after
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React 会匹配两个 <li>first</li> 对应的树、两个 <li>second</li> 对应的树,然后插入 <li>third</li> 树。
但如果就这样简单实现的话,那么在列表头部插入会很影响性能,更变的开销会比较大。比如:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
React 会认为每个子元素都「改变(mutate)」了,而不会认为可以保持 <li>Duke</li> 和 <li>Villanova</li> 子树不变,从而导致重新渲染。这种情况下的低效可能会带来性能问题。
为了解决以上问题,React 支持 key 属性。当子传入 key 到子元素时,React 通过 key 来匹配比较原有树上的子元素以及最新树上的子元素的差异。以下例子在新增 key 之后使得之前的低效转换变得高效:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
现在 React 知道只有带着 '2014' key 的元素是新元素,带着 '2015' 以及 '2016' key 的元素仅仅移动了位置。
所以一般在开发的时候最好使用一个有唯一属性的 id 来作为 key
<li key={item.id}>{item.name}</li>
在开发者自己确定数组数据不会轻易改变的情况下才可以用数组下表来作为 key。
上述只是 协调算法(reconciliation algorithm)的实现细节而已。React 可以响应每一次 action 后重新渲染整个应用,最终结果也会是一样的。
需要明确知道的是,在当前上下文(this context)中重新渲染(rerender)意味着会调用所有的 component 的 render(),但并不意味着 React 会卸载(unmount)或重载(remount)它们。它(协调算法)只会用上述规则在其过程中找出不同。
React 是 facebook 出的一个前端框架. 设计的关键处就是性能问题。在本文中,我主要是介绍 Diff 算法以及 React 渲染 ,这样你可以更好的优化你的应用程序。
如果不了解virtual dom,要理解diff的过程是比较困难的。虚拟dom对应的是真实dom, 使用document.CreateElement 和 document.CreateTextNode创建的就是真实节点。vue2.0才开始使用了virtual dom,有向react靠拢的意思。
关于react的虚拟dom以及每次渲染更新的dom diff,网上文章很多。但是我一直信奉一个原则,即:但凡复杂的知识,理解之后都只需要记忆简单的东西,而想简单、精确描述一个复杂知识,是极困难的事。
传统diff计算两颗树形结构差异并进行转换,传统diff算法是这样做的:循环递归每一个节点;传统diff算法复杂度达到O(n^3 )这意味着1000个节点就要进行数10亿次的比较,这是非常消耗性能的
为什么在Vue3.0都已经出来这么久了我还要写这篇文章,因为目前自己还在阅读Vue2.x的源码,感觉有所悟。作为一个刚毕业的新人,对Vue框架的整体设计和架构突然有了一点认知,所以才没头没尾地突然写下了diff算法。
fiber上的updateQueue经过React的一番计算之后,这个fiber已经有了新的状态,也就是state,对于类组件来说,state是在render函数里被使用的,既然已经得到了新的state
Vue 源码中虚拟 DOM 与 Diff 算法的实现借鉴了 snabbdom 这个库,snabbdom 是一个虚拟 DOM 库,它专注于简单,模块化,强大的功能和性能。要彻底明白虚拟 DOM 与 Diff 算法就得分析 snabbdom 这个库到底做了什么?
所谓虚拟DOM就是用js对象来描述真实DOM,它相对于原生DOM更加轻量,因为真正的DOM对象附带有非常多的属性,另外配合虚拟DOM的diff算法,能以最少的操作来更新DOM,除此之外
那么需要真实的操作DOM100w次,触发了回流100w次。每次DOM的更新都会按照流程进行无差别的真实dom的更新。所以造成了很大的性能浪费。如果循环里面是复杂的操作,频繁触发回流与重绘
目前前端使用最多的就是 vue 或 react 了,我们在学习这两个框架的过程中,总有一个绕不开的话题:vnode,也就是虚拟 dom。什么是虚拟 DOM,引用一段 vue 官方的解释就是:
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!