分享最近学习react源码的经历

分享最近学习react源码的经历

写在前面:最近一段时间在学习React源码,写这篇文章的目的有二:

  1. 分享自己学习的经历,希望之后有相关学习需求的同学有址可寻,少走弯路。
  2. 将学习到的内容转化为文字输出,便于之后回顾(所以可能文中的文字大部分存在生搬硬套的问题,仅有少部分是自己的理解)

我又将这段时间分成如下几个阶段:

  1. 根据教程,手写一个简单的React
  2. 对比React调用栈,了解React大体上的一个工作流程
  3. 下载React源码,根据React源码分析教程,边学习,边调试
  4. 再回顾一遍教程解读,结合其他材料(包括但不限于官网、其他解读材料、视频),在React源码中写上详细注释
  5. 写一篇关于React源码的笔记(正在做的事)

再次声明,本篇文章主要是为了分享学习经理,想要系统学习React源码的同学可以参照文末的优秀文章~

如何手写一个简单的react框架

刚开始学习react源码之前,建议自己先手动实现一个简单的react。推荐跟着下面的视频教程进行学习。

react17源码训练营

照着视频教程撸的源码

如何调试React的源码

  1. 首先从react官方将react源码clone到本地,我这边使用的是V17.0.2的版本
# 拉取代码
1. git clone https://github.com/facebook/react.git
# 也可以使用cnpm镜像
2. git clone https://github.com.cnpmjs.org/facebook/react 
# 或者使用码云的镜像
3. git clone https://gitee.com/mirrors/react.git
  1. 安装依赖
cd react
yarn
  1. 执行打包命令,将react、react-dom、scheduler三个包单独打包成文件,方便调试阅阅读(build过程需要安装jdk)
yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE

tips: 如果自己build有困难的话,可以直接使用我打包好的

打包好的地址

  1. 使用cra创建自己的react项目
npx create-react-app my-app
  1. 在my-app项目中使用我们打包好的js文件
step1: 在打包好的react目录下执行 yarn link
step2: 在my-app项目目录下执行 yarn link react

step3: 在打包好的react-dom目录下执行 yarn link
step4: 在my-app项目目录下执行 yarn link react-dom
  1. 测试下yarn link 是否生效
// 在react-dom/cjs/react-dom.development.js中加上自己的log

// 1. 应用程序中 调用的入口函数 在这里
function render(element, container, callback) {
  console.log('react render函数执行了');
  if (!isValidContainer(container)) {
    {
      throw Error( "Target container is not a DOM element." );
    }
  }

  {
    var isModernRoot = isContainerMarkedAsRoot(container) && container._reactRootContainer === undefined;

    if (isModernRoot) {
      error('You are calling ReactDOM.render() on a container that was previously ' + 'passed to ReactDOM.createRoot(). This is not supported. ' + 'Did you mean to call root.render(element)?');
    }
  }

  return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}

启动my-react项目,打开控制台… 可以看到,react,react-dom这两个包使用的是我们打包好的而不是node_modules里的。接下来我们就可以愉快地调试了!

React的Fiber架构理念

最新的react架构可以分为三层:

  • Scheduler(调度器) ———— 调度任务的优先级,高优先级任务优先进入Reconciler
  • Reconciler(协调器) ———— 负责对比更新前后节点的变化(diff算法)
  • Renderer(渲染器) ———— 负责将需要变化的节点渲染到页面上
Scheduler(调度器)

scheduler包, 核心职责只有 1 个, 就是执行回调.

  • react-reconciler提供的回调函数, 包装到一个任务对象中.
  • 在内部维护一个任务队列, 优先级高的排在最前面.
  • 循环消费任务队列, 直到队列清空.
Reconciler(协调器)

react-reconciler包, 有 3 个核心职责:

  1. 装载渲染器, 渲染器必须实现HostConfig协议(如: react-dom), 保证在需要的时候, 能够正确调用渲染器的 api, 生成实际节点(如: dom节点).
  2. 接收react-dom包(初次render)和react包(后续更新setState)发起的更新请求.
  3. fiber树的构造过程包装在一个回调函数中, 并将此回调函数传入到scheduler包等待调度.

