目录:
  1. 前言
    1. 源码阅读
      1. 准备
        1. 必要知识
          1. 源码跟踪
            1. 解读
              1. 通用变量及函数
              2. 部分hooks源代码

          React-Hooks源码阅读

          阅读时间:全文 2746 字,预估用时 14 分钟
          创作日期:2020-05-10
          文章标签:
           
          BEGIN

          前言

          最近一直在使用React Hooks的相关API, 当时使用时有这样的一个疑惑:

          在函数组件内定义的变量总是会在组件刷新时重新赋值, 但是调用useMemo或者useCallback等Hooks API返回的却不会?

          由于拖延症, 一直没有进去一探究竟, 直到今天阿里面试的时候遇到类似问题:

          用useMemo或者useCallback等Hooks API初始化的数据保存在哪里?

          解答

          通过以下源码的阅读和分析, 试着解答下上面的问题

          假如我们在组件中先后使用了useStateuseCallback, 示例:

          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']

          1. 用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
                ...
              },
            },
          }
          1. 在函数组件内定义的变量总是会在组件刷新时重新赋值, 但是调用useMemo或者useCallback等Hooks API返回的却不会?

          对于这个问题在搞懂hooks链表数据结构和代码上下文后, 可以分析如下:

          对于像useState这样的通过dispatch分发变更的hooks, dispatch方法用于修改节点的memoizedState的值, 返回的也是节点的memoizedState值.

          而如果想useCallback这样的, react会缓存传入的参数到节点的memoizedState属性下, 在每次调用时会判断传入的第二个参数是否变更, 如没变则返回之前缓存的第一个参数, 否则更新节点的memoizedState属性值, 并返回新值.

          源码阅读

          准备

          配置调试环境应该是一个有经验的开发会想到的第一个步骤.

          1. 我们直接使用create-react-app脚手架构建项目: npx create-react-app my-app
          2. 修改src/App.js文件增加Hooks API代码的使用, 如在App.js中使用useCallback方法
          3. 执行cp node_modules/react/cjs/react.development.js src/react.js
          4. 执行cp node_modules/react-dom/cjs/react-dom.development.js src/react-dom.js
          5. 在文件src/react.jssrc/react-dom.js头部添加/* eslint-disable */, 用以过滤eslint检查
          6. 修改src/react-dom.jssrc/App.jssrc/index.js中引用reactreact-dom路径为./react.js./react-dom
          7. 通过在src/App.js中使用Hooks API并结合debugger断点方式调试并阅读源码.

          必要知识

          NodeJS引用模块包时通过模块包的package.jsonmain键确定入口文件, 一般为模块包/index.js文件

          react模块和react-dom模块的关系: react-dom模块封装了渲染及核心功能, react模块相当于超市, react-dom模块给超市(react模块)供货, 我们从超市(react模块)拿取(购买)物资

          使用命令console.trace()查看程序调用栈

          源码跟踪

          1. 跟踪useCallback方法, 发现所有的Hooks API方法都取自./src/react.js文件中的函数resolveDispatcher() => ReactSharedInternals.ReactCurrentDispatcher.current = null, 而整一个ReactSharedInternals对象又通过__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED向外部提供
          2. 根据必要知识继续跟踪, 我们找到了./src/react-dom.js文件, 其中有三个对象赋值了React Hooks, 分别是HooksDispatcherOnUpdateInDEVHooksDispatcherOnMountWithHookTypesInDEVHooksDispatcherOnMountInDEV
          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);
            }
          }
          FINISH

          随机文章
          人生倒计时
          default