likes
comments
collection
share

React18.2x源码解析:类组件的加载过程

作者站长头像
站长
· 阅读数 13

在之前的章节我们讲述了FiberTree的创建过程,但是对组件的加载过程这方面的细节没有深入。

本节将深入理解React18.2x 类组件的具体加载过程。

1,加载阶段

首先准备一个类组件案例:

import React, { Component } from 'react';

export default class MyClass extends Component {
  constructor(props) {
    super(props)
    console.log('MyClass组件运行了')
    this.state = {
      count: 1
    }
  }
  componentWillMount() {
    console.log('MyClass组件WillMount完成')
  }
  componentDidMount() {
    console.log('MyClass组件mount完成')
  }
  handleClick = () => {
    this.setState((state, props) => {
      return {
        count: state.count + 1
      }
    }, () => {
      console.log('回调钩子1执行', this.state.count)
    })
    this.setState((state, props) => {
      return {
        count: state.count + 1
      }
    }, () => {
      console.log('回调钩子2执行', this.state.count)
    })
  }
  // 组件的渲染
  render() {
    return (
      <div className='MyClass'>
        <div>MyClass组件</div>
        <div>state: {this.state.count}</div>
        <div>name: {this.props.name}</div>
        <button onClick={this.handleClick}>更新</button>
      </div>
    );
  }
}

然后直接进入到class组件对应的Fiber节点加载:

React18.2x源码解析:类组件的加载过程

执行该Fiber节点的beginWork工作,根据tag类型,进入class组件的逻辑处理【case ClassComponent】:

React18.2x源码解析:类组件的加载过程

React18.2x源码解析:类组件的加载过程

这里要注意:当Fiber节点对应的是组件类型时,它的type属性存储的就是我们定义的组件原型,类组件即为原始的class定义,它会在类组件首次加载时被调用,创建一个对应的组件实例。

下面我们直接进入updateClassComponent方法,查看class组件的加载。

updateClassComponent

查看updateClassComponent方法:

// packages\react-reconciler\src\ReactFiberBeginWork.new.js