react中最广为人知的是可中断渲染。之所以react16之后UNSAFE_componentWillMount, UNSAFE_componentWIllReceivePropsrender阶段执行的声明周期函数是不安全的,是因为render阶段是可中断的。但是!只有在HostRootFiber.mode === ConcurrentRoot | BlockingRoot才会开启。如果是legacy模式,即通过ReactDOM.render(<App/>, dom)这种方式启动时,这种情况下无论是首次 render 还是后续 update 都只会进入同步工作循环, reconciliation没有机会中断, 所以生命周期函数只会调用一次。所以,虽然在react17中可中断渲染已经实现,但目前为止,还是实验性功能

Renderer(渲染器)

react-dom包, 有 2 个核心职责:

  1. 引导react应用的启动(通过ReactDOM.render).
  2. 实现HostConfig协议(源码在 ReactDOMHostConfig.js 中), 能够将react-reconciler包构造出来的fiber树表现出来, 生成 dom 节点(浏览器中), 生成字符串(ssr).

React的Fiber架构工作原理

双缓存Fiber树

什么叫做双缓存:在内存中构建并直接替换的技术叫做双缓存。

在React中最多会同时存在两颗Fiber树.当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树

current Fiber树中的Fiber节点被称为current fiberworkInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点(fiberRootNode)通过改变current指针在不同Fiber树的指向来完成current树workInProgress树的替换。

当workInProgress树构建完成交给Renderer渲染在页面上后,应用跟节点(fiberRootNode)指针指向workInProgress Fiber树,此时workInProgress 树变为current Fuber树

每次状态更新都会产生新的workInProgress Fiber树,通过currentworkInProgress的替换,完成DOM更新。

mount时
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 1
    }
  }

  render() {
    const { number } = this.state;
    return (
      <div onClick={() => {this.setState({number: number+1})}}>
         classComponent: { number}
      </div>
    )
  }
}


ReactDOM.render(
  <App />,
  document.getElementById('root')
)
  1. 首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber<App/>所在组件的根节点。

由于我们的应用中,可以调用多次ReactDOM.render。那么rootFiber就会有多个,但是fiberRootNode仅有一个。fiberRootNode永远指向当前页面上渲染的Fiber 树,即current Fiber树

fiberRootNode.current = rootFiber
  1. 接下来进入render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。(下图中右侧为内存中构建的树,左侧为页面显示的树)

在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性,在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)。

图片引自https://react.iamkasong.com/

  1. 上图中右侧构建完的workInProgress Fiber树commit阶段渲染到页面上后。此时DOM更新为右侧对应的样子。fiberRootNodecurrent指针指向workInProgress Fiber树使其变为current树

图片引自https://react.iamkasong.com/

对应的代码:

// commitRootImpl函数中 重点关注下这行代码!!! workInProgress树和current树的切换
root.current = finishedWork;
update时
  1. 接下来我们几点,调用setState触发更新。这一次会开启一次新的render阶段并且构建一颗新的workInProgress 树

mount时一样,workInProgress fiber的创建可以复用current Fiber树对应的节点数据。

这个决定是否复用的过程就是Diff算法

  1. workInProgress Fiber 树render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber树变为current Fiber树。

React中的高频对象解释

Fiber 对象

先看数据结构, 其 type 类型的定义在ReactInternalTypes.js中:

