从React Hooks发布以来,整个社区都以积极的态度去拥抱它、学习它。期间也涌现了很多关于React Hooks 源码解析的文章。本文就以笔者自己的角度来写一篇属于自己的文章吧。希望可以深入浅出、图文并茂的帮助大家对React Hooks的实现原理进行学习与理解。本文将以文字、代码、图画的形式来呈现内容。主要对常用Hooks中的 useState、useReducer、useEffect 进行学习,尽可能的揭开Hooks的面纱。
Hooks的面世让我们的Function Component逐步拥有了对标Class Component的特性,比如私有状态,生命周期函数等。useState与useReducer这两个Hooks让我们可以在 Function Component里使用到私有状态。而useState其实就是阉割版的useReducer,这也是我那它们两个放在一起讲的原因。应用一下官方的例子:
function PersionInfo ({initialAge,initialName}) {
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
return (
<>
Age: {age}, Name: {name}
<button onClick={() => setAge(age + 1)}>Growing up</button>
</>
);
}
useState 我们可以初始化一个私有状态,它会返回这个状态的最新值和一个用来更新状态的方法。而useReducer则是针对更复杂的状态管理场景:
const initialState = {age: 0, name: 'Dan'};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {...state, age: state.age + action.age};
case 'decrement':
return {...state, age: state.age - action.age};
default:
throw new Error();
}
}
function PersionInfo() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Age: {state.age}, Name: {state.name}
<button onClick={() => dispatch({type: 'decrement', age: 1})}>-</button>
<button onClick={() => dispatch({type: 'increment', age: 1})}>+</button>
</>
);
}
同样也是返回当前最新的状态,并返回一个用来更新数据的方法。在使用这两个方法的时候也许我们会想过这样的问题:
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
Function Component 不像 Class Component那样可以将私有状态挂载到类实例中并通过对应的key来指向对应的状态,而且每次的页面的刷新或者说组件的重新渲染都会使得 Function 重新执行一遍。所以React中必定有一种机制来区分这些Hooks。
const [age, setAge] = useState(initialAge);
// 或
const [state, dispatch] = useReducer(reducer, initialState);
Class Component因为自身的特点可以将私有状态持久化的挂载到类实例上,每时每刻保存的都是最新的值。而 Function Component 由于本质就是一个函数,并且每次渲染都会重新执行。所以React必定拥有某种机制去记住每一次的更新操作,并最终得出最新的值返回。当然我们还会有其他的一些问题,比如这些状态究竟存放在哪?为什么只能在函数顶层使用Hooks而不能在条件语句等里面使用Hooks?
我们先来了解useState以及useReducer的源码实现,并从中解答我们在使用Hooks时的种种疑惑。首先我们从源头开始:
import React, { useState } from 'react';
在项目中我们通常会以这种方式来引入useState方法,被我们引入的这个useState方法是什么样子的呢?其实这个方法就在源码 packages/react/src/ReactHook.js 中。
// packages/react/src/ReactHook.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
// ...
return dispatcher;
}
// 我们代码中引入的useState方法
export function useState(initialState) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState)
}
从源码中可以看到,我们调用的其实是 ReactCurrentDispatcher.js 中的dispatcher.useState(),那么我们继续前往ReactCurrentDispatcher.js文件:
import type {Dispacther} from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
好吧,它继续将我们带向 react-reconciler/src/ReactFiberHooks.js这个文件。那么我们继续前往这个文件。
// react-reconciler/src/ReactFiberHooks.js
export type Dispatcher = {
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>],
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
// 其他hooks类型定义
}
兜兜转转我们终于清楚了React Hooks 的源码就放 react-reconciler/src/ReactFiberHooks.js 目录下面。在这里如上图所示我们可以看到有每个Hooks的类型定义。同时我们也可以看到Hooks的具体实现,大家可以多看看这个文件。首先我们注意到,我们大部分的Hooks都有两个定义:
// react-reconciler/src/ReactFiberHooks.js
// Mount 阶段Hooks的定义
const HooksDispatcherOnMount: Dispatcher = {
useEffect: mountEffect,
useReducer: mountReducer,
useState: mountState,
// 其他Hooks
};
// Update阶段Hooks的定义
const HooksDispatcherOnUpdate: Dispatcher = {
useEffect: updateEffect,
useReducer: updateReducer,
useState: updateState,
// 其他Hooks
};
从这里可以看出,我们的Hooks在Mount阶段和Update阶段的逻辑是不一样的。在Mount阶段和Update阶段他们是两个不同的定义。我们先来看Mount阶段的逻辑。在看之前我们先思考一些问题。React Hooks需要在Mount阶段做什么呢?就拿我们的useState和useReducer来说:
我们一下React的实现,先来看mountState的实现。
// react-reconciler/src/ReactFiberHooks.js
function mountState (initialState) {
// 获取当前的Hook节点,同时将当前Hook添加到Hook链表中
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 声明一个链表来存放更新
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer,
lastRenderedState,
});
// 返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
// 返回当前状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
关于第一件事,初始化状态并返回状态和更新状态的方法。这个没有问题,源码也很清晰利用initialState来初始化状态,并且返回了状态和对应更新方法 return [hook.memoizedState, dispatch]。那么我们来看看React是如何区分不同的Hooks的,这里我们可以从 mountState 里的 mountWorkInProgressHook方法和Hook的类型定义中找到答案。
// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null, // 指向下一个Hook
};
首先从Hook的类型定义中就可以看到,React 对Hooks的定义是链表。也就是说我们组件里使用到的Hooks是通过链表来联系的,上一个Hooks的next指向下一个Hooks。这些Hooks节点是怎么利用链表数据结构串联在一起的呢?相关逻辑就在每个具体mount 阶段 Hooks函数调用的 mountWorkInProgressHook方法里:
// react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// 当前workInProgressHook链表为空的话,
// 将当前Hook作为第一个Hook
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// 否则将当前Hook添加到Hook链表的末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
在mount阶段,每当我们调用Hooks方法,比如useState,mountState就会调用mountWorkInProgressHook 来创建一个Hook节点,并把它添加到Hooks链表上。比如我们的这个例子:
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
useEffect(() => {})
那么在mount阶段,就会生产如下图这样的单链表:
而关于第三件事,useState和useReducer都是使用了一个queue链表来存放每一次的更新。以便后面的update阶段可以返回最新的状态。每次我们调用dispatchAction方法的时候,就会形成一个新的updata对象,添加到queue链表上,而且这个是一个循环链表。可以看一下 dispatchAction 方法的实现:
// react-reconciler/src/ReactFiberHooks.js
// 去除特殊情况和与fiber相关的逻辑
function dispatchAction(fiber,queue,action,) {
const update = {
action,
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.last = update;
// 进行调度工作
scheduleWork();
}
也就是我们每次执行dispatchAction方法,比如setAge或setName。就会创建一个保存着此次更新信息的update对象,添加到更新链表queue上。然后每个Hooks节点就会有自己的一个queque。比如假设我们执行了下面几个语句:
setAge(19);
setAge(20);
setAge(21);
那么我们的Hooks链表就会变成这样:
在Hooks节点上面,会如上图那样,通过链表来存放所有的历史更新操作。以便在update阶段可以通过这些更新获取到最新的值返回给我们。这就是在第一次调用useState或useReducer之后,每次更新都能返回最新值的原因。再来看看mountReducer,你会发现和mountState几乎一摸一样,只是状态的初始化逻辑有那么一点区别。毕竟useState其实就是阉割版的useReducer。这里就不详细介绍mountReducer了。
// react-reconciler/src/ReactFiberHooks.js
function mountReducer(reducer, initialArg, init,) {
// 获取当前的Hook节点,同时将当前Hook添加到Hook链表中
const hook = mountWorkInProgressHook();
let initialState;
// 初始化
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = initialArg ;
}
hook.memoizedState = hook.baseState = initialState;
// 存放更新对象的链表
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
// 返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
// 返回状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
然后我们来看看update阶段,也就是看一下我们的useState或useReducer是如何利用现有的信息,去给我们返回最新的最正确的值的。先来看一下useState在update阶段的代码也就是updateState:
// react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
可以看到,updateState底层调用的其实就会死updateReducer,因为我们调用useState的时候,并不会传入reducer,所以这里会默认传递一个basicStateReducer进去。我们先看看这个basicStateReducer:
// react-reconciler/src/ReactFiberHooks.js
function basicStateReducer(state, action){
return typeof action === 'function' ? action(state) : action;
}
在使用useState(action)的时候,action通常会是一个值,而不是一个方法。所以baseStateReducer要做的其实就是将这个action返回。来继续看一下updateReducer的逻辑:
// react-reconciler/src/ReactFiberHooks.js
// 去掉与fiber有关的逻辑
function updateReducer(reducer,initialArg,init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 拿到更新列表的表头
const last = queue.last;
// 获取最早的那个update对象
first = last !== null ? last.next : null;
if (first !== null) {
let newState;
let update = first;
do {
// 执行每一次更新,去更新状态
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
}
const dispatch = queue.dispatch;
// 返回最新的状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
在update阶段,也就是我们组件第二次第三次。。执行到useState或useReducer的时候,会遍历update对象循环链表,执行每一次更新去计算出最新的状态来返回,以保证我们每次刷新组件都能拿到当前最新的状态。useState的reducer是baseStateReducer,因为传入的update.action为值,所以会直接返回update.action,而useReducer 的reducer是用户定义的reducer,所以会根据传入的action和每次循环得到的newState逐步计算出最新的状态。
看到这里我们在回头看看最初的一些疑问:
1 . React 如何管理区分Hooks?
2 . useState和useReducer如何在每次渲染时,返回最新的值?
3 . 为什么不能在条件语句等中使用Hooks?
比如如图所示,我们在mount阶段调用了useState('A'), useState('B'), useState('C'),如果我们将useState('B') 放在条件语句内执行,并且在update阶段中因为不满足条件而没有执行的话,那么没法正确的重Hooks链表中获取信息。React也会给我们报错。
好的,现在我们已经了解了React 通过链表来管理 Hooks,同时也是通过一个循环链表来存放每一次的更新操作,得以在每次组件更新的时候可以计算出最新的状态返回给我们。那么我们这个Hooks链表又存放在那里呢?理所当然的我们需要将它存放到一个跟当前组件相对于的地方。那么很明显这个与组件一一对应的地方就是我们的FiberNode。 如图所示,组件构建的Hooks链表会挂载到FiberNode节点的memoizedState上面去。
看到这,相信你已经对Hooks的源码实现模式已经有一定的了解了,所以你尝试去看一下Effect的实现你会一下子就看懂。首先我们先回忆一下useEffect是怎么样工作的?
function PersionInfo () {
const [age, setAge] = useState(18);
useEffect(() =>{
console.log(age)
}, [age])
const [name, setName] = useState('Dan');
useEffect(() =>{
console.log(name)
}, [name])
return (
<>
...
</>
);
}
PersionInfo组件第一次渲染的时候会在控制台输出age和name,在后面组件的每次update中,如果useEffect中的deps依赖的值发生了变化的话,也会在控制台中输出对应的状态,同时在unmount的时候就会执行清除函数(如果有)。React中是怎么实现的呢?其实很简单,在FiberNode中通过一个updateQueue来存放所有的effect,然后在每次渲染之后依次执行所有需要执行的effect。useEffect 也分为mountEffect和updateEffect
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
function mountEffect( create,deps,) {
return mountEffectImpl(
create,
deps,
);
}
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 获取当前Hook,并把当前Hook添加到Hook链表
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 将当前effect保存到Hook节点的memoizedState属性上,
// 以及添加到fiberNode的updateQueue上
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
next: (null: any),
};
// componentUpdateQueue 会被挂载到fiberNode的updateQueue上
if (componentUpdateQueue === null) {
// 如果当前Queue为空,将当前effect作为第一个节点
componentUpdateQueue = createFunctionComponentUpdateQueue();
// 保持循环
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 否则,添加到当前的Queue链表中
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
可以看到在mount阶段,useEffect做的事情就是将自己的effect添加到了componentUpdateQueue上。这个componentUpdateQueue会在renderWithHooks方法中赋值到fiberNode的updateQueue上。
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
export function renderWithHooks() {
const renderedWork = currentlyRenderingFiber;
renderedWork.updateQueue = componentUpdateQueue;
}
也就是在mount阶段我们所有的effect都以链表的形式被挂载到了fiberNode上。然后在组件渲染完毕之后,React就会执行updateQueue中的所有方法。
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
function updateEffect(create,deps){
return updateEffectImpl(
create,
deps,
);
}
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
// 获取当前Hook节点,并把它添加到Hook链表
const hook = updateWorkInProgressHook();
// 依赖
const nextDeps = deps === undefined ? null : deps;
// 清除函数
let destroy = undefined;
if (currentHook !== null) {
// 拿到前一次渲染该Hook节点的effect
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 对比deps依赖
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果依赖没有变化,就会打上NoHookEffect tag,在commit阶段会跳过此
// effect的执行
pushEffect(NoHookEffect, create, destroy, nextDeps);
return;
}
}
}
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
update阶段和mount阶段类似,只不过这次会考虑effect 的依赖deps,如果此次更新effect的依赖没有变化的话,就会被打上NoHookEffect标签,最后会在commit阶段跳过改effect的执行。
function commitHookEffectList(unmountTag,mountTag,finishedWork) {
const updateQueue = finishedWork.updateQueue;
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount 阶段执行tag !== NoHookEffect的effect的清除函数 (如果有的话)
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount 阶段执行所有tag !== NoHookEffect的effect.create,
// 我们的清除函数(如果有)会被返回给destroy属性,一遍unmount执行
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
useEffect做了什么?
到此为止,useState/useReducer/useEffect源码也阅读完毕了,相信有了这些基础,剩下的Hooks的源码阅读不会成问题,最后放上完整图示:
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/FWLj9e7RuaUpPqMkeVyLvQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。