2022年了,用React开发不使用hook是不行的。同时一方面,由于我在日常开发中已经许久没有使用class组件,所以一直对于hook的设计理念、实现原理和相关源码有一定的兴趣。原因无他,用hook真的太爽了。
开始之前,先抛出几个问题:
useMemo
和useCallback
是怎么做缓存的?从这里开始,我们一一解答。
协调器目录 github.com/facebook/re…
不知道诸位有没有使用class
组件的经历,属实是又臭又长,繁多且命名复杂的生命周期给开发者带来的体验并不好。在这之前的function
组件由于没有状态的概念,只能用来承载简单的UI,这显然不行,react的数据驱动意味着状态逻辑实际上是无处不在的。
依据官方文档的解释,引入hook解决了三个以及更多的问题
实际体现上,我也无比认同引入hook的实际效果
我有一篇文章讲的是Fiber结构的实现https://juejin.cn/post/7030069221342052389
,这里只给一下代码:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 实例变量,从字面意思也应该可以看出这里保存了tag、key、type、state类似这样的有很强实际意义的属性
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
...
}
除了Fiber结构,还需要强调的一点是,react16.8之后的Fiber架构:
开始之前,贴一下我整个hook篇的调试代码
import {useState, useEffect, useRef} from 'react';
const useMockRef = init => {
const [ref] = useState({current: init});
return ref;
};
const CountButton = () => {
const [count, setCount] = useState(0);
const [tick, setTick] = useState(0);
const realRef = useRef(0);
const mockRef = useMockRef(0);
const handleClick = () => {
setCount(count + 1);
mockRef.current += 1;
realRef.current += 1;
};
const handleRefCountClick = () => {
mockRef.current += 10;
realRef.current += 10;
console.log('mockRef.current', mockRef.current);
};
useEffect(() => {
setTick(count + Math.random());
}, [count]);
return (
<>
<button onClick={handleClick}>{count}:Render by state</button>
<button onClick={handleRefCountClick}>Click to add ref</button>
<div style={{color: 'red'}}>{tick}</div>
<div style={{color: 'yellow'}}>{mockRef?.current}</div>
<div style={{color: 'green'}}>{realRef?.current}</div>
</>
);
};
export {CountButton};
进入到这里,我们需要进入到react源码的部分,这里我们需要关注的是FiberBeginWork
调试代码
const CountButton = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<>
<button onClick={handleClick}>{count}:Render by state</button>
</>
);
};
在之前的文章中,我们其实已经对Fiber
有一定的了解,也知道了在react中一个Fiber
其实也就是对应一个虚拟DOM。那么我们现在看到function beginWork
#L3829
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
...
}
可以发现beginWork
的入参为「current、workInProgress、renderLanes」,前两者对应react架构中的两颗Fiber树,renderLanes
则是和优先级相关的参数,和Scheduler
相关,这一部分我还没仔细研究🧐暂时MARK。
更详细点说,在这里函数的入参中:
Fiber节点
上一次更新时的节点Fiber节点
当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树 之所以要有两根Fiber树是因为react使用了一种“双缓存机制”,这种机制的意义是可以把当前页面下一帧放到内存中绘制,在绘制完毕后直接用当前帧替换上一帧,省去两帧替换的计算时间(diff瓶颈/),减少白屏闪现的情况。这也是Fiber架构的重要工作原理
我们目前研究的是函数式组件,那么在#L3942
我们可以看到,react基于当前Fiber节点的tag(即FunctionComponent) 进行updateFunctionComponent
的调用:
switch (workInProgress.tag) {
case xx:
...
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
}
我们转到updateFunctionComponent
,忽略掉一些代码,可以发现返回值workInProgress.child
其实就是nextChildren
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {
...
if (__DEV__) {
...
} else {
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
}
...
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
//https://github.com/facebook/react/blob/e225fa43ada4f4cf3d3ba4982cdd81bb093eaa46/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L299
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
// 如果这是一个尚未渲染的新组件,我们不会通过应用最小的副作用来更新其子集。
// 相反我们将在渲染子对象之前将它们全部添加到子对象。
// 这意味着我们可以通过不跟踪副作用来优化这个调节过程
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 如果当前子项与正在进行的工作相同,则表示我们还没有开始对这些子项进行任何研究。
// 因此,我们使用克隆算法,用于创建所有当前子项的副本。
// 如果我们已经有任何进展的工作,在这一点上是无效的,所以我们把它抛出。
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
显然,renderWithHooks这个函数的作用非常关键,这里隐藏了hook工作原理的关键,
往下阅读会发现renderWithHooks
做的第一件事就是把当前Fiber
节点的「memoizedState、updateQueue、lanes」置空了。
那么这里涉及两个概念:
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
为什么要这么做呢?看注释是
// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;
// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
// This is tricky because it's valid for certain types of components (e.g. React.lazy)
// Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
// Non-stateful hooks (e.g. context) don't get added to memoizedState,
// so memoizedState would be null during updates and mounts.
翻译过来就是,我的理解是这里的作用是清除当前Fiber节点的遗留状态。
// 以下内容应已重置
// currentHook = null;
// workInProgressHook = null;
// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
// TODO 警告如果在挂载过程中根本没有使用钩子,那么在更新过程中就会使用一些钩子。
// 目前,我们将更新呈现标识为挂载,因为 memoizedState === null.
// 这很棘手,因为它对某些类型的组件是有效的 (e.g. React.lazy)
// 只有在至少使用一个有状态钩子的情况下,才使用memoizedState去区分挂载/更新
// 非状态钩子(例如上下文)不会被添加到 memizedState,
// 因此,在更新和挂载期间,memizedState 将为 null。
经历了上边的步骤,终于来到创建新的节点。
这里的Component
实际上就是我们的组件函数
而这里实际上的运行流程大抵如下:
createSignatureFunctionForTransform
--- 这里实际上是react-refresh的热更新这块的东西,可以暂时不看HooksDispatcherOnMountInDEV["useState"]
解析一下,这里主要是两部分:
一步步来:
其实mountHookypesDev并不是一个很难理解的部分,但是为什么要拿出来说呢?
可以看到,该函数的作用很简单,获得当前hook的名字,塞入hookTypesDev或者创建hookTypesDev
那么记不记得上边抛出的一个问题:为什么hook必须在顶层调用?
其实这里就给出了答案,或者说这就是原因之一,我们可以看到图中另一个函数updateHookTypesDev
可以看到哈,mountHookTypesDev
往hookTypesDev中填入所有hookName之后,后续的update会按照索引递增的方式来获取函数名,此时如hook调用顺序变化,获得的hookName就会存在问题,react也会在此抛出警告。
到达这里,我们可以看到mountState
这个关键函数
而mountState
中又有另一个关键函数mountWorkInProgressHook
function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
看到这里可以知道,关键全局变量workInProgressHook
的作用是记录每次生成的hook对象,用来指向组件中正在调用哪个hook。每一次调用hook函数都会把workInProgressHook
指向hook函数产生的hook对象。
TIPS: currentlyRenderingFiber即为workInProgressFiber
那么首次挂载即会有这么条链路: workInProgressFiber->memoizedState = workInProgressHook = hook 之后再次挂载则会不断进行: workInProgressHook = workInProgressHook.next = hook; 这也是我们熟知的hook存储成单向链表保存的由来
而再往下走
其实已经很好理解了,react在hook上记录下baseState,memoizedState
并初始化quene
,然后返回我们熟悉的[state,setState]
注意:关于
quene
的含义以及和dispatchSetState的内容我们放在组件更新的时候讲
组件更新的第一步,即调用上边的dispatchSetState
会发现,我们使用的setState
操作的本质是dispatchSetState.bind()
,那么我们继续下钻
function dispatchSetState(fiber, queue, action) {
...
var lane = requestUpdateLane(fiber);
var update = {
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null
};
...
}
会发现在dispach的时候会出现一个新的类型update
,我们翻到编译前的代码可以对这个结构更清晰。显然到这里你知道了update
又是一个链表
update并不是一个单纯的单向链表,为了体现这个规则,我在调试代码中加入了新的两次setState
我们来看看
我们看一下其中一个setCount
的update
,加深一下印象,继续前进.
function dispatchSetState(fiber, queue, action) {
if (isRenderPhaseUpdate(fiber)) {
...
} else {
...
var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
var eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}
往下我们来到enqueueConcurrentHookUpdate
,这里即涉及了我们一直搁置的queue
与上边的update
特殊结构,前两步忽略,我们需要继续下钻enqueueUpdate
但是我们首先需要先知道这个函数的前两个参数此时都指代什么,我们此时需要回头来到mountState
,会发现Fiber
指的就是currentluRenderingFiber$1
。同理其实queue
在mount挂载在hook之后,被通过bind的方式送入了dispatch。
不过有一点值得注意的是,通过.bind产生的闭包函数有个特点,Fiber
指向的仍然是当时传入的fiber,也就是说此时的fiber
是我们曾经传入的workInProgressFiber
而enqueueUpdate
做了什么呢?这是一个关键步骤,看图我们会发现,enqueueUpdate
用index持续自增的方式向concurrentQueues
中分别添加「fiber、queue、update、lane」
所以concurrentQueues经历我们多次setCount
之后呈现图中的数据
接下来其实比较麻烦,有一些调度上的代码,为了易于理解,我们找到使得concurrentQueuesIndex变化的代码处继续调试
我们看看这个函数做了什么:
🤔诶,这不得懂了,这里创建了一个环形链表,也就是说所有的更新,即update
,会组合成一个单向环形链表挂载在queue.pending
上。
之所以单向环形是因为react的更新是有优先级的,
update
的执行顺序并不是固定的,通过单向链表更新可能会导致第一个update
丢失。而环形链表一个显然的优势就是可以从任何节点开始循环链表,由此保证了状态依赖的连续性。
OK,那么dispatchSetState
的内容我们可以咔了,往下继续前进。
插一句,为什么我们setState的时候重复值不会引起重渲染,就是因为这个函数。 看到这个TODO了么😭麻烦亲把这个限制去了吧
而紧接着就是我们比较熟悉的组件更新,组件更新部分细节上会比挂载更多,但是实际难度也不大
可以看到,这里有两个关键函数「updateHookTypeDev」和「updateState」
其实前文已经提过这里了,所以我们跳过一下🐶
首先updateState的操作其实依赖于updateReducer
而最终会走到一个我们熟悉又陌生的关键函数updateWorkInProgressHook
我们先来看第一部分
function updateWorkInProgressHook() {
...
var nextCurrentHook;
if (currentHook === null) {
var current = currentlyRenderingFiber$1.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
...
}
这里涉及一个地方currentlyRenderingFiber$1.alternate
,「alternate」翻译过来即为「候补」。
那它是什么呢?我们上边曾经讲过react的双缓存树架构,这里的alternate实际上就指向当前workInProgress
节点对应的渲染在屏幕上的current
节点。
那么纵览这整个第一步,其实就是获取当前hook
第二部分较为简单,作用是获取当前待更新的hook
function updateWorkInProgressHook() {
...
var nextWorkInProgressHook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
...
}
而最后一部分,知道了上述一二部分变量含义之后同样很容易理解,无非react创建了一个新的hook然后用同样的方式做一个单向链表的更新。
剩下的部分涉及到了不少我们提过但是实际上并没有解释的变量:
currentHook
,这个变量实际上和workPorgressHook
对应,指的是current Fiber树上对应的当前hookbaseQueue
,该变量取自currentHook
,含义是本次更新之前剩余的待更新队列queue
更新队列,本次更新增加的待更新队列,pending
中存放着环形单项链表式的update
如果你在上边已经知道了queue->pending
,那么实际上对于这里进行hook
的更新并不会难以理解。
这里分成几个简单的步骤:
那么此时已经开始逐级返回,我们来到updateFunctionComponent
调用的最后一个方法reconcileChildren
而这里我们不深入看内部逻辑其实也是很容易理解这里是为当前Fiber节点更新child的过程,由于具体的协调器实际上就是我们常说的React diff
这一部分,所以暂时跳过一下,mark以后说。
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
//如果这是一个尚未渲染的新组件,我们
//不会通过应用最小的副作用来更新其子集。相反
//我们将在渲染子对象之前将它们全部添加到子对象。这意味着
//我们可以通过不跟踪副作用来优化这个调节过程。
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
//如果当前子项与正在进行的工作相同,则表示
//我们还没有开始对这些孩子进行任何研究。因此,我们使用
//克隆算法,用于创建所有当前子项的副本。
//如果我们已经有任何进展的工作,在这一点上是无效的,所以
//我们把它扔掉吧。
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
}
}
根据我们对于react的渲染流程了解,render阶段分为beginWork
和completeWork
而这里紧接着触发的两个函数popTreeContext
和bubbleProperties
,看上去都属于在完成react的render递归返回的工作。
由于具体工作原理中实质上和我们想说的useState已经没有多少关系,我们暂时忽略即可。这里在下一章动手
React源码的阅读之路必定是漫长的,定一些TODO:
那个人再不曾出现,他只跟我说继续前进
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有