React-hook源码阅读(中) - 趁热打铁

接力React-hook源码阅读 - useState

我们继续按照useState的套路来看其他hook。 主打一个快速高效

如果没有看过useState的逻辑直接看这一篇的话可能会有点看不懂。 这一张主打砍菜, 作为过渡篇, 把简单常见的hook先过一遍

一、useReducer

上一篇文章我们得知, 我们执行的updateState本质上就是执行了updateReducer的逻辑。 故我们先把useReducer给搞定

useReducer的使用频率相对于useState的使用频率低很多,但是他的能力又要比useState强。 可以说useStateuseReducer的阉割版。

  • 直接定位mountReducer。 可以看到这里的逻辑和useState基本也一致的。但是就是lastRenderedReducer的赋值上体现了差异。
    • 对于useState来说, 赋值的是定义好的basicStateReducer。 该函数就是先判断了action是函数还是值。 是值的话直接就返回了。 是函数的话就传入当前的state。 然后返回该函数的返回值。 他的逻辑是固定的
    • 对于useReducer来说。 赋值的是传入的参数reducerupdateReducer在调用它的时候就传入当前的stateaction。 具体的state变更逻辑是我们可以自定义的, 所以会更灵活一些。

所以他们的应用场景也所区别的。 useState解决组件内状态更新的问题。 useReducer解决组件复杂的状态更新问题

js 复制代码
function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = mountWorkInProgressHook();
  let initialState;
  // 这里是用来解决直接传入函数的话, 组件函数在多次调用的情况下。 函数参数也会被多次调用
  // 故设置了可选参数Init用来解决该问题, 如果有设置的话此时就调用init(initialArg)
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, A> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
     // 这里就是和useState的区别
     // useState的话这里的逻辑赋值的是定义好的
     // useReducer的话这里赋值的是传入的参数reducer
    lastRenderedReducer: reducer, 
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

核心区别讲了, 其他逻辑都差不多的,这里就不赘述了。 转战下一个

二、 useRef

  • 直接看mountRef, 啊? 老铁四句话就给我打发了? 不过也是,mountRef就是用来存数据的,也不提供函数变更, 也不会触发组件重新渲染, 那还需要啥自行车呀。 主要一个你给我啥, 我就给你存啥。 这里的逻辑就是将传入的东西包一层。 放在current属性上 。然后将这个对象挂上hookmemoizedState。 完事
js 复制代码
function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}
  • 再看updateRef。就是拿到对应的hook对象。 然后拿到memoizedState上的值。这就是在mountRef挂上的了。 其实useRef本质就是利用了JS的引用对象。 通过hook链表保存了对应的对象引用, 所以我们怎么去修改current都无所谓。 React拿到了引用就能够获取到current的值。
js 复制代码
function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

三、 useMemo

  • 直接看mountMemo,啊? 你小子也是几句话就给我打发了(看源码的日子是越来越好过了啊(bushi))。逻辑上就是把当前的valuedeps给保存了
js 复制代码
function mountMemo<T>(
  nextCreate: () => T, 
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps; // 这里对传入的deps做了一下转换
  const nextValue = nextCreate(); // 执行传入函数拿到当前值
  hook.memoizedState = [nextValue, nextDeps]; // 挂到hook上
  return nextValue;
}
  • 再看updateMemo。 其实和想的也一样, 就是比较deps, 一样就直接返回保存的值。 不一样的话则再调用一次获取值
js 复制代码
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 如果比较完一致的话,直接返回之前保存的值就OK了
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  // 走到这里就说明, deps发生了变化,故会再调用一次nextCreate获取值
  const nextValue = nextCreate();
  // 也更新在hook上保存的值
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
  • 其中的areHookInputsEqual也可以看看。 还是对两个进行了遍历对比, 只要有一个不一样就返回false。 这里采用的还是is()函数, 浅比较来的(可以说就是Object.is的补丁包)
js 复制代码
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  if (prevDeps === null) {
    return false;
  }
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

四、useCallback

  • 老规矩, 这里和mountMemo如出一辙啊。只不过这里没有调用,而是直接存起来
js 复制代码
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
  • 再看updateCallback, 这里也是采用了areHookInputsEqual。 其他逻辑和updateMemo的都很像, 不赘述了
js 复制代码
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

五、useContext

这个hook又区别于上面其他的hook。 这个hook还不能单纯使用, 需要配合createContext一起。 不急, 我们,挨个解决。 先思考一下有哪些关键步骤需要了解

  • 调用了useContext的逻辑, 具体是干了什么? 获取了哪里的值? 挂载阶段和更新阶段有什么区别吗?
  • createContext创建的context结构是什么样的? 有哪些关键值?
  • <SomeContext.Provider>又做了什么? 怎么让传入的值和context关联起来 ?

先看createContext做了些什么吧。毕竟其他的工作都是围绕着他创建的context对象进行展开的。 可以看到里面有三个重要的属性。 第一个是currrentValue就是用来存放值的, 然后是ProviderConsumer一个作为提供者一个作为消费者都保存了context对象。

再看调用useContext会做什么吧,从 HooksDispatcherOnMount/ HooksDispatcherOnUpdate 可以看到。 无论在mount阶段还是update阶段调用它最终都是调用了readContext。 所以我们先看readContext。 这里一共做了两个事情

  • 拿到context对象的_currentValue, 返回
  • 生成contextItem。挂载到当前Fiberdependencies