export type Fiber = {|
  tag: WorkTag, // 标识fiber类型,根据ReactElemnt组件的type进行生成
  key: null | string, // 该节点的唯一表示,用于diff算法的优化
  elementType: any, // 一般来讲和ReactElement组件的 type 一致
  type: any, // 一般来讲和fiber.elementType一致. 一些特殊情形下, 比如在开发环境下为了兼容热更新(HotReloading), 会对function, class, ForwardRef类型的ReactElement做一定的处理, 这种情况会区别于fiber.elementType
  stateNode: any, // 与fiber关联的局部状态节点(比如: HostComponent类型指向与fiber节点对应的 dom 节点; 根节点fiber.stateNode指向的是FiberRoot; class 类型节点其stateNode指向的是 class 实例).
  
  return: Fiber | null, // 该节点的父节点
  child: Fiber | null, // 该节点的第一个子节点
  sibling: Fiber | null, // 该节点的下一个子节点
  index: number, // 该节点的下标
  ref:
    | null
    | (((handle: mixed) => void) & { _stringRef: ?string, ... })
    | RefObject,
  pendingProps: any, // 从`ReactElement`对象传入的 props. 用于和`fiber.memoizedProps`比较可以得出属性是否变动
  memoizedProps: any, // 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中
  updateQueue: mixed, // 存储state更新的队列, 当前节点的state改动之后, 都会创建一个update对象添加到这个队列中.
  memoizedState: any, // 用于输出的state, 最终渲染所使用的state
  dependencies: Dependencies | null, // 该fiber节点所依赖的(contexts, events)等
  mode: TypeOfMode, // 二进制位Bitfield,继承至父节点,影响本fiber节点及其子树中所有节点. 与react应用的运行模式有关(有ConcurrentMode, BlockingMode, NoMode等选项).

  // Effect 副作用相关
  flags: Flags, // 标志位
  subtreeFlags: Flags, //替代16.x版本中的 firstEffect, nextEffect. 当设置了 enableNewReconciler=true才会启用
  deletions: Array<Fiber> | null, // 存储将要被删除的子节点. 当设置了 enableNewReconciler=true才会启用

  nextEffect: Fiber | null, // 单向链表, 指向下一个有副作用的fiber节点
  firstEffect: Fiber | null, // 指向副作用链表中的第一个fiber节点
  lastEffect: Fiber | null, // 指向副作用链表中的最后一个fiber节点

  // 优先级相关
  lanes: Lanes, // 本fiber节点的优先级
  childLanes: Lanes, // 子节点的优先级
  alternate: Fiber | null, // 指向内存中的另一个fiber, 每个被更新过fiber节点在内存中都是成对出现(current和workInProgress)

  // 性能统计相关(开启enableProfilerTimer后才会统计)
  // react-dev-tool会根据这些时间统计来评估性能
  actualDuration?: number, // 本次更新过程, 本节点以及子树所消耗的总时间
  actualStartTime?: number, // 标记本fiber节点开始构建的时间
  selfBaseDuration?: number, // 用于最近一次生成本fiber节点所消耗的实现
  treeBaseDuration?: number, // 生成子树所消耗的时间的总和
|};
Update 与 UpdateQueue 对象

当react程序触发状态更新的时候,我们首先会去创建一个update对象。

状态更新的流程: 触发状态更新 —> 创建Update对象 -> 从fiber到root(markUpdateLaneFromFiberToRoot) -> 调度更新(ensureRootIsScheduled) -> render阶段(performSyncWorkOnRootperformConcurrentWorkOnRoot) -> commit阶段(commitRoot)

export type Update<State> = {|
  eventTime: number, // 发起update事件的时间(17.0.1中作为临时字段, 即将移出)
  lane: Lane, // update所属的优先级

  tag: 0 | 1 | 2 | 3, //  共 4 种. UpdateState,ReplaceState,ForceUpdate,CaptureUpdate
  payload: any, // 载荷, 根据场景可以设置成一个回调函数或者对象(setState中的第一个参数)
  callback: (() => mixed) | null, // 回调函数(setState中的第二个参数)

  next: Update<State> | null, // 指向链表中的下一个, 由于UpdateQueue是一个环形链表, 最后一个update.next指向第一个update对象
|};

// =============== UpdateQueue ==============
type SharedQueue<State> = {|
  pending: Update<State> | null,
|};

export type UpdateQueue<State> = {|
  baseState: State,
  firstBaseUpdate: Update<State> | null,
  lastBaseUpdate: Update<State> | null,
  shared: SharedQueue<State>,
  effects: Array<Update<State>> | null,
|};

fiber对象中有一个updateQueue,是一个链式队列,下面通过一张图来描述Fiber,UpdateQueue,Update对象之间的关系

图片引自https://github.com/7kms/react-illustration-series

Hook 对象

