自己动手写 React源码 ——【8】实现 useState

深入理解 React 源码,带你从零实现 React v18 的核心功能,构建自己的 React 库。

电子书地址:2xiao.github.io/leetcode-js...

源代码地址:github.com/2xiao/my-re...

送我一个免费的 ⭐️ Star,这是对我最大的鼓励和支持。

1. 支持 FuctionComponent

上一节我们实现了简单版的 react-dom 包,支持了首屏渲染触发更新,而 React 还有很多触发更新的方式,如类组件的 this.setState()、函数组件的 useState useEffect,这一节我们来实现函数组件的 useState

useState 是 React 中的一个 Hook,React Hooks 是 React 16.8 引入的一项特性,目的是让你在函数组件中使用状态和其他 React 特性,以替代类组件中的状态和生命周期方法。在函数组件中使用 Hooks 有一些规则和限制:

  • 只能在函数组件中调用 Hooks: Hooks 依赖于 React 的函数组件机制,所以只能在函数组件中使用,而不能在类组件中使用。如果你需要在类组件中使用类似的功能,可以考虑使用 React 的类组件生命周期方法和状态管理机制。
  • 只能在顶层调用 Hooks: 不可以在条件语句、循环语句或嵌套函数中调用 Hooks。这确保 React 能够按照相同的顺序调用 Hooks,以确保状态之间的关系保持一致。

首先,我们需要将函数组件 FunctionComponent 加入到更新流程里面去,和 HostComponetHostText 一样,FunctionComponent 的更新流程同样根植于 beginWorkcompleteWork 函数。

先在 beginWork 函数中增加 FunctionComponent 的情况判断,若 FiberNode 是函数组件,就调用 updateFunctionComponent 函数;

ts{9-10} 复制代码
// packages/react-reconciler/src/beginWork.ts
// 比较并返回子 FiberNode
export const beginWork = (workInProgress: FiberNode) => {
	switch (workInProgress.tag) {
		case HostRoot:
			return updateHostRoot(workInProgress);
		case HostComponent:
			return updateHostComponent(workInProgress);
		case FunctionComponent:
			return updateFunctionComponent(workInProgress);
		case HostText:
			return updateHostText();
		default:
			if (__DEV__) {
				console.warn('beginWork 未实现的类型', workInProgress.tag);
			}
			break;
	}
};

updateFunctionComponent函数会调用函数组件本身来获取其返回的 React 元素树,例如函数组件 function App() { return 123 },只需调用 App() 就可以得到其子节点,进而将子节点传给 reconcileChildren 协调处理子节点的更新逻辑;

ts 复制代码
// packages/react-reconciler/src/beginWork.ts
function updateFunctionComponent(workInProgress: FiberNode) {
	const nextChildren = renderWithHooks(workInProgress);
	reconcileChildren(workInProgress, nextChildren);
	return workInProgress.child;
}

调用函数组件的工作由 renderWithHooks 负责,函数保存在 FiberNode 的 type 字段中,因此只需要取出 type 字段执行以下即可得到其子节点;

ts 复制代码
// packages/react-reconciler/src/fiberHooks.ts
// 执行函数组件中的函数
export function renderWithHooks(workInProgress: FiberNode) {
	// 函数保存在 type 字段中
	const Component = workInProgress.type;
	const props = workInProgress.pendingProps;
	// 执行函数
	const children = Component(props);

	return children;
}

同样,在 completeWork 函数中增加 FunctionComponent 的情况判断,和 HostRoot 一样,不需要做其他的处理,直接向上冒泡即可;

