React Fiber&Hook 实战解读原理

846 阅读13分钟

前言

本文通过实战一个 Demo 来了解 React v16 版本之后的 Fiber 配合 Scheduler 调度器的解决了 React 之前由于系统庞大,diff 整棵树过程导致游览器卡顿问题。

其次,通过案例中简单实现一个 useReducer 来了解到 Hook 的工作流程的。

创建项目

React 创建项目一般都是使用官方的脚手架 Cli

npx create-react-app my-app
cd my-app
npm start

通过上面创建出来的项目的入口文件大概长这样子的:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';

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

reportWebVitals();

以上创建的项目可以看到还是 v18 版本之前的,这里简单的尝新一下 V18 版本:

npm install react@alpha react-dom@alpha
# or
yarn add react@alpha react-dom@alpha

你可能会遇到一个由于 react-scripts 引起的 could not resolve dependency 错误:

Could not resolve dependency:
peer react@">= 16" from react-scripts@4.0.3

你可以在安装的时候尝试加上 --force 来解决这个问题:

npm install react@alpha react-dom@alpha --force
# or
yarn add react@alpha react-dom@alpha --force

接下来就来改变一下入口文件,因为 React v18 在之后不再使用 ReactDOM.render(), 而是使用 ReactDOM.createRoot() 替代了通常作为程序入口的 ReactDOM.render() 方法。

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';

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

reportWebVitals();

接下来就来写一个使用 Hook 的小案例

案例

// App.js
import { Component, useReducer } from 'react';

function FunctionComponent(props) {
  const [count, setCount] = useReducer(x => x + 1, 0);

  return (
    <div className="border-func">
      <p>{props.name}</p>
      <button onClick={() => {
        setCount()
      }}>
      { `useReducer -> ${count}` }
      </button>
    </div>
  );
}

function App() {

  return (
    <div className="App">
      <FunctionComponent name="函数" />
    </div>
  );
}

export default App;

以上就写好了一个简单的 Demo 了,大概的样子就是这样的:

image

到目前这里使用的都是 react 和 react-dom 库所提供的功能,下面就来写一下 Mini 版的这两个库,完成以上的基本功能。

Mini React & ReactDOM & Fiber

首先吧改造一下以上代码:

// index.js
import React from 'react';
// import ReactDOM from 'react-dom';
import ReactDOM from "./iReact/react-dom"; // 替换为自己的库
import App from './App';
import reportWebVitals from './reportWebVitals';

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

reportWebVitals();
// App.js
// import { Component, useReducer } from 'react';
import { useReducer } from './iReact/react'; // 替换为自己的库

// 增加一个类组件
class ClassComponent extends Component {
  render() {
    return (
      <div className="border-class">
        <p>{this.props.name}</p>
      </div>
    );
  }
}

// 函数组件
function FunctionComponent(props) {
  const [count, setCount] = useReducer(x => x + 1, 0);

  return (
    <div className="border-func">
      <p>{props.name}</p>
      <button onClick={() => {
        setCount()
      }}>
      { `useReducer -> ${count}` }
      </button>
    </div>
  );
}

function App() {

  return (
      <FunctionComponent name="函数" />
      <ClassComponent name="class" />
    </div>
  );
}

export default App;

ReactDOM

首先 React v18 使用的是 ReactDOM.createRoot 那我们就简单实现以下该方法:

import createFiber from './createFiber';
import { scheduleUpdateOnFiber } from './ReactFiberWorkLoop';

function createRoot(container) {
  const root = { containerInfo: container }; // 包装 root 节点
  
  return new ReactDOMRoot(root);
}

function ReactDOMRoot(internalRoot) {
  this._internalRoot = internalRoot; // root 节点放到实例上
}

ReactDOMRoot.prototype.render = function (children) {
  const root = this._internalRoot;
  updateContainer(children, root); // 将 App 组件 与 root 节点进行绑定
}

function updateContainer(element, root) {
  const { containerInfo } = root;
  const fiber = createFiber(element, { // 创建一个 Fiber 节点
    type: containerInfo.nodeName.toLocaleLowerCase(),
    stateNode: containerInfo,
  });

  // 更新 fiber
  scheduleUpdateOnFiber(fiber); // 将 Fiber 节点交由调度器处理
}