function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {

  let hasContext;
  if (isLegacyContextProvider(Component)) {
    hasContext = true;
    pushLegacyContextProvider(workInProgress);
  } else {
    hasContext = false;
  }
  prepareToReadContext(workInProgress, renderLanes);
  // 类组件节点的stateNode 不是存储dom元素,而是组件实例 【hostComponent的stateNode才是dom元素】
  const instance = workInProgress.stateNode;
  let shouldUpdate;
  # 第一次class组件加载,instance都为null
  if (instance === null) {
    // 初始化构建组件实例
    constructClassInstance(workInProgress, Component, nextProps);
    // 加载组件实例
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    shouldUpdate = true;
  } else if (current === null) {
    // In a resume, we'll already have an instance we can reuse.
    shouldUpdate = resumeMountClassInstance(
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  } else {
    // update阶段:判断是否更新class组件
    shouldUpdate = updateClassInstance(
      current,
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  }
  
  # 最后:创建class组件child
  const nextUnitOfWork = finishClassComponent(
    current,
    workInProgress,
    Component,
    shouldUpdate,
    hasContext,
    renderLanes,
  );
  # 返回child子节点
  return nextUnitOfWork;
}

updateClassComponent方法的内容并不复杂,主要就是两个逻辑的执行:

  • class组件实例的创建或者更新。
  • 创建组件的child子节点,最后返回子节点。

这里主要讲解第一点的内容,关于Fiber节点的创建在《React18.2x源码解析(三)Reconciler协调流程》有已经完整的讲述。

const instance = workInProgress.stateNode;

首先从Fiber节点的stateNode属性取出组件实例,当前为类组件的初次加载,所以instance肯定为null

关于Fiber节点的stateNode属性:如果为类组件的节点,则该属性存储的为组件实例,如果为hostComponent类型的节点,则存储的是真实的DOM结构。

React18.2x源码解析:类组件的加载过程

满足第一个if条件,进入内部代码执行:

# 首次加载
if (instance === null) {
  // 初始化构建组件实例
  constructClassInstance(workInProgress, Component, nextProps);
  // 加载组件实例
  mountClassInstance(workInProgress, Component, nextProps, renderLanes);
  shouldUpdate = true;
}

这里主要是调用了两个方法:

  • constructClassInstance:构建class组件实例。
  • mountClassInstance:加载组件。

下面我们按执行顺序进行讲解。

constructClassInstance

查看constructClassInstance方法:

// packages\react-reconciler\src\ReactFiberClassComponent.new.js

function constructClassInstance(
  workInProgress: Fiber,
  ctor: any,
  props: any,
): any {

  # 创建class实例对象,参数为props和context
  let instance = new ctor(props, context);

  // 将instance实例对象的state数据同步到Fiber节点的memoizedState属性
  const state = (workInProgress.memoizedState =
    instance.state !== null && instance.state !== undefined
      ? instance.state
      : null);

  # 确定class组件实例:即链接FiberNode与对应的组件实例
  adoptClassInstance(workInProgress, instance);

  // dev开发环境下的警告:如果class组件使用了过时的生命周期钩子,发出对应的警告
  ...
  
  # 返回创建完成的实例
  return instance;
}

首先直接通过new关键调用我们定义的class,创建一个实例对象Instance

let instance = new ctor(props, context);

React18.2x源码解析:类组件的加载过程

这里创建class的实例时,就会调用类的构造器函数,执行我们在constructor中编写的代码:

constructor(props) {
  super(props)
  console.log('MyClass组件运行了')
  this.state = {
    count: 1
  }
}

控制台打印出我们准备的日志:

React18.2x源码解析:类组件的加载过程

然后使用实例对象中的state数据更新Fiber节点中的数据,因为在组件更新时,需要使用Fiber节点上的数据参与计算:

// 更新数据
workInProgress.memoizedState = instance.state || null;

然后调用了一个adoptClassInstance方法:它主要有两个作用:

  • 确定类组件的更新器updater对象。
  • 关联Fiber节与之对应的组件实例Instance
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
  instance.updater = classComponentUpdater;
  // FIber节点存储instance实例
  workInProgress.stateNode = instance;
  // instance对象定义一个_reactInternal内部属性存储Fiber节点
  setInstance(instance, workInProgress);
}

更新器updatersetState方法有关,我们在类组件中使用this.setState时,实际上就是调用的updater对象中的方法,更多的细节可以查看《React18.2x源码解析:React常用API原理》中Component原理。

workInProgress.stateNode = instance;

这里将创建完成的组件实例存储到Fiber节点的stateNode属性,同时就可以印证前面的updateClassComponent方法为何通过这个属性是否有值来判断当前类组件是加载阶段还是更新阶段。

adoptClassInstance方法执行完成后,会有一些开发环境下的校验【代码较多已省略】:如果在class组件使用了过时的生命周期钩子,则会发出相应的警告进行提示:

  • UNSAFE_componentWillMount
  • UNSAFE_componentWillReceiveProps
  • UNSAFE_componentWillUpdate

最后返回创建完成的instance实例对象,constructClassInstance方法执行完成。

mountClassInstance

查看mountClassInstance方法:

// packages\react-reconciler\src\ReactFiberClassComponent.new.js

function mountClassInstance(
  workInProgress: Fiber,
  ctor: any,
  newProps: any,
  renderLanes: Lanes,
): void {

  // 取出class组件实例
  const instance = workInProgress.stateNode;
  // props
  instance.props = newProps;
  // 数据
  instance.state = workInProgress.memoizedState;
  // ref:默认为空对象
  instance.refs = emptyRefsObject;
  // 初始化一个Fiber节点的更新队列
  // 设置更新队列对象:fiber.updateQueue = queue;
  initializeUpdateQueue(workInProgress);

  // 同步组件实例的state数据
  instance.state = workInProgress.memoizedState;

  const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
  // 调用getDerivedStateFromProps钩子
  if (typeof getDerivedStateFromProps === 'function') {
    applyDerivedStateFromProps(
      workInProgress,
      ctor,
      getDerivedStateFromProps,
      newProps,
    );
    instance.state = workInProgress.memoizedState;
  }

  // In order to support react-lifecycles-compat polyfilled components,
  // Unsafe lifecycles should not be invoked for components using the new APIs.
  // 不应该使用旧的生命周期钩子
  if (
    typeof ctor.getDerivedStateFromProps !== 'function' &&
    typeof instance.getSnapshotBeforeUpdate !== 'function' &&
    (typeof instance.UNSAFE_componentWillMount === 'function' ||
      typeof instance.componentWillMount === 'function')
  ) {
    // 触发WillMount生命周期钩子
    callComponentWillMount(workInProgress, instance);
    // If we had additional state updates during this life-cycle, let's
    // process them now.
    processUpdateQueue(workInProgress, newProps, instance, renderLanes);
    instance.state = workInProgress.memoizedState;
  }

  // 如果设置了class组件的componentDidMount生命周期钩子函数,则需要在组件的FiberNode上设置对应的flags
  if (typeof instance.componentDidMount === 'function') {
    let fiberFlags: Flags = Update;
    if (enableSuspenseLayoutEffectSemantics) {
      fiberFlags |= LayoutStatic;
    }

    workInProgress.flags |= fiberFlags;
  }
}

首先更新了instance组件实例上的一些属性,然后初始化了当前组件Fiber节点的updateQueue属性:

initializeUpdateQueue(workInProgress);
export function initializeUpdateQueue<State>(fiber: Fiber): void {
  // 创建一个更新队列对象
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState, // 初始state数据
    firstBaseUpdate: null, 
    lastBaseUpdate: null, 
    shared: {
      pending: null, // 存储update链表
      lanes: NoLanes,
    },
    effects: null,
  };

  // 设置updateQueue属性
  fiber.updateQueue = queue;
}

关于updateQueue属性的更多细节我们会在更新阶段详细讲解。

代码中出现了多次更新instance.state属性,因为contextprops都有可能引起Fiber对象的数据变化,需要时刻与instance的数据保持同步。

instance.state = workInProgress.memoizedState;

然后就是针对componentWillMount生命周期函数钩子的处理,虽然是过时的API,但是如果定义了还是需要在这里触发。

// 触发WillMount生命周期钩子
callComponentWillMount(workInProgress, instance);

此时控制台就会打印出对应的日志:

React18.2x源码解析:类组件的加载过程

这些钩子函数中有可能会引起数据的变化,需要再次同步更新instance.state属性。

最后如果定义了componentDidMount钩子函数,则需要给该Fiber节点的flags属性设置对应的副作用标记。

if (typeof instance.componentDidMount === 'function') {
  workInProgress.flags |= fiberFlags;
}

flags标记的作用是在commit阶段执行对应的副作用操作,所以componentDidMount钩子函数会在commit阶段中进行触发调用。

到此为止,类组件的加载过程就完成了。

2,更新阶段

点击案例的更新按钮,触发一次组件更新,进入类组件的更新阶段。

React18.2x源码解析:类组件的加载过程

点击更新按钮,就会执行this.setState方法,这里的enqueueSetState函数其实就是this.setState真正执行的内容。

