前言
本文通过实战一个 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 了,大概的样子就是这样的:
到目前这里使用的都是 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 节点交由调度器处理
}
上面代码中,有两个点
- 创建 Fiber 节点,Fiber 本质上来说其实就是 React 重写后的 Virtrued Dom,从原来的树形结构变成了链表结构
- 第二点就是将 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 节点的更新、创建。
首先先来看第一步:处理当前的任务
第一步:处理当前的任务
该节点有三个关键的函数:updateHostComponent、updateClassComponent、updateFunctionComponent
当判断当前 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 的创建和绑定。
至于 updateNode、reconcileChildren 做了什么后续再看,当前只需要了解他们所干的事情。
接下来再来看看另一个函数 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 数据。
看到这里,目前还留下了很多疑点,比如:updateNode、reconcileChildren、renderWithHooks,接下来就来讲解一下 updateNode、reconcileChildren 而 renderWithHooks 还需要等到讲解 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 的树形结构转换成链表结构的工作了,规则就是:
- 当前 Fiber 的 child 属性指向第一个子节点 newFiber
- 当前的 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);
这时候当前 useReducer 的 hook 就保存这最新的状态值,接下来执行
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 赋值给 wipRoot、wip 作为下一次调度器执行的时候,即将要处理的工作 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 这几个重要的概念了,希望读者读到这里能有所帮助。
如若读者对本文内容感受不错,那就伸出小爪爪为本文来一个小小的赞吧~