React Hooks 源码解析
自我期望:能引起团队成员的共鸣,让团队成员有所收获,加强自身对 react 源码的理解。
大纲
- React Hooks 出现的背景
- 如何使用及使用过程中需要注意什么 ? (简单过一下)
- React Fiber 的结构 (稍微介绍一下)
- Hooks 是如何工作的 ? (详细讲解)
- 实现自己的 hooks (没这个必要,梳理好流程自然知道怎么实现了)
- 附录
- React 源码调试 版本号为 v17.0.3
React Hooks 出现的背景
- 解决在组件间复用状态逻辑困难的问题,可以让我们在无需改动组件结构的情况下复用状态逻辑。
- 解决复杂组件变得难以理解的问题,将组件中互相关联的部分拆分成更小的函数,还可以通过 reducer 来管理组件的内部状态,使其更加可预测。
- class 中 this 指向难以理解,Hook 让我们在非 class 的情况下可以使用更多的 react 特性。(函数式编程)
Hooks 的使用
useState
import React, { useState } from 'react';function Example() {// 声明一个叫 "count" 的 state 变量const [count, setCount] = useState(0);return (<div><p>You clicked {count} times</p><button onClick={() => setCount(count + 1)}>Click me</button></div>);}/*** import {defineComponent, ref} from "vue"* export default defineComponent({* setup () {* const count = ref(0)* return () => {* return (* <div>* <p>You clicked {count.value} times</p>* <button onClick={() => count.value++}>* Click me* </button>* </div>* )* }* }* })* /
上述代码等价于:
class Example extends React.Component {constructor(props) {super(props);this.state = {count: 0};}render() {return (<div><p>You clicked {this.state.count} times</p><button onClick={() => this.setState({ count: this.state.count + 1 })}>Click me</button></div>);}}
useEffect
import React, { useState, useEffect } from 'react';function FriendStatus(props) {const [isOnline, setIsOnline] = useState(null);useEffect(() => {function handleStatusChange(status) {setIsOnline(status.isOnline);}ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);// Specify how to clean up after this effect:return function cleanup() {ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);};});if (isOnline === null) {return 'Loading...';}return isOnline ? 'Online' : 'Offline';}
等价于
class FriendStatus extends React.Component {constructor(props) {super(props);this.state = { isOnline: null };this.handleStatusChange = this.handleStatusChange.bind(this);}// 等价于 vue 中的 mounted 生命周期componentDidMount() {ChatAPI.subscribeToFriendStatus(this.props.friend.id,this.handleStatusChange);}// 等价于 vue 中的 unmounted 也就是 destroycomponentWillUnmount() {ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id,this.handleStatusChange);}handleStatusChange(status) {this.setState({isOnline: status.isOnline});}render() {if (this.state.isOnline === null) {return 'Loading...';}return this.state.isOnline ? 'Online' : 'Offline';}}
useContext
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。// 为当前的 theme 创建一个 context(“light”为默认值)。const ThemeContext = React.createContext('light');class App extends React.Component {render() {// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。// 无论多深,任何组件都能读取这个值。// 在这个例子中,我们将 “dark” 作为当前的值传递下去。return (<ThemeContext.Provider value="dark"><Toolbar /></ThemeContext.Provider>);}}// 中间的组件再也不必指明往下传递 theme 了。function Toolbar() {return (<div><ThemedButton /></div>);}class ThemedButton extends React.Component {// 指定 contextType 读取当前的 theme context。// React 会往上找到最近的 theme Provider,然后使用它的值。// 在这个例子中,当前的 theme 值为 “dark”。static contextType = ThemeContext;render() {return <Button theme={this.context} />;}}
useReducer
const initialState = {count: 0};function reducer(state, action) {switch (action.type) {case 'increment':return {count: state.count + 1};case 'decrement':return {count: state.count - 1};default:throw new Error();}}function Counter() {const [state, dispatch] = useReducer(reducer, initialState);return (<>Count: {state.count}<button onClick={() => dispatch({type: 'decrement'})}>-</button><button onClick={() => dispatch({type: 'increment'})}>+</button></>);}
useRef
useRef() 可以方便地保存任何可变值,当 ref 对象发生改变时,useRef 并不会通知我们,变更
.current属性不会引发组件重新渲染。如果想要
function TextInputWithFocusButton() {const inputEl = useRef(null);const onButtonClick = () => {// `current` 指向已挂载到 DOM 上的文本输入元素inputEl.current.focus();};return (<><input ref={inputEl} type="text" /><button onClick={onButtonClick}>Focus the input</button></>);}
……
Hooks 使用的注意事项
- 只能在最顶层使用 Hook,不要在循环,条件或者嵌套函数中调用 Hook
- 只在 React 函数中调用 Hook,不要在普通的 Javascript 函数中调用 Hook
React Fiber
为什么要讲 React Fiber,因为 React 源码的实现都围绕 Fiber 的数据结构展开,了解 Fiber 的结构有助于我们理解 Hook 源码。
Fiber 分为 currentRenderingFiber 及 workInProgressFiber,顾名思义 currentRenderingFiber 就是当前在屏幕中显示的内容对应的 Fiber 树就是 currentRenderingFiber。 workInProgressFiber 是当内容更新时,React 在内存中重新构建的一棵新的 Fiber 树,当其更新完成之后,React 会使用它直接替换 currentRenderingFiber 树达到快速更新 DOM 的目的。
React Fiber 的数据结构
// 相对于 Fiber 还是缺少 nextEffect、firstEffect、lastEffect 等属性的function FiberNode(tag: WorkTag,pendingProps: mixed,key: null | string,mode: TypeOfMode,) {// Instance// 节点类型标记 大概有 24 种 FunctionComponent || classComponent || IndeterminateComponent || HostRoot || ……this.tag = tag;// 节点索引信息this.key = key;// 节点类型this.elementType = null;// 节点类型this.type = null;// dom 节点对象,组件实例this.stateNode = null;// Fiber// 父节点的 Fiber 信息this.return = null;// 子节点的 Fiber 信息this.child = null;// 兄弟节点的 Fiber 信息this.sibling = null;// 当前位置,相对于兄弟节点this.index = 0;// 存放当前 ref 的信息this.ref = null;// 构建中的 props 属性this.pendingProps = pendingProps;// 存放的 props 属性this.memoizedProps = null;// 更新的队列信息this.updateQueue = null;// 当前的节点信息this.memoizedState = null;// 记录当前节点的上下文事件依赖关系this.dependencies = null;// 标记节点类型this.mode = mode;// Effects// 用于表示 fiber 节点的创建时间this.flags = NoFlags;// 同上this.subtreeFlags = NoFlags;// 缺失的 Fiber 数组?this.deletions = null;// 这个没记错的话应该是任务执行的优先级this.lanes = NoLanes;this.childLanes = NoLanes;// Fiber 备份的数据,用于新旧节点比对的时候使用this.alternate = null;if (enableProfilerTimer) {...}if (__DEV__) {...}}
关于 lanes 相关的定义,感兴趣的小伙伴,可以自行了解 /react-debug/src/react/packages/react-reconciler/src/ReactFiberLane.new.js
Hook 如何挂载到 Fiber 上
useState 的执行流程 useState -> mountState -> mountWorkInProgressHook
// /react-debug/src/react/packages/react-reconciler/ReactFiberHooks.old.jsHooksDispatcherOnMountInDEV = {...,useState<S>(initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] {currentHookNameInDev = 'useState';mountHookTypesDev();const prevDispatcher = ReactCurrentDispatcher.current;ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;try {return mountState(initialState);} finally {ReactCurrentDispatcher.current = prevDispatcher;}},...}// 初始化 state 的值function mountState<S>(initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] {// 这里进行初始化const hook = mountWorkInProgressHook();if (typeof initialState === 'function') {// $FlowFixMe: Flow doesn't like mixed typesinitialState = initialState();}// 重点注意一下这里,将 hook 相关的值存放到 fiber 的 memoizedState 属性hook.memoizedState = hook.baseState = initialState;const queue: UpdateQueue<S, BasicStateAction<S>> = {pending: null,interleaved: null,lanes: NoLanes,dispatch: null,lastRenderedReducer: basicStateReducer,lastRenderedState: (initialState: any),};hook.queue = queue;const dispatch: Dispatch<BasicStateAction<S>,> = (queue.dispatch = (dispatchSetState.bind(null,currentlyRenderingFiber,queue,): any));return [hook.memoizedState, dispatch];}
useEffect 的执行流程 useEffect -> mountEffect -> mountEffectImpl -> mountWorkInProgressHook
HooksDispatcherOnMountInDEV = {...,useEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,): void {currentHookNameInDev = 'useEffect';mountHookTypesDev();checkDepsAreArrayDev(deps);return mountEffect(create, deps);},...}function mountEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,): void {if (__DEV__ &&enableStrictEffects &&(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode) {return mountEffectImpl(MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,HookPassive,create,deps,);} else {return mountEffectImpl(PassiveEffect | PassiveStaticEffect,HookPassive,create,deps,);}}function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {const hook = mountWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;currentlyRenderingFiber.flags |= fiberFlags;// 看一下这里,将 useEffect 相关的副作用存放到 fiber 的 memoizedState 属性上hook.memoizedState = pushEffect(HookHasEffect | hookFlags,create,undefined,nextDeps,);}
useContext -> readContext
- useRef -> mountRef -> mountWorkInProgressHook
- useReducer -> mountReducer -> mountWorkInProgressHook
- useMemo -> mountMemo -> mountWorkInProgressHook
- useCallback -> mountCallback -> mountWorkInProgressHook
- ……
接下来,看一下 Hook 挂载到 Fiber 上的核心函数:
function mountWorkInProgressHook(): Hook {const hook: Hook = {memoizedState: null,baseState: null,baseQueue: null,queue: null,next: null,};if (workInProgressHook === null) {// This is the first hook in the listcurrentlyRenderingFiber.memoizedState = workInProgressHook = hook;} else {// Append to the end of the listworkInProgressHook = workInProgressHook.next = hook;}return workInProgressHook;}
再来看一段 debug 用的代码
// /react-debug/src/index.jsconst container = document.getElementById('root');const root = ReactDOM.createRoot(container);root.render(<App />);// /react-debug/src/app.jsfunction App() {const [count, setCount] = useState(1)const appRef = useRef()useEffect(() => {console.log("init")}, [])const [username, setUsername] = useState("jq")return (<div className="App" key="app" ref={appRef}><p>this count value is: {count}</p><button onClick={() => setCount(count+1)}>+1</button></div>);}
接下来看看它执行之后的 fiber

通过以上信息可以了解到 Hook 是挂载到 Fiber 上的,这也就说明了,为什么不能在普通的 javascript 函数中调用 Hook,因为普通的 javascript 函数中并不存在 Fiber 信息。
另外通过 Fiber.memoizedState 的链式结构,我们也可以知道 Hook 要写在顶层,是避免指针异常,导致不可预估的错误。
Hooks 更新中的内容发生变化后
state 更新过程
- 执行 requestUpdateLane 查找 fiber 更新的优先级
- 判断是否为渲染阶段的更新
- 渲染阶段,执行 enqueueRenderPhaseUpdate 函数更新当前的队列信息
- 非渲染阶段,执行 enqueueUpdate 函数 -> 判断是否交叉执行
- 执行 requestEventTime 方法 获取当前事件的时间
- 执行 scheduleUpdateOnFiber 方法 更新 fiber 的优先级,回调事件,时间的执行时间等。
- scheduleUpdateOnFiber
- ensureRootIsScheduled
- cancelCallback
- scheduleLegacySyncCallback
- scheduleSyncCallback
- scheduleMicrotask
- scheduleCallback
- Scheduler
- unstable_scheduleCallback
- unstable_cancelCallback
- unstable_shouldYield
- unstable_requestPaint
- ……
- ……
function dispatchSetState<S, A>(fiber: Fiber,queue: UpdateQueue<S, A>,action: A,) {...const lane = requestUpdateLane(fiber);const update: Update<S, A> = {lane,action,hasEagerState: false,eagerState: null,next: (null: any),};if (isRenderPhaseUpdate(fiber)) {enqueueRenderPhaseUpdate(queue, update);} else {enqueueUpdate(fiber, queue, update, lane);const alternate = fiber.alternate;if (fiber.lanes === NoLanes &&(alternate === null || alternate.lanes === NoLanes)) {const lastRenderedReducer = queue.lastRenderedReducer;if (lastRenderedReducer !== null) {let prevDispatcher;if (__DEV__) {prevDispatcher = ReactCurrentDispatcher.current;ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;}try {const currentState: S = (queue.lastRenderedState: any);const eagerState = lastRenderedReducer(currentState, action);// lastRenderedReducer = basicStateReducer 在此处更新 相应的 fiber 节点信息// function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {// // $FlowFixMe: Flow doesn't like mixed types// return typeof action === 'function' ? action(state) : action;// }update.hasEagerState = true;update.eagerState = eagerState;if (is(eagerState, currentState)) {return;}} catch (error) {} finally {if (__DEV__) {ReactCurrentDispatcher.current = prevDispatcher;}}}}const eventTime = requestEventTime();const root = scheduleUpdateOnFiber(fiber, lane, eventTime);if (root !== null) {entangleTransitionUpdate(root, queue, lane);}}markUpdateInDevTools(fiber, lane, action);}
渲染阶段更新
function enqueueRenderPhaseUpdate<S, A>(queue: UpdateQueue<S, A>,update: Update<S, A>,) {// This is a render phase update. Stash it in a lazily-created map of// queue -> linked list of updates. After this render pass, we'll restart// and apply the stashed updates on top of the work-in-progress hook.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;}
非渲染阶段更新
function enqueueUpdate<S, A>(fiber: Fiber,queue: UpdateQueue<S, A>,update: Update<S, A>,lane: Lane,) {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 transferred 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;}}
React 源码调试(v17.0.3)
- 创建react项目
npx create-react-app react-debug - 切换到项目目录下,弹射出 webpack 的配置文件
npm run eject - 将 react 的项目克隆到
/react-debug/src目录下git clone https://github.com/facebook/react.git网络不好的可以通过镜像克隆git clone https://github.com.cnpmjs.org/facebook/react.git - 修改配置文件
这里理论上报找不到什么模块,我们就按照提示添加对应的模块即可
// /react-debug/config/webpack.config.jsresolve: {alias: {"react-native": "react-native-web","react": path.resolve(__dirname, "../src/react/packages/react"),"react-dom": path.resolve(__dirname, "../src/react/packages/react-dom"),"shared": path.resolve(__dirname, "../src/react/packages/shared"),"react-reconciler": path.resolve(__dirname, "../src/react/packages/react-reconciler"),"react-devtools-timeline": path.resolve(__dirname, "../src/react/packages/react-devtools-timeline"),"react-devtools-shared": path.resolve(__dirname, "../src/react/packages/react-devtools-shared"),}}
- 修改项目的环境变量
// /react-debug/config/env.jsconst stringified = {"process.env": Object.keys(raw).reduce((env, key) => {env[key] = JSON.stringify(raw[key])return env}, {}),__DEV__: true,SharedArrayBuffer: true,spyOnDev: true,spyOnDevAndProd: true,spyOnProd: true,__PROFILE__: true,__UMD__: true,__EXPERIMENTAL__: true,__VARIANT__: true,gate: true,trustedTypes: true}
- 处理一下类型检测报错的问题,简单粗暴一点,整个删了
// webpack.config.js{...,plugins: [...,- !disableESLintPlugin &&- new ESLintPlugin({- // Plugin options- extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],- formatter: require.resolve('react-dev-utils/eslintFormatter'),- eslintPath: require.resolve('eslint'),- failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),- context: paths.appSrc,- cache: true,- cacheLocation: path.resolve(- paths.appNodeModules,- '.cache/.eslintcache'- ),- // ESLint class options- cwd: paths.appPath,- resolvePluginsRelativeTo: __dirname,- baseConfig: {- extends: [require.resolve('eslint-config-react-app/base')],- rules: {- ...(!hasJsxRuntime && {- 'react/react-in-jsx-scope': 'error',- }),- },- },- }),- ],...}
- 导出 HostConfig
// /react-debug/src/react/packages/react-reconciler/src/ReactFiberHostConfig.js+ export * from './forks/ReactFiberHostConfig.dom';- throw new Error('This module must be shimmed by a specific renderer.');
- 修改 ReactSharedInternals.js 文件
```javascript // /react-debug/src/react/packages/shared/ReactSharedInternals.js
- const ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
- import ReactSharedInternals from ‘../react/src/ReactSharedInternals’; ```
关闭 eslint 扩展
// /react-debug/src/react/.eslingrc.js// 删除 extends// extends: [// 'fbjs',// 'prettier'//]
禁止 invariant 报错
// /react-debug/src/react/packages/shared/invariant.jsexport default function invariant(condition, format, a, b, c, d, e, f) {if (condition) return;throw new Error('Internal React error: invariant() is meant to be replaced at compile ' +'time. There is no runtime version.',);}
修改 react react-dom 引入方式
```javascript import as React from “react” import as ReactDOM from “react-dom” import ‘./index.css’; import App from ‘./App’; import reportWebVitals from ‘./reportWebVitals’;
- const container = document.getElementById(‘root’);
// Create a root.
const root = ReactDOM.createRoot(container);
root.render(
);
- ReactDOM.render(
- ,
- document.getElementById(‘root’)
- ); ```
