React函数组件性能优化三部曲(一)

最近在看react应用更新时的一些逻辑处理,因为之前的fiber reconciler协调流程重点学习的是FiberTree的创建过程,对整个更新流程没有进行深入的了解。本次在学习应用更新时也对函数组件性能优化有了更加深刻的认识,所以也是趁着有时间整理了一下文档,记录一下关于函数组件性能优化的相关内容。

本次内容会分成以下三个章节来讲解函数组件性能优化的逻辑原理:

  • 普通函数组件更新。
  • memo组件更新。
  • 性能优化使用总结。

本章节将讲解普通函数组件的更新逻辑,因为我们只有先知道普通函数组件更新特点,才能更好地理解下一章节React.memo方法是如何来优化普通函数组件的。

1,案例准备

首先我们准备一个函数组件更新的测试案例:

js 复制代码
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
js 复制代码
// App.js
import React from 'react'
import MyFun from './views/MyFun';

export default function App() {
  console.log('App组件运行了')
  return (
    <div className="App">
      <div>react源码调试</div>
      <MyFun name='MyFun'></MyFun>
    </div>
  );
}
js 复制代码
// MyFun.js
import { useState } from 'react'
import Child from './Child'

export default function MyFun(props) {
  console.log('MyFun组件运行了')
  const [count, setCount] = useState(1)
  function handleClick() {
    setCount(count + 1)
  }
  return (
    <div className='MyFun'>
      <div>state: {count}</div>
      <Child name='Child'></Child>
      <button onClick={handleClick}>更新</button>
    </div>
  )
}
js 复制代码
// Child.js
export default function Child(props) {
  console.log('Child组件运行了')
  return (
    <div className='Child'>
      <div>Child子组件</div>
      <div>name: {props.name}</div>
    </div>
  )
}

案例加载完成的页面内容:

Fiber树结构

案例对应的Fiber树结构:

这里没有展开Child组件内部的Fiber结构,因为我们这里关注的是它组件节点的更新。

注意: 案例唯一的更新按钮在MyFun组件中:

  • 它的父组件App自身没有状态,也没有给MyFun传递动态props
  • 它的子组件Child自身没有状态,也没有给它传递动态props

下面我们来看MyFun组件更新,到底会对它的父组件和子组件造成什么样的影响。

2,触发更新

点击更新按钮,查看打印结果:

可以发现MyFun组件重新渲染了,它的子组件Child也重新渲染了,它的父组件App没有重新渲染。

下面我们就来分析这整个的更新流程。

触发状态变化

点击更新按钮,就会触发DOM事件回调,然后执行我们的状态修改操作:

js 复制代码
function handleClick() {
  setCount(count + 1)
}

此时就会来到useState对应的dispatchSetState方法中:

该方法会执行以下几个步骤:

  • 创建一个update更新对象。
  • update对象添加到queue队列中,形成一个单向环状链表。
  • 调用scheduleUpdateOnFiber方法,开启一个新的调度更新任务。

注意:useState hookdispatch方法触发的状态变化,在触发调度之前,会有一个eagerState优化策略校验,也就是快速计算出本次状态变化的结果,与原来的状态进行对比,判断是否发生了真的状态变化:

  • 如果没有发生变化,则会进入eagerState策略,不会发起调度更新任务。
  • 如果发生了变化,则开启一个新的调度更新任务。
js 复制代码
if (is(eagerState, currentState)) {
 /**
 * 如果相等,则满足优化策略,只是将update更新对象添加到queue队列,不会开启新的更新调度。
 * 【注意:】这里是针对单次修改state做的eagerState优化策略:本次修改无变化,就不发起调度更新【只需要将update对象添加到临时链 	* 表即可,等会真正变化的dispatchSetState来触发调度更新】
 * updateReducer里面的Bailout策略是针对一个hook对象的整个update链表计算完成后,即一个state多次修改最终计算之后,再判断   
 * state有没有变化:1,有变化进行正常的组件更新; 2,无变化则进入Bailout优化策略。
 *
 */
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return;
}

注意: eagerState策略校验针对的是单次修改状态的校验。

js 复制代码
function handleClick() {
    // 每次修改都会执行一次校验
    setCount(1)
    setCount(2)
}

比如在DOM事件中一次性执行多次状态修改,则每次dispatch方法都会执行一次eagerState的策略校验。

当前我们的状态发生了真实的变化,所以就会发起一个新的调度更新任务,触发一次应用的更新流程。