Hook的出现使得function函数具有状态管理的能力,从react@16.8版本之后,官方开始推荐Hook语法。官方一共定义了14种Hook类型

export type Hook = {|
  memoizedState: any, // 内存状态,用于输出给行程最终的fiber树
  baseState: any, // 基础状态,当Hook.queue更新过后,baseState也会更新 
  baseQueue: Update<any, any> | null, // 基础状态队列, 在reconciler阶段会辅助状态合并.
  queue: UpdateQueue<any, any> | null, // 指向一个Update队列
  next: Hook | null, // 指向该function组件的下一个Hook对象, 使得多个Hook之间也构成了一个链表.
|};

// UpdateQueue和Update是为了保证Hook对象能够顺利更新, 与上文fiber.updateQueue中的UpdateQueue和Update是不一样的(且它们在不同的文件)
type Update<S, A> = {|
  lane: Lane,
  action: A,
  eagerReducer: ((S, A) => S) | null,
  eagerState: S | null,
  next: Update<S, A>,
  priority?: ReactPriorityLevel,
|};

//UpdateQueue和Update是为了保证Hook对象能够顺利更新, 与上文fiber.updateQueue中的UpdateQueue和Update是不一样的(且它们在不同的文件)
type UpdateQueue<S, A> = {|
  pending: Update<S, A> | null,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
|};

更多详细的高频对象解请参考图解React

初次体验React的工作流程

打开浏览器的performance,我们可以看到react框架的调用栈,首次渲染时大体上可以分为三个部分

点击事件,触发setState更新时的调用栈

React项目的详细启动过程

ReactDOM.render(
  <App />,
  document.getElementById('root')
)

在之前的Fiber架构工作原理中,我们提到,在mount时会创建fiberRootNoderootFiber对象。其实还创建了一个ReactDOMRoot对象,并且调用其render方法,开始渲染我们的react程序。

render阶段

render阶段的主要任务是创建Fiber节点并且构建Fiber树

render阶段开始于performSyncWorkOnRootperformConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新(由于我们渲染时调用的是render方法,那么就默认接下来的更新都是同步更新)。

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

performUnitOfWork的工作可以分为两部分:“递”和“归”。

在构建Fiber树之前,我想用一个’王朝’的故事来描述深度优先遍历。说是当皇帝驾崩之后(当前Fiber节点处理完了),会将王位传给太子(第一个儿子也就是Fiber中的child),如果没有太子,就会传给自己的兄弟(兄弟也就是Fiber节点中的sibling),如果找不到兄弟节点时,又向上找父亲的兄弟,当找到的人又是刚开始那个皇帝时,说明后继无人。这个王朝也就覆灭了(该Fiber树的遍历也就结束了)

“递”阶段

首先从rootFiber开始向下深度优先遍历。为每个遍历到的Fiber节点调用beginWork方法(后面会详细解释),该方法会根据传入的Fiber节点创建子节点,并将这两个Fiber节点连接起来,当遍历到叶子节点时,就会进入“归”阶段

“归”阶段

在“归”阶段会调用completeWork处理Fiber节点当某个Fiber节点执行完completeWork(后面会详细解释),如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。

如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。

“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了

例子
function App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById("root"));

图片引自https://react.iamkasong.com/

render阶段会依次执行

1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "i am" Fiber beginWork
5. "i am" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork
render阶段中的beginWork
/**
 * @desc beginWork的工作是传入当前Fiber节点,创建子Fiber节点,并根据diff算法给对应的Fiber打上effectTag
 * @params current  当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
 * @params workInProgress 当前组件对应的Fiber节点
 * @params renderLanes 优先级相关
*/
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
}


流程概览
  • current: 当前组件对应的Fiber节点在上一次更新时的Fiber节点,即fiberRootNode指向的节点
  • workInProgress: 当前组件对应的Fiber节点(jsx和state的共同结果)
  • renderLanes: 优先级相关

从性能上考虑,React程序在运行时候,不可能每次重新渲染都重新创建Fiber节点,相信大家多少也都听说过diff算法。所以在beginWork中,需要区分一下是首次渲染(mount)还是更新(update),减少不必要的渲染。

