玩命加载中 . . .

React 源码


作为框架的使用者,会首先关注 React 的 API ,但 React 团队作为框架的开发者,他们首先面对的是 React 的设计理念。所以,要学懂 React 的源码,我们需要将自己的角色,从框架的使用者转变为框架的开发者,从理念出发,自顶向下的学习。

调试源码

理念篇

设计理念

React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。

什么因素制约了快速响应:可能是由于我们正在进行大量的计算,或者是因为当前网络状况不佳,我们正在等待一个请求的返回。换句话说,就是 计算能力网络延迟 这两个原因导致我们的应用不能快速响应。究其原因,就是 CPU 的瓶颈和 IO 的瓶颈。

解决 CPU 瓶颈
主流浏览器的刷新频率为 60Hz,也就是说,每16.6ms会刷新一次。在这16.6ms中,浏览器会依次执行 JS脚本执行 - 样式布局 - 样式绘制 ,如果 JS 脚本执行时间超过了16.6ms,那么这一帧就没有时间继续执行后面的步骤了,浏览器就会掉帧,表现形式有:浏览器的滚动不流畅、在输入框输入的字符不能及时响应在页面上。

demo:输入框输入字符并更新页面
我们通常会使用防抖和节流这两种方案,限制触发更新的频率,来减少掉帧的可能性,但这是治标不治本的。随着我们输入的字符越来越多,我们每一次更新所需要的时间都超过了16.6ms,那么即使使用节流和防抖,也会造成浏览器掉帧。

为了解决这一难题,React 将同步更新变为异步可中断更新。简单来说,React 和浏览器做了一个约定,浏览器从16.6ms中抽出一部分给 React,React 利用这一部分时间完成自己的工作,如果某一个工作需要的时间超过了浏览器给的时间,React 会中断自己的工作,将控制权交给浏览器,等待下一帧留给 React 的那部分时间到来再继续执行之前被中断的工作。这样,浏览器在每一帧都会有时间执行样式布局和样式绘制。

解决 IO 瓶颈:比如一些需要等待数据请求结果才能做出进一步响应的场景下,用户如何才能感知到快速响应。

React 的答案是:

将人机交互研究的结果整合到真实的 UI 中。例如,研究表明,对 UI 交互来说,延迟显示中间的加载状态,能让用户感觉更流畅。

这话翻译一下就是,异步加载时,在结果出来前会有一段空白期,本来在用户看来这段空白期会显示 loading 状态,但如果我们稍微推后一点时间再显示这个 loading 状态,从用户的感知来说,其实就跟同步更新没什么区别。这是基于人的主观感受来实现的。

架构演进史

老的 React (React15)架构
这个架构整体分为两部分:Reconciler(协调器)【决定渲染什么组件】和 Renderer(渲染器)【将组件渲染到视图中】。

Fiber 架构

Fiber (纤程)可以理解为协程的一种实现,在 js 中,协程已经有一种实现方式:Generator 。

那 React 为什么不使用 Generator ?

  • 首先,Generator 和 async/await 一样也是具有传染性的。
  • 其次,Fiber 架构需要达成两个目的:①更新可以中断并继续;②更新可以拥有不同的优先级,高优先级的更新可以打断低优先级的更新。
    然而,使用 Generator 只能达到第一个目的,却不能达到第二个目的。

工作原理
看一段程序:

当我们首次调用ReactDOM.render()时,会创建整个应用的根节点FiberRootNode【由于我们可以多次调用ReactDOM.render(),将不同的应用挂载到不同的 DOM 节点下,所以每个应用都有它自己的根 Fiber 节点RootFiber(在一个页面中可以有多个RootFiber,但只能有一个FiberRootNode来管理这些RootFiber)】。

函数组件APP 会创建一个对应的 Fiber 节点,该节点的类型为function Component;APP 的子节点p会创建一个对应的 Fiber 节点,该节点的类型为host Component,也就是原生 DOM 节点对应的 Fiber 节点;p 节点的第一个子节点 kasong 会创建一个对应的文本 Fiber 节点;kasong的兄弟节点 num 也会创建一个文本 Fiber 节点。

那这些 Fiber 节点是如何连接的呢:FiberRootNode.current = RootFiber, RootFiber.child = App, App.child = p, p.child = kasong, kasong.sibling = num, num.return = p

双缓存

定义:在内存中构建并直接替换的技术

架构篇

render 阶段

render 阶段的主要工作是 构建 Fiber 树生成 effectList 。我们知道,react 入口的两种模式会进入performSyncWorkOnRoot或者performConcurrentWorkOnRoot,这取决于本次更新是同步更新还是异步更新。而这两个方法分别会调用workLoopSync或者workLoopConcurrent方法。

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