由点击事件触发的更新任务为同步优先级,但是react会默认使用微任务来处理同步任务。

最终会来到workLoopSync方法中,开始创建Fiber树,也就是应用更新的核心流程。

前面省略了一部分调度更新的逻辑,因为这部分不是本章的重点。关于这部分的详细逻辑可以查看《React18.2x源码解析:函数组件的加载过程》,这里不会过多关注。

根节点的处理

在解析workLoopSync方法之前,我们还得先知道Fiber树根节点的处理。

注意: react应用的每次更新渲染都会构建一颗新的Fiber树,也就是说构建Fiber树的过程就是应用更新的核心流程。

在这个流程中每个Fiber节点都会执行两项工作:

  • beginWork工作
  • completeWork工作

组件节点的工作重点在beginWork中,普通DOM节点的工作重点在completeWork中。

HostFiber 节点是Fiber树的根节点【可以看前面的树结构】,每次构建Fiber树的工作都是从这个根节点开始的,而HostFiber 是在workLoopSync方法执行之前就已经创建完成的,也就是说在每次构建Fiber树之前它的根节点就已经确定了。所以workLoopSync第一个处理的就是HostFiber 节点,开始执行它的beginWork工作。

在开始它的beginWork工作之前,我们得先知道HostFiber 的创建,因为这个非常重要。

在执行workLoopSync方法之前,会调用一个prepareFreshStack方法来做本次更新的准备工作。

prepareFreshStack
js 复制代码
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
  ...
  
  // 使用旧的hostFiber节点,来创建一个工作中的hostFiber,即WorkInProgress【新的】
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  // 并且将全局变量:workInProgress赋值为 新的hostFiber节点
  workInProgress = rootWorkInProgress;
  ...
}

这个准备工作首先一点就是:确定了本次构建Fiber树的根节点,也就是hostFiber

即调用createWorkInProgress方法来创建一个新的Fiber节点,因为当前传递的参数是root.current,它的意思就是根据旧的hostFiber节点来创建新的hostFiber。并且将这个节点赋值给全局变量workInProgress,所以下面我们进入workLoopSync方法时,全局变量workInProgress存储的就是hostFiber根节点对象。

这里我们需要再关注一下createWorkInProgress方法:它是更新阶段一个通用的创建Fiber节点的方法 ,并不只是用于创建hostFiber节点,其他的Fiber节点更新时也会用到这个方法。

createWorkInProgress
js 复制代码
// packages\react-reconciler\src\ReactFiber.new.js
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    /**
     * 该Fiber节点第一次更新的情况:
     * 它的alternate属性为null,即workInProgress为null。需要调用FiberNode构造函数创建新的Fiber节点,
     * 然后复用current节点信息
     */
    workInProgress = createFiber(
      current.tag,
      pendingProps, // 复用props
      current.key,
      current.mode,
    );
    // 复用相关信息
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;

    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    
    /**
     * 表示该Fiber节点的第二次更新及以上:
     * 不需要再使用FiberNode构造函数来创建Fiber对象了,直接复用current节点相关的信息
     * 
     */
    workInProgress.pendingProps = pendingProps;
    workInProgress.type = current.type;
    workInProgress.flags = NoFlags;

    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null;
  }

  # 复用通用的current节点信息
  workInProgress.flags = current.flags & StaticMask;
  workInProgress.childLanes = current.childLanes;
  workInProgress.lanes = current.lanes;

  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;

  const currentDependencies = current.dependencies;
  workInProgress.dependencies =
    currentDependencies === null
      ? null
      : {
          lanes: currentDependencies.lanes,
          firstContext: currentDependencies.firstContext,
        };

  // These will be overridden during the parent's reconciliation
  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;

  # 返回新的Fiber节点
  return workInProgress;
}

我们首先查看这个方法的两个参数:

  • current:旧的Fiber节点。
  • pendingProps:新的props对象。

根据这两个参数我们就可以得知这个方法的作用:复用current旧的节点信息以及新的props对象来创建对应的新的Fiber节点。

虽然这个方法看着内容很多,但是它的核心就是一个if else结构,下面我将它的逻辑整理一下:

js 复制代码
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
	let workInProgress = current.alternate;
	if (workInProgress === null) {
		// 表示该Fiber节点第一次更新
        workInProgress = createFiber(
      		current.tag,
      		pendingProps, // Fiber节点的beginWork工作时,就会判断新旧props
      		current.key,
      		current.mode,
    	);
		...
        
	} else {
	    // 表示该Fiber节点非第一次更新
        workInProgress.pendingProps = pendingProps;
	    ...
	}
	
	# 复用通用信息
    ...
    
    // 返回新的节点
    return workInProgress
}

整理之后我们就可以发现它的逻辑并不复杂,这里根据当前Fiber节点的alternate属性值判断当前节点是否为第一次更新:

  • null时,代表当前Fiber节点为第一次更新,需要使用FiberNdoe构造函数创建新的Fiber实例,然后再复用旧的节点信息。
  • 不为null时,代表当前Fiber节点不是第一次更新,直接复用旧节点的相关信息。

if else结构之后是复用旧节点的一些通用信息,最后返回对应的新的Fiber节点。

注意: 此方法的核心在于通过复用旧的节点信息来创建新的Fiber节点,至于此节点是多少次更新并不是重点。

虽然createWorkInProgress方法内部复用了旧节点的很多属性内容,但是我们这里的重点是关注它的第二个参数:pendingProps

也就是Fiber.pendingProps属性的设置,它代表该节点新的props数据,此属性会影响该节点更新时的beginWork工作中能否进入Bailout策略,后面就会讲解这部分逻辑。

比如当前创建新的hostFiber节点时,第二个属性默认为null

js 复制代码
const newFiber = createWorkInProgress(root.current, null);

也就是说:所以无论多少次更新,hostFiber节点的pendingProps属性都是null

以上就是createWorkInProgress方法的主要内容,下面我们就正式进入workLoopSync方法,开始Fiber树的构建。

3,更新流程

执行流程图

beginWork工作执行流程图:

下面我们会通过分析几个重要的Fiber节点来学习react的更新流程逻辑。

HostFiber节点

通过上面我们已经知道了第一个执行的是hostFiber根节点:

下面我们直接来到它的beginWork工作:

js 复制代码
beginWork(current, workInProgress, lanes)
js 复制代码
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {

  if (current !== null) {
    # 更新流程
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    // 主要判断:props变化和context上下文变化
    if (oldProps !== newProps || hasLegacyContextChanged() ) {
      // props有变化或者上下文有变化时,Fiber节点自身需要更新就会进入这里
      didReceiveUpdate = true;
    } else {

      // props和上下文都没有变化的情况,检查当前Fiber节点自身是否有挂起的调度更新或者上下文修改
      // 在修改state触发的更新,这里检查的结果就会为true
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
        current,
        renderLanes,
      );
      if (!hasScheduledUpdateOrContext && (workInProgress.flags & DidCapture) === NoFlags) {
        // hasScheduledUpdateOrContext为false,即Fiber自身没有更新的情况下:进入Bailout策略
        // No pending updates or context. Bail out now.
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
        
      // 当前Fiber存在更新的情况:需要进入正常的组件更新流程,重新调用组件函数,生成新的react元素对象
      didReceiveUpdate = false;
    }
  } else {
	# 加载流程
  }

  workInProgress.lanes = NoLanes;
  // 根据Fiber.tag值走不同的组件处理逻辑
  switch(workInProgress.tag) {
    case FunctionComponent: {}
    case ClassComponent: {}
    ...
  }
}

beginWork方法通过判断current是否有值来区分此节点是加载还是更新,当前我们是更新流程,current都是有值的,直接查看更新逻辑。不过在这里还是要提前说明一下:在react中,影响一个Fiber节点更新的只有这三种因素:

  • state
  • props
  • context

首先取出新旧Fiber节点的对应的props对象,即新旧props数据:

js 复制代码
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;

然后判断新旧props是否相等,前面已经说了hostFiber节点的props默认是null,所以这里是相等的。

js 复制代码
if (oldProps !== newProps || hasLegacyContextChanged() ) {
  // props变化的情况:
  didReceiveUpdate = true;
} else {
  // props无变化的情况:
  const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
  // 判断有没有等待的更新
  if (!hasScheduledUpdateOrContext && (workInProgress.flags & DidCapture) === NoFlags) {
      didReceiveUpdate = false;
      // 无更新,进入Bailout策略
      return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
      );
  }
  // 有更新,进入正常的更新流程
  didReceiveUpdate = false;
}