# setState原理
Component.prototype.setState = function(partialState, callback) {
  // 调用updater中的一个方法
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
enqueueSetState

查看enqueueSetState方法:

// packages\react-reconciler\src\ReactFiberBeginWork.new.js

const classComponentUpdater = {
  isMounted,
  # 这就是setState调用的方法
  enqueueSetState(inst, payload, callback) {
    // 取出instance实例对应的FiberNode节点
    const fiber = getInstance(inst);
    const eventTime = requestEventTime();
    // 获取FIber对应的lane优先级
    const lane = requestUpdateLane(fiber);
    // 创建update更新对象
    const update = createUpdate(eventTime, lane);
    // 确定更新内容 { count: 1 }
    update.payload = payload; 
    if (callback !== undefined && callback !== null) {
      // 存储传入的cb回调函数
      update.callback = callback;
    }
    # 将update更新对象添加到更新队列,并返回应用的root根节点
    const root = enqueueUpdate(fiber, update, lane);
    if (root !== null) {
      # 开启一个从root根节点开始的更新调度
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitions(root, fiber, lane);
    }
  },
      
  enqueueReplaceState() {},
  enqueueForceUpdate() {}
}

这里我们要重点关注一下类组件关于update对象和updateQueue属性的处理。

首先创建本次的update更新对象【update1】:

const update = createUpdate(eventTime, lane);
// 设置更新的内容
update.payload = payload;
// 设置回调函数
update.callback = callback;
// 第一次的setState定义
this.setState((state, props) => {
  return {
    count: state.count + 1
  }
}, () => {
  console.log('回调钩子1执行', this.state.count)
})

这里的payload属性存储的就是setState的第一个参数,也就是修改数据的内容。

callback属性存储的就是setState的第二个参数,我们定义的回调函数。

React18.2x源码解析:类组件的加载过程

所以这也就是为啥说update对象存储的就是更新操作相关的信息。

下面我们接着再看updateQueue属性的处理。

const root = enqueueUpdate(fiber, update, lane);

首先看当前类组件Fiber节点的updateQueue属性:

React18.2x源码解析:类组件的加载过程

可以看出当前的updateQueue对象中除了baseState属性存储着更新前的state数据之外,其他内容都是空的。

下面我们接着看enqueueUpdate方法对updateQueue属性的处理:

export function enqueueUpdate() {
  const updateQueue = fiber.updateQueue;
  const sharedQueue = updateQueue.shared;
  return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
}

取出了updateQueue属性和shared属性作为enqueueConcurrentClassUpdate方法的参数传入:

接着查看enqueueConcurrentClassUpdate方法:

function enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane) {
  const interleaved = sharedQueue.interleaved;
  // 第一个update对象入队
  if (interleaved === null) {
    update.next = update;
    pushConcurrentUpdateQueue(sharedQueue);
  } else {
    // 其他的update对象入队
    update.next = interleaved.next;
    interleaved.next = update;
  }
  sharedQueue.interleaved = update;
}

首先取出sharedQueueinterleaved属性【即updateQueue.shared.interleaved】,如果interleavednull,表示为当前的update1为第一个入队的更新对象,将此update1next属性指向自身,形成一个单向环状链表。

React18.2x源码解析:类组件的加载过程

React18.2x源码解析:类组件的加载过程

然后调用了一个pushConcurrentUpdateQueue方法,这个方法的作用是将sharedQueue备份到一个并发队列concurrentQueues之中,方便在之后将sharedQueue.interleaved的内容转移到sharedQueue.pending之上。

interleaved只是一个临时存储update链表的属性,最终会在更新之前转移到pending属性之上用于计算。

pushConcurrentUpdateQueue(sharedQueue);

最后设置sharedQueue.interleaved为当前的update对象。

至此,第一个this.setState操作的update1入队处理完成。

React18.2x源码解析:类组件的加载过程

回到enqueueSetState方法中,这个方法最后会调用scheduleUpdateOnFiber函数进入更新的调度程序。

关于调度任务的细节可以查看《React18.2x源码解析(二)scheduler调度程序》。

click事件触发的更新任务为同步任务:下面直接快进,来到同步任务的处理:

React18.2x源码解析:类组件的加载过程

这里首先会调用scheduleSyncCallback方法,将处理同步任务的performSyncWorkOnRoot回调函数添加到同步队列syncQueue

然后在支持微任务的环境下:就会使用scheduleMicrotask方法,这个方法等同于Promise.then

Promise.then(flushSyncCallbacks)

这里就会将冲刷同步任务队列syncQueueflushSyncCallbacks函数添加到微任务中,然后继续向下执行。

注意: 我们在DOM事件中执行了两次this.setState

  handleClick = () => {
    // 第一次
    this.setState((state, props) => {
      return {
        count: state.count + 1
      }
    }, () => {
      console.log('回调钩子1执行', this.state.count)
    })
    // 第二次
    this.setState((state, props) => {
      return {
        count: state.count + 1
      }
    }, () => {
      console.log('回调钩子2执行', this.state.count)
    })
  }

上面第一次setState执行完成,主要就是将第一个update对象进行了入队处理,同时将冲刷同步任务队列的flushSyncCallbacks函数添加到了微任务之中,等待异步处理。

但是我们的同步代码还没有执行完成,还有第二个setState等待执行:

React18.2x源码解析:类组件的加载过程

再次进入enqueueSetState方法:

React18.2x源码解析:类组件的加载过程

第二次调用setState还会新建一个update更新对象【update2】,依然会执行入队操作。

function enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane) {
  const interleaved = sharedQueue.interleaved;
  // 第一个update对象入队
  if (interleaved === null) {
      ...
  } else {
    // 其他的update对象入队
    update.next = interleaved.next;
    interleaved.next = update;
  }
  sharedQueue.interleaved = update;
}

此时update2非第一个入队的对象,所以就会进入else分支处理:

  • 将当前的update2对象的next属性执行第一个update1
  • 将第一个update1next属性指向当前的update2对象。

最后将sharedQueue.interleaved设置为最新的update2

React18.2x源码解析:类组件的加载过程

React18.2x源码解析:类组件的加载过程

至此,update2也已经入队完成,此时shared.interleaved指向的就是最新的update2

回到enqueueSetState方法中,最后还是会调用scheduleUpdateOnFiber函数进入更新的调度程序。