之前我们讲过,React中使用双缓存机制,但是在首次渲染的时候,current树是不存在的,可以作为我们判断是否是首次渲染的依据(即 current === null)。

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
  if (current !== null) {
    // ...省略

    // 复用current
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderLanes,
    );
  } else {
    didReceiveUpdate = false;
  }

  // mount时:根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...省略
    case LazyComponent: 
      // ...省略
    case FunctionComponent: 
      // ...省略
    case ClassComponent: 
      // ...省略
    case HostRoot:
      // ...省略
    case HostComponent:
      // ...省略
    case HostText:
      // ...省略
    // ...省略其他类型
  }
}
mount时

从上面的代码中可以看出,当我们通过current===null来确定是首次渲染时,我们需要根据Fiber中的tag属性,创建不同的内容(所以,首屏渲染实际上是很耗时的,这也是单页面应用存在的一个问题)。

update时

我们可以看到,满足如下情况时didReceiveUpdate === false(即可以直接复用前一次更新的子Fiber,不需要新建子Fiber)

  1. oldProps === newProps && workInProgress.type === current.type,即propsfiber.type不变
  2. !includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够

updateFunctionComponent举例,如果我们经过一系列判断,后发现该Fiber节点是可以复用的。那么,就不需要花费大量的操作去diff,直接复用现有的Fiber节点

function updateFunctionComponent {
  ...
   if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  } // React DevTools reads this flag.
}
reconcileChildren

从该函数名就能看出这是Reconciler模块的核心部分。

  • 对于mount的组件,他会创建子Fiber节点
  • 对于update的组件,他会将当前组件于上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点(复用)
/**
 * reconcileChildren 调和函数
 * 调和函数是 `updateXXX`函数中的一项重要逻辑, 它的作用是向下生成子节点, 并设置`fiber.flags`.
 * 初次创建时 `fiber`节点没有比较对象, 所以在向下生成子节点的时候没有任何多余的逻辑, 只管创建就行.
 * 对比更新时 需要把`ReactElement`对象与`旧fiber`对象进行比较, 来判断是否需要复用`旧fiber`对象.
*/
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 对于mount的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 对于update的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

对于diff算法,这边不详细展开,想要深入了解的同学可以参考diff算法

Flags(react16中的effectTag)

我们知道,render阶段的工作是在内存中进行的,当工作结束后需要通知渲染器Renderer执行具体的DOM操作。需要执行DOM,执行哪种DOM操作呢?就需要根据fiber.flags

比如:

// DOM需要插入到页面中
export const Placement = /*                */ 0b00000000000010;
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM需要删除
export const Deletion = /*                 */ 0b00000000001000;
function markUpdate(workInProgress) {
  workInProgress.flags |= Update; // 打上Update的标签(react16中的 effectTag), beginWork阶段还是completeWork阶段???
}
updateClassComponent{
  ...
  // 打上Placement标签
  workInProgress.flags |= Placement;
}
render阶段中的completeWork
/**
 * 在上一个节点diff完成之后,对他进行一些收尾工作。
 * 1. 将需要更新的属性名放入到Fiber节点的updateQueue属性中
 * 2. 生成EffectList(subtreeFlags)
*/
function completeWork(current, workInProgress, renderLanes) {
  var newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    ...
  }
}
流程概览

beginWrok中我们提到了执行了beginWork之后会创建子Fiber节点,该Fiber节点上可能存在effectTag

类似beginWorkcompleteWork也是针对不同fiber.tag调用不同的处理逻辑。

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  // ...省略

我们重点关注页面渲染所必须的HostComponent(即原生DOM组件对应的Fiber节点)。

处理HostComponent

和beginWork一样,我们根据current === null ?判断是mount还是update。

Update时

当update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:

  • onClickonChange等回调函数的注册
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop

在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。

workInProgress.updateQueue = (updatePayload: any);

其中updatePayload为数组形式,他的偶数索引的值为变化的prop key,奇数索引的值为变化的prop value

Mount时

