React-Hooks源码阅读
前言
最近一直在使用React Hooks的相关API, 当时使用时有这样的一个疑惑:
在函数组件内定义的变量总是会在组件刷新时重新赋值, 但是调用useMemo或者useCallback等Hooks API返回的却不会?
由于拖延症, 一直没有进去一探究竟, 直到今天阿里面试的时候遇到类似问题:
用useMemo或者useCallback等Hooks API初始化的数据保存在哪里?
解答
通过以下源码的阅读和分析, 试着解答下上面的问题
假如我们在组件中先后使用了useState
和useCallback
, 示例:
import React, { useState, useCallback } from './react';
function App() {
const [idx, setIdx] = useState(0);
const clickHandle = useCallback(() => setIdx(prev => prev + 1), []);
return <JSXElement />;
}
此时会生成标记执行顺序的变量hookTypesDev = ['useState', 'useCallback']
- 用useMemo或者useCallback等Hooks API初始化的数据保存在哪里?
react-dom
存在全局变量workInProgress
用于保存执行上下文的数据及状态, 其中hooks数据以链表的数据结构通过next链接, hooks链表头结点绑定在workInProgress.alternate.memoizedState
上
workInProgress.alternate.memoizedState = {
// 对应示例中的useState,
memoizedState: 0, // 节点保存的数据, 即返回的idx
baseState: 0, // 传入的默认参数
baseQueue: null,
queue: {
...,
pending: {},
dispatch: () => null, // 用与控制memoizedState值的函数, 即返回的setIdx方法
},
next: {
// 对应示例中的useCallback
memoizedState: [() => setIdx(prev => prev + 1), []], // 节点保存的数据, 即useCallback传入的参数保存在这里, 并返回第一个参数
baseState: null,
baseQueue: null,
queue: null,
next: {
// 继续下一个hooks
...
},
},
}
- 在函数组件内定义的变量总是会在组件刷新时重新赋值, 但是调用useMemo或者useCallback等Hooks API返回的却不会?
对于这个问题在搞懂hooks链表数据结构和代码上下文后, 可以分析如下:
对于像useState这样的通过dispatch
分发变更的hooks, dispatch
方法用于修改节点的memoizedState
的值, 返回的也是节点的memoizedState
值.
而如果想useCallback这样的, react会缓存传入的参数到节点的memoizedState
属性下, 在每次调用时会判断传入的第二个参数是否变更, 如没变则返回之前缓存的第一个参数, 否则更新节点的memoizedState
属性值, 并返回新值.
源码阅读
准备
配置调试环境应该是一个有经验的开发会想到的第一个步骤.
- 我们直接使用create-react-app脚手架构建项目:
npx create-react-app my-app
- 修改
src/App.js
文件增加Hooks API代码的使用, 如在App.js
中使用useCallback
方法 - 执行
cp node_modules/react/cjs/react.development.js src/react.js
- 执行
cp node_modules/react-dom/cjs/react-dom.development.js src/react-dom.js
- 在文件
src/react.js
和src/react-dom.js
头部添加/* eslint-disable */
, 用以过滤eslint检查 - 修改
src/react-dom.js
、src/App.js
、src/index.js
中引用react
和react-dom
路径为./react.js
和./react-dom
- 通过在
src/App.js
中使用Hooks API并结合debugger
断点方式调试并阅读源码.
必要知识
NodeJS引用模块包时通过模块包的package.json
的main
键确定入口文件, 一般为模块包/index.js
文件
react模块和react-dom模块的关系: react-dom模块封装了渲染及核心功能, react模块相当于超市, react-dom模块给超市(react模块)供货, 我们从超市(react模块)拿取(购买)物资
使用命令console.trace()
查看程序调用栈
源码跟踪
- 跟踪useCallback方法, 发现所有的Hooks API方法都取自
./src/react.js
文件中的函数resolveDispatcher() => ReactSharedInternals.ReactCurrentDispatcher.current = null
, 而整一个ReactSharedInternals对象又通过__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
向外部提供 - 根据必要知识继续跟踪, 我们找到了
./src/react-dom.js
文件, 其中有三个对象赋值了React Hooks, 分别是HooksDispatcherOnUpdateInDEV
、HooksDispatcherOnMountWithHookTypesInDEV
和HooksDispatcherOnMountInDEV
if (current !== null && current.memoizedState !== null) {
// 组件更新时选择HooksDispatcherOnUpdateInDEV
ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
// 组件第一次加载选择HooksDispatcherOnMountInDEV
ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
}
...
// 组件首次加载时执行的hooks
HooksDispatcherOnMountInDEV = {
readContext: (context, observedBits) => {},
useCallback: (callback, deps) => {},
useContext: (context, observedBits) => {},
useEffect: (create, deps) => {},
useImperativeHandle: (ref, create, deps) => {},
useLayoutEffect: (create, deps) => {},
useMemo: (create, deps) => {},
useReducer: (reducer, initialArg, init) => {},
useRef: (initialValue) => {},
useState: (initialState) => {},
useDebugValue: (value, formatterFn) => {},
useResponder: (responder, props) => {},
useDeferredValue: (value, config) => {},
useTransition: (config) => {},
};
// 组件更新时执行的hooks
HooksDispatcherOnUpdateInDEV = {
...HooksDispatcherOnMountInDEV
}
HooksDispatcherOnMountWithHookTypesInDEV = {
...HooksDispatcherOnMountInDEV
};
解读
在大概理解了执行流和数据流后觉得还是应该在前面记录下对workInProgress
的跟踪和理解
首先需要看下renderWithHooks
方法的关键代码, 可以看到, 方法以内运行组件生成渲染内容, 前后都有初始化部分值并选择hooks方法容器
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderExpirationTime) {
// 每次渲染页面都会将workInProgress赋值给currentlyRenderingFiber$1, 所以我们组件里执行的Hooks用到的currentlyRenderingFiber$1其实就是workInProgress的一个引用
currentlyRenderingFiber$1 = workInProgress;
...
hookTypesDev = current !== null ? current._debugHookTypes : null;
hookTypesUpdateIndexDev = -1; // Used for hot reloading:
...
workInProgress.memoizedState = null;
...
// 这个判断语句用于选择要执行的hooks对象
if (current !== null && current.memoizedState !== null) {
// 组件初次更新时选择HooksDispatcherOnUpdateInDEV
ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
// 组件初次加载时选择HooksDispatcherOnMountInDEV
ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
}
// 这一行需要知道, Component就是我们自己写的组件, 用于执行组件并返回渲染内容
var children = Component(props, secondArg);
...
// 注意函数开头将workInProgress赋值给currentlyRenderingFiber$1
currentlyRenderingFiber$1 = null;
currentHook = null;
workInProgressHook = null;
currentHookNameInDev = null;
hookTypesDev = null;
hookTypesUpdateIndexDev = -1;
...
return children;
}
这里就解释了在之后跟踪时初始化hook链式结构头节点放在currentlyRenderingFiber$1.memoizedState
, 但组件更新时头节点却从workInProgress.alternate.memoizedState
这里拿的问题
我们继续看下生成workInProgress
的关键代码:
function FiberNode(tag, pendingProps, key, mode) {
// FiberNode类
...
this.memoizedState = null;
...
this.alternate = null;
...
}
var createFiber = function (tag, pendingProps, key, mode) {
// 用于实例化FiberNode类
return new FiberNode(tag, pendingProps, key, mode);
};
function createWorkInProgress(current, pendingProps) {
// 产生workInProgress并返回
var workInProgress = current.alternate;
if (workInProgress === null) {
// current中workInProgress不可用时, 则创建和初始化workInProgress
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
...
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// 存在workInProgress则对workInProgress属性值做特殊处理
...
}
...
// 对workInProgress初始化赋值
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
...
return workInProgress;
}
function prepareFreshStack(root, expirationTime) {
...
// 生成workInProgress
workInProgress = createWorkInProgress(root.current, null);
...
}
通用变量及函数
变量
// 用于标记当前执行的hooks名, 如: useCallback或useStatus等
let currentHookNameInDev: string | null = null;
// 维护一个队列, 程序中所有使用的hooks都注册在这里(push进去)
let hookTypesDev: string[] | null = null;
// 组件运行中每使用一次Hooks Api则hookTypesUpdateIndexDev自增一
// 存在关系 hookTypesDev[hookTypesUpdateIndexDev] === currentHookNameInDev
let hookTypesUpdateIndexDev: number = -1;
// hook的链式数据结构
type Hook = {
memoizedState: any,
baseState: any,
baseQueue: any,
queue: any,
next: Hook,
}
// workInProgressHook是指向当前hook的指针
let workInProgressHook: Hook | null = null;
// 在组件更新时用于保存当前执行的hooks, 初始值为null
let currentHook: Hook | null = null;
函数
mountHookTypesDev
方法
mountHookTypesDev
用于往hookTypesDev
中注册hooks, 及hookTypesDev = [...hookTypesDev, currentHookNameInDev]
function mountHookTypesDev() {
var hookName = currentHookNameInDev;
if (hookTypesDev === null) {
hookTypesDev = [hookName];
} else {
hookTypesDev.push(hookName);
}
}
updateHookTypesDev
方法
updateHookTypesDev
用于检查对应关系hookTypesDev[++hookTypesUpdateIndexDev] === currentHookNameInDev
是否成立
function updateHookTypesDev() {
var hookName = currentHookNameInDev;
if (hookTypesDev !== null) {
hookTypesUpdateIndexDev++;
if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) {
warnOnHookMismatchInDev(hookName);
}
}
}
checkDepsAreArrayDev
方法
checkDepsAreArrayDev
用于检查hooks第二个参数是否为数组, 非数组就报错
function checkDepsAreArrayDev(deps) {
if (deps !== undefined && deps !== null && !Array.isArray(deps)) {
error('%s received a final argument that is not an array (instead, received `%s`). When ' + 'specified, the final argument must be an array.', currentHookNameInDev, typeof deps);
}
}
mountWorkInProgressHook
方法
mountWorkInProgressHook
用于创建hook
hooks存储数据用的链式数据结构, currentlyRenderingFiber$1.memoizedState
是hooks链式结构的第一个节点的引用.
变量workInProgressHook
用于指向最新执行hooks所在的节点, 新hook会赋值给旧hook的next属性, 下方是程序代码, 及注释
function mountWorkInProgressHook() {
// 新建hook节点
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
// 当当前hook(workInProgressHook)为null时表示当前新建的hook节点为起始节点, 即程序内运行的第一个hooks
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
// 否则除了第一个运行的hooks, 其它的都接入最新hook节点引用(workInProgressHook)的next属性下
workInProgressHook = workInProgressHook.next = hook;
}
// 返回最后一个(最新的)hook节点
return workInProgressHook;
}
updateWorkInProgressHook
方法
updateWorkInProgressHook
用于更新hook, 并返回当前hook
需要注意的是在方法mountWorkInProgressHook
中, hook链式结构是存储在currentlyRenderingFiber$1.memoizedState
中的, 但在更新时, 执行第一个hooks时发现链式结构从currentlyRenderingFiber$1.alternate.memoizedState
中取
function updateWorkInProgressHook() {
var nextCurrentHook;
if (currentHook === null) {
// 执行第一个hooks时执行
var current = currentlyRenderingFiber$1.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 非第一个hooks时直接从链式结构上取
nextCurrentHook = currentHook.next;
}
var nextWorkInProgressHook;
if (workInProgressHook === null) {
// 执行第一个hooks时执行
nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
} else {
// 非第一个hooks时直接从链式结构上取
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// 更新时执行
// 这段代码和mountWorkInProgressHook方法中的相似, 用于生成新的hook链式结构并挂载在currentlyRenderingFiber$1.memoizedState(头节点)上
if (!(nextCurrentHook !== null)) {
throw Error( "Rendered more hooks than during the previous render." );
}
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null
};
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
部分hooks源代码
基于上面的通用变量和函数我们可以看下部分hooks的源代码
hooks使用入口常常看到这种结构:
hooksName: function () {
...
var prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOn...InDEV;
try {
return otherHundles()
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
其实就是使用闭包加模块的方式构建的程序, 程序公用一个全局数据, 当hooks操作时就将Invalid的赋值给hooks, 避免多程序同时使用全局数据.
useState源码摘要
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
function updateReducer(reducer, initialArg, init) {
...
}
function updateState(initialState) {
return updateReducer(basicStateReducer);
}
HooksDispatcherOnMountInDEV = {
...,
useState: function (initialState) {
currentHookNameInDev = 'useState';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
}
HooksDispatcherOnUpdateInDEV = {
...,
useState: function (initialState) {
currentHookNameInDev = 'useState';
updateHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
}
useCallback源码摘要
function mountCallback(callback, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
function updateCallback(callback, deps) {
var hook = updateWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var prevState = hook.memoizedState;
if (prevState !== null && nextDeps !== null) {
var prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
HooksDispatcherOnMountInDEV = {
...,
useCallback: function (callback, deps) {
currentHookNameInDev = 'useCallback';
mountHookTypesDev();
checkDepsAreArrayDev(deps);
return mountCallback(callback, deps);
}
}
HooksDispatcherOnUpdateInDEV = {
...,
useCallback: function (callback, deps) {
currentHookNameInDev = 'useCallback';
updateHookTypesDev();
return updateCallback(callback, deps);
}
}