React18.2x源码解析:类组件的加载过程

但是这次在调度时发现新的调度优先级和现存的优先级相同,可以归为同一个任务处理,就不会再重复调度。

最后触发return关键字,结束本次同步代码的执行。

flushSyncCallbacks

来微任务队列,开始执行flushSyncCallbacks方法:

React18.2x源码解析:类组件的加载过程

可以看出syncQueue同步任务队列之中就有一个任务,即performSyncWorkOnRoot函数。

后面的逻辑就直接简单介绍了,方便快速进入到类组件的更新程序:

callback = callback(isSync);

循环syncQueue队列,从中取出callback回调函数,然后调用回调函数【performSyncWorkOnRoot】。

直接进入到performSyncWorkOnRoot方法中:

    function performSyncWorkOnRoot(root) {
      ...
      var exitStatus = renderRootSync(root, lanes);
    }

调用renderRootSync方法,开始FiberTree的创建过程。

在这之前,还有一个处理要注意:

    function renderRootSync() {
        ...
        prepareFreshStack()
    }
    function prepareFreshStack() {
        ...
        finishQueueingConcurrentUpdates()
    }

renderRootSync中会调用一个prepareFreshStack方法,这个方法主要是确定参数本次创建FiberTreehostFiber根节点,但是这个方法最后调用了finishQueueingConcurrentUpdates函数,这个函数作用就是循环并发队列concurrentQueues,将之前存储的queue对象的更新链表从share.interleaved中转移到share.pending中,代表此节点有等待处理的更新操作。

interleaved属性主要是插入时存储,现在已经转移到pending属性中:

React18.2x源码解析:类组件的加载过程

React18.2x源码解析:类组件的加载过程

下面我们直接快进到类组件的Fiber节点处理:

React18.2x源码解析:类组件的加载过程

进入beginWork工作的classComponent处理分支,开始类组件的更新:

React18.2x源码解析:类组件的加载过程

继续查看updateClassComponent方法:

React18.2x源码解析:类组件的加载过程

更新阶段时:这里组件实例instance和旧的Fiber节点【current】都存在,只能进入else分支,开始类组件的更新执行。

updateClassInstance

进入updateClassInstance方法:

    // packages\react-reconciler\src\ReactFiberClassComponent.new.js

    function updateClassInstance(
      current: Fiber,
      workInProgress: Fiber,
      ctor: any,
      newProps: any,
      renderLanes: Lanes,
    ): boolean {

        //... 省略代码
    }

updateClassInstance方法里面的内容比较多,我们分成以下几个部分来讲解:

  • 根据updateQueue计算state
  • 调用getDerivedStateFromProps钩子。
  • 调用shouldComponentUpdate钩子,检查组件是否应该更新。
  • 为组件Fiberflags属性设置componentDidUpdategetSnapshotBeforeUpdate副作用标记。

(一)根据updateQueue计算state

    function updateClassInstance(
      current: Fiber,
      workInProgress: Fiber,
      ctor: any,
      newProps: any,
      renderLanes: Lanes,
    ): boolean {
      // 取出组件实例
      const instance = workInProgress.stateNode;
      // 从之前的节点上克隆updateQueue信息,包含了shared.pending中等待处理的更新操作
      cloneUpdateQueue(current, workInProgress);
      // 旧的props
      const unresolvedOldProps = workInProgress.memoizedProps;
      const oldProps =
        workInProgress.type === workInProgress.elementType
          ? unresolvedOldProps
          : resolveDefaultProps(workInProgress.type, unresolvedOldProps);
      instance.props = oldProps;
      // 新的props
      const unresolvedNewProps = workInProgress.pendingProps;
      ...

      const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
      const hasNewLifecycles =
        typeof getDerivedStateFromProps === 'function' ||
        typeof instance.getSnapshotBeforeUpdate === 'function';

      // 取出旧的数据
      const oldState = workInProgress.memoizedState;
      let newState = (instance.state = oldState);
      # 更新组件实例的数据
      processUpdateQueue(workInProgress, newProps, instance, renderLanes);
      newState = workInProgress.memoizedState;
      
      ...
    }

首先处理之前的updateQueue信息:

    cloneUpdateQueue(current, workInProgress);

这里就是将current节点中的updateQueue信息克隆到workInProgress节点中对应的属性中。

注意: react应用每次更新都会执行Fiber Reconciler流程【即FiberTree的创建流程】,在进入此流程之前,current节点会存储本次更新相关的一些信息,但是在进入此流程之后,current就变成了旧的节点,workInProgress代表新建的节点,此时就需要将current节点上的一些信息保存到新的节点之中,也就是对应的workInProgress

在这里更新workInProgress.updateQueue属性信息,是为了下面通过计算后生成新的state数据。

定义两个变量存储旧的props和新的props,方便后续使用:

    // 旧的props
    const unresolvedOldProps = workInProgress.memoizedProps;
    // 新的props
    const unresolvedNewProps = workInProgress.pendingProps;

class类中取出getDerivedStateFromProps钩子,判断当前类组件有没有使用此钩子,如果没有使用则设置变量hasNewLifecyclesfalse,此变量的作用是后续判断其他生命周期钩子的执行与否。

下面我们开始进入state的处理,这是类组件更新的重点逻辑:

首先取出旧的state数据:

    // memoizedState代表旧的数据,就像memoizedProps代表旧的props
    const oldState = workInProgress.memoizedState;

重点来了:调用processUpdateQueue方法,根据updateQueue信息,计算生成新的state数据。

    processUpdateQueue(workInProgress, newProps, instance, renderLanes);
计算state