可以看到,mount时的主要逻辑包括三个:

  • Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点
  • update逻辑中的updateHostComponent类似的处理props的过程

commit阶段

commitRoot方法是commit阶段工作的起点。fiberRootNode会作为传参。

rootFiber.firstEffect上保存了一条需要执行副作用的Fiber节点的单向链表effectList,这些Fiber节点updateQueue中保存了变化的props

这些副作用对应的DOM操作commit阶段执行。

除此之外,一些生命周期钩子(比如componentDidXXX, useEffect)需要在commit阶段执行。

commit阶段的主要工作(即Renderer的工作流程),主要分成三部分:

  • before mutation 阶段(执行DOM操作之前)
  • mutation阶段(执行DOM操作)
  • layout阶段(执行DOM操作后)
before mutation阶段

before mutation阶段的代码很短,整个过程就是遍历effectList并调用commitBeforeMutationEffects函数处理。

/** 
 * 1. 处理DOM节点渲染/删除后的 autoFocus、blur逻辑
 * 2. 调用getSnapshotBeforeUpdate生命周期钩子
 * 3. 调度useEffect
*/
function commitBeforeMutationEffects(root, firstChild) {
  // 1. 处理DOM节点渲染/删除后的 autoFocus、blur逻辑
  focusedInstanceHandle = prepareForCommit(root.containerInfo);
  nextEffect = firstChild;
  // 调用 getSnapshotBeforeUpdate useEffect 生命周期函数
  commitBeforeMutationEffects_begin(); // We no longer need to track the active instance fiber

  var shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;
  return shouldFire;
}
getSnapshotBeforeUpdate

说到getSnapshotBeforeUpdate这个生命周期函数,我们不得不想起componentWillXXX钩子前新增的UNSAFE_前缀。由于render阶段是可中断/重新开始的,所以这些UNSAFE的生命周期函数可能会重复执行,但是commit阶段是同步的,所以不会遇到重复执行的问题。

mutation阶段

类似before mutation阶段,mutation阶段也是遍历effectList,执行函数。这里执行的是commitMutationEffects。

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  // 遍历effectList
  while (nextEffect !== null) {

    const effectTag = nextEffect.effectTag;

    // 根据 ContentReset effectTag重置文字节点
    if (effectTag & ContentReset) {
      commitResetTextContent(nextEffect);
    }

    // 更新ref
    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current);
      }
    }

    // 根据 effectTag 分别处理
    const primaryEffectTag =
      effectTag & (Placement | Update | Deletion | Hydrating);
    switch (primaryEffectTag) {
      // 插入DOM
      case Placement: {
        commitPlacement(nextEffect);
        nextEffect.effectTag &= ~Placement;
        break;
      }
      // 插入DOM 并 更新DOM
      case PlacementAndUpdate: {
        // 插入
        commitPlacement(nextEffect);

        nextEffect.effectTag &= ~Placement;

        // 更新
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // SSR
      case Hydrating: {
        nextEffect.effectTag &= ~Hydrating;
        break;
      }
      // SSR
      case HydratingAndUpdate: {
        nextEffect.effectTag &= ~Hydrating;

        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // 更新DOM
      case Update: {
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // 删除DOM
      case Deletion: {
        commitDeletion(root, nextEffect, renderPriorityLevel);
        break;
      }
    }

    nextEffect = nextEffect.nextEffect;
  }
}

commitMutationEffects会遍历effectList,对每个Fiber节点执行如下三个操作:

  1. 根据ContentReset effectTag重置文字节点
  2. 更新ref
  3. 根据effectTag分别处理,其中effectTag包括(Placement | Update | Deletion | Hydrating)
Placement effect

Fiber节点含有Placement effectTag,意味着该Fiber节点对应的DOM节点需要插入到页面中。

调用的方法为commitPlacement

我们可以看下,最终调用的原生DOM操作

function appendChildToContainer(container, child) {
  var parentNode;

  if (container.nodeType === COMMENT_NODE) {
    parentNode = container.parentNode;
    parentNode.insertBefore(child, container); // 熟悉的原生DOM操作
  } else {
    parentNode = container;
    parentNode.appendChild(child); // 熟悉的原生DOM操作
  }
}
Update effect

Fiber节点含有Update effectTag,意味着该Fiber节点需要更新。调用的方法为commitWork,他会根据Fiber.tag分别处理。

fiber.tagFunctionComponent,会调用commitHookEffectListUnmount。该方法会遍历effectList,执行所有useLayoutEffect hook的销毁函数。

useLayoutEffect(() => {
  // ...一些副作用逻辑

  return () => {
    // ...这就是销毁函数
  }
})

fiber.tagHostComponent,会调用commitUpdate

最终会在updateDOMProperties(opens new window)中将render阶段 completeWork (opens new window)中为Fiber节点赋值的updateQueue对应的内容渲染在页面上。

for (let i = 0; i < updatePayload.length; i += 2) {
  const propKey = updatePayload[i];
  const propValue = updatePayload[i + 1];

  // 处理 style
  if (propKey === STYLE) {
    setValueForStyles(domElement, propValue);
  // 处理 DANGEROUSLY_SET_INNER_HTML
  } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
    setInnerHTML(domElement, propValue);
  // 处理 children
  } else if (propKey === CHILDREN) {
    setTextContent(domElement, propValue);
  } else {
  // 处理剩余 props
    setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
  }
}
Deletion effect

