React核心技术浅析

本文深入探讨React的核心技术,包括JSX与虚拟DOM的概念,虚拟DOM的生成与渲染,以及React的Diffing算法。文章详细阐述了虚拟DOM如何提高效率,以及Diffling算法在处理不同类型的元素、组件时的策略。此外,还介绍了Fiber架构,解释了如何通过时间切片和深度优先遍历来优化更新过程。
摘要由CSDN通过智能技术生成

1. JSX与虚拟DOM

我们从React官方文档开头最基本的一段Hello World代码入手:

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

这段代码的意思是通过 ReactDOM.render() 方法将 h1 包裹的JSX元素渲染到id为“root”的HTML元素上. 除了在JS中早已熟知的 document.getElementById() 方法外, 这段代码中还包含两个知识点:

  • h1 标签包裹的JSX元素
  • ReactDOM.render() 方法

而这两个知识点则对应着React中要解决的核心问题:

  • 为何以及如何使用(JSX表示的)虚拟DOM?
  • 如何对虚拟DOM进行处理, 使其高效地渲染出来?

1.1 虚拟DOM是什么? 为何要使用虚拟DOM?

虚拟DOM其实就是用JavaScript对象表示的一个DOM节点, 内部包含了节点的 tag , propschildren .

为何使用虚拟DOM? 因为直接操作真实DOM繁琐且低效, 通过虚拟DOM, 将一部分昂贵的浏览器重绘工作转移到相对廉价的存储和计算资源上.

1.2 如何将JSX转换成虚拟DOM?

通过babel可以将JSX编译为特定的JavaScript对象, 示例代码如下:

// JSX
const e = (
    <div id="root">
        <h1 className="title">Title</h1>
  </div>
);
// babel编译结果(React17之前), 注意子元素的嵌套结构
var e = React.createElement(
    "div",
  {
    id: "root"},
    React.createElement(
        "h1",
        {
    className: "title" },
        "Title"
    )
);
// React17之后编译结果有所区别, 创建节点的方法由react导出, 但基本原理大同小异

1.3 如何将虚拟DOM渲染出来?

从上一节babel的编译结果可以看出, 虚拟DOM中包含了创建DOM所需的各种信息, 对于首次渲染, 直接依照这些信息创建DOM节点即可.

但虚拟DOM的真正价值在于“更新”: 当一个list中的某些项发生了变化, 或删除或增加了若干项, 如何通过对比前后的虚拟DOM树, 最小化地更新真实DOM? 这就是React的核心目标.

2. React Diffing

"Diffing"即“找不同”, 就是解决上文引出的React的核心目标——如何通过对比新旧虚拟DOM树, 以在最小的操作次数下将旧DOM树转换为新DOM树.

在算法领域中, 两棵树的转换目前最优的算法复杂度为 O(n**3) , n为节点个数. 这意味着当树上有1000个元素时, 需要10亿次比较, 显然远远不够高效.

React在基于以下两个假设的基础上, 提出了一套复杂度为 O(n) 的启发式算法

  1. 不同类型(即标签名、组件名)的元素会产生不同的树;
  2. 通过设置 key 属性来标识一组同级子元素在渲染前后是否保持不变.

在实践中, 以上两个假设在绝大多数场景下都成立

2.1 Diffling算法描述

不同类型的元素/组件

当元素的标签或组件名发生变化, 直接卸载并替换以此元素作为根节点的整个子树.

同一类型的元素

当元素的标签相同时, React保留此DOM节点, 仅对比和更新有改变的属性, 如className、title等, 然后递归对比其子节点.

对于 style 属性, React会继续深入对比, 仅更新有改变的属性, 如color、fontSize等.

同一类型的组件

当组件的props更新时, 组件实例保持不变, React调用组件的 componentWillReceiveProps() componentWillUpdate()componentDidUpdate() 生命周期方法, 并执行 render() 方法.

Diffing算法会递归比对新旧 render() 执行的结果.

参考React实战视频讲解:进入学习

对子节点的递归

当一组同级子节点(列表)的末尾添加了新的子节点时, 上述Diffing算法的开销较小; 但当新元素被插入到列表开头时, Diffing算法只能按顺序依次比对并重建从新元素开始的后续所有子节点, 造成极大的开销浪费.

解决方案是为一组列表项添加 key 属性, 这样React就可以方便地比对出插入或删除项了.

关于 key 属性, 应稳定、可预测且在列表内唯一(无需全局唯一), 如果数据有ID的话直接使用此ID作为 key, 或者利用数据中的一部分字段哈希出一个key值.

避免使用数组索引值作为 key, 因为当插入或删除元素后, 之后的元素和索引值的对应关系都会发生错乱, 导致错误的比对结果.

避免使用不稳定的key(如随机数), 因为每次渲染都会发生改变, 从而导致列表项被不必要地重建.

2.2 递归的Diffing

在1.2节中的虚拟DOM对象中可以得知: 虚拟DOM树的每个节点通过 children 属性构成了一个嵌套的树结构, 这意味着要以递归的形式遍历和比较新旧虚拟DOM树.

2.1节的策略解决了Diffing算法的时间复杂度的问题, 但我们还面临着另外一个重大的性能问题——浏览器的渲染线程和JS的执行线程是互斥的, 这意味着DOM节点过多时, 虚拟DOM树的构建和处理会长时间占用主线程, 使得一些需要高优先级处理的操作如用户输入、平滑动画等被阻塞, 严重影响使用体验.

时间切片(Time Slice)

为了解决浏览器主线程的阻塞问题, 引出 时间切片 的策略——将整个工作流程分解成小的工作单元, 并在浏览器空闲时交由浏览器执行这些工作单元, 每个执行单元执行完毕后, 浏览器都可以选择中断渲染并处理其他需要更高优先级处理的工作.

浏览器中提供了 requestIdleCallback 方法实现此功能, 将待调用的函数加入执行队列, 浏览器将在不影响关键事件处理的情况下逐个调用.

考虑到浏览器的兼容性以及 requestIdleCallback 方法的不稳定性, React自己实现了专用于React的类似 requestIdleCallback 且功能更完备的 Scheduler 来实现空闲时触发回调, 并提供了多种优先级供任务设置.

递归与时间切片

时间切片策略要求我们将虚拟DOM的更新操作分解为小的工作单元, 同时具备以下特性:

  • 可暂停、可恢复的更新;
  • 可跳过的重复性、覆盖性更新;
  • 具备优先级的更新.

对于递归形式的程序来说, 这些是难以实现的. 于是就需要一个处于递归形式的虚拟DOM树上层的数据结构, 来辅助完成这些特性.

这就是React16引入的重构后的算法核心——Fiber.

3. Fiber

从概念上来说, Fiber就是重构后的虚拟DOM节点, 一个Fiber就是一个JS对象.

Fiber节点之间构成 单向链表 结构, 以实现前文提到的几个特性: 更新可暂停/恢复、可跳过、可设优先级.

3.1 Fiber节点

一个Fiber节点就是一个JS对象, 其中的关键属性可分类列举如下:

  • 结构信息(构成链表的指针属性)
    • return: 父节点
    • child: 第一个子节点
    • sibling: 右侧第一个兄弟节点
    • alternate: 本节点在相邻更新时的状态, 用于比较节点前后的变化, 3.3节详述
  • 组件信息
    • tag: 组件创建类型, 如FunctionComponent、ClassComponent、HostComponent等
    • key: 即key属性
    • type: 组件类型, Function/Class组件的type就是对应的Function/Class本身, Host组件的type就是对应元素的TagName
    • stateNode
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值