js 复制代码
export function readContext<T>(context: ReactContext<T>): T {
  // _currentValue和_currentValue2都有可能拿到值。 这里考虑适配吧
  // 那我们是在哪里地方给他赋值的呢, 请看后面讲解
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  if (lastFullyObservedContext === context) {
    // Nothing to do. We already observe everything in this context.
  } else {
    // 生成一个contextItem对象。 看到next就知道这又是一个链表
    // next存放下一个contextItem对象
    const contextItem = {
      context: ((context: any): ReactContext<mixed>),
      memoizedValue: value,
      next: null,
    };
    // lastContextDependecy就是保持了context链表。
    if (lastContextDependency === null) {
         .....
      lastContextDependency = contextItem;
      // 这里把context对象挂上Fiber的dependencies
      currentlyRenderingFiber.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
      };
      if (enableLazyContextPropagation) {
        currentlyRenderingFiber.flags |= NeedsPropagation;
      }
    } else {
      // Append a new context item.
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  // 返回拿到的值
  return value;
}
  • 这里为什么要挂上Fiber呢。 可以看下源码中以下两个用法。 也就是说你哪个组件用到context了, 当context变化的时候也该组件应该也变化。判断是否改变的就要依赖current Fiber的存在

我们继续进<SomeContext.Provider>看一下。 我们处理Fiber的入口在beginwork, 这里对不同Fibertag进行了Switch Case的处理。 对于<SomeContext.Provider>而言此时tagContextProvider。 故调用了updateContextProvider, 其中又有一句关键的语句pushProvider(workInProgress, context, newValue);,

我们直接看pushProvider。可以看到这里就是一个赋值操作。 这也就解释了为什么我们在<SomeContext.Provider>传值, 然后可以通过useContext拿到。 其实就是通过context对象的_currentValue去对其进行保存

js 复制代码
export function pushProvider<T>(
  providerFiber: Fiber,
  context: ReactContext<T>,
  nextValue: T,
): void {
  if (isPrimaryRenderer) {
    push(valueCursor, context._currentValue, providerFiber);
    context._currentValue = nextValue;
  } else {
    push(valueCursor, context._currentValue2, providerFiber);
    context._currentValue2 = nextValue;
  }
}

所以所有的hook都会通过mountWorkInProgressHook绑定到FibermemoizedState上吗? 答案肯定不是啦。 你看看useContext就不是这样做。

六、useId

描述: useId 是一个 React Hook,可以生成传递给无障碍属性的唯一 ID。 具体使用可以参考 为了生成唯一id,React18专门引入了新Hook:useId。里面描述的也很清楚

  • 我们先看mountId。 这里的主逻辑是分成了两步。 一个是跟服务端渲染相关的, 另一个则是正常的客户端渲染。这里的identifierPrefix则是一开始调用createRoot可以传入的options属性。 为useId的前缀
    • 先看客户端渲染, 就是维护了一个全局的递增的变量globalClientIdCounter。然后和前缀以固定的形态组成最后的id
    • 再看涉及hydrate的, 上面的文章也讲述到了, 由于React Fizz的到来, 渲染顺序可能不一致, 如果采用之前的常规方法可能会导致服务端渲染和客户端渲染拿到的ID不一致. 但是对于服务端和客户端来说,Fiber层级是一致的。这里是通过getTreeId()的方法。 对于同一个组件内使用多个useId的情况又使用了递增数字localIdCounter进行处理。

拿到ID之后再挂上hook和返回即可

js 复制代码
function mountId(): string {
  const hook = mountWorkInProgressHook();
  const root = ((getWorkInProgressRoot(): any): FiberRoot);
  const identifierPrefix = root.identifierPrefix;

  let id;
  if (getIsHydrating()) {
    const treeId = getTreeId();
    id = ':' + identifierPrefix + 'R' + treeId;
    // 在一个组件内多次调用了useId, 那么此时又需要保持一个递增的数字
    const localId = localIdCounter++;
    if (localId > 0) { // 第一个使用的是不会到这一步的, 后面使用的就会加上Hxx
      id += 'H' + localId.toString(32);
    }
    id += ':';
  } else {
    const globalClientId = globalClientIdCounter++;
    id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
  }

  hook.memoizedState = id;
  return id;
}
  • updateId的逻辑就很简单了, 只有初次调用useId的时候才会生成,后面调用的话都是直接将存起来的值取出来
js 复制代码
function updateId(): string {
  const hook = updateWorkInProgressHook();
  const id: string = hook.memoizedState;
  return id;
}

这一篇根据之前useState涉及的逻辑把一些简单的hook通关了, 下一篇讲解跟Effect相关的hook

相关推荐
Asort17 小时前
JavaScript 从零开始(六):控制流语句详解——让代码拥有决策与重复能力
前端·javascript
无双_Joney17 小时前
[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)
前端·后端·nestjs
在云端易逍遥17 小时前
前端必学的 CSS Grid 布局体系
前端·css
ccnocare17 小时前
选择文件夹路径
前端
艾小码17 小时前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月17 小时前
JavaScript作用域与作用域链详解
前端·面试
泉城老铁17 小时前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅17 小时前
JavaScript 作用域常见问题及解决方案
前端·javascript
司宸18 小时前
Prompt结构化输出:从入门到精通的系统指南
前端