Fiber节点含有Deletion effectTag,意味着该Fiber节点对应的DOM节点需要从页面中删除。调用的方法为commitDeletion

该方法会执行如下操作:

  1. 递归调用Fiber节点及其子孙Fiber节点fiber.tagClassComponentcomponentWillUnmount (opens new window)生命周期钩子,从页面移除Fiber节点对应DOM节点
  2. 解绑ref
  3. 调度useEffect的销毁函数
layout阶段

该阶段之所以称为layout,因为该阶段的代码都是在DOM渲染完成(mutation阶段完成)后执行的。

与前两个阶段类似,layout阶段也是遍历effectList,执行函数。

具体执行的函数是commitLayoutEffectscommitLayoutEffects主要做两件事

  1. commitLayoutEffectOnFiber(调用生命周期钩子和hook相关操作)
  2. commitAttachRef(赋值 ref)
root.current = finishedWork;

nextEffect = firstEffect;
do {
  try {
    commitLayoutEffects(root, lanes);
  } catch (error) {
    invariant(nextEffect !== null, "Should be working on an effect.");
    captureCommitPhaseError(nextEffect, error);
    nextEffect = nextEffect.nextEffect;
  }
} while (nextEffect !== null);

nextEffect = null;
commitLayoutEffectOnFiber

commitLayoutEffectOnFiber方法会根据fiber.tag对不同类型的节点分别处理。

对于classComponent,他会根据是mount还是update调用componentDidMountcomponentDidUpdate

触发状态更新的this.setState如果赋值了第二个参数回调函数,也会在此时调用。

对于FunctionComponent及相关类型,他会调用useLayoutEffect hook的回调函数,调度useEffect的销毁与回调函数

对于HostRoot,即rootFiber,如果赋值了第三个参数回调函数,也会在此时调用。

ReactDOM.render(<App />, document.querySelector("#root"), function() {
  console.log("i am mount~");
});
commitAttachRef

commitLayoutEffects会做的第二件事是commitAttachRef

代码逻辑很简单:获取DOM实例,更新ref。

current Fiber树切换

之前我们提过React中的双缓存技术,workInProgress Fiber树在commit阶段完成渲染后会变为current Fiber树。这行代码的作用就是切换fiberRootNode指向的current Fiber树。

我的React源码注释

仅供参考

阅读React源码之后解决的问题

因为使用数组下标作为key产生的bug
// 大概是这样的,由于使用了数组下标作为key
// react的在diff时当判断componenet.type 和 key相同,那么就会复用之前的节点,顶多是个Update,那么这样一来,componentDidMount/useState中的初始值都无效了
list.map((item, key) => (
  <Component key={key} dataSource={item}/>
))

Q&A

参考链接

React技术揭秘

图解React原理

react17源码解析

