看过前设想篇与数据篇后,相信你已对本文的标题虚拟DOM,到底快在哪有了答案,如果你不曾看过前两篇文章,建议先阅读一下,方便更好的理解本章内容。
本章内容将参考React 文档、源码、The Inner Workings Of Virtual DOM (Virtual DOM的工作原理)做更进一步的解析。
The Inner Workings Of Virtual DOM 文档地址:
https://medium.com/@rajaraodv/the-inner-workings-of-virtual-dom-666ee7ad47cf
01
—
React Virtual DOM简介
在上一章内容中,解释了React用JSX编程带来的好处,它让我们在JS中可以写HTML,在 { } 中又能方便的写JS。它方便了开发的编程专注于JS上,开启了函数式编程的时代。
React也详细的推出了它的好处:
https://reactjs.org/docs/introducing-jsx.html
它也将作为本章内容的导火索,因为我们编译时需要将JSX转为JS,JS生成VDOM,再转为真实DOM,才能交付到浏览器上,这是一套完整的流程。
当然,React也指出你也可以不用JSX,像这样去写DOM
React.createElement( type, [props], [...children])
const e = React.createElement;ReactDOM.render( e('div', null, 'Hello World'), document.getElementById('root'));
如果你对React中DOM基础的使用还不熟悉,建议阅读React DOM的标准:
React中dom:
https://reactjs.org/docs/dom-elements.html
React文档中对虚拟DOM只是很简单的阐述了一下概念,但是对Reconciliation(和解,调和)提出了比较有意义的几点,此处我将其译为对比,React DOM Tree对比的标准:
Reconciliation:
https://reactjs.org/docs/dom-elements.html
通过它你可以了解到 render 函数只是创建了React DOM Tree,而每次状态更新时,它将配合其他生命周期,返回不一样的Tree,然后再对比、有效的生成匹配最新的树。
02
—
React算法源码解析
这么大的项目,找到React这部分的源码实属不易。搜索了Reconciliation关键字后,位置锁定在了react-dom依赖包上。
也在React官网中找到如下解释,没错了就是它:
这里面也已有了将在17版本废弃的render,及新星hydrate的处理。
在React中SSR与CSR渲染的模板希望是一致的,否则将会给出警告。如果你不修复它们属性上的差异,将会严重影响算法的性能。如果无法避免差异它也提供了suppressHydrationWarning={true}消除警告,或采用双重(two-pass)渲染,但是多一次渲染也会影响速度。
打开react-dom:
目录结构很清晰。有客户端渲染如 ReactDOM.render,有服务端渲染如 ReactDOM.renderToString,及事件、公共部分的文件夹。
我们主要看 client 部分
打开index.js, 我们可以看到如下export
顺着render一路追下去找到了ReactDOMLegacy.js文件,这里面是封装render的入口
同时我们也找到了hydrate与它的使用区别,面上看就第一个参数不同
他们都先走了isValidContainer节点验证,然后调用 legacyRenderSubtreeIntoContainer,字面意思理解它为 render一个container的产物。主要第二个参数接收的是个节点。
进入这个函数,首先引用 ReactDOMRoot.js 的 RootType 创建了根实例,这期间我们先略过,然后更新清除了该节点,最后将创建好的根配置给了函数 getPublicRootInstance 来自另一个包 react-reconciler,它的主要作用是自定义渲染器,然后暴露出来。这是render的一套大体流程。
接下来主要看一下创建root中都做了什么操作:
这里面暴露了几种 createRoot 的方法,实例化了 ReactDOMRoot 函数,赋予了原型 render、unmount。ReactDOMRoot 中_internalRoot属性被赋值了由react-reconciler返回的container。然后监听这个总root。
而这里的根配置来自 ReactDOMHostConfig.js/Container,这里创建了各种实例,及对真实节点、文本、原型的操作,如创建、删除、更新、警告、监听等。创建的操作均来自ReactDOMComponent.js文件。打开可以看到如下函数:
命名上可以看到它对props、state、事件、节点类型、节点原型、文本、合并后节点的对比操作,直至生成节点。它对节点类型做了很细的处理,如下:
而在 Hydrated 内做了更细致的处理:
03
—
React对比算法结论
依照官网声明:一些通用的算法实现复杂度为O(n³),当前React的算法依照归并树及key的方式将复杂度降为O(n),n为树中元素的数量。区分树时,将先对比根元素,
1. 类型不同的DOM元素
如果类型不同将从头构建新树。拆除树时,旧的DOM节点将被破坏。组件实例接收componentWillUnmount()。建立新树时,会将新的DOM节点插入到DOM中。组件实例接收componentWillMount()然后componentDidMount()。与旧树关联的任何状态都将丢失。根目录下的所有组件也将被卸载并破坏。
2. 类型相同的DOM元素
比较两个相同类型的React DOM元素时,React会查看两者的属性,保留相同的基础DOM节点。并仅更新更改了属性的元素。如果是className属性,它还知道对比style。在两个节点转换时,还知道仅需修改有差异的样式属性。处理完DOM节点后,再去子节点上递归。
3. 相同类型的组成元素
组件更新时,实例保持不变,因此在渲染之间保持状态。React更新基础组件实例的属性以匹配新元素,并在基础实例上调用componentWillReceiveProps()和componentWillUpdate()。然后render(),diff算法根据先前的结果和新的结果递归。
4. 递归子节点
默认情况下,在DOM节点的子节点上递归时,React只会同时遍历两个子节点列表,并在存在差异时生成一个标记。在子元素的 "尾" 添加元素时,两个树会很好的转换。如果在"首"插入元素会降低性能,这是React的一个问题。为了解决它,React支持了key属性。它一般常用业务中的ID,如果没有也可用hash处理以便生成密钥,但是它需要稳定,而不是随机产生的 Math.random(),这样将导致不必要的重新创建许多组件实例和DOM节点,性能下降,组件状态也会丢失。key只需在该列表中是唯一的,而不用全局都唯一。如果你不对该数组重新排序,那index的效果也很好。否则:不但会很慢,组件状态出现问题。组件实例将根据其键进行更新和重用。如果键是索引,则移动项目会对其进行更改。结果,诸如不受控制的输入之类的组件状态可能会以意外方式混合和更新。
总结:
1. 对比根节点,根节点不同不继续对比子节点,因为业务中这种情况很少见
2. 向上跨级移动整个根节点,删除重新创建处理,因为情况少见,遵循1
3. 对比子节点,类型相同对比属性,递归新老结果
4. 对比兄弟节点,存在重新排序时,对比key移动复用处理
React的render在重新渲染所有子组件时并不会卸载它们重新安装,它遵循如上的对比机制执行diff算法依赖于以上内容,如果违背将会导致多走程序,性能慢,甚至影响状态的准确性。
如果你不太清楚算法复杂度的效率,我将简单的阐述如下,如果你已知晓请略过。
算法复杂度:是指算法在编写成可执行程序后,运行时所需要的资源
频度:一个算法中的语句执行次数。记为T(n)。
时间复杂度:算法在计算机内执行时所需时间的度量,T(n)=O(f(n))。
空间复杂度:算法在计算机内执行时所需存储空间的度量,S(n)=O(f(n))
一个算法的评价主要从时间复杂度和空间复杂度来考虑。
看频度最大的语句:
例如执行一句 console.log(1),只执行一次,频度为1,那它是个常数阶 例如执行一个i=10的for循环,那里面的console.log执行了10次,也是个常数阶 例如执行一个双层for循环,那么整个规模问题n是双层的,执行次数是n²,所以是平方阶 例如执行一个三层for循环,那么整个规模问题n是三层的,执行次数是n³,所以是立方阶
04
—
Virtual-dom
最后推出一个Virtual-dom的库,有兴趣可以了解一下。
Virtual-dom:
https://github.com/Matt-Esch/virtual-dom
我将在后续章节,推出前端必备的浏览器渲染原理,让你彻底明白从打开一个页面它干了什么,该怎么注意让性能更好,关注公众号进前端大牛群,在线答疑。
如果觉得本文对你起到了些许作用,那么作为知恩图报的码农,您的 "赞赏" 是我继续前行的动力,点击下方喜欢作者,一分也是爱,感谢您的鼓励~
快来识别二维码关注吧,发布好文时时推送到您的订阅号中!
长按二维码关注公众号
随笔好文与你共度地铁时光