上面代码中,有两个点

  1. 创建 Fiber 节点,Fiber 本质上来说其实就是 React 重写后的 Virtrued Dom,从原来的树形结构变成了链表结构
  2. 第二点就是将 Fiber 节点交由了 Schedule 调度器处理了,至于调度器原理感兴趣的可以查看该文章:React Scheduler 任务调度平民分析

创建 Fiber

本文 Demo 中只列出部分的 Fiber 字段,感兴趣的可以自行去源码中输出查看:

源码 React/src/client/ReactDOMRoot.js 文件找到 ReactDOMRoot.prototype.render 可以打印 Fiber 的数据结构。

import { Placement } from './utils';

export default function createFiber(vnode, returnFiber) {
  const newFiber = {
    // 原生标签 string
    type: vnode.type,
    key: vnode.key,
    props: vnode.props,
    // 第一个子 fiber
    child: null,
    // 下一个兄弟 fiber
    sibling: null,
    // 父 fiber
    return: returnFiber,
    // 如果是原生标签 dom 节点
    // 类组件 类实例
    stateNode: null,
    // 标记当前 fiber 提交的是什么操作,比如:插入、更新、删除
    flags: Placement,
    // 存放上一个 Fiber
    alternate: null
  };
  return newFiber;
}

更新 Fiber

Fiber 的更新就是交给 React 的调度器实现的,React 的调度器是自己实现了一个 requestIdleCallback,本文就直接使用该 Api 了

//  wip work in progress 当前正在工作中的
let wipRoot = null;
let wip = null;

export function scheduleUpdateOnFiber(fiber) {
  // 这里的 fiber 一般为根 Fiber,比如 Root、Func Finber、Class Fiber
  fiber.alternate = { ...fiber }; // 在当前 Fiber 上保存当前的 Fiber,因为后面 Fiber 将会更新内容
  wipRoot = fiber;
  wip = fiber;
}

// 直接使用游览器提供的调度器进行创建更新 fiber
requestIdleCallback(workLoop);

通过调用 scheduleUpdateOnFiber 后,将传入的 Fiber 作为当前工作的 root,之后交由 requestIdleCallback 在游览器空闲时间触发更新

workLoop 工作流程

function workLoop(IdleDeadline) {
  // 源码中,这里会根据 fiber 类型来计算剩余时间可执行,这里直接判断 0
  while (wip && IdleDeadline.timeRemaining() > 0) {
    // 处理各级 fiber 的关系,并创建 dom
    performUnitOfWork();
  }

  // 重新安排调度器
  requestIdleCallback(workLoop);
  
  if (!wip && wipRoot) {
    // 所有 fiber 完成任务创建 Dom ,调和完成子节点后,进入提交阶段
    commitRoot(wipRoot);
  }
}

在游览器空闲时间就会去执行 performUnitOfWork 将 fiber 更新以及子节点的 fiber 创建之类的工作。

performUnitOfWork

function performUnitOfWork() {
  // 1. 处理当前的任务
  const { type } = wip;
  // 完成当前 fiber 节点的 dom 构建和子 fiber 的调和(深度优先遍历),开始走下一个 fiber
  if (isStr(type)) {
    updateHostComponent(wip);
  } else if (isFn(type)) {
    type.prototype.isReactComponent
      ? updateClassComponent(wip)
      : updateFunctionComponent(wip);
  }
  
  // 2. 处理下一个任务,深度优先遍历
  if (wip.child) {
    // 从链表结构中查找子 fiber,准备下一个 fiber 任务
    wip = wip.child;
    return;
  }

  while (wip) {
    if (wip.sibling) {
      // 兄弟节点有 child 代表是更新阶段了,但是没有 alternate 老 Fiber 那代表不需要更新
      if (wip.sibling.child && !wip.sibling.alternate) break;
      // 查找兄弟节点 fiber,作为下一个 fiber 任务
      else wip = wip.sibling;
      return;
    }
    wip = wip.return;  
  }
  // 3. 结束任务
  wip = null; // 找不到下一个待处理 fiber 节点时,将 wip 清除
}

performUnitOfWork 方法将会按照深度优先遍历的方式,处理每一个 Fiber 节点的更新、创建。
首先先来看第一步:处理当前的任务