react17源码训练营

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 学习 React 源码是一项很有益的学习活动,可以帮助你加深对 React 的理解,并且为你提供有关如何实现组件化和声明式编程的知识。 在学习 React 源码时,我有几个建议: 1. 先从官方文档入手,了解 React 的基本概念和使用方法。这将有助于你更好地理解 React 源码。 2. 认真阅读源码注释。React源码非常详细,并且有很多注释,这些注释很有帮助,可以帮助你理解源码的目的和工作原理。 3. 使用调试工具,以帮助你更好地理解源码的工作流程。比如,可以使用浏览器的开发者工具调试 React 代码,或者使用断点调试。 4. 尝试实现一些自己的功能,并结合源码来调试和理解。这将有助于你更好地理解 React 的工作原理,并且能够让你在实际使用中更加熟练。 5. 可以尝试加入 React 开源社区,与其他 React 开发者交流经验,也可以参与 React 的开源项目,为 React 贡献代码。这将有助于你不断学习和提升。 ### 回答2: 学习React源码是一个非常有挑战性但也非常有价值的任务。首先,我建议你先熟悉React的基本概念和使用方法,理解React的核心思想和设计理念,然后再深入研究源码。以下是一些具体的建议: 1. 掌握JavaScript和ES6的基础知识:React源码是用JavaScript编写的,因此对于JavaScript的理解是必要的。确保你对JavaScript中ES6的新特性例如模块导入导出、箭头函数、解构赋值等有一定的了解。 2. 阅读官方文档和源码注释:React官方提供了非常详细的文档和源码注释,阅读它们可以帮助你快速了解React的内部工作原理和实现细节。 3. 从顶层开始分析:从React的顶层入手,逐步深入源码。首先阅读React的入口文件,了解React是如何初始化和渲染组件的。然后再深入了解React组件的生命周期和更新机制。 4. 使用工具进行调试:使用Chrome DevTools等工具进行调试可以帮助你更好地理解和分析React的运行时行为。通过在源码中设置断点并观察变量的值,可以深入理解React的执行流程。 5. 理解虚拟DOM和协调算法:React的核心是虚拟DOM和协调算法,学习和理解它们对于深入理解React源码非常重要。阅读相关的源码实现,将有助于你理解React如何高效地更新DOM。 6. 参考社区资源和开源项目:React有一个非常庞大的社区,许多优秀的开源项目可以帮助你更好地理解React源码。与其他人一起学习和讨论,参与到React相关的开源项目中,将加快你的学习进度。 总结起来,学习React源码需要坚实的JavaScript基础和对React的理解。通过仔细阅读官方文档、源码注释和调试工具的使用,以及参考社区资源和开源项目,你将能够逐渐深入了解React的实现细节,提高自己的技术水平。 ### 回答3: 学习React源码的过程可以相当艰巨,但也是丰富而值得的。以下是几些建议帮助你更好地学习React源码: 1. 先理解React的核心概念:在深入研究源码前,先确保你对React的基本理念和工作方式有了清晰的理解。了解虚拟DOM、组件生命周期、状态管理和数据流等核心概念将有助于更好地理解源码。 2. 创建一个简单的React应用:在学习React源码时,建议你尝试创建一个简单的React应用并深入研究它的实现细节。这样可以帮助你更好地理解React的工作原理和内部机制。 3. 阅读React官方文档和源码注释:React源码中有丰富的注释,它们会对源码的实现细节进行解释和说明。同时,React官方文档也提供了很多有价值的内容,包括API文档和设计原则。阅读这些内容能够帮助你更好地理解React的思想和设计理念。 4. 利用调试工具:学习源码的过程中,调试工具是非常有用的辅助资源。使用断点和调试器来观察源码的执行路径和状态变化,可以帮助你更好地理解代码的运行机制。 5. 参考优秀的源码解析和开源项目:很多开发者在学习React源码后,会产生一些优秀的博客文章、视频教程和开源项目,其中包含了他们对源码的解析和实践。阅读这些资源可以帮助你更好地理解React源码并从中获取灵感。 学习React源码需要耐心和持续努力,希望以上建议能够帮助你更好地进行这个过程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值