当某一个 fiber 节点进入beginWork时,它最终的目的是创建当前fiber节点的第一个子fiber节点。

beginWork的工作流程:

  • 判断当前fiber节点的类型,以进入不同的update逻辑;
  • update的逻辑中,会判断当前workInProgressFiber是否存在对应的current fiber,以决定是否标记effect Tag
  • 接着进入reconcile逻辑

commit 阶段

实现篇

Diff 算法

diff 操作本身是有性能损耗的,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n³),其中n是树中元素的数量。

为了降低算法复杂度,React 的 diff 设置了三个限制条件,以确保复杂度能降为O(n)

  • 只对同级元素进行 diff 。如果一个DOM 节点在前后两次更新中跨越了层级,那么 React 不会复用该节点。
  • 只复用同类型的 DOM 节点。如果元素由div变为p,React 会销毁div及其子孙节点,并新建p及其子孙节点。
  • 通过元素的key属性判断是否可以复用。

diff 口诀:

  • diff 口诀有三条:
  • 第一条,看元素,类型相同可复用,类型不同不复用
  • 第二条,看 key ,key 同类型同,这才能复用
  • 第三条,看同级,diff 更新不跨级
  • 那么先看第一条
  • 有 key 先看 key ,key 同看类型
  • 类型相同可复用,类型不同要删除
  • key 不同,不强求,不能复用就算了
  • 没有 key ,看成 key = null ,接着就看第二条
  • 类型相同可复用,类型不同不复用

React 源码:Diff 的入口函数 reconcileChildFibers

我们从 Diff 的入口函数reconcileChildFibers看看该算法的实现过程,该函数会根据newChild(即JSX对象)类型调用不同的处理函数。

function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
  lanes: Lanes
): Fiber | null {
  
  const isUnkeyedTopLevelFragment: boolean =
    typeof newChild === 'object' &&
    newChild !== null &&
    newChild.type === REACT_FRAGMENT_TYPE &&
    newChild.key === null;
  
  if (isUnkeyedTopLevelFragment) {
    newChild = newChild.props.children;
  }

  // 判断 newChild 的类型
  const isObject = typeof newChild === 'object' && newChild !== null;

  if (isObject) {
    // newChild是object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE 或 REACT_LAZY_TYPE
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        // 调用 reconcileSingleElement 处理
      case REACT_PORTAL_TYPE:
        // xxx
      case REACT_LAZY_TYPE:
        // xxx
    }
  }

  if (typeof newChild === 'string' || typeof newChild === 'number') {
    // 调用 reconcileSingleTextNode 处理
  }

  if (isArray(newChild)) {
    // 调用 reconcileChildrenArray 处理
  }

  // 一些其他情况调用处理函数
  // ...省略

  // 以上都没有命中,删除节点
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

对于单个节点,我们以类型object为例,会进入reconcileSingleElement

function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement
): Fiber {
    const key = element.key;
    let child = currentFirstChild;

    // 首先判断是否存在对应DOM节点
    while (child !== null) {
        // 上一次更新存在DOM节点,接下来判断是否可复用

        // 首先比较key是否相同
        if (child.key === key) {

            // key相同,接下来比较type是否相同

            switch (child.tag) {
                // ...省略case

                default: {
                    if (child.elementType === element.type) {
                        // type相同则表示可以复用
                        // 返回复用的fiber
                        return existing;
                    }

                    // type不同则跳出switch
                    break;
                }
            }
            // 代码执行到这里代表:key相同但是type不同
            // 将该fiber及其兄弟fiber标记为删除
            deleteRemainingChildren(returnFiber, child);
            break;
        } else {
            // key不同,将该fiber标记为删除
            deleteChild(returnFiber, child);
        }
        child = child.sibling;
    }

    // 创建新Fiber,并返回 ...省略
}

状态更新

React 中一共有6种优先级,从 官网 中可以看到:

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;

export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

几个关键的节点

  1. render 阶段的开始

render阶段开始于performSyncWorkOnRootperformConcurrentWorkOnRoot方法的调用。这取决于本次更新时同步更新还是异步更新。

  1. commit 阶段的开始

commit阶段开始于commitRoot方法的调用。其中rootFiber会作为传参。

开始串联

我们知道,render阶段完成后会进入commit阶段。那么状态更新时,从触发状态更新render阶段会经过什么步骤呢?

在 React 中,有如下方法可以触发状态更新:

  • ReactDOM.render
  • this.setState
  • this.forceUpdate
  • useState
  • useReducer

Q:这些方法调用的场景各不相同,他们是如何接入同一套状态更新机制呢?
A:每次状态更新都会创建一个保存更新状态相关内容的对象,我们叫它Update。在render阶段beginWork中会根据Update计算新的state

生命周期

Hooks

异步调度


文章作者: hcyety
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hcyety !
评论
  目录