注意: 这里虽然有context上下文的变化检查,它针对的是旧版的LegacyContext,新版的context变化表现在lanes中,所以第一部分的判断我们主要关注props的变化即可。

propsLegacyContext都无变化的时候,进入else分支处理:

js 复制代码
// 检查是否存在调度更新或者context变化
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
js 复制代码
function checkScheduledUpdateOrContext(current: Fiber, renderLanes: Lanes,): boolean {
  // 检查是否存在更新
  const updateLanes = current.lanes;
  // 检查当前节点lanes,有没有renderLanes
  if (includesSomeLane(updateLanes, renderLanes)) {
    return true;
  }
  return false;
}

继续检查当前Fiber节点是否有state变化触发的更新或者context变化。

  • 这里检查的是current节点的lanes,因为组件在修改state触发更新时,会将renderLanes绑定到在current节点之上。
  • 同时这里也检查了context的变化,因为新版context在发生变化时也会为Consumer对应的Fiber节点的lanes属性绑定renderLanes,所以这里通过Fiber.lanes的值同时检查了Fiber节点可能会出现的两种更新。

然后定义一个变量hasScheduledUpdateOrContext接收校验的结果:

  • 如果结果为false,代表不需要更新,设置全局变量didReceiveUpdatefalse,然后进入Bailout策略逻辑,执行更进一步的优化程度判断。
  • 如果结果为true,表示有更新内容,但注意的是:有更新并不意味着state确实发生了变化,比如一个组件多次修改了state,它存在更新的任务,但有可能多次状态修改的最终结果是没有变化,所以只有在更新时计算state后明确发生了真正的状态变化,才会同步更新这个变量的值为true。所以这里还是设置的didReceiveUpdatefalse,进入下面的switch case结构,执行对应的组件更新逻辑,之后再根据计算后的状态值再次判断是否进行真正的更新。

同时在这里我们也可以发现,react内部存在默认的优化策略 :即在每个Fiber节点的beginWork工作中,通过一系列的判断来校验当前Fiber节点是否可以进入Bailout策略,来跳过该节点的更新工作。

当前我们的hostFiber节点满足Bailout策略,进入attemptEarlyBailoutIfNoScheduledUpdate方法。

js 复制代码
function attemptEarlyBailoutIfNoScheduledUpdate() {
	...
	return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

直接查看最后一行代码,继续调用了一个Bailout策略方法:

js 复制代码
// Bailout策略核心方法
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    workInProgress.dependencies = current.dependencies;
  }
  markSkippedUpdateLanes(workInProgress.lanes);

  /***
   * 
   * 检查子节点是否存在有更新的工作:这里应该是判断该组件的整个子树有没有更新工作?
   * 1,如果没有则可以直接跳过整个组件的更新流程
   * 2,如果有,则直跳过组件自身节点的更新流程
   * 
   **/
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
      return null;
  }

  // 来到这里:表示当前Fiber节点没有更新工作,但是它的子节点有,所以只能跳过该节点自身的更新流程
  // 在更新阶段,hostFiber,App根组件节点这些节点自身没有变化基本都会走这里
  cloneChildFibers(current, workInProgress);
  // 比如当前workInProgress为HostFiber, 它的child就是App根组件节点
  // 下一步就是开始新的App组件节点的beginWork工作
  return workInProgress.child;
}

这个方法是Bailout优化策略的核心方法,它的作用执行进一步的优化程度判断:

  • 高度优化:跳过整个子树的更新流程。
  • 一般优化:跳过自身节点的更新流程。

在这里我们还要了解Fiber节点上两个属性的区别:

  • lanes:如果存在renderLanes,则说明节点自身存在更新。
  • childLanes:如果存在renderLanes,则说明该节点的子树存在更新。

在进入Bailout策略之前,已经对Fiber.lanes进行了校验。而能够进入Bailout策略,已经说明节点自身是不存在更新需求的,所以bailoutOnAlreadyFinishedWork方法内只需要再对childLanes进行校验,就可以判断出最终的优化程度了。

这里检查当前节点的childLanes判断子树是否存在更新的工作:

  • 如果没有:则可以直接跳过该节点整个子树的更新流程。
  • 如果有:则只跳过该节点自身的更新流程。

在更新流程中:Fiber树的根节点一般都不会存在更新,但是它的子树一定是存在更新的,比如当前的MyFun组件节点。

所以这里不满足高级优化的条件,就会执行cloneChildFibers方法,通过克隆复用来生成它的下一层子节点。

