从源码剖析React Hooks之useReducer、useState
前言
自React16.8
引入hooks以来,我们可以在函数组件中完成之前类组件才独有的状态、生命周期等功能,加之函数组件性能优于类组件,大家开始大量使用函数组件代替原来的类组件。而这正是得益hooks中的useState
、useReducer
和useEffect
的使用,本文主要介绍useState
和useReducer
,两者比较相似都是实现状态
的功能,后者主要是运用于一些场景比较复杂的情况。在源码中,useState
其实就是内置reducer
函数的useReducer
,本文基于React18.2
带大家进行深入的了解。
源码分析
基本使用
import React from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return count++;
case 'decrement':
return count--;
default:
throw new Error();
}
}
function PersionInfo() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<>
{count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
上述代码是关于useReducer
的基本使用,reducer
是一个纯函数,我们通过dispatch
来分发不同的动作借此来控制状态count
的改变,进而引起页面的改变,完成页面的更新。
我们知道我们可以在一个函数组件中使用多种hook
,而每种hook
又可以使用多个,而且每个hook
又可以多次使用setState
或着dispatch
方法来改变状态,React又是怎么管理每个hooks,每个hook又是怎么管理每一次更新,以及React要求不能在判断条件中使用hooks又是为什么,所有答案都会在源码中揭晓,我们一一探究。
实现状态共享
import { useState, useReducer, useEffect } from 'react';
我们通过上述的方式引入hooks
,那我们引入的这个useReducer
又是什么呢,我们点开这个这个文件,发现我们调用的useReducer
实际上是ReactCurrentDispatcher.current
这个变量提供的方法。那React为什么要去重新定义这样的一个值去间接实现呢?
// src/react/src/ReactHooks.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
function resolveDispatcher() {
// 默认值是null
return ReactCurrentDispatcher.current;
}
/**
* useReducer
* @param {*} reducer 处理函数,用于根据老状态和动作计算新状态
* @param {*} initialArg 初始状态
* @returns
*/
export function useReducer(reducer, initialArg) {
const dispatch = resolveDispatcher();
return dispatch.useReducer(reducer, initialArg);
}
hooks
实际上是在src/react-reconciler
文件中配合fiber
实现的,React必定是在这里做了状态共享,有意思的是,React在这里使用了一个十分'俏皮'的变量名
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
(秘密的,内部的,不要使用否则你会被解雇),初听这个变量着实是给我吓到了。而这个变量也是React实现hooks的关键变量。
// src/react/src/React.js
import { useReducer } from './ReactHooks';
import ReactSharedInternals from './ReactSharedInternals';
export {
...,
ReactSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
};
// src/react/src/ReactSharedInternals.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
const ReactSharedInternals = {
ReactCurrentDispatcher,
};
export default ReactSharedInternals;
React在上面两个文件中将这个内部变量共享出去,这个内部变量配合Fiber
参与React调度过程,在不同的阶段执行不同的操作,主要是分为下面的挂载过程
和更新过程
。
挂载阶段
由上一小节我们知道,我们实际上调用的hooks
来自于ReactCurrentDispatcher.current
上面挂载的函数,显然这个函数在不同阶段是不同的,
// src/react-reconciler/src/ReactFiberHooks.js
/**
* 渲染组件函数
* @param {*} current 老fiber
* @param {*} workInProcess 新fiber
* @param {*} Component 组件定义
* @param {*} props 组件属性
* @returns 虚拟DOM
*/
export function renderWithHooks(current, workInProgress, Component, props) {
currentlyRenderingFiber = workInProgress; // Function组件对应的fiber
// 需要在函数组件执行之前给ReactCurrentDispatcher.current赋值
if (current !== null && current.memoizedState !== null) {
// 存在老fiber且有hook链表,走更新逻辑
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
}
const children = Component(props);
...
return children;
}
const HooksDispatcherOnMount = {
useReducer: mountReducer,
useState: mountState,
...
};
const HooksDispatcherOnUpdate = {
useReducer: updateReducer,
useState: updateState,
};
实际上我们在挂载阶段调用的useReducer
最终是指向了mountReducer
这个函数,我们借助这个函数观察一下hooks的结构。
function mountReducer(reducer, initialArg) {
const hook = mountWorkInProgressHook();
// 这里注意甄别memoizedState,函数组件Fiber的memoizedState指向hooks链表,而hook的memoizedState指向状态
hook.memoizedState = initialArg;
const queue = {
pending: null,
dispatch: null,
};
hook.queue = queue;
const dispatch = (queue.dispatch = dispatchReducerAction.bind(
null,
currentlyRenderingFiber,
queue,
));
return [hook.memoizedState, dispatch];
}
这个主要是做了四件事情:
- 通过
mountWorkInProgressHook
方法创建了一个hook
结构,其中包括memoizedState
用来保存状态值,queue
表示这个hook
更新队列,next
表示该指向下一个hook
; - 保存初始值,
useReducer
允许输入一个初始值; - 初始化该
hook
的更新队列,更新队列包括一个pending
和dispatch
方法; - 绑定
dispatch
方法,该方法的主要作用是,创建一个更新,加到任务队列中,再就是引导React的更新。
到了这里,我们通过分析挂载阶段各个步骤来分析hook
的结构。
function mountState(initialState) {
return mountReducer(baseStateReducer, initialState);
}
function baseStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
同时,我们mountState
简单复用了mountReducer
。
其实真正的源码中作了相同状态的识别,我们主要是懂原理,这里不作为重点只是简单的做了一个复用。
mountWorkInProgressHook
/**
* 挂载构建中的hook
*/
function mountWorkInProgressHook() {
const hook = {
memoizedState: null, // hook的状态
queue: null, // 存放本hook的更新队列,queue.pending = update 的循环链表
next: null, // 指向下一把hook,一个函数里里面可能会有多个hook,它们会组成一个单向链表
};
if (workInProgressHook === null) {
// 当前函数对应的fiber的状态等于第一个hook对象,currentlyRenderingFiber指的当前函数组件的fiber
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
从上面的函数我们可以分析到,我们每次调用mountWorkInProgressHook
函数都会生成一个hook
,而这些hooks
是以一个单向链表的方式挂在函数组件的fiber
上的,其中currentlyRenderingFiber.memoizedState
指向第一个hook
,workInProgressHook
指向最后一个hook
,hook
之间通过next
链接。我们简单画出下面的过程图。
执行如下代码,
const [a, setA] = useState();
const [b, dispatchB] = useReducer(reducer1);
const [c, dispatchC] = useReducer(reducer2);
const [d, setD] = useState();
生成如下的hooks
结构
通过该结构我们也可以知道为什么不能在判断使用hooks, 正是这种链表结构,如果条件不成立,就会导致这种顺序错误,React也会抛错。
更新阶段
我们在上面阶段把每个hook
按照顺序组成了一个单向链表
,同时有两个指针方便我们取到链表的头尾,当进入到更新阶段,即运行如下代码:
dispatchB({action: 1});
dispatchB({action: 2});
dispatchB({action: 3});
由上一小节知道,我们调用dispatch
方法实际上就是调用dispatchReducerAction
方法,我们看看该方法主要是完成了哪些事情。
dispatchReducerAction
// src/react-reconciler/src/ReactFiberHooks.js
/**
* 执行派发动作的方法,他要更新状态,并且让界面重新更新
* @param {*} fiber function对应的fiber
* @param {*} queue hook对应的更新队列
* @param {*} ation 派发的动作
*/
function dispatchReducerAction(fiber, queue, action) {
// 在每个hook里会存放一个更新队列,更新队列是一个更新对象的循环链表update1.next = update2.next = update1
const update = {
action, // {action: 1} 派发的动作
next: null, // 指向下一个更新对象或者第一更新对象
};
// 将update对象添加到循环链表中
const last = queue.last;
if (last === null) {
// 链表为空,将当前更新作为第一个,并保持循环
update.next = update;
} else {
const first = last.next;
if (first !== null) {
// 在最新的update对象后面插入新的update对象
update.next = first;
}
last.next = update;
}
// 将表头保持在最新的update对象上
queue.pending = update;
// 调度更新
scheduleUpdateOnFiber();
}
对于每个单独的hook
来说,他的每次更新都会生成一个update
对象,这个对象会包括一个action
派发动作和一个next
指向下一个更新的指针,最后生成一个如图所示的环形链表,hook
的队列指向这个最后一个更新。
到这里我们在更新前的所有的准备工作已经完成了,我们有一个关于hooks
的单向链表,上面会按照顺序依次记载着我们使用过的hook
。在每个hook
上面又会挂上每一次更新的环形链表,这样的结构方便我们拿到每一次的更新。当我们进入更新阶段的时候,即当我们函数再次运行到useReducer
的时候,react就会帮我们调用updateReducer
来计算每一次更新结果,并返回最新的状态。
updateReducer
下面我们根据源码来看看react的整个计算过程。
function updateReducer(reducer) {
// 根据上一次hook创建一个新的hook,这里有属性的复用。
const hook = {
memoizedState: currentHook.memoizedState,
queue: currentHook.queue,
next: null,
};
// 获取新的hook的更新队列
const queue = hook.queue;
// 获取老的hook
const current = currentHook;
// 获取将要生效的更新队列
const pendingQueue = queue.pending;
// 初始化一个新的状态,取值为当前状态
let newState = current.memoizedState;
if (pendingQueue !== null) {
queue.pending = null; // 重置更新队列
const firstUpdate = pendingQueue.next; // 取出第一更新
let update = firstUpdate;
do {
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== firstUpdate);
}
hook.memoizedState = newState;
return [hook.memoizedState, queue.dispatch];
}
我们看到在更新阶段主要是做了下面几件事:
- 创建了一个新的
hook
,这里复用了之前的hook
的属性,(这里的复用可能是比较奇怪,源码里面还进行了其他的指针改变的操作,这里我们关注点不在这里,所以做了一个简化); - 取出更新队列
queue
,和当前的状态值memoizedState
; - 如果存在更新队列,条件成立,断开
hook
的更新队列,完成如下图的操作;
- 循环操作,根据条件和你传入的
reducer
计算每一次更新,得到的计算结果newState
做为下一次的计算的入参,直到循环结束,返回最后的计算结果和dispatch
方法,完成单个hook
的更新。
//useState其实就是一个内置了reducer的useReducer
function baseStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
function updateState() {
return updateReducer(baseStateReducer);
}
总结
- react通过单向链表的方式记录使用的每一个
hook
,fiber的memoizedState
指针指向第一个hook
,workInProgressHook
指向最后一个hook
。其中有指针指向第一个是因为方便从头计算,指向尾是因为方便向后添加新的hook
。也是这种结构的原因导致不能在判断条件里面使用hook
。 - 每个
hook
的更新都是一个环形链表,计算更新时,会依次计算每一个更新。 useState
其实是内置了一个baseStateReducer
的useReducer
,可以是看成一个阉割版的useReducer
。
转载自:https://juejin.cn/post/7200750017215365175