进入processUpdateQueue方法:

    // packages\react-reconciler\src\ReactFiberClassUpdateQueue.new.js

    export function processUpdateQueue<State>(
      workInProgress: Fiber,
      props: any,
      instance: any,
      renderLanes: Lanes,
    ): void {

      const queue = workInProgress.updateQueue;
      let firstBaseUpdate = queue.firstBaseUpdate; // 第一个处理的update,一个完整指向的队列
      let lastBaseUpdate = queue.lastBaseUpdate; // 最后一个处理的update

      // Check if there are pending updates. If so, transfer them to the base queue.
      // pending:等待处理的更新操作
      // 如果pending存在内容,则重置它,将它的内容转移到基础队列 firstBaseUpdate
      let pendingQueue = queue.shared.pending;
      if (pendingQueue !== null) {
        // 重置pending:因为workInProgress和current同用一个shared对象,所以current的pengding也重置了
        queue.shared.pending = null;

        # 重构pendingQueue,将单向环状链表变成普通的单向链表
        // 最后处理的update对象
        const lastPendingUpdate = pendingQueue;
        // 第一个处理的update对象
        const firstPendingUpdate = lastPendingUpdate.next;
        // 断开链接:最后一个update不再指向第一个update,形成一个单向链表,不再首尾相连
        lastPendingUpdate.next = null;
        // Append pending updates to base queue
        // 将等待处理的更新对象添加到firstBaseUpdate基础更新队列
        if (lastBaseUpdate === null) {
          firstBaseUpdate = firstPendingUpdate;
        } else {
          lastBaseUpdate.next = firstPendingUpdate;
        }
        lastBaseUpdate = lastPendingUpdate;
      }

      // These values may change as we process the queue.
      // 当我们处理队列时,这些值可能会发生变化。
      if (firstBaseUpdate !== null) {
        // Iterate through the list of updates to compute the result.
        let newState = queue.baseState;
        let newLanes = NoLanes;

        let newBaseState = null;
        let newFirstBaseUpdate = null;
        let newLastBaseUpdate = null;
        // 第一个更新的update
        let update = firstBaseUpdate;

        # 循环update链表计算state
        do {
          // TODO: Don't need this field anymore
          const updateEventTime = update.eventTime;

          const updateLane = removeLanes(update.lane, OffscreenLane);
          const isHiddenUpdate = updateLane !== update.lane;

          // 根据每个update对象的lane来判断是否更新:
          // 检查此更新update是否是在隐藏树时进行的。如果是,组件在隐藏时不需要更新
          // 跳过更新
          const shouldSkipUpdate = isHiddenUpdate
            ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
            : !isSubsetOfLanes(renderLanes, updateLane);

          if (shouldSkipUpdate) {
            // 不必更新【跳过更新】
            ...
          } else {
            // This update does have sufficient priority.
            // 此更新具有足够的优先级【正常更新】

            if (newLastBaseUpdate !== null) {
              const clone: Update<State> = {
                eventTime: updateEventTime,
                // This update is going to be committed so we never want uncommit
                // it. Using NoLane works because 0 is a subset of all bitmasks, so
                // this will never be skipped by the check above.
                lane: NoLane,

                tag: update.tag,
                payload: update.payload,
                callback: update.callback,

                next: null,
              };
              newLastBaseUpdate = newLastBaseUpdate.next = clone;
            }

            // Process this update.
            // 处理当前update对象
            newState = getStateFromUpdate(
              workInProgress,
              queue,
              update,
              newState,
              props,
              instance,
            );
            const callback = update.callback;
            // If the update was already committed, we should not queue its
            // callback again.
            // 如果存在回调函数,则添加到队列的effects数组中,表示有副作用等待执行
            if (callback !== null && update.lane !== NoLane) {
              workInProgress.flags |= Callback;
              const effects = queue.effects;
              if (effects === null) {
                queue.effects = [update];
              } else {
                effects.push(update);
              }
            }
          }

          // 更新update为下一个等待处理的对象
          update = update.next;

          // update有值,开启下一次循环
          if (update === null) {
            // 如果为null,代表链表执行完成,退出循环,表示本次更新state计算完成
            pendingQueue = queue.shared.pending;
            if (pendingQueue === null) {
              break;
            } else {
              // An update was scheduled from inside a reducer. Add the new
              // pending updates to the end of the list and keep processing.
              const lastPendingUpdate = pendingQueue;
              // Intentionally unsound. Pending updates form a circular list, but we
              // unravel them when transferring them to the base queue.
              const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>);
              lastPendingUpdate.next = null;
              update = firstPendingUpdate;
              queue.lastBaseUpdate = lastPendingUpdate;
              queue.shared.pending = null;
            }
          }
        } while (true);

        if (newLastBaseUpdate === null) {
          newBaseState = newState;
        }

        queue.baseState = ((newBaseState: any): State);
        queue.firstBaseUpdate = newFirstBaseUpdate;
        queue.lastBaseUpdate = newLastBaseUpdate;

        markSkippedUpdateLanes(newLanes);
        workInProgress.lanes = newLanes;
        // 更新state数据
        workInProgress.memoizedState = newState;
      }
    }

虽然processUpdateQueue方法里面的内容不少,但是其逻辑并不复杂,下面我们开始按代码顺序开始解析。

    const queue = workInProgress.updateQueue;

首先用一个变量queue存储当前类组件节点的updateQueue属性值:

React18.2x源码解析:类组件的加载过程

然后从queue对象取出firstBaseUpdatelastBaseUpdate两个属性值,并用两个变量来存储它们的值:

  • firstBaseUpdate:表示第一个处理的update对象。
  • lastBaseUpdate:表示最后一个处理的update对象。

这两个值在正常更新情况下默认都是为null的,只有在某个update优先级较低【比如当前组件节点处于hidden状态】,遗留到下次更新才会被保存到这两个值中,这种情况较少,对我们来说,只需要掌握正常更新的逻辑即可。

