Front End

[FE] React 初窥门径(六):React 组件的更新过程

2021-10-29  本文已影响0人  何幻

1. 回顾

前两篇文章,我们介绍了 React 函数组件的加载过程,总共分成了两个阶段,

在函数组件的加载和更新的过程中,React 会在内存维护两个 Fiber Tree。

函数组件在首次加载的时候,
(1)render 阶段:React 只会创建一棵完整的 Fiber Tree(标记为 workInProgress),
另一颗 Fiber Tree 只创建一个根节点(Fiber Node tagHostRoot)(标记为 current)。
(2)commit 阶段:将标记为 workInProgress 的 Fiber Tree 实际写入到 DOM 中

函数组件在更新的时候,
(1)render 阶段:React 设置当前 workInProgress 的 Fiber Tree 为之前 current 的那个,
然后开始创建(或更新)这个 Fiber Tree 的 Fiber Node。
(从上到下,一层一层的创建 Fiber Node,改变 childreturnalternate 的指向)(下文详细介绍)
(2)commit 阶段:将标记为 workInProgress Fiber Tree(连同它的更新) 实际写入到 DOM 中

本文重点介绍一下 React 函数组件的更新全流程。

2. 示例项目的一些调整

为了能展示组件的更新过程,我们的示例项目需要做一些调整,
github: thzt/react-tour

(1)example-project/src/AppState.js

import { useState } from 'react';

const App = () => {
  debugger;
  const [state, setState] = useState(0);
  debugger;

  const onDivClick = () => {
    debugger;
    setState(1);
    debugger;
  };
  return <div onClick={onDivClick}>
    {state}
  </div>
};

export default App;

我们使用了 useStateApp 组件增加了状态。
组件首次加载的时候,state 的值为 0,鼠标点击后触发 onDivClick 事件,更新状态为 1
并且为了便于调试,增加了一些 debugger 语句。

(2)example-project/src/index.js

import React from 'react';
import ReactDOM from 'react-dom';

// import App from './App';
import App from './AppState';

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

改变了 ReactDOM.render 的组件,为带状态的这个 App

3. Fiber Tree 的一些变化

由于 App 组件发生了变化,

const App = () => {
  debugger;
  return 'hello world'
};

export default App;

变成了,

const App = () => {
  debugger;
  const [state, setState] = useState(0);
  debugger;

  const onDivClick = () => {
    debugger;
    setState(1);
    debugger;
  };
  return <div onClick={onDivClick}>
    {state}
  </div>
};

所以 Fiber Tree 结构也有了一些变化。

在组件首次加载的时候,
(1)render 阶段 结束后,内存中两棵 Fiber Tree 的关系如下,

之前(上一篇) Fiber Tree 结构的不同点在于,
最底下那个 Fiber Node,

(2)commit 阶段 在写入 DOM 之后,React 会更改 FiberRootNodecurrent 指向,

现在我们有了一棵 3 节点的 Fiber Tree 了(current Fiber Tree)。

4. 组件更新全流程

借用了 VSCode 插件 CodeTour,我们记录了以上函数组件的 首次加载更新 过程。

可以查看这里 6. 函数组件更新,它包含了两个阶段,

下文重点介绍从用户点击 div 到 React 组件更新完毕的整个过程。
CodeTour 的数据保存在了这里,
github: thzt/react-tour
VSCode 插件 CodeTour 的安装可以参考 第四篇 文章。

值得一提的是,本篇文章介绍的代码逻辑会在 ExampleProject(示例项目)和 ReactProject(React 源码目录)之间来回跳转,
因此为了让 code tour 更加连贯,我们在 ReactProject 根目录又增加了一个软连接,指向 ExampleProject

$ ln -s $ExampleProject $ReactProject/example-project

这样就可以在 ReactProject 的 code tour 中定位到 ExampleProject 中的文件了。
相关的修改可参考这里,github: thzt/react-tour/tool/link.bash

(1)setState

用户点击 div 之后,会触发 React 在 DOM 中绑定的事件 listener,最后调用 App 中编写的 onClick 回调。
上文还原了 React 的整个 dispatchEvent 调用栈。(并未包含 listener 的挂载过程,事件绑定是在组件载入时做的,后续文章会介绍)
onDivClick 就是 App 中编写的 onClick 回调。

const App = () => {
  debugger;
  const [state, setState] = useState(0);
  debugger;

  const onDivClick = () => {
    debugger;
    setState(1);
    debugger;
  };
  return <div onClick={onDivClick}>
    {state}
  </div>
};

我们看到 onDivClick 在执行的过程中调用了 setState,它做的事情是把一个 callbackperformSyncWorkOnRoot) 放到了 syncQueue 中,然后就返回了。
(参考 react-dom.development.js#L11501

然后 React 在后续 flushSyncCallbackQueue 中,从 syncQueue 中拿到这个 callbackperformSyncWorkOnRoot)进行操作。

(2)performSyncWorkOnRoot

performSyncWorkOnRoot 这个函数名字很熟悉啊,正是 React 源码阅读(四):React 组件的加载过程(render 阶段) 介绍过的执行 render 和 commit 两个阶段事情的函数。

performSyncWorkOnRoot
  renderRootSync  <- render 阶段
  commitRoot      <- commit 阶段

可以看到 React 在组件更新的过程中,又重新执行了一遍 rendercommit

具体过程如下,


如此这般,React 函数组件的第一次更新过程就讲解完了。第二次更新的过程会有所不同(主要体现在 createWorkInProgress),因为现在内存中已经有完整的两棵 Fiber Tree 了(不会再新建 Fiber Node 了)。

这时候 React 会复用 Fiber Tree 的节点,让同一层 Fiber Tree 最多只有 currentalternate 两个重复节点。createWorkInProgress 函数位于 react-dom.development.js#L25780

function createWorkInProgress(current, pendingProps) {
  var workInProgress = current.alternate;

  if (workInProgress === null) {  // <- 会尽量复用 workInProgress 节点,不会重复创建 Fiber Node
    workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
    ...

    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    workInProgress.pendingProps = pendingProps; // Needed because Blocks store data on type.
    ...
  }

  ...
  workInProgress.child = current.child;
  ...

  return workInProgress;
} // Used to reuse a Fiber for a second pass.

参考

React 初窥门径(四):React 组件的加载过程(render 阶段)
React 初窥门径(五):React 组件的加载过程(commit 阶段)
VSCode: CodeTour
github: thzt/react-tour
6. 函数组件更新

上一篇下一篇

猜你喜欢

热点阅读