js 复制代码
export function cloneChildFibers(
  current: Fiber | null,
  workInProgress: Fiber,
): void {
  if (current !== null && workInProgress.child !== current.child) {
    throw new Error('Resuming work not yet implemented.');
  }
  if (workInProgress.child === null) {
    return;
  }
  // 注意:current.child和workInProgress.child是指向的同一个child。
  // 这里的目的是通过现在的child内容,创建另外一个即alternate属性指向的新Fiber节点
  let currentChild = workInProgress.child;
  # 复用生成新的Fiber节点
  let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
  // 比如当前的workInProgress是hostFiber时,就会创建新的App组件节点,然后更新hostFiber.child
  workInProgress.child = newChild;

  newChild.return = workInProgress;

  # 如果旧child还有兄弟节点,则循环创建它们对应workInProgress
  // 比如<div>react源码调试</div>它有兄弟阶段MyFun函数组件,此时就会继续新建它的Fiber节点
  while (currentChild.sibling !== null) {
    currentChild = currentChild.sibling;
    newChild = newChild.sibling = createWorkInProgress(
      currentChild,
      currentChild.pendingProps,
    );
    newChild.return = workInProgress;
  }
  // 无兄弟节点,执行结束
  newChild.sibling = null;
}

cloneChildFibers方法的作用就是克隆下一层子节点,比如当前hostFiber的子节点就只有一个App组件,所以这里只需要克隆生成App组件对应的新的Fiber节点即可,同时这里我们也可以看到这个方法的内部也是调用的createWorkInProgress方法:

js 复制代码
let currentChild = workInProgress.child;
// 创建新的Fiber节点
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
// 比如当前的workInProgress是hostFiber时,就会创建新的App组件节点,然后更新hostFiber.child
workInProgress.child = newChild;

前面已经讲过这个方法,它的作用就是:复用旧的节点信息生成新的Fiber节点,当前就是生成一个属于App组件的新的Fiber节点,然后更新hostFiber节点的child属性为新的App组件节点。

js 复制代码
return workInProgress.child;

最后返回子节点即App组件节点,开始App组件节点的beginWork工作。

App组件节点

下面再次来到App组件节点的beginWork工作处理:

注意: App组件节点是通过克隆复用旧的节点信息生成的,它的props对象也是原props对象,即新旧props数据相等,所以这里App组件节点依然可以进入Bailout策略,会执行跟HostFiber节点一样的处理逻辑。

最后还是会来到cloneChildFibers方法中,生成App组件节点的子节点【div.App】。

div.App】节点也是一样的beginWork工作逻辑,就不再重复说明了。这里我们加快进度,直接来到cloneChildFibers方法中,再次来到该方法中时,因该节点有两个子节点,所以在第一个子节点生成之后,判断发现它的sibling属性有值,就会循环继续生成它的兄弟节点,即我们的MyFun函数组件节点。

后面的执行逻辑就是:

js 复制代码
// 1,执行:div 源码调试 节点的beginWork工作
// 2,执行:div 源码调试 节点的completeWork工作
// 3,执行:MyFun 节点的beginWork工作
MyFun组件节点

下面我们直接来MyFun组件节点的beginWork工作。

因为MyFun组件节点也是通过克隆生成的,所以它的新旧props也是相等的。

但是重点来了: 本次的更新流程是由该组件修改state触发的,所以该Fiber.lanes含有renderLanes即存在更新。

所以这里再调用checkScheduledUpdateOrContext校验时就会返回trueMyFun函数组件节点就无法进入Bailout策略。

js 复制代码
if (oldProps !== newProps || hasLegacyContextChanged() ) {
  // props变化的情况:
  didReceiveUpdate = true;
} else {
  // props无变化的情况:
  const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
  // 判断有没有等待的更新
  if (!hasScheduledUpdateOrContext && (workInProgress.flags & DidCapture) === NoFlags) {
	// 无更新的情况:
  }
  # 有更新,进入正常的更新流程
  didReceiveUpdate = false; // 还是设置为false
}

...
switch (workInProgress.tag) {
    case FunctionComponent: {
        return updateFunctionComponent() // 执行正常的函数组件更新
    }
}

最后来到下面的switch case结构,执行函数组件正常的更新流程。

下面查看updateFunctionComponent方法:

js 复制代码
// 更新函数组件
function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {

  // 调用组件函数
  const nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
  );

  /**
   * 函数组件默认的更新优化策略:【极少:可以通过两次修改state,加一减一来测试】
   * 一般情况下都无法进入此处的Bailout策略,除非是多次修改一个state情况,
   * 同时这个state必须为原始数据类型,比如number或者布尔值,并且多次修改的最终结果是【无变化】。
   * 这样的情况didReceiveUpdate才能保持false的值,然后进入此处的Bailout优化策略。
   */
  if (current !== null && !didReceiveUpdate) {
    // 多次修改state,虽然最终结果无变化。但是effect相关的hook,依赖也是变化了。
    // 这里需要清除hook的副作用标记,表示不需要更新effect
    bailoutHooks(current, workInProgress, renderLanes);
    /**
     * 如果能够进入Bailout策略:代表函数组件自身Fiber无需要更新
     * 1,如果子节点也没有更新,则跳过整个组件的更新流程
     * 2,如果子节点有更新,则跳过自身节点的更新,复用child子节点的内容;
     */
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

  // 创建子节点
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

updateFunctionComponent方法就是专门处理函数组件的加载或者更新逻辑,这个方法的重点就是调用了一个renderWithHooks方法,

js 复制代码
function renderWithHooks() {
	const children = Component() // 调用MyFun函数
}

renderWithHooks方法里面的内容很多,但我们主要关注这一行代码,即调用我们定义的组件函数:

在调用我们定义的组件函数时,主要会执行以下几个内容:

  • 遇到useState hook时,循环update链表,计算最新的state数据。
  • 遇到effect副作用类的hook时,如果依赖变化,则打上HookHasEffect更新标记。
  • 最后根据return返回的jsx内容,生成MyFun函数组件内所有节点对应的新的react元素对象。

注意:state计算完成后,会执行一个判断:

js 复制代码
function updateReducer(reducer, initialArg, init) {
	...
	// state循环计算完成后
	 if (!objectIs(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
}

判断本次state修改后最终的结果有没有变化,如果发生了变化则会调用一个markWorkInProgressReceivedUpdate方法,更新全局变量didReceiveUpdatetrue,代表确实需要执行真正的更新。

js 复制代码
export function markWorkInProgressReceivedUpdate() {
  didReceiveUpdate = true;
}

这就是为啥之前变量didReceiveUpdate还是为false的原因,要确保真实的状态变化才需要执行真正的更新。

关于renderWithHooks方法具体的执行逻辑可以查看《React18.2x源码解析:函数组件的加载过程》

renderWithHooks方法执行完成后,返回值就是新的react元素对象。

注意:调用renderWithHooks会为该组件内所有子节点创建对应的新的react元素对象。

js 复制代码
// 加载函数组件
function updateFunctionComponent() {

  // 调用组件函数
  const nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
  );

  // Bailout策略校验
  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

  // 重新渲染:创建子节点
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

react元素对象创建完成之后,就会根据此对象来创建函数组件内新的子节点。不过在执行之前,这里也有一个Bailout优化策略校验,这里判断条件的重点就是didReceiveUpdate变量,不过因为当前MyFun函数组件确实发生了真实的状态变化,已经将didReceiveUpdate更新为了true,所以这里就不满足Bailout策略,必须进入正常的更新流程。

到这里我们其实可以发现:函数组件类型的Fiber节点在执行beginWork工作时,前后总共会执行两次Bailout策略校验。这说明react内部是存在默认的优化策略的,类组件也有。

注意: 实际上想要满足函数组件内部默认的Bailout优化策略,条件是比较苛刻的。首先必须是针对同一个state进行多次修改,然后state必须是原始数据类型,比如常见的字符串,数字和布尔值,并且必须保证多次修改的最终结果是无变化,这样才能进入默认的Bailout策略。

js 复制代码
function handleClick() {
	setCount(state => state + 1)
	setCount(state => state - 1)
}

比如我们执行这样的修改操作,虽然修改了多次state,但是最终的计算结果缺没有变化,就可以进入函数组件内部的Bailout策略,同时在这里还会清除由于state依赖变化而被打上标记的副作用。

js 复制代码
// 清除副作用标记
bailoutHooks(current, workInProgress, renderLanes);

回到案例,当前MyFun组件发生了真实的状态变化,所以会调用reconcileChildren方法创建新的子节点。

js 复制代码
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;

注意: 满足Bailout策略时是通过cloneChildFibers来创建子节点;不满足此策略时,即正常的组件更新是通过reconcileChildren来创建子节点。虽然这两种方式最终都是调用的createWorkInProgress方法来创建子节点,但是reconcileChildrencloneChildFibers方法最大的不同就是第二个参数传递的值不同:

  • cloneChildFibers: 传递的是current旧节点的props对象。
  • reconcileChildren:传递的是由新生成的的react元素对象上的props
js 复制代码
// cloneChildFibers
const newChild = createWorkInProgress(currentChild, currentChild.pendingProps); // 复用旧props

// reconcileChildren
const newChild = createWorkInProgress(fiber, pendingProps); // react元素对象传递的新的props对象

注意:一个Fiber对应的新建的react元素对象,它的props绝对不会和原来props对象相等,即使是空对象。

yaml 复制代码
{} !== {}

学习到这里,我们就能知道MyFun函数组件重新渲染,为何它的所有子节点也都会重新渲染了,因为这些子节点的pendingProps属性都已经是新的props对象了,在子节点执行它的beginWork工作时:

js 复制代码
if (oldProps !== newProps || hasLegacyContextChanged() ) {
    // props发生变化,直接进入更新流程
    didReceiveUpdate = true; 
}

props校验结果不相等,就会直接进入它的更新流程了。

Child组件节点

最后直接来看Child组件的beginWork工作流程:

根据调试截图可以发现,虽然Child子组件新旧props数据内容并没有实际的变化,但是此时的props已经是一个新的对象了。

js 复制代码
const oldProps = {name: 'child'};
const newProps = {name: 'child'};
if (oldProps !== newProps) // true

所以下面的新旧props就不会相等了,判断结果为true,直接进入该组件正常的更新流程。

这就是为何MyFun组件更新,Child子组件在无实际props变化和自身状态变化的情况下也被动重新渲染了。

4,总结

1,在react中,一次更新流程的核心逻辑就是创建一颗新的Fiber树,每个Fiber节点的创建工作都分为beginWorkcompleteWork两个模块,在beginWork工作开始时,每个Fiber节点都会执行一次Bailout优化策略校验。

2,在react中,影响一个Fiber节点更新的有三种因素:

  • state
  • props
  • context

statecontext的变化会将renderLanes附加到该Fiber节点的lanes属性上。

3,Bailout策略会依次校验这三个因素变化:

  • props的变化:是直接判断新旧props对象是否全等。
  • statecontext的变化:是校验Fiber.lanes是否包含renderLanes

校验不通过,则该节点进入正常的更新流程。

校验通过,则会进入bailoutOnAlreadyFinishedWork方法,进一步判断满足的优化程度:

  • 高度优化:跳过整个子树的更新流程【beginWorkcompleteWork】。
  • 一般优化:通过节点自身的更新流程,也就是直接克隆生成下一层子节点。

4,一个Fiber节点在更新时,执行正常的更新逻辑和进入Bailout策略逻辑最大的不同在于创建子节点的方式:

  • Bailout策略:调用cloneChildFibers方法,克隆旧节点生成新的Fiber节点,重点是保留了原props
  • 正常更新逻辑:调用reconcileChildren方法,复用旧节点生成新的Fiber节点,重点是使用了新建的react元素对象的props

5,一个组件在执行正常的更新逻辑时,会重新生成组件内所有子节点的react元素对象,然后调用reconcileChildren方法,根据react元素对象创建对应的Fiber节点,即子组件节点的pendingProps属性存储的是新的props对象,所以子节点在执行它的beginWork时无法通过Bailout策略校验,只能被动的重新渲染了。

6,一个组件在执行正常的更新逻辑时,在计算出最新的state之后,在调用reconcileChildren方法之前,其实中间还有一次Bailout策略校验,也就是说组件类型的Fiber前后会执行两次Bailout策略校验,但是后面这一次也就是组件内部的这次Bailout策略校验条件比较苛刻,实际项目中难以满足。

7,下章我们会讲解React.memo方法,它的源码是包裹我们传入的普通函数组件,在react内部生成一个新的memo组件节点,它的优化原理就是在beginWork之后,组件更新之前,再执行一次Bailout策略校验。

结束语

以上就是react普通组件更新流程的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!

相关推荐
有梦想的刺儿17 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具38 分钟前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
清灵xmf1 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据1 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_390161771 小时前
防抖函数--应用场景及示例
前端·javascript
334554322 小时前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json