下面开始处理pendingQueue

      let pendingQueue = queue.shared.pending;
      if (pendingQueue !== null) {
        // 重置pending:因为workInProgress和current同用一个shared对象,所以current的pengding也重置了
        queue.shared.pending = null;

        // 最后处理的update对象
        const lastPendingUpdate = pendingQueue;
        // 第一个处理的update对象
        const firstPendingUpdate = lastPendingUpdate.next;
        // 断开链接:最后一个update不再指向第一个update,形成一个单向链表,不再首尾相连
        lastPendingUpdate.next = null;
        // Append pending updates to base queue
        // 将等待处理的更新对象添加到基础队列
        if (lastBaseUpdate === null) {
          firstBaseUpdate = firstPendingUpdate;
        } else {
          lastBaseUpdate.next = firstPendingUpdate;
        }
        lastBaseUpdate = lastPendingUpdate;
      }

这里取出pending的内容存储到pendingQueue,代表本次计算state相关的update队列。

  • 如果pendingQueuenull,代表没有需要处理的update队列,state将不会得到更新。
  • 如果pendingQueue不为null,代表有需要处理的update队列,开始计算更新state

当前我们的pendingQueue是有值的,所以需要更新state。这里重置pendingnull【已经拿到内容,不再需要保留,重置后方便下次更新重新添加】。

然后设置lastBaseUpdatependingQueue,因为pendingQueue自身就是指向最后一个update对象。

这里我们在回顾一下之前的图例印证:

React18.2x源码解析:类组件的加载过程

然后设置firstBaseUpdate的值为lastBaseUpdate.next,最后一个update对象的next属性指向第一个update

所以firstBaseUpdate就是第一个处理的update对象。

注意: 这里设置lastBaseUpdatenext属性为null,不再指向第一个update

lastBaseUpdate.next = null;

至此,firstBaseUpdate将变成一个普通的单向链表,而不是环状。

React18.2x源码解析:类组件的加载过程

这是因为后面循环处理update更新队列时,会一直从next属性取出下一个要处理的update,直到为null时结束循环。

    if (firstBaseUpdate !== null) {
        ...
    }

然后判断firstBaseUpdate是否有值,有值则循环取出update对象,开始计算更新state

    // 更新之前的state
    let newState = queue.baseState;
    // 第一个处理的update
    let update = firstBaseUpdate;

这里取出baseState,它是更新之前的state数据,也是代表参与本次计算的基础state

然后定义了一个update变量,代表第一个处理的更新对象。

    do {
        ...
    } while (true);

处理update对象的逻辑是一个do while循环结构,它的判断条件永远为真,只有处理完所有update对象才会跳出循环。

    const shouldSkipUpdate = isHiddenUpdate
            ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
            : !isSubsetOfLanes(renderLanes, updateLane);

在处理update之前,有一个优先级lane的判断。如果当前update对象的优先级较低,则会设置shouldSkipUpdate变量为true,表示应该跳过当前更新,被遗留的update就会存储到之前所说的queue.firstBaseUpdate属性中。

一般DOM事件触发的更新都是普通的更新【BaseUpdate】,有足够的优先级,不会被跳过更新,我们主要掌握它的更新逻辑即可。

下面开始解析update对象处理的具体过程:

    // 1,计算state
    newState = getStateFromUpdate(
      workInProgress,
      queue,
      update,
      newState,
      props,
      instance,
    );

    // 2,处理回调函数
    const callback = update.callback;
    // 如果存在回调函数,则添加到队列的effects数组中,表示有副作用等待执行
    if (callback !== null && update.lane !== NoLane) {
      workInProgress.flags |= Callback;
      const effects = queue.effects;
      if (effects === null) {
        queue.effects = [update];
     } else {
        effects.push(update);
      }
    }

处理update对象的逻辑主要有两点:

  • 根据update对象的payload属性计算state
  • 处理update对象的callback回调函数。

这里调用了一个getStateFromUpdate方法来计算state,所以我们还得查看这个方法的内容。

getStateFromUpdate

查看getStateFromUpdate方法:

    function getStateFromUpdate<State>(
      workInProgress: Fiber,
      queue: UpdateQueue<State>,
      update: Update<State>,
      prevState: State,
      nextProps: any,
      instance: any,
    ): any {
      switch (update.tag) {
        case ReplaceState: {
        }
        case CaptureUpdate: {
        }
        # state计算更新
        case UpdateState: {
          const payload = update.payload;
          let partialState;
          if (typeof payload === 'function') {
            // Updater function
            partialState = payload.call(instance, prevState, nextProps);
          } else {
            // Partial state object
            partialState = payload;
          }
          if (partialState === null || partialState === undefined) {
            // Null and undefined are treated as no-ops.
            return prevState;
          }
          // Merge the partial state and the previous state.
          // 返回合并生成的新对象【浅拷贝】
          return assign({}, prevState, partialState);
        }
        // 拥有强制更新标识
        case ForceUpdate: {
          hasForceUpdate = true;
          return prevState;
        }
      }
      return prevState;
    }

getStateFromUpdate方法内就是一个switch case结构,根据update对象的tag值进行不同的逻辑处理。

类组件调用this.setState创建的update对象的属于UpdateState场景,这里就会进入UpdateState分支计算state

React18.2x源码解析:类组件的加载过程

取出update对象的payload属性,前面已经讲解过payload属性存储的就是state数据,即this.setState的第一个参数。

这里判断payload是否为函数:

  • 如果是函数,则直接调用payload(),将计算后最新的state数据赋值给partialState变量。
    partialState = payload.call(instance, prevState, nextProps);
  • 如果不是函数则为对象,直接赋值给partialState变量。

最后调用assign方法,浅拷贝变化的属性,返回处理后最新的state数据。

    // 返回合并生成的新对象【浅拷贝】
    return assign({}, prevState, partialState);

到此,第一点处理state数据的逻辑就此执行完成。

