virtual DOM和真实DOM的区别_虚拟DOM,到底快在哪(算法篇)

看过前设想篇数据篇后,相信你已对本文的标题虚拟DOM,到底快在哪有了答案,如果你不曾看过前两篇文章,建议先阅读一下,方便更好的理解本章内容。

e8d20482e01d7fd80d0f991dd7eef732.png

本章内容将参考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官网中找到如下解释,没错了就是它:

cb038365160ca08b660ba140df6299a5.png

这里面也已有了将在17版本废弃的render,及新星hydrate的处理。

在React中SSR与CSR渲染的模板希望是一致的,否则将会给出警告。如果你不修复它们属性上的差异,将会严重影响算法的性能。如果无法避免差异它也提供了suppressHydrationWarning={true}消除警告,或采用双重(two-pass)渲染,但是多一次渲染也会影响速度。


打开react-dom:

40e079d3690c7ba1e3966bde02dbfb27.png

目录结构很清晰。有客户端渲染如 ReactDOM.render,有服务端渲染如 ReactDOM.renderToString,及事件、公共部分的文件夹。

我们主要看 client 部分

打开index.js, 我们可以看到如下export

f1fcff402ce1b88bba8d906f5b244fb1.png

顺着render一路追下去找到了ReactDOMLegacy.js文件,这里面是封装render的入口

0d5a946557ab666d0369360811d9be7f.png

同时我们也找到了hydrate与它的使用区别,面上看就第一个参数不同

a05b6e54b6dbcaa24cf72fa7824f95f5.png

他们都先走了isValidContainer节点验证,然后调用 legacyRenderSubtreeIntoContainer,字面意思理解它为 render一个container的产物。主要第二个参数接收的是个节点。

d5c6c5c5cc43e3cba4fadf19cc1eb182.png

进入这个函数,首先引用 ReactDOMRoot.js 的 RootType 创建了根实例,这期间我们先略过,然后更新清除了该节点,最后将创建好的根配置给了函数 getPublicRootInstance 来自另一个包 react-reconciler,它的主要作用是自定义渲染器,然后暴露出来。这是render的一套大体流程。

接下来主要看一下创建root中都做了什么操作

这里面暴露了几种 createRoot 的方法,实例化了 ReactDOMRoot 函数,赋予了原型 render、unmount。ReactDOMRoot 中_internalRoot属性被赋值了由react-reconciler返回的container。然后监听这个总root。

而这里的根配置来自 ReactDOMHostConfig.js/Container,这里创建了各种实例,及对真实节点、文本、原型的操作,如创建、删除、更新、警告、监听等。创建的操作均来自ReactDOMComponent.js文件。打开可以看到如下函数:

888cb6306f2b29d31727708cd068fee2.png

命名上可以看到它对props、state、事件、节点类型、节点原型、文本、合并后节点的对比操作,直至生成节点。它对节点类型做了很细的处理,如下:

3e2c29703d9980563a59989117c6a358.png

而在 Hydrated 内做了更细致的处理:

62f20398a83608ccd23c2c1c5405a367.png

386ab373eccc8affe2a74a27c8c33504.png


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))

一个算法的评价主要从时间复杂度和空间复杂度来考虑。

04854348def699022f3c5590226d0c24.png

看频度最大的语句:

例如执行一句 console.log(1),只执行一次,频度为1,那它是个常数阶 例如执行一个i=10的for循环,那里面的console.log执行了10次,也是个常数阶 例如执行一个双层for循环,那么整个规模问题n是双层的,执行次数是n²,所以是平方阶 例如执行一个三层for循环,那么整个规模问题n是三层的,执行次数是n³,所以是立方阶

40d8bb43d935f8dc6d3d45514957cceb.pngfe03f15e3aecb8f6f07b5361c63a4daf.png

7b7bbaabdb16e65a7881b88c281df130.png

04

Virtual-dom

最后推出一个Virtual-dom的库,有兴趣可以了解一下。

Virtual-dom:

https://github.com/Matt-Esch/virtual-dom


我将在后续章节,推出前端必备的浏览器渲染原理,让你彻底明白从打开一个页面它干了什么,该怎么注意让性能更好,关注公众号进前端大牛群,在线答疑。

如果觉得本文对你起到了些许作用,那么作为知恩图报的码农,您的 "赞赏" 是我继续前行的动力,点击下方喜欢作者一分也是爱感谢您的鼓励~


快来识别二维码关注吧,发布好文时时推送到您的订阅号中!

a03a28cda1e01a2b372ce796f52ac521.png

长按二维码关注公众号

随笔好文与你共度地铁时光

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值