第一步:处理当前的任务

该节点有三个关键的函数:updateHostComponentupdateClassComponentupdateFunctionComponent 当判断当前 Fiber 节点为一个字符串的时候,就走 updateHostComponent

updateHostComponent

export function updateHostComponent(wip) {
  if (!wip.stateNode) {
    // 创建 Dom
    wip.stateNode = document.createElement(wip.type);
    // 对真实节点挂载属性
    updateNode(wip.stateNode, {}, wip.props);
  }

  // 调和子节点,将 wip 下的虚拟节点 children 都进行构建 fiber 结构
  reconcileChildren(wip, wip.props.children);
}

如果 stateNode 不存在,代表没有真实节点,执行 updateNode 创建真实节点,挂载属性,绑定事件,之后就是执行 reconcileChildren 调和子节点,对子节点或者兄弟节点进行 Fiber 的创建和绑定。

至于 updateNodereconcileChildren 做了什么后续再看,当前只需要了解他们所干的事情。

接下来再来看看另一个函数 updateClassComponent

updateClassComponent

export function updateClassComponent(wip) {
  const { type, props } = wip;
  const children = (new type(props)).render();
  reconcileChildren(wip, children);
}

该方法主要是用来处理类组件的,因为通过 react 的解析,类组件虚拟节点 VNode.type 得到的是组件的构造函数。通过 new 实例执行 render 函数才能获取到接下来的 child 虚拟节点,之后在进入 reconcileChildren 函数进行调和子节点、兄弟节点。

最后再来看看最后一个函数 updateFunctionComponent

updateFunctionComponent

export function updateFunctionComponent(wip) {
  // 执行函数更新时,初始化当前工作 fiber,用于给 type() 函数组件执行时,hook 能够找到当前的 fiber 是谁
  renderWithHooks(wip);
  const { type, props } = wip;
  const children = type(props); // 获取函数组件的子组件 Virtual Dom
  reconcileChildren(wip, children);
}

和类组件处理函数一样,针对于函数组件处理的喊出,对函数组件的 VNode.type 进行了调用,VNode.type 其实就是函数组件本身,之后也是相同的调用了 reconcileChildren 方法,这里有一点不一样的是,这里多了一个 renderWithHooks

renderWithHooks 该函数看名称就能知道和 Hook 相关了。

其实没错,该函数就是将当前函数组件的 Fiber 保存起来,在执行 type(props) 的时候,处理函数组件内部的 Hook 的时候,能够从事先保存起来的当前函数组件的 Fiber 获取出来处理各项事务

// react.js
let currentlyRendingFiber = null;
let workInProressHook = null;

export function renderWithHooks(wip) {
  currentlyRendingFiber = wip;
  currentlyRendingFiber.memeorizedState = null;
  workInProressHook = null;
}

这里保存了接下来执行 const children = type(props); 所需要的 Fiber 数据。

看到这里,目前还留下了很多疑点,比如:updateNodereconcileChildrenrenderWithHooks,接下来就来讲解一下 updateNodereconcileChildrenrenderWithHooks 还需要等到讲解 Hook 的时候才能开展开来。

updateNode

首先来看看 updateNode 函数,其实该函数所做的事情就是将 VNode 节点的 属性和事件之类的绑定到真实节点上:

// 更新原生标签的属性,如className、href、id、(style、事件)等
export function updateNode(node, prevVal, nextVal) {
  Object.keys(prevVal).forEach(k => {
    if (k.slice(0, 2) === "on") {
      const eventName = k.slice(2).toLocaleLowerCase();
      node.removeEventListener(eventName, prevVal[k]);
    }
  });

  Object.keys(nextVal)
    // .filter(k => k !== "children")
    .forEach((k) => {
      if (k === "children") {
        // 有可能是文本
        if (isStringOrNumber(nextVal[k])) {
          node.textContent = nextVal[k] + "";
        }
      } else if (k.slice(0, 2) === "on") {
        const eventName = k.slice(2).toLocaleLowerCase();
        node.addEventListener(eventName, nextVal[k]);
      } else {
        node[k] = nextVal[k];
      }
    });
}

该函数较为简单,相信阅读应该 so easy~

