Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • plan 计划
  • 基础

  • 调和(Reconciliation)

  • 调度器(Scheduler)

  • 更新器(Updater)

  • 渲染器(Render)

  • hooks原理

  • 总结

  • React源码漂流记

    • 开始上手
    • Plan 计划
    • 前言
    • React 源码漂流记:ReactElement 与基础概念
    • React 源码漂流记:ReactChildren 与节点操纵
    • React 源码漂流记:React 整体结构和理念初认识
    • React 源码漂流记:React 调和器核心源码解读(一)
    • React 源码漂流记:React 调和器核心源码解读(二)
    • React 源码漂流记:React 调和器核心源码解读(三)
    • React 源码漂流记:React 调和器核心源码解读(四)
      • 目录
      • 前言
      • beginWork
      • mountIndeterminateComponent
      • mountLazyComponent
      • updateFunctionComponent
      • updateClassComponent
      • updateHostRoot
      • updateHostComponent
      • updateHostText
      • 扩展
      • 问题
      • 总结
    • React 源码漂流记:React 调和器核心源码解读(五)
    • React 源码漂流记:React 调和器核心源码解读(六)
    • React 源码漂流记:React 调和器核心源码解读(七)
    • React 源码漂流记:React 调和器核心源码解读(八)
    • React 源码漂流记:React 调和器核心源码解读(九)
    • React 源码漂流记:React 调和器核心源码解读(十)
    • React 源码漂流记:React 调度器核心源码解读(一)
    • 带着原理重读 React 官方文档(一)
    • 带着原理重读 React 官方文档(二)
  • react
  • React源码漂流记
jonsam
2022-07-20
目录

React 源码漂流记:React 调和器核心源码解读(四)

标签: React17精简

# 目录

  • 目录
  • 前言
  • beginWork
    • didReceiveUpdate
    • beginWork
  • mountIndeterminateComponent
  • mountLazyComponent
  • updateFunctionComponent
  • updateClassComponent
  • updateHostRoot
  • updateHostComponent
  • updateHostText
  • 扩展
  • 问题
    • Component 即 workInProgress.type 是如何初始化的?
    • mountIndeterminateComponent为什么都能够执行 Component(props, context)?
  • 总结

# 前言

在上一篇文章中,我们探讨了 React 调和器中 renderRootSync 、 renderRootConcurrent 、 workLoopSync 、 workLoopConcurrent 、 performUnitOfWork 5 个核心函数,整体过程偏于梗概和流程,虽然过程较为简单,但是对于理解整体调和的过程却是至关重要的。因此,上文与本文的衔接较为紧密。

先回顾一下上文的情节,5 个函数,从 Render 阶段的开始,到 WorkLoop 的启动,再到 performUnitOfWork 的具体的调和工作,整个调和过程其实比较清晰。在以下几篇文章中,将着重分析调和中的捕获和冒泡过程,即单个 Fiber 节点的调和过程。

注意

因篇幅限制,本文只探讨重要类型的组件的调和过程的梗概,具体组件的调和过程细节不探讨,后文详述。

# beginWork

从整体上看, beginWork 是根据 workInProgress Fiber 的类型,而决定采取不同的 mount 或者 update 的策略。

# didReceiveUpdate

didReceiveUpdate 对于理解这部分代码很重要,因此我们先来分析下 didReceiveUpdate 的含义和意义。 didReceiveUpdate 表示是否接收到变化(更新),如果未接收到更新则可以复用 Fiber 节点(意思是复用下一级的子节点提前返回 null 阻断后续的捕获过程)。

简化代码如下:

if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (oldProps !== newProps) {
      // 如果有 props 变化,则标记接收到更新
      didReceiveUpdate = true;
    } else {
      // ......
      didReceiveUpdate = false;
    }
  } else {
    didReceiveUpdate = false;
    // ......
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

分析如下:

  • 参数含义:通过上文中的探讨,我们可以明了的是: current 表示 current FiberTree 的 FiberNode,是已经渲染过的节点; workInProgress 是 workInProgress FiberTree 的 FiberNode,是即将要被渲染的节点。
  • current === null 可以区分 mount 阶段和 update 阶段(注意:这里不是指调和的阶段,而是指从 React 的应用的渲染周期来看,包含初次渲染即 mount 阶段和更新渲染即 update 阶段)。

注意

这里为什么比较 Props 的引用而不是浅比较呢?为什么在执行 Component 之前需要比较 Props? 在 performUnitOfWork 中 unitOfWork.memoizedProps = unitOfWork.pendingProps 中有如下代码,那次此处 oldProps 大概率会与 newProps 相等?

弄清楚 didReceiveUpdate 含义和复用节点的条件之后,下面我们再具体探讨 beginWork 的内容。

# beginWork

注意

以下代码只保留了核心内容,细节内容有删改。

// src/react/packages/react-reconciler/src/ReactFiberBeginWork.new.js
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      return mountIndeterminateComponent(/*......*/);
    case LazyComponent: 
      return mountLazyComponent(/*......*/);
    case FunctionComponent: 
      return updateFunctionComponent(/*......*/);
    case ClassComponent: 
      return updateClassComponent(/*......*/);
    case HostRoot:
      return updateHostRoot(/*......*/);
    case HostComponent:
      return updateHostComponent(/*......*/);
    case HostText:
      return updateHostText(/*......*/);
    case SuspenseComponent:
      return updateSuspenseComponent(/*......*/);
    case HostPortal:
      return updatePortalComponent(/*......*/);
    case ForwardRef: 
      return updateForwardRef(/*......*/);
    case Fragment:
      return updateFragment(/*......*/);
    // ......
    case ContextProvider:
      return updateContextProvider(/*......*/);
    case ContextConsumer:
      return updateContextConsumer(/*......*/);
    case MemoComponent: 
      return updateMemoComponent(/*......*/);
    case SimpleMemoComponent: 
      return updateSimpleMemoComponent(/*......*/);
    case IncompleteClassComponent: 
      return mountIncompleteClassComponent(/*......*/);
    // ......
    case OffscreenComponent: 
      return updateOffscreenComponent(/*......*/);
    // ......
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

# mountIndeterminateComponent

此函数针对首次渲染时的未知类型的组件(并且是立即挂载的组件)进行判断,区分函数式组件和 伪装成函数的类组件 进行相关处理,并且调和 ReactChildren(本质上是 ReactElement)。

源码如下:

// src/react/packages/react-reconciler/src/ReactFiberBeginWork.new.js
function mountIndeterminateComponent(
  _current,
  workInProgress,
  Component,
  renderLanes,
) {
  // ......
  const props = workInProgress.pendingProps;
  // 清理 workInProgress context,即 workInProgress.dependencies.firstContext = null
  prepareToReadContext(workInProgress, renderLanes);
  // 执行 Component 并获得返回值,如果是类组件,value 是组件实例
  const value = Component(props, context)

  // 判断是否是合法的类组件
  if (
    typeof value === 'object' &&
    value !== null &&
    typeof value.render === 'function' &&
    value.$$typeof === undefined
  ) {
    // 将 workInProgress 标记为类组件
    workInProgress.tag = ClassComponent;
    workInProgress.updateQueue = null;
    // ......
    // 更新 state
    workInProgress.memoizedState =
      value.state !== null && value.state !== undefined ? value.state : null;
    // 初始化 state 更新队列
    initializeUpdateQueue(workInProgress);
    // 将类组件实例设置到 workInProgress Fiber 上
    adoptClassInstance(workInProgress, value);
    // 挂载类组件实例,触发 `getDerivedStateFromProps` 和 `componentWillMount`
    mountClassInstance(workInProgress, Component, props, renderLanes);
    // 调和 ReactElement
    return finishClassComponent(
      null,
      workInProgress,
      Component,
      true,
      false,
      renderLanes,
    );
  } else {
    // 将 workInProgress 标记为 FunctionComponent
    workInProgress.tag = FunctionComponent;
    // ......
    // 调和 ReactElement
    reconcileChildren(null, workInProgress, value, renderLanes);
    // 返回 child 节点
    return workInProgress.child;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

需要注意一下几点:

  • 此处传入的 Component,实际上是 workInProgress.type ,在前文中我们已经了解到 Fiber.type 挂载着 fiber 所对应的 function/class/module 类型的组件,属于 Fiber 和 ReactElement 沟通的媒介。
  • 如果函数 Component 执行之后返回 object 且包含 render 方法,则会被当做类组件处理。
  • 注意:此函数只在初次渲染函数式组件或者 伪装成函数的类组件 时调用。调用之后,该组件会被判定为 ClassComponent 或者 FunctionComponent ,调用结束后返回 child 节点,以继续完成 WorkLoop 中的捕获过程。

# mountLazyComponent

mountLazyComponent 是针对 LazyComponent (延迟加载组件) 而言的,需要加载组件时本质上还是根据 beginWork 中的组件类型策略进行处理,因此此处不再赘述。Lazy 组件将在之后的章节单独探讨。

提示

在下文的几种类型的组件的处理中,我们需要强化两条思路,复用(简单的复用 current ChildrenFiberTree 以提前结束当前节点的捕获过程)和不复用。

# updateFunctionComponent

updateFunctionComponent 函数调和 FunctionComponent 类型的组件(组件为函数式组件,一般非初次调和)。

function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {
  // ......
  // 获取 ReactChildren
  const nextChildren = renderWithHooks(
    current,
    workInProgress,
    Component,
    nextProps,
    undefined,
    renderLanes,
  );
  // 如果非初次渲染且未接收到更新则复用 Fiber 节点,提前退出
  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    // 调用 `cloneChildFibers` 复用节点,返回 child 节点
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  // ......
  // 调和 ReactChildren
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

从以上过程可以看出调和组件大概分为如下的步骤:

  1. 获取组件的 ReactChildren。
  2. 判断是否可以复用节点,如果可以则调用 cloneChildFibers 复用子层级节点并提前返回(返回子节点),否则则调用 reconcileChildren 重新调和子层级节点并返回子节点。

# updateClassComponent

updateClassComponent 函数调和 ClassComponent 类型的组件(class 语法的类组件)。

function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  // ......
  // 获取类组件实例
  const instance = workInProgress.stateNode;
  let shouldUpdate;
  if (instance === null) {
    // ......
    // 若实例尚未创建,则构建组件实例,new Component(props, context)
    constructClassInstance(workInProgress, Component, nextProps);
    // 挂载组件,调用相关生命周期钩子
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    shouldUpdate = true;
  } else if (current === null) {
    // 应用加载时,如果已经有组件实例,则复用此实例,并且调用相关的生命周期钩子 `componentWillReceiveProps`、`getDerivedStateFromProps`
    shouldUpdate = resumeMountClassInstance(
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  } else {
    // 如果在更新阶段,且已经有组件实例,则更新组件实例,并且调用相关的生命周期函数 `componentWillReceiveProps`、`getDerivedStateFromProps`,同时更新实例的 props, state 和 context
    shouldUpdate = updateClassInstance(
      current,
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  }
  // 根据 shouldUpdate 决定是否需要复用节点,reconcileChildren 或者 cloneChildFibers,并返回子节点
  const nextUnitOfWork = finishClassComponent(
    current,
    workInProgress,
    Component,
    shouldUpdate,
    false,
    renderLanes,
  );
  // ......
  return nextUnitOfWork;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

这里根据 instance 和 current 分成了 3 种情况,如下:

阶段 / 有无实例 无实例 有实例
mount 构建实例,挂载实例(应用初始化,mount 钩子) 复用实例(update 钩子)
update 构建实例,挂载实例(suspended 组件,mount 钩子) 更新实例(update 钩子)

挂载或者更新完组件实例之后,根据 shouldUpdate 判断是否可以复用节点,复用则调用 cloneChildFibers ,否则调用 reconcileChildren (与 FunctionComponent 一致)。

# updateHostRoot

updateHostRoot 处理 HostRootFiber 节点(FiberTree 的根节点,与 FiberRoot 容器双向链接)的调和过程。

function updateHostRoot(current, workInProgress, renderLanes) {
  // ......
  const nextProps = workInProgress.pendingProps;
  const prevChildren = workInProgress.memoizedState.element;
  // 从 current 上复制 updateQueue 到 workInProgress
  cloneUpdateQueue(current, workInProgress);
  // 消费 workInProgress 上的 updateQueue 更新 workInProgress.memoizedState
  processUpdateQueue(workInProgress, nextProps, null, renderLanes);
  const nextChildren = workInProgress.memoizedState.element;
  // 判断消费 updateQueue 之后 children 是否发生了变化,如果没有发生变化则复用节点,否则重新调和子树,并返回子节点
  if (nextChildren === prevChildren) {
    // ......
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  // ......
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  // ...... 
  return workInProgress.child;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • cloneUpdateQueue 从 current 复制 updateQueue 到 workInProgress ,以确保 processUpdateQueue 在处理时并不是直接在 current 上操作。 processUpdateQueue reduce updateQueue 中的 update,并且获得最新的 state 和 effect。
  • 在处理 updateQueue 前后,判断 children 是否发生变化,如未发生变化可直接复用节点。

# updateHostComponent

HostComponent 指原生的 HTML 节点。 updateHostComponent 调和原生的 HTML 节点。

function updateHostComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  // ......
  const type = workInProgress.type;
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;
  let nextChildren = nextProps.children;
  // 判断该节点是否应该直接设置文本内容,如 textarea, noscript 等,对于此类节点不需要再建立 nextChildren 
  // 为 HostText 以节省性能
  const isDirectTextChild = shouldSetTextContent(type, nextProps);

  if (isDirectTextChild) {
    nextChildren = null;
  // 如果原节点存在,原来是 DirectTextChild,现在不是了,标记 ContentReset,表示重置文本内容
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
    workInProgress.flags |= ContentReset;
  }
  // ......
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • 对于 HostComponent 而言不需要复用节点,一律重新调和节点。

# updateHostText

updateHostText 调和 HostText 节点。

function updateHostText(current, workInProgress) {
  // ......
  return null;
}
1
2
3
4

文本节点一定是叶子节点,因此不需要再调和 children。注意:这里返回 null,会使捕获过程暂时结束,在 performUnitOfWork 函数中,会进而转入冒泡阶段。

# 扩展

# 问题

# Component 即 workInProgress.type 是如何初始化的?

在应用挂载阶段, Component 所对应的 FiberNode 是在 prepareFreshStack 函数中创建的,参见 workInProgress 是如何初始化的?,而具体的 Fiber.type 的初始化请参见 React 首次渲染过程解读。

# mountIndeterminateComponent 为什么都能够执行 Component(props, context) ?

在 mountIndeterminateComponent 是有做类组件和函数式组件的判断的,那么大家可能会有这样的疑问,类组件时 class ,应该是无法被执行的才对? class 定义的类组件确实无法被执行,但是并非所有的类组件都是 class 定义的、都是无法执行的。

  • 利用 class 写类组件只是一种语法糖,并非只有这一种写法。其实,类组件还可以这样写:
function TestIndeterminateComponent() {
  return {
    componentDidMount() {
      console.log('componentDidMount')
    },
    state: { count: 1 },
    updateCount() {
     const { count } = this.state;
     this.updater.enqueueSetState(
      this,
      { count: count + 1 },
      undefined,
      "setState"
     );
    },
    render() {
     return <div onClick={() => this.updateCount()}>{this.state.count}</div>;
    },
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

我们可能注意到,在 mountIndeterminateComponent 判断为 function 语法的类组件 (暂时这样称呼)之后,相比于函数式组件,多出 initializeUpdateQueue 、 adoptClassInstance 、 mountClassInstance 的步骤,实际上是为了挂载相关的属性、调用声明周期钩子等以完成完成的类组件的行为。注意一下以下几点:

  • 类组件继承自 React.component 实际上是语法糖的实现,帮助我们初始化组件的必要属性如 props、state、updater 等,并且实现 setState 、 forceUpdate 等方法。(参见源码文件 src/react/packages/react/src/ReactBaseClasses.js )
  • mountIndeterminateComponent 只在首次渲染时处理函数式组件或者像如上这种 伪装成函数式组件的类组件 。

# 总结

  • 本文介绍了常用的组件类型的捕获过程。主要分成一下 3 个主要步骤:获取 Component 的 nextChildren ;采用一定的复用策略判断是否可以复用子节点调和过程,如果可以则通过复用子层级节点提前进入下一层的捕获,否则则重新调和子层级节点;返回 workInProgress.child 继续捕获过程。
  • beginWork 的返回值有两种情况:返回当前节点的子节点,然后会以该子节点作为下个工作单元继续 beginWork ,不断往下生成 fiber 节点,构建 workInProgress 树(捕获);返回 null,当前 fiber 子树的遍历就此终止,从当前 fiber 节点开始往回进行 completeWork (冒泡)。
  • beginWork 主要作用就是针对当前捕获到的节点进行处理,并且返回子节点继续捕获,捕获过程中逐渐创建 workInProgress FiberTree。
  • 本文只是探讨 beginWork 的大致流程,具体的流程会在后文继续分析,包括状态的更新,fiber 的 diff 算法等。
编辑 (opens new window)
上次更新: 2022/09/06, 14:25:16
React 源码漂流记:React 调和器核心源码解读(三)
React 源码漂流记:React 调和器核心源码解读(五)

← React 源码漂流记:React 调和器核心源码解读(三) React 源码漂流记:React 调和器核心源码解读(五)→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式