前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >2020-5-21-理解React的渲染更新

2020-5-21-理解React的渲染更新

作者头像
黄腾霄
发布2020-06-10 15:24:13
8300
发布2020-06-10 15:24:13
举报
文章被收录于专栏:黄腾霄的博客

今天来和大家聊React的渲染更新过程。


React是JavaScript代码

在聊渲染更新之前,我们不能忽视的一个概念是——React是JavaScript代码。

我们都知道React传给浏览器的并不是一个HTML代码,而是一段js脚本。

而在浏览器接收到js脚本之后,再执行并生成对应的html元素,插入到DOM中。

这种做法提供了前端组件化的能力,能够让前端的同学不再“面向UI”进行操作。

例如上面的例子,我们把一段原生的HTML元素封装成了一个Component组件。

组件成了一个独立的模型概念,而组件内部的div等HTML元素,就成了封装的UI细节。

这样一来,我们就可以在开发时把更多精力放在模型实现上(功能),而暂时不需要视觉显示(UI)。

React框架会帮我们将模型装换成相应的HTML元素,挂载至DOM树上。

这就实现了Model和View的关注点分离。

这样我们如果需要进行业务模型的变化,组件就能够高效复用。

比如我们这里把收藏改为点赞。就可以这样写。

从虚拟DOM到DOM

渲染是一个“重”操作

React将我们从复杂的HTML的DOM节点操作中解放出来。

但是浏览器终究只能解析渲染真实的HTML元素,而不是jsx定义的语法糖。

任何在对React组件进行的变更操作,最终还是要转换成HTML才能在浏览器渲染。

然而,重绘整个HTML的DOM是一件非常耗性能的工作。

原本只需要操作一个div元素进行修改,如果现在用React需要对整个DOM进行刷新,肯定是不能接受的。

那能不能每次React组件更新,只修改组件对应的DOM节点内容呢?

构建虚拟DOM

在React中,组件是一个封装后的概念。组件的渲染还是会依赖于HTML的元素。

那么如果我们把React从root挂载的组件开始“解封装”,会得到一个只有HTML元素组成的树。

这颗“React 树”的结构在大部分情况下和实际渲染后的DOM树结构是一模一样的。

(注:“小部分”使两颗树不一样的点在于React的Portals – React概念,这里暂时不讲,不会影响后续内容)

这样一来,我们只要观察这颗虚拟的“DOM”树上的结点变化,再刷新真实DOM树上对应的结点,就能实现对特定的HTML元素的更改,进而达到高性能更新UI。

虚拟DOM之间的比较

有了上述的知识,我们现在开始看看React的更新过程。

React出现了”更新”,意味着树的结构出现了变化。

为了性能考虑,我们比较变更时,只会选择新旧两颗虚拟DOM树之间的变化比较。

以上面图中的变更为例。

对于我们肯定很容易想到,只要对节点B和节点C交换,再向节点E添加一个子节点F就可以了。

但是对于React来说,不是这样。

对两颗树做“完全”的比较是一个复杂度为O($n^3$)的算法。

这么高的时间复杂度是不能接受的。

启发式Diffing算法

React在做比较算法时做了一个假设——在HTML的DOM节点中,叶节点(包括子树)的添加和删除是常态,而插入和移动是罕见的。

这个假设带来的就是,在React的比较算法中,只要发现对应节点位置的元素不一致,就会将整个节点对应的子树全部更新。

以上图为例,在比较到B节点位置发现节点类型变化了,所以整个子树发生了替换。从而对应的HTML的DOM子树也发生了更新。

这种启发式算法只需要对整个虚拟DOM树做一次遍历,所以复杂度降低到了O(n)。

没有第二颗树

聊到这里,我们再思考一下。我们真的有必要把新的虚拟DOM树完全构建出来么?

既然我们的diff算法只是遍历一次虚拟DOM树,那么我们就可以把生成节点和比较放在一起。

还是以上一个小节的例子,模拟一遍React的diff过程。

首先是节点A的state发生了变更,所以触发了自身的render方法,得到子节点C和E。

注意,此时节点C和E还没有完全“创建”,即没有走构造函数,只是一个用于比较的中间对象。

接着就可以对节点B和C进行diff。

diff结果,发现是不一样类型的对象。

因此,需要对虚拟DOM中B的子树进行销毁,然后替换为节点C。

此时B子树的组件都会执行componentWillUnmount

然后创建组件C的实例,挂载到虚拟DOM,并且执行componentDidMount

C节点作为一个新挂载的节点,不需要进行diff,直接将子节点生成并挂载即可。

接着对比节点E,此时发现节点E的类型相同。

此时React不会创建新的实例,而是复用E原有实例,并且传入props,进行render。

由于节点E原来没有子节点,因此直接挂载节点F即可。

我们可以看到React的整个渲染更新过程,只有在一个虚拟DOM树上进行更新。

这样可以尽量减少时间和内存消耗

render和虚拟DOM树更新是两个过程

最后有一个小问题,下图中由于A节点的state发生改变,导致虚拟DOM更新,会导致哪些组件触发render

(注:图中每个节点都是React.Component,线段上表示传入的props)

当当当当,答案是全都会触发render。

有同学问了,为什么会这样,明明B节点子树都没有变化,为什么要触发这些“多余”的render。

这里,其实很容易混淆的一点是,render和虚拟DOM树更新,是两个过程。

当我们在对节点B进行diff算法的时候,我们并不知道,节点B的子节点渲染出的结果一定是一致的。

所以React必须对每一个组件调用render方法,再进行对比。

props和state决定render结果

有同学会不服气,明明是传入的相同的props,渲染结果为什么会不一致?

能这么问,说明你平时开发保持很好的习惯。

对于React组件来说,推荐的情况是props和state决定render结果。

如果这么做了,的确能够在比较节点B后,就确定B子树的render结果一定相同。

但是现实是,React没有办法约束大家这么做。

开发权在我们手里,我们完全可以让一个组件每次都随机生成render的结果。

所以React没有办法,只能依次比较。

React.PureComponent

那有没有解决方案呢?

有,你可以把一个组件继承自React.PureComponent

这样会对React表明,这个组件一定是由props和state决定render结果。

这样React在节点B对比后,就不会继续比较它的子树了。

(注意:React.PureComponent还是有一些使用前提的,这里暂时不展开,大家可以去看官网文档)

小结

这次我们探索了React的渲染和更新机制,发现了以下几点:

  • React通过js控制渲染
  • 通过启发式diff算法,减少时间复杂度
  • 通过单独的虚拟DOM减少空间复杂度
  • 发现render和DOM更新属于不同的过程

正是这些算法的一步步优化,实现了React的高性能渲染和更新方案。

让React的使用者提升了开发效率,也增强了产品的用户体验


参考文档:


本文会经常更新,请阅读原文: https://xinyuehtx.github.io/post/%E7%90%86%E8%A7%A3React%E7%9A%84%E6%B8%B2%E6%9F%93%E6%9B%B4%E6%96%B0.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名黄腾霄(包含链接: https://xinyuehtx.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-05-21 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • React是JavaScript代码
  • 从虚拟DOM到DOM
    • 渲染是一个“重”操作
      • 构建虚拟DOM
      • 虚拟DOM之间的比较
        • 启发式Diffing算法
        • 没有第二颗树
        • render和虚拟DOM树更新是两个过程
          • props和state决定render结果
            • React.PureComponent
            • 小结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档