下面再处理this.setState的第二个参数:callback回调函数。

    // 2,处理回调函数
    const callback = update.callback;
    // 如果存在回调函数,则添加到队列的effects数组中,表示有副作用等待执行
    if (callback !== null && update.lane !== NoLane) {
      // 副作用标记
      workInProgress.flags |= Callback;
      const effects = queue.effects;
      if (effects === null) {
        queue.effects = [update];
     } else {
        effects.push(update);
      }
    }

callback的处理比较简单,主要就是将有callback回调函数的update对象存储到queue.effects属性中,effects属性是一个数组,专门用于存储含有副作用的update对象,并且这里给对应的类组件节点workInProgress打上了Callbackflags标记。

它的作用是在之后的commit阶段【commitLayoutEffects函数】中,循环执行effects中的这些回调函数。

React18.2x源码解析:类组件的加载过程

同时在这里我们也可以得出一个原理: 一个DOM事件中,如果调用了多次this.setState(state, cb),那么它cb回调函数一定是在所有的state计算完成之后才执行的,也就是说,在第一个cb回调函数执行时,它使用的state就已经是最新的state数据了。

到这里,一个update对象就已经处理完成了,然后从当前update对象的next属性中取出下一个处理的update

update = update.next;

只要update有值,就会一直循环处理,直到最后一个update对象,因为它的next属性为null

    if (update === null) {
        break;
    }

跳出do while循环,pendingQueue中的内容就此处理完成。

最后存储最新的newStatequeue.baseState中作为下一次更新计算的基础state数据。

同时更新当前类组件节点workInProgressmemoizedState属性,表示为当前最新的state数据。

    // 更新state
    queue.baseState = newState;
    workInProgress.memoizedState = newState;

到此,updateClassInstance方法第一点逻辑,也就是最重要的计算state执行完成。

(二)调用getDerivedStateFromProps钩子。

    ...

    if (typeof getDerivedStateFromProps === 'function') {
      // 调用getDerivedStateFromProps钩子
      applyDerivedStateFromProps(
        workInProgress,
        ctor,
        getDerivedStateFromProps,
        newProps,
      );
      newState = workInProgress.memoizedState;
    }

    ...

如果类组件定义了getDerivedStateFromProps钩子函数,则在此触发此回调。

(三)调用shouldComponentUpdate钩子,检查组件是否应该更新。

    const shouldUpdate = 
        // 检查是否为强制更新
        checkHasForceUpdateAfterProcessing() ||
        // 调用shouldComponentUpdate钩子,检查是否应该更新
        checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext)

这里检查组件更新其实有两个条件:

  • 检查是否为强制更新。
  • 调用shouldComponentUpdate钩子,检查组件是否应该更新。

满足任何一个条件即返回true,表示组件需要更新。

在强制更新场景中,shouldComponentUpdate钩子其实是失效的,因为在||的逻辑中,第一个状态为true,就不会再执行后面的条件,所以shouldComponentUpdate钩子不会被调用。

而强制更新的逻辑也很简单,checkHasForceUpdateAfterProcessing方法仅仅是返回一个变量的状态:

    export function checkHasForceUpdateAfterProcessing(): boolean {
      return hasForceUpdate;
    }

hasForceUpdate是一个全局变量,它默认为false,表示非强制更新,它的修改就在之前计算state的逻辑中:

    newState = getStateFromUpdate(...)
    function getStateFromUpdate<State>() {
        switch (update.tag) {
            case ReplaceState: {}
            case CaptureUpdate: {}
            case UpdateState: {}
            // 强制更新 场景
            case ForceUpdate: {
                hasForceUpdate = true;
                return prevState;
            }
        }
    }

如果为this.forceUpdate触发的场景,就会进入ForceUpdate分支,更新变量hasForceUpdate的值为true

这时在校验组件是否应该更新时,就会返回true,代表组件需要更新。

而我们当前是通过this.setState修改数据触发的更新,所以当前hasForceUpdate是为false的,这也是绝大部分类组件更新的场景。

React18.2x源码解析:类组件的加载过程

当第一个条件为false时,就需要执行第二个条件,调用shouldComponentUpdate钩子来检查类组件是否需要更新。

查看checkShouldComponentUpdate方法:

    function checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    ) {
      const instance = workInProgress.stateNode;
      if (typeof instance.shouldComponentUpdate === 'function') {
        let shouldUpdate = instance.shouldComponentUpdate(
          newProps,
          newState,
          nextContext,
        );
        return shouldUpdate;
      }

      // 针对PureComponent纯组件的 内部校验
      if (ctor.prototype && ctor.prototype.isPureReactComponent) {
        return (
          !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
        );
      }

      return true;
    }

checkShouldComponentUpdate方法也比较简单,主要就是调用一次我们定义的shouldComponentUpdate钩子,根据调用的返回值来决定类组件是否应该更新,如果我们没有使用shouldComponentUpdate钩子,则类组件是默认需要更新的。

同时在这里我们还可以发现有一个针对PureComponent纯组件的更新校验,这其实就是PureComponentComponent唯一的区别,纯组件PureComponentreact内部自动帮助我们对propsstate进行了浅比较,任何一个变化则返回true,需要更新组件。

(四)为类组件Fiberflags属性设置对应生命周期钩子的副作用标记。

    // 执行componentWillUpdate钩子
    if (typeof instance.componentWillUpdate === 'function') {
        instance.componentWillUpdate(newProps, newState, nextContext);
    }
    // 更新flags标记
    if (typeof instance.componentDidUpdate === 'function') {
        workInProgress.flags |= Update;
    }
    if (typeof instance.getSnapshotBeforeUpdate === 'function') {
        workInProgress.flags |= Snapshot;
    }

如果类组件定义了componentWillUpdate生命周期钩子函数,则会在此处调用此函数。

然后如果类组件定义了componentDidUpdate或者getSnapshotBeforeUpdate生命周期钩子函数,就会更新组件Fiberflags属性标记。

这些生命周期钩子会在commit阶段,真实DOM渲染完成之后,被触发执行。

最后同步组件实例instancepropsstate

    instance.props = newProps;
    instance.state = newState;

到此,一个类组件的更新程序基本执行完成。

commit阶段

前面全部的加载逻辑都是在Fiber Reconciler协调流程中执行的,即类组件大部分的加载或者更新逻辑都是在reconciler协调流程中完成的,还有剩下的一部分逻辑在commit阶段之中处理,这里我们继续讲解。

这里简单介绍一下commit阶段的内容,更多处理逻辑可以查看《React18.2x源码解析(四)commit阶段》。

commit阶段的逻辑主要分为三个子阶段内容:

  • BeforeMutation
  • Mutation
  • Layout
    function commitRootImpl() {
      // 1,BeforeMutation阶段
      commitBeforeMutationEffects()
      // 2,Mutation阶段,渲染真实DOM加载到页面
      commitMutationEffects()
      // 3,Layout阶段
      commitLayoutEffects()
    }

对于类组件的更新来说,在commit阶段主要还有以下两部分逻辑需要处理:

  • 执行类组件的componentDidUpdate生命周期钩子函数。
  • 执行this.setState方法传入的回调函数。

这两部分逻辑都是在Layout阶段中处理的,下面我们查看具体的处理逻辑:

    // packages\react-reconciler\src\ReactFiberCommitWork.new.js

    function commitLayoutEffectOnFiber(
      finishedRoot: FiberRoot,
      current: Fiber | null,
      finishedWork: Fiber,
      committedLanes: Lanes,
    ): void {
      if ((finishedWork.flags & LayoutMask) !== NoFlags) {
        // 根据组件类型:进行不同的处理
        switch (finishedWork.tag) {

          // 类组件的处理
          case ClassComponent: {
            // 组件实例
            const instance = finishedWork.stateNode;
            if (finishedWork.flags & Update) {
              if (!offscreenSubtreeWasHidden) {
                if (current === null) {
                  // mount加载阶段
                    
                  # 触发componentDidMount 生命周期钩子函数【这类静态方法:是存储在instance对象原型上的】
                  instance.componentDidMount();
                } else {
                    
                  // update更新阶段
                  const prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps
                      : resolveDefaultProps( finishedWork.type, current.memoizedProps);
                  const prevState = current.memoizedState;
                  # 触发componentDidUpdate 生命周期钩子函数
                  instance.componentDidUpdate( prevProps,prevState,
                      instance.__reactInternalSnapshotBeforeUpdate,
                    );
                }
              }
            }
            
            # 取出当前组件节点的updateQueue更新对象
            const updateQueue: UpdateQueue = finishedWork.updateQueue;
            if (updateQueue !== null) {
              // 触发更新
              commitUpdateQueue(finishedWork, updateQueue, instance);
            }
            break;
          }
            
          ...
        }
      }
    }

finishedWork代表当前处理的类组件Fiber节点,首先从fiber.stateNode属性中取出组件实例instance。然后根据fiber.flags进行判断,只有存在相关的副作用标记才会继续内部的逻辑:

    if (finishedWork.flags & Update) {
      ...
    }

当前条件是满足的,只要定义了相关的钩子函数,就会在之前的reconciler协调流程中被标记,它的作用就是在此刻进行判断,然后执行相关的副作用回调。

然后判断current是否为nullcurrent表示旧的虚拟DOM节点,在组件的更新阶段,它肯定是存在的。

    if (current === null) {
        // 加载
    } else {
        // 更新
        instance.componentDidUpdate(prevProps, prevState);
    }

然后直接调用componentDidUpdate生命周期钩子函数即可。

下面我们再继续查看对this.setState回调的处理。

    const updateQueue: UpdateQueue = finishedWork.updateQueue;
    if (updateQueue !== null) {
      // 触发更新
      commitUpdateQueue(finishedWork, updateQueue, instance);
    }

类组件更新阶段updateQueue属性都是有值的,直接查看commitUpdateQueue方法。

commitUpdateQueue
    // packages\react-reconciler\src\ReactFiberClassUpdateQueue.new.js

    export function commitUpdateQueue<State>(
      finishedWork: Fiber,
      finishedQueue: UpdateQueue<State>,
      instance: any,
    ): void {
      // Commit the effects
      const effects = finishedQueue.effects;
      // 重置
      finishedQueue.effects = null;
      if (effects !== null) {
        for (let i = 0; i < effects.length; i++) {
          const effect = effects[i];
          const callback = effect.callback;
          if (callback !== null) {
            effect.callback = null;
            callCallback(callback, instance);
          }
        }
      }
    }

这里从updateQueue属性中取出effects副作用回调数组,然后立即重置为null【已经拿到内容,不再需要保留,重置后方便下次更新重新添加】。

接着判断effects是否为null,很明显当前是有值的,因为我们之前调用了两次this.setState都传递了回调函数,所以当前的effects数组应该会有两个元素内容,以调试截图印证:

React18.2x源码解析:类组件的加载过程

effects数组有值,则循环effects数组:

    for (let i = 0; i < effects.length; i++) {
      const effect = effects[i];
      const callback = effect.callback;
      if (callback !== null) {
        effect.callback = null;
        // 执行回调
        callCallback(callback, instance);
      }
    }

从中取出effect对象,其实就是之前的update对象,然后取出callback回调函数,最后触发回调函数。

    callback()

循环结束,控制台依次打印出我们设置的日志:

React18.2x源码解析:类组件的加载过程

到此类组件的更新内容全部执行完成。

总结

类组件的更新逻辑重点内容是以下两点:

  • reconciler协调流程中循环update链表计算出最新的state
  • commit阶段中触发componentDidUpdate生命周期钩子函数以及循环执行this.setState的回调。
转载自:https://juejin.cn/post/7280435532987842615
评论
请登录