下面就来看看,被多次调用的 reconcileChildren 函数,该函数还算是一个比较核心的方法

reconcileChildren


function reconcileChildren(parentFiber, children) {
  // 存文本子节点就不构建 fiber
  if (isStr(children)) {
    return;
  }
  
  const newChildren = isArray(children) ? children : [children];

  // 记录上一个 fiber 节点
  let previousNewFiber = null;
  // 获取当前虚拟节点的老 fiber 节点, 这里获取的是 children 中链表的第一个 child
  let oldFiber = parentFiber.alternate && parentFiber.alternate.child;

  for (let i = 0; i < newChildren.length; i +=1) {
    const newChild = newChildren[i];
    const newFiber = createFiber(newChild, parentFiber);
    const same = sameNode(newFiber, oldFiber);

    if (same) {
      /**
       * 如果当前的虚拟节点所对应的新老 Fiber 是同一个 Fiber,
       * 其实也就是虚拟节点没什么变化,可能也就节点属性变化了,节点内容变更了或者节点位置变化了,当然位置变化了这情况就先不管了
       */
      Object.assign(newFiber, {
        alternate: oldFiber, // 这里对 alternate 进行了更新赋值,一开始新 Fiber 的 alternate 为 null
        stateNode: oldFiber.stateNode, // 复用 Dom 节点
        flags: Update // 代表 Fiber 需要更新
      })
    }

    // 如果存在老 Fiber,遍历后,按照 Fiber 链表遍历规则,接下来要找当前 Fiber 的兄弟节点
    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    if (i === 0) {
      parentFiber.child = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
}

reconcileChildren 所做的事情就比较多了,首先如果子节点是一个字符串,那就没必要创建 Fiber 了,直接字符串作为内容即可。

接下来,先获取当前 Fiber 的老 Fiber(可能没有),然后遍历当前 Fiber 的子节点,创建子 VNode 的 Fiber,并且创建新的子 Fiber 的时候检查一下是否和老 Fiber 有 VNode type 的变化,没有就复用老的 Fiber 所构建的真实节点 stateNode,新 Fiber alternate 属性绑定老 Fiber,打上更新标记。

接下来就是将 VNode 的树形结构转换成链表结构的工作了,规则就是:

  1. 当前 Fiber 的 child 属性指向第一个子节点 newFiber
  2. 当前的 Fiber 子节点从第一个 child 的 sibling 指向同级的 newFiber

到此就完成了当前节点的第一层子节点的 Fiber 创建和关系绑定了,对于更下一层的处理,就是再次回到了上述的 performUnitOfWork 中变更当前工作 Fiber 为子 Fiber 来处理了。

目前了解完 performUnitOfWork 第一步的逻辑后,就得再次回到 performUnitOfWork 的第二步骤了。

第二步:处理下一个任务

在第一步完成当前工作 Fiber 的任务和子 VNode 的 Fiber 创建于关联后,就按照深度优先遍历的方式继续处理下一个任务了,这里在来看看第二步的代码:

// 2. 处理下一个任务,深度优先遍历
if (wip.child) {
  // 从链表结构中查找子 fiber,准备下一个 fiber 任务
  wip = wip.child;
  return;
}

while (wip) {
  if (wip.sibling) {
    // 兄弟节点有 child 代表是更新阶段了,但是没有 alternate 老 Fiber 那代表不需要更新
    if (wip.sibling.child && !wip.sibling.alternate) break;
    // 查找兄弟节点 fiber,作为下一个 fiber 任务
    else wip = wip.sibling;
    return;
  }
  wip = wip.return;  
}

其实这里也很好理解,无法就是变更当前工作节点 wip 为:子节点 or 兄弟节点,注意的是,变更后你会发现没有递归,或者再去调用上面函数了,感觉啥也没做了,不知道读者是否还有印象,其实变更当前工作节点后,下一步工作是交由调度器:requestIdleCallback(workLoop); 处理了,在 workLoop 函数的空余时间执行完 performUnitOfWork 就会再次注册调度器 requestIdleCallback(workLoop); 了。

到这里就剩下最后一个阶段了,其实就是第三步结束任务了

第三步:结束任务

wip = null; // 找不到下一个待处理 fiber 节点时,将 wip 清除

处理到此,就解析完 workLoop 函数的核心步骤了,但别忘了,还有一个函数为解析,那就是 renderWithHooks,在此再拿出来回忆一下,留一个印象,但该函数还得放到后续讲解。

回到 workLoop 函数,还剩下最后的提交阶段

if (!wip && wipRoot) {
  // 所有 fiber 完成任务创建 Dom ,调和完成子节点后,进入提交阶段
  commitRoot(wipRoot);
}

当所有当前 Fiber 任务 diff 处理完毕后,就进入提交阶段了。

commitRoot 提交阶段

function commitRoot() {
  commitWorker(wipRoot);
  wipRoot = null;
}

function commitWorker(wip) {
  if (!wip) {
    return;
  }
  // 1. commit 自己
  const { flags, stateNode } = wip;

  // 父 dom 节点
  let parentNode = getParentNode(wip.return);
  if (flags & Placement && stateNode) {
    parentNode.appendChild(stateNode);
  }

  if (flags & Update && stateNode) {
    updateNode(wip.stateNode, wip.alternate.props, wip.props);
  }

  // 2. commit child
  commitWorker(wip.child);

  // 3. commit sibling
  commitWorker(wip.sibling);
}

提交阶段这里所实现的就是简单的新增和更新之类的操作,通过深度优先遍历+递归进行处理。

这里也分为几个步骤:

第一步:commit 自己

在第一步获取了当前节点的操作标示 flags 和真实节点 stateNode,之后获取一下父节点 getParentNode(wip.return);

function getParentNode(wip) {
  let _wip = wip;
  while (_wip) {
    if (_wip.stateNode) {
      return _wip.stateNode;
    }
    _wip = _wip.return;
  }
}

就是获取当前 wip 的父节点,在这里判断了 _wip.stateNode,为什么呢?试想一下 wip 这里存储的是什么?

wip 存储的是 Fiber,Fiber包含 VNode 节点创建出来的,也包含“函数组件”、“类组件” 本身的 Fiber,这类的 Fiber 没有真实节点的,因为它本身也不是一个 VNode 节点。

由此在 getParentNode 就获取到了父级真实节点,接下来进行判断 if (flags & Placement && stateNode) 通过“与”判断是否为新增节点,如果是则将当前 Fiber 真实节点 stateNode 通过 appendChild 到父节点中。

如果不是新增的 Fiber,那就接着判断是否为更新操作:if (flags & Update && stateNode) 如果是则调用 updateNode 更新节点即可。

第二步:commit child

第一步中处理完当前节点后,按照深度优先遍历规则查找子 Fiber,进行递归:

// 2. commit child
commitWorker(wip.child);

第三步:commit sibling

如果当前阶段没有子 Fiber,按照深度优先遍历规则,那就轮到兄弟节点 Fiber 了,再进行递归:

// 3. commit sibling
commitWorker(wip.sibling);

到此就完成了VNode 到 Fiber 构建再到 RNode 的渲染的整个简单的流程了,相信看到这里,应该对 Fiber 和 调度器 Schedule 有一定的了解了。

接下来,再来抛出还有一个为讲解的方法:renderWithHooks,要讲解该函数,要回溯到解析函数组件 Fiber 的处理方法上去:updateFunctionComponent(wip);,就是该方法调用了 renderWithHooks

Mini Hook

先来回顾一下代码:

// 直接使用游览器提供的调度器进行创建更新 fiber
requestIdleCallback(workLoop);

function workLoop(IdleDeadline) {
  // 源码中,这里会根据 fiber 类型来计算剩余时间可执行,这里直接判断 0
  while (wip && IdleDeadline.timeRemaining() > 0) {
    // 处理各级 fiber 的关系,并创建 dom
    performUnitOfWork();
  }
  
  // ...
}

function performUnitOfWork() {
  // 1. 处理当前的任务
  const { type } = wip;
  // 完成当前 fiber 节点的 dom 构建和子 fiber 的调和(深度优先遍历),开始走下一个 fiber
  if (isStr(type)) {
    updateHostComponent(wip);
  } else if (isFn(type)) {
    type.prototype.isReactComponent
      ? updateClassComponent(wip)
      : updateFunctionComponent(wip);
  }
}

export function updateFunctionComponent(wip) {
  // 执行函数更新时,初始化当前工作 fiber,用于给 type() 函数组件执行时,hook 能够找到当前的 fiber 是谁
  renderWithHooks(wip); // -> 这里!这里!保存了当前函数组件的自身的 Fiber,用于 type(props);
  const { type, props } = wip;
  const children = type(props); // 获取函数组件的子组件 Virtual Dom
  reconcileChildren(wip, children);
}

useReducer

执行 updateFunctionComponent 的时候,调用了 renderWithHooks(wip); 保存了当前函数组件的 Fiber

let currentlyRendingFiber = null; // 当前 Fiber
let workInProressHook = null; // 指向最后一个 hook

export function renderWithHooks(wip) {
  currentlyRendingFiber = wip;
  currentlyRendingFiber.memeorizedState = null;
  workInProressHook = null;
}

之后执行

const children = type(props); // 获取函数组件的子组件 Virtual Dom

实际就是调用了 FunctionComponent 函数组件:

function FunctionComponent(props) {
  const [count, setCount] = useReducer(x => x + 1, 0);

  return (
    <div className="border-func">
      <p>{props.name}</p>
      <button onClick={() => {
        setCount()
      }}>
      { `useReducer -> ${count}` }
      </button>
    </div>
  );
}

这里执行了 useReducer

const [count, setCount] = useReducer(x => x + 1, 0);

接下来就来看看 useReducer 干了什么:

export function useReducer(reducer, initalState) {
  const hook = updateWorkInProgressHook();
  // 将 Hook 的寄主函数 Fiber 绑定到 Hook 身上,方便能在 dispatch 找到自己的寄主 Fiber,从而触发更新
  hook.funcFiber = currentlyRendingFiber;

  if (!currentlyRendingFiber.alternate) {
    hook.memeorizedState = initalState;
  }

  const dispatch = () => {
    hook.memeorizedState = reducer(hook.memeorizedState);
    scheduleUpdateOnFiber(hook.funcFiber);
  }

  return [hook.memeorizedState, dispatch]
}

useReducer 函数首先执行的就是 const hook = updateWorkInProgressHook(); 创建或者获取一个 hook,这个 hook 就是函数组件的内部状态的一个存储对象。

想象一下,函数组件每次更新的时候都会重新执行,然后都会重新执行 useReducer 这个时候 useReducer 都会获取一个 hook ,然而为了保持更新的组件的时候,拿到的状态都是最新的,必然这里的 hook 就是最新的一个状态存储对象了,下面来看看 updateWorkInProgressHook() 做了什么,以此更好的了解如果获取最新的 hook

updateWorkInProgressHook

function updateWorkInProgressHook() {
  let hook;
  // alternate 存放着老的 fiber
  let current = currentlyRendingFiber.alternate;
  if (current) {
    // 更新阶段
    currentlyRendingFiber.memeorizedState = current.memeorizedState;
    if (workInProressHook) {
      // 不是第一个了
      workInProressHook = hook =  workInProressHook.next;
    } else {
      // 更新的是第一个
      workInProressHook = hook = currentlyRendingFiber.memeorizedState;
    }
  } else {
    // 初始渲染
    hook = {
      memeorizedState: null, // 状态值
      next: null, // 指向下一个 hook
    }
    if (workInProressHook) {
      // 已存在有 hook 了,将新的 hook 拼接到最后一个,并且将 workInProressHook 指向最后一个
      workInProressHook = workInProressHook.next = hook;
    } else {
      workInProressHook = currentlyRendingFiber.memeorizedState = hook;
    }
  }
  return hook;
}

updateWorkInProgressHook 函数一开始先获取了当前 Fiber 的老 Fiber 节点,一开始当然是不存在老 Fiber 的,所以走 else 逻辑,创建一个 hook 对象

hook = {
  memeorizedState: null, // 状态值
  next: null, // 指向下一个 hook
}

这里的 memeorizedState 保存的就是当前工作的函数组件的 Fiber useReducer 状态值 hook,每一次函数组件 Fiber 节点更新执行的时候,就会在旧的 Fiber 上的 hook 基础上生成新的 hook 然后它们通过 next 链表结构关联。

定一个 hook 后判断了指向最新的当前 workInProressHook 存不存在,如果存在,则指向最新的 hook,并将最新的 hook 链接到上一个 hook 的 next 上,如果不存在 workInProressHook,那就是指向当前最新的 hook,并让当前函数组件的 Fiber memeorizedState 指向首个 hook

workInProressHook = currentlyRendingFiber.memeorizedState = hook;

到此这是初始情况的逻辑,如果是函数组件的更新,那就会存在老 Fiber,也就是 current 存在,这时候就获取老 Fiber 节点上 memeorizedState 复用到新节点 Fiber memeorizedState 上,这里其实就是为什么函数组件也能够有保留状态的原因了,后面就是判断 workInProressHook 是否存在,如果不存在,那就是函数组件中的第一个 hook 状态,后面的 useReducer 就是链表上的下一个 next 所指向 的 hook这里其实就是所谓的为什么 React Hook 需要有顺序要求了

最后获取当前状态 hook 状态,再回到 useReducer 函数,之后的逻辑判断,是否存在老 Fiber

if (!currentlyRendingFiber.alternate) {
  hook.memeorizedState = initalState;
}

如果不存在老 Fiber 就使用,也就是一开始的情况,那会应用初始值 initalState,更新场景下,就不会理会这个初始参数了。

在继续往后看

const dispatch = () => {
  hook.memeorizedState = reducer(hook.memeorizedState);
  scheduleUpdateOnFiber(hook.funcFiber);
}

return [hook.memeorizedState, dispatch]

返回一个 状态值 hook.memeorizedState 也就是一开始的 initalState,第二个参数就是一个 dispatch,再来看一下 useReducer 的使用:

const [count, setCount] = useReducer(x => x + 1, 0);

这样就能拿到 count 状态值了,接下来看看调用 setCount 变更状态,如何触发组件更新,并获取到最新的状态值

状态更新

<button onClick={() => {
  setCount()
}}>

还行后,也就是设置最新的

hook.memeorizedState = reducer(hook.memeorizedState);

这时候当前 useReducerhook 就保存这最新的状态值,接下来执行

scheduleUpdateOnFiber(hook.funcFiber);

注意到这里的 hook.funcFiber 是什么东西呢?

再回想一下前面的 useReducer 函数中,有这么一段内容

export function useReducer(reducer, initalState) {
  const hook = updateWorkInProgressHook();
  
  hook.funcFiber = currentlyRendingFiber;
  
  // ...
 }

其实这里的意思就是:将 Hook 的寄主函数 Fiber 绑定到 Hook 身上,方便能在 dispatch 找到自己的寄主 Fiber,从而触发更新

因此可以知道 hook.funcFiber 就是该 hook 的寄主函数组件 Fiber,

dispatch 后触发 scheduleUpdateOnFiber(hook.funcFiber);,

export function scheduleUpdateOnFiber(fiber) {
  // 这里的 fiber 一般为根 Fiber,比如 Root、Func Finber、Class Fiber
  fiber.alternate = { ...fiber }; // 在当前 Fiber 上保存当前的 Fiber,因为后面 Fiber 将会更新内容
  wipRoot = fiber;
  wip = fiber;
}

将传入进来的 Fiber 作为老 Fiber 存放到 alternate 字段,并将当前 Fiber 赋值给 wipRootwip 作为下一次调度器执行的时候,即将要处理的工作 Fiber 节点,想象一下,Fiber 上已保存了 dispatch 之后最新的状态值,在 Fiber.memeorizedState.memeorizedState 根据案例这里为 Fiber.memeorizedState 第一个 hook 的属性 memeorizedState, 调度器在进行工作的时候就会再次触发:

requestIdleCallback -> workLoop -> performUnitOfWork -> updateFunctionComponent -> type(props)

再次调用函数组件,然后执行 useReducer 从老 Fiber alternate 字段拿到上一个状态值 count 最后 render 到直接节点上,到此为止 useReducer 的一个声明周期流程大致就讲完了。

由以上 Demo 和对 Fiber、调度器、useReducer 的使用和讲解,应该能较为清楚的了解 React 这几个重要的概念了,希望读者读到这里能有所帮助。

如若读者对本文内容感受不错,那就伸出小爪爪为本文来一个小小的赞吧~

附录

源码