前言
自从react 官方发布react hooks以来,项目开发组件时几乎都是使用函数式组件来开发。在使用hooks过程中可以起到简化代码并逻辑清晰,对比于类组件能更易于理解代码。网上也有很多关于这两种组件的优劣势对比,读者可自行去翻阅。本主主要是想通过阅读源码来了解这背后的原理。(因为我不想成为API工程师,哈哈哈)
在这篇文章中,我想通过源码的角度来分析下react hooks中的useState。假定读者已经对react hooks有一定的使用并初步了解。(如果不了解建议去官网学习,react官网)
函数式组件和类组件
使用react开发基本上都是组件式开发,react组件分函数式组件和类组件。那函数式组件跟类组件的区别是什么。下面看两段代码
函数式组件
import React from 'react';
const App = (props) => {
return <div>hello {props.user}<div>;
}
类组件
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
name: 'Damon',
};
}
componentDidMount() {}
render() {
return (
<>
<div>hello {this.props.user}</div>
<div>hello {this.state.name}</div>
</>
)
}
}
这两段代码除了没有自身的状态和生命周期外基本上算是等价的,那react发布hooks想解决的问题就是让函数式组件能拥有自身的状态和生命周期。
在hooks还没发布时,函数式组件一直都是作为UI组件负责渲染视图,没有自身的状态和 "生命周期函数" (当使用hooks开发时建议忘却生命周期函数的概念),类组件有自身的状态和生命周期函数可以处理复杂业务逻辑。
这两种组件有个本质的区别:函数式组件会捕获每次渲染时所用的值,类组件会创建一个实例来保存最新的值。
想深入了解的话可以阅读此文函数式组件与类组件有何不同?作者是Dan Abramov(react核心开发人员之一)。
hooks 初始阶段
分析源码首先从引入hooks开始,以useState为例子。
import { useState } from 'react';
react/src/ReactHooks.js
可以来看下useState这个方法做了什么 (简化了代码,去掉了类型和开发环境的提示)
export function useState(initialState){
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
调用useState等于是执行了 dispatcher.useState(initialState)
dispatcher是resolveDispatcher方法返回的,来看下这个方法做了什么。
resolveDispatcher
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
ReactCurrentDispatcher
react/src/ReactCurrentDispatcher.js
const ReactCurrentDispatcher = {
current: null,
};
export default ReactCurrentDispatcher;
其实看到这里第一阶段就已经结束了。ReactCurrentDispatcher.current初始化为null,先暂时记住这个接下来我们得从函数执行阶段来看。
useState 初始化
看react渲染逻辑兜兜转转到 react-reconciler/src/ReactFiberBeginWork.js beginWork这个函数。benginWork函数通过传入的当前的Fiber来创建子Fiber节点。会根据Fiber.tag来判断生成不同的字节点。
function beginWork(
current, // 每次渲染的时候会产生一个current fiber树,commit阶段会替换成真实的dom
workInProgress, // 更新过程中,每个Fiber都会有一个跟其对应的Fiber,在更新结束后current会和workInProgress交换位置
renderLanes,
){
//... 先不关心其他逻辑
switch (workInProgress.tag) {
// 当function component第一次创建Fiber的时候,组件类型是 IndeterminateComponent (具体可以看createFiberFormTypeAndProps)
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
}
}
function mountIndeterminateComponent(
_current,
workInProgress,
Component,
renderLanes,
) {
//...
value = renderWithHooks(
null, // 第一次渲染是null
workInProgress, // workInProgress fiber
Component, // 组件本身
props, // props
context, // 上下文
renderLanes,
);
}
export function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderLanes,
) {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// current === null 第一次渲染 current !== null 更新阶段
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 函数组件被执行
let children = Component(props, secondArg);
// 这里的逻辑先放一放
if (didScheduleRenderPhaseUpdateDuringThisPass) {}
// 当你不在函数组件内部执行hooks时候会抛出异常, ContextOnlyDispatcher对象方法都是抛出异常的方法
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
invariant(
!didRenderTooFewHooks,
'Rendered fewer hooks than expected. This may be caused by an accidental ' +
'early return statement.',
);
if (enableLazyContextPropagation) {
if (current !== null) {
if (!checkIfWorkInProgressReceivedUpdate()) {
const currentDependencies = current.dependencies;
if (
currentDependencies !== null &&
checkIfContextChanged(currentDependencies)
) {
markWorkInProgressReceivedUpdate();
}
}
}
}
return children;
}
renderWithHooks 这个方法是函数执行的主要方法,首先是把memoizedState和updateQueue等于null,然后通过判断current是否为null来赋值不同的hooks对象,current为null说明是第一次渲染不为null说明是更新,这里第一次渲染跟更新是执行不同的hooks对象方法的。还记得第一阶段看到ReactCurrentDispatcher.current吗?就是在这里被赋值的。
Component 调用是我们的函数组件被执行了,我们写的逻辑就是在这里被执行的。hooks也会依次按照顺序执行。
hookDispatchOnMount 和 hookDispatchOnUpdate
hookDispatchOnMount是第一次渲染,hookDispatchOnUpdate是更新阶段。
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useOpaqueIdentifier: mountOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useOpaqueIdentifier: updateOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
};
我们先来看看第一次渲染的mountState做了什么。
function mountState(initialState){
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = { // 更新队列
pending: null, // 待更新
interleaved: null,
lanes: NoLanes,
dispatch: null, // 更新函数
lastRenderedReducer: basicStateReducer, // 获取最新的state
lastRenderedState: initialState, // 最后一次的state
});
// dispatchActio负责更新ui的函数
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
));
return [hook.memoizedState, dispatch];
}
首先调用了mountWorkInProgressHook函数得到一个hook(待会来详细看看这个方法干了什么),然后判断initialState是否function,是的话执行得到state数据。接着把state赋值给了memoizedState和baseState。申明了一个queue对象,queue是个待更新队列,dispatch是负责更新的函数,具体怎么更新的在dispatchAction方法内可查看。
mountWorkInProgressHook
function mountWorkInProgressHook() {
const hook: Hook = {
memoizedState: null, // 不同的hook保存的不同 useState保存的是state useEffect保存的是 effect对象
baseState: null, // 最新的值
baseQueue: null, // 最新的队列
queue: null, // 待更新队列
next: null, // 指向下一个hook对象
};
// react hooks的数据结构是链表的方式,具体的逻辑就在这里。
if (workInProgressHook === null) {
// 函数内的第一个hooks就会走到这里
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 接下来每个hook都会被添加到链接到未尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
每次执行一个hooks函数都会调用这个方法来创建一个hook对象,这个对象里保存了不同hook所对应的数据,最新的state数据,更新的队列和指向下一个hook的对象。
来看个例子,看看为什么不能在条件语句中申明hook
import React, { useState, useEffect, useRef } from 'react';
const App = () => {
const [name, setName] = useState('Damon');
const [age, setAge] = useState(23);
if (age !== 23) {
const Ref = useRef(null);
}
useEffect(() => {
console.log(name, age);
}, []);
return (
<div>
<span>{name}</span>
<span>{age}</span>
</div>
)
}
export default App;
当这个App组件被渲染的时候,workInProgressHook.memoizedState中会以链表的形式来保存这些hook。
如果在条件语句中申明hook,那么在更新阶段链表结构会被破坏,Fiber树上缓存的hooks信息就会和当前的workInProgressHook不一致,不一致的情况下读取数据可能就会出现异常。
dispatchAction
dispatchAction这个是负责更新的函数,在mountState中通过bind绑定然后赋值给了dispatch。dispatch就是上面例子结构出来的setName。我们来看看这个函数又干了什么。
function dispatchAction(
fiber, // 当前的fiber数
queue, // mountState申明的更新队列
action, // 这个就是我们setState传进来的参数
) {
// 计算 expirationTime
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber);
// react更新中都会有一个update
const update = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
const alternate = fiber.alternate;
// 判断是否处于渲染阶段
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
} else {
if (isInterleavedUpdate(fiber, lane)) {
const interleaved = queue.interleaved;
if (interleaved === null) {
// This is the first update. Create a circular list.
update.next = update;
// At the end of the current render, this queue's interleaved updates will
// be transfered to the pending queue.
pushInterleavedQueue(queue);
} else {
update.next = interleaved.next;
interleaved.next = update;
}
queue.interleaved = update;
} else {
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState = queue.lastRenderedState; // 上一次state
// 这段逻辑会去进行浅对比,上一个state和当前state相等的话,就return界面不会更新
const eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {}
}
}
// 渲染更新
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (isTransitionLane(lane) && root !== null) {
let queueLanes = queue.lanes;
queueLanes = intersectLanes(queueLanes, root.pendingLanes);
const newQueueLanes = mergeLanes(queueLanes, lane);
queue.lanes = newQueueLanes;
markRootEntangled(root, newQueueLanes);
}
}
if (enableSchedulingProfiler) {
markStateUpdateScheduled(fiber, lane);
}
}
这段代码还是比较复杂的,有一些和fiber相关的逻辑。但在这里关于hooks的就是会创建一个update对象然后添加到queue链表上面,然后会判断当前是否处于渲染阶段,不是的话就会去获取上一个state和当前的state进行浅对比,相等就会return不会执行更新,不相等就会执行scheduleUpdateOnFiber进行更新。
useState 更新
上面讲了初始阶段react会给ReactCurrentDispatcher.current赋值HooksDispatcherOnMount,更新阶段赋值HooksDispatcherOnUpdate。在更新阶段实际上调用的是updateState。
function updateState(initialState){
return updateReducer(basicStateReducer, initialState);
}
function basicStateReducer(state, action){
// 这里的action就是例子中的Damon。 const [name, SetName] = useState('Damon');
return typeof action === 'function' ? action(state) : action;
}
其实useState就是个简化版的useReducer,看下useReducer干了些啥。
updateReducer
function updateReducer(
reducer,
initialArg,
init,
){
//这里是获取当前的hooks,每一次函数更新的时候都会执行到hook,这个方法会保证每次更新状态不丢失
const hook = updateWorkInProgressHook();
//拿到更新队列
const queue = hook.queue;
invariant(
queue !== null,
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
// 调用lastRenderedReducer可获取到state
queue.lastRenderedReducer = reducer;
// 拿到当前的函数内的hook
const current = currentHook;
let baseQueue = current.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// 这里主要是把baseQueue和pendingQueue做了交换,然后赋值到current上。
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
// 这里会循环的遍历update
do {
const updateLane = update.lane;
// 这里有涉及到优先级相关到逻辑
if (!isSubsetOfLanes(renderLanes, updateLane)) {
const clone = {
lane: updateLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: null,
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
// 这个更新有足够的优先级
if (newBaseQueueLast !== null) {
const clone = {
lane: NoLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: null,
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
if (update.eagerReducer === reducer) {
newState = update.eagerState;
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 替换hook上的值
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const lastInterleaved = queue.interleaved;
if (lastInterleaved !== null) {
let interleaved = lastInterleaved;
do {
const interleavedLane = interleaved.lane;
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
interleavedLane,
);
markSkippedUpdateLanes(interleavedLane);
interleaved = interleaved.next;
} while (interleaved !== lastInterleaved);
} else if (baseQueue === null) {
queue.lanes = NoLanes;
}
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
首先获取了当前正在工作的hook,然后把queue.pending合并到baseQueue,这样做其实是有可能新的更新还没有处理,一次更新中可能会有多个setName,所以需要把queue.pending中的update合并到baseQueue内。接着会通过循环遍历链表,执行每一次更新去得到最新的state,把hook对象的值更新到最新。然后返回最新的memoizedState和dispatch。这里的dispatch其实就是dispatchAction,dispatchAction主要是负责更新界面的函数。
updateWorkInProgressHook
react中每一个hook更新阶段都会调用updateWorkInProgressHook来获取当前的hook。
function updateWorkInProgressHook() {
let nextCurrentHook;
if (currentHook === null) {
// 第一个hooks会走到这,然后在fiber memoizedState中获取
// currentlyRenderingFiber.alternate 这个是当前的fiber树
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 因为是链表数据结构 其他的hook直接从next获取
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook;
if (workInProgressHook === null) {
// 跟上面一样 第一次执行hooks时候
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
// 创建一个新的hook
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// 第一个hook添加到memoizedState和workInProgressHook
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// 其他的hook添加到链表末尾
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
这段代码的作用主要是当函数每次更新的时候都会执行到hook,需要从fiber树中找到对应的hook然后赋值到workInProgressHook上,这样每次函数更新的时候状态都不会丢失。
总结
最后来个分析的流程图吧(本人水平有限,哈哈哈。欢迎一起交流学习)