ts{8-10} 复制代码
// packages/react-reconciler/src/completeWork.ts
// 生成更新计划,计算和收集更新 flags
export const completeWork = (workInProgress: FiberNode) => {
	const newProps = workInProgress.pendingProps;
	const current = workInProgress.alternate;
	switch (workInProgress.tag) {
		case HostRoot:
		case FunctionComponent:
			bubbleProperties(workInProgress);
			return null;
        // ...

这样,我们就实现了 FunctionComponent 的基本功能。

2. 实现共享数据层

我们知道 Hooks 只能在函数组件中调用,若我们在一个 Hook 的回调函数中调用另一个 Hook 会报错,类似下面这样:

jsx 复制代码
function App() {
    useEffect(() => {
        useState(1)
    }
}

那 Hooks 如何感知被调用的上下文环境呢?

方法是在不同上下文中,调用的 Hooks 不是同一个函数。在 mount 时、update 时、以及其它上下文中,分别实现不同的 Hooks 函数,从而确保 Hooks 在正确的上下文环境中执行。

我们调用useState 时的一般语法是:

jsx 复制代码
import { useState } from 'react';

const [state, setState] = useState(initialState);

可以看出,useState 是被 react 包导出,而要想感知上下文环境,则需要依赖 react-reconciler 包中的更新流程,也就是说两个包之间需要共享数据,因此就需要实现一个内部数据共享层(ReactSharedInternals)。

先在 react 包中新建一个 currentDispatcher.ts 文件,里面保存了当前使用的 Hooks 指针 currentDispatcher,同时导出一个 resolveDispatcher 函数,方便查询当前使用的 Hooks 集合:

ts 复制代码
// packages/react/src/currentDispatcher.ts
import { Action } from 'shared/ReactTypes';

// const [data, setData] = useState(0);
// or
// const [data, setData] = useState(0data) => data + 1);
export interface Dispatcher {
	useState: <S>(initialState: (() => S) | S) => [S, Dispatch<S>];
}

export type Dispatch<State> = (action: Action<State>) => void;

const currentDispatcher: { current: Dispatcher | null } = {
	current: null
};

export const resolveDispatcher = (): Dispatcher => {
	const dispatcher = currentDispatcher.current;
	if (dispatcher == null) {
		throw new Error('Hooks 只能在函数组件中执行');
	}
	return dispatcher;
};

export default currentDispatcher;

接着,在react/index.ts 中对外暴露 useState 函数,这个函数返回的是 currentDispatcher.current.useState

同事将内部共享数据层暴露出去,里面包含了 currentDispatcher 的数据。React 官方为内部共享数据层取了一个很长的名字,以警告开发者不要随便修改里面的数据:

__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED

ts{8-16} 复制代码
// packages/react/index.ts
import currentDispatcher, {
	Dispatcher,
	resolveDispatcher
} from './src/currentDispatcher';
import { jsx } from './src/jsx';

export const useState: Dispatcher['useState'] = (initialState) => {
	const dispatcher = resolveDispatcher();
	return dispatcher.useState(initialState);
};

// 内部数据共享层
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
	currentDispatcher
};

export default {
	version: '1.0.0',
	createElement: jsx
};

为了将 react-reconciler 包和 react 包解耦,我们不直接从react 包中调用数据共享层,而是通过 shared 包中转一下:

ts 复制代码
// packages/shared/internals.ts
import * as React from 'react';

// 为了将 react-reconciler 和 react 解耦,在 shared 中转,方便 react-reconciler 使用
const internals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;

export default internals;

现在,我们就可以在 react 包通过数据共享层获取当前的上下文环境,并返回不同的 Hooks 集合了,接下来,我们需要在 react-reconciler 包中实现对应不同上下文环境的 Hooks 集合。

3. 实现 useState

前面我们提到,在 beginWork 阶段,为了获取函数组件的 children,在 updateFunctionComponent 函数中会调用 renderWithHooks 方法。renderWithHooks 方法会调用函数组件,并在执行的过程中,执行相应的 hook 方法。

因此,我们需要在 renderWithHooks 方法中判断当前的上下文环境,来决定要调用哪个 Hooks 集合,判断的方法是根据 workInProgress.alternate

  • 若它为 null,代表此时还没有真实 DOM 树(首屏还没有渲染),所以是 mount 阶段,应该调用 mount 阶段对应的 Hooks 集合: HooksDispatcherOnMount,将它赋值给 currentDispatcher
  • 否则就是 update 阶段,应该调用 update 阶段对应的 Hooks 集合: HooksDispatcherOnUpdate
ts{13-21} 复制代码
// packages/react-reconciler/src/fiberHooks.ts
// ...
import internals from 'shared/internals';

const { currentDispatcher } = internals;

// 执行函数组件中的函数
export function renderWithHooks(workInProgress: FiberNode) {
	// 赋值
	currentlyRenderingFiber = workInProgress;
	workInProgress.memoizedState = null;

	// 判断 Hooks 被调用的时机
	const current = workInProgress.alternate;
	if (current !== null) {
		// 组件的更新阶段(update)
		currentDispatcher.current = HooksDispatcherOnUpdate;
	} else {
		// 首屏渲染阶段(mount)
		currentDispatcher.current = HooksDispatcherOnMount;
	}

	// 函数保存在 type 字段中
	const Component = workInProgress.type;
	const props = workInProgress.pendingProps;
	// 执行函数
	const children = Component(props);

	// 重置
	currentlyRenderingFiber = null;
	workInProgressHook = null;

	return children;
}

const HooksDispatcherOnMount: Dispatcher = {
	useState: mountState
};

const HooksDispatcherOnUpdate: Dispatcher = {
	useState: updateState
};

其中,currentlyRenderingFiber 是一个全局变量,用于跟踪当前正在被处理的 FiberNode 节点,以便在调用 Hooks 时能找到正确的 FiberNode 节点,将状态和上下文与之相关联。workInProgressHook 也是一个全局变量,用于跟踪当前正在进行工作的 Hook。

接着,我们来实现 mount 阶段对应的 useState 方法:mountState,先定义一下 Hook 的数据结构:

ts 复制代码
// 定义 Hook 数据结构
export interface Hook {
	memoizedState: any; // 保存 Hook 的数据
	queue: any;
	next: Hook | null;
}

注意,Hook 和 FiberNode 都有 memoizedState 字段,但是二者的含义不一样,两者的关系如下图:

Reconciler 更新流程中,会遍历 Fiber 树,而 workInProgress 字段指向了当前正在被执行的 FiberNode 节点,该节点的 memoizedState 字段中保存着待执行的 Hooks 链表;链表中的每个 Hook 通过 next 指针连接在一起,每个 Hook 都有一个 memoizedState 字段,指向了对应的 Hook 数据。

接着实现 mountState,现在需要做两件事:

  • 从 Hooks 链表中获取当前正在工作的 useState
  • 获取当前 useState 对应的 Hook 数据

实现 mountWorkInProgressHook 函数,负责从 Hooks 链表中获取当前正在工作的 Hook。对于 mount 阶段,我们需要新建一个 Hook,然后将它赋值给 workInProgressHook 变量,这个变量的含义就是当前正在工作的 Hook,同时还需要注意,Hooks 是一个链表,链表中的每个 Hook 通过 next 指针连接在一起,所以要维护好 next 指针。

ts 复制代码
// 获取当前正在工作的 Hook
function mountWorkInProgressHook(): Hook {
	const hook: Hook = {
		memoizedState: null,
		queue: null,
		next: null
	};
	if (workInProgressHook == null) {
		// mount 时的第一个hook
		if (currentlyRenderingFiber !== null) {
			workInProgressHook = hook;
			currentlyRenderingFiber.memoizedState = workInProgressHook;
		} else {
			// currentlyRenderingFiber == null 代表 Hook 执行的上下文不是一个函数组件
			throw new Error('Hooks 只能在函数组件中执行');
		}
	} else {
		// mount 时的其他 hook
		// 将当前工作的 Hook 的 next 指向新建的 hook,形成 Hooks 链表
		workInProgressHook.next = hook;
		// 更新当前工作的 Hook
		workInProgressHook = hook;
	}
	return workInProgressHook;
}

对于 mount 阶段来说,当前 useState 对应的 Hook 数据就是 initialState,我们需要将这个数据存放在 Hook 的 memoizedState 变量中。另外,因为 useState 可以触发更新,所以我们创建一个 UpdateQueue,存放在 Hook 的 queue 变量中。

ts 复制代码
function mountState<State>(
	initialState: (() => State) | State
): [State, Dispatch<State>] {
	// 当前正在工作的 useState
	const hook = mountWorkInProgressHook();

	// 当前 useState 对应的 Hook 数据
	let memoizedState;
	if (initialState instanceof Function) {
		memoizedState = initialState();
	} else {
		memoizedState = initialState;
	}
	hook.memoizedState = memoizedState;

	const queue = createUpdateQueue<State>();
	hook.queue = queue;

	// 实现 dispatch
	const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
	queue.dispatch = dispatch;

	return [memoizedState, dispatch];
}

最后我们还要实现 useStatedispatch 方法,并接入当前的更新流程。

ts 复制代码
// 用于触发状态更新的逻辑
function dispatchSetState<State>(
	fiber: FiberNode,
	updateQueue: UpdateQueue<State>,
	action: Action<State>
) {
	const update = createUpdate(action);
	enqueueUpdate(updateQueue, update);
	// 调度更新
	scheduleUpdateOnFiber(fiber);
}

至此,我们就实现了函数组件中的 useState 方法,但是 useStatedispatch 还没有完全实现,因为我们还没有实现 update 流程,如:删除节点、更新节点等,这些功能将在第 10 节中实现。

相关代码可在 git tag v1.8 查看,地址:github.com/2xiao/my-re...


《自己动手写 React 源码》遵循 React 源码的核心思想,通俗易懂的解析 React 源码,带你从零实现 React v18 的核心功能。

学完本书,你将有这些收获:

  • 面试加分:框架底层原理是面试必问环节,熟悉 React 源码会为你的面试加分,也会为你拿下 offer 增加不少筹码;

  • 提升开发效率:熟悉 React 源码之后,会对 React 的运行流程有新的认识,让你在日常的开发中,对性能优化、使用技巧和 bug 解决更加得心应手;

  • 巩固基础知识:学习本书也顺便巩固了数据结构和算法,如 reconciler 中使用了 fiber、update、链表等数据结构,diff 算法要考虑怎样降低对比复杂度;

本书的特色:

  • 教程详细,代码开源,带你构建自己的 React 库;

  • 功能全面,可跑通官方测试用例;

  • 按 Git Tag 划分迭代步骤,记录每个功能的实现过程;

电子书地址:2xiao.github.io/leetcode-js...

源代码地址:github.com/2xiao/my-re...

送我一个免费的 ⭐️ Star,这是对我最大的鼓励和支持。

相关推荐
孜然卷k3 分钟前
前端导出word文件,并包含导出Echarts图表等
前端·javascript
家里有只小肥猫24 分钟前
uniApp小程序保存canvas图片
前端·小程序·uni-app
前端大全26 分钟前
Chrome 推出全新的 DOM API,彻底革新 DOM 操作!
前端·chrome
八角丶38 分钟前
元素尺寸的获取方式及区别
前端·javascript·html
冴羽1 小时前
Svelte 最新中文文档教程(16)—— Context(上下文)
前端·javascript·svelte
前端小臻1 小时前
关于css中bfc的理解
前端·css·bfc
白嫖不白嫖1 小时前
网页版的俄罗斯方块
前端·javascript·css
HappyAcmen1 小时前
关于Flutter前端面试题及其答案解析
前端·flutter
顾比魁1 小时前
pikachu之CSRF防御:给你的请求加上“网络身份证”
前端·网络·网络安全·csrf
林的快手1 小时前
CSS文本属性
前端·javascript·css·chrome·node.js·css3·html5