从react源码看hooks的原理

2022-10-18 10:00:16 浏览数 (1)

React暴露出来的部分Hooks

代码语言:javascript复制
//packages/react/src/React.js
export {
  ...
  useCallback,
  useContext,
  useEffect,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
  ...
}

功能描述

  • useStateuseReducer: 状态值相关
  • useEffectuseLayoutEffect: 生命周期相关
  • useContext: 状态共享相关
  • useCallbackuseMemo: 性能优化相关
  • useRef: 属性相关

源码

代码语言:javascript复制
export function useContext<T>(
  Context: ReactContext<T>,
  unstable_observedBits: number | boolean | void,
): T {
  const dispatcher = resolveDispatcher();
  ...
  return dispatcher.useContext(Context, unstable_observedBits);
}

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

export function useReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}

export function useRef<T>(initialValue: T): {|current: T|} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}

export function useEffect(
  create: () => (() => void) | void,  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}

export function useLayoutEffect(
  create: () => (() => void) | void,  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useLayoutEffect(create, deps);
}

export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

export function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}


// resolveDispatcher
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  invariant(
   ...
  );
  return dispatcher;
}

// ReactCurrentDispatcher
const ReactCurrentDispatcher = {
  /**   * @internal
   * @type {ReactComponent}   */
  current: (null: null | Dispatcher),
};

其实hooks的定义都来自dispatcher,那我们根据Dispatcher依次去看看他们的实际实现。

Dispatcher

代码语言:javascript复制
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>],
  useContext<T>(
    context: ReactContext<T>,
    observedBits: void | number | boolean,
  ): T,
  useRef<T>(initialValue: T): {|current: T|},
  useEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  useLayoutEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  useCallback<T>(callback: T, deps: Array<mixed> | void | null): T,
  useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T,
  ...
|};

很明显这就是各种hooks的定义,但是总归都要是参加到执行的的流程里面去的,函数组件也属于ReactComponent的一种,他也有mountupdate阶段。

函数组件Mount阶段

我们在前面提到执行beginWork函数中,我们发现会有tagFunctionComponent的选项,他会调用updateFunctionComponent进而调用renderWithHooks进行更新。

代码语言:javascript复制
// packages/react-reconciler/src/ReactFiberBeginWork.old.js
function beginWork(
  current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes,
): Fiber | null {
  const updateLanes = workInProgress.lanes;
  switch (workInProgress.tag) {
    case FunctionComponent: {
      const Component = workInProgress.type; // 组件类型
      const unresolvedProps = workInProgress.pendingProps; // 未处理的props
      ...
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
          // 渲染classComponent
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot:
      ...
    case HostComponent:
      ...
    case HostText:
      ...
    }
}

renderWithHooks

代码语言:javascript复制
export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  // 下一个渲染任务的优先级
  renderLanes = nextRenderLanes;
  // fiber树
  currentlyRenderingFiber = workInProgress;

  ...
  // 重置memoizedState、updateQueue、lanes
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  ...

  if (__DEV__) {
    ...
  } else {
    // 判断mount update阶段
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }
}
  • 如果是mount阶段,则执行HooksDispatcherOnMount,之后再执行reconcileChildren
  • 如果是update阶段,则执行HooksDispatcherOnUpdate,之后再执行reconcileChildren
  • 相关参考视频讲解:进入学习

HooksDispatcherOnMount

代码语言:javascript复制
const HooksDispatcherOnMount: Dispatcher = {
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  ...
};

mount的情况下,每个hook是都有自己对应的mountXxxuseState的对应的就是mountState,不过在讲mountState之前我们要去看一个东西 -- type hook,因为实际的开发当中不可能只用一个hook,多个hook他就会形成链表,保存在fiber的memoizedState上面。

type Hook

代码语言:javascript复制
export type Hook = {|
  memoizedState: any, // 单独的hook唯一值
  baseState: any, // 初始state
  baseQueue: Update<any, any> | null, // 初始更新队列
  queue: UpdateQueue<any, any> | null, // 更新队列
  next: Hook | null, // hooks链表下一个指针
|};

那么我们去看看他是如何关联起来的:const hook = mountWorkInProgressHook()

代码语言:javascript复制
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 list
    // 第一个hook
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    // 往后面添加,形成链表
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

mountState

代码语言:javascript复制
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {

  // 创建并关联hook链表  
  const hook = mountWorkInProgressHook();
  // 如果是函数入参,则是把函数的执行结果当做入参
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  // 把hook的memoizedState变为初始值
  hook.memoizedState = hook.baseState = initialState;

  // 更新队列
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  // 回调
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  // 返回 memoizedState, dispatch
  return [hook.memoizedState, dispatch];
}

根据mountState我们就知道在写useState的时候,解构的语法为什么会解构出想要的东西了。

const mutation, setMutation = useState(0) // 纯数字作为初始值const mutation, setMutation = useState(()=>handle(1)) // 函数作为初始值,函数的返回值作为初始值

这里我们遗留了一个问题

  • 我们知道第二个参数dispatch执行的时候会触发渲染更新,以及二次更新,那么他是怎么实现的呢?

针对于上述问题,我们必须去看一下dispatch到底干了什么事情。

代码语言:javascript复制
const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));

dispatchAction

代码语言:javascript复制
function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  if (__DEV__) {
    ...
  }
  // 获取触发更新的时间
  const eventTime = requestEventTime();
  // 获取更新优先级
  const lane = requestUpdateLane(fiber);

  // 创建更新
  const update: Update<S, A> = {
    lane,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };

  // 维护链表,在update阶段,如果有更新任务则一直延续
  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;

  const alternate = fiber.alternate;

  // 如果是render阶段更新
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    ...
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;

  } else {
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {

      // 当前没有更新,是第一次调用dispatchAction,计算state
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        ...
        try {
          const currentState: S = (queue.lastRenderedState: any);
          // render阶段,如果reducer没有变化,直接取值eagerState
          const eagerState = lastRenderedReducer(currentState, action);

          // render阶段,如果reducer没有变化,直接取值eagerState
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // 如果当前的值遇前面的值相同直接返回上一个值,不发起更新
            return;
          }
        } catch (error) {
          ...
        } finally {
          ...
        }
      }
    }
    ...

    //更新
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
}

这里我们可以看到usetate的回调,就是创建更新,维护了一个链表结构,在render阶段我们根据eagerState与这次的currentState比较,决定要不要发起更新,执行scheduleUpdateOnFiber进行更新

函数组件Update阶段

代码语言:javascript复制
const HooksDispatcherOnUpdate: Dispatcher = {
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  ...
};

update阶段中的useState调用了updateState,而在updateState中再次调用updateReducer

代码语言:javascript复制
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  //   
  return updateReducer(basicStateReducer, (initialState: any));
}

basicStateReducer中进行了如下处理:

代码语言:javascript复制
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  // $FlowFixMe: Flow doesn't like mixed types
  // 如果更新为函数则是函数执行返回值为更新值
  return typeof action === 'function' ? action(state) : action;
}

这里也就是说明了我们在写hook的时候:

setMutation(mutation 1) // 直接赋值setMutation(()=>handle(1)) // 函数值返回值作为新值赋值

useReducer

代码语言:javascript复制
function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {

  // 获取更新的hook
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  invariant(
    queue !== null,
    'Should have a queue. This is likely a bug in React. Please file an issue.',
  );

  // 更新
  queue.lastRenderedReducer = reducer;

  const current: Hook = (currentHook: any);

  // The last rebase update that is NOT part of the base state.
  let baseQueue = current.baseQueue;

  // 如果最后一个更新还没有完毕,则继续更新
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {

    // 如果在更新的过程中有新的更新,加入更新队列
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    if (__DEV__) {
      if (current.baseQueue !== baseQueue) {
        // Internal invariant that should never happen, but feasibly could in
        // the future if we implement resuming, or some form of that.
        console.error(
          'Internal error: Expected work-in-progress queue to be a clone. '  
            'This is a bug in React.',
        );
      }
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  if (baseQueue !== null) {
    // We have a queue to process.
    const first = baseQueue.next;
    let newState = current.baseState;

    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;

    do {
      //获取每一个任务的更新优先级
      const updateLane = update.lane;

      // 如果当前的执行任务优先级不高,则跳过当前任务,去执行优先级高的任务
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // Priority is insufficient. Skip this update. If this is the first
        // skipped update, the previous update/state is the new base
        // update/state.

        // 克隆更新任务
        const clone: Update<S, A> = {
          lane: updateLane,
          action: update.action,
          eagerReducer: update.eagerReducer,
          eagerState: update.eagerState,
          next: (null: any),
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }

        // 如果都是低级任务,则合并优先级
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        markSkippedUpdateLanes(updateLane);
      } else {
        // This update does have sufficient priority.

        if (newBaseQueueLast !== null) {
          // 如果更新队列还有更新任务
          const clone: Update<S, A> = {
            // This update is going to be committed so we never want uncommit
            // it. Using NoLane works because 0 is a subset of all bitmasks, so
            // this will never be skipped by the check above.
            lane: NoLane,
            action: update.action,
            eagerReducer: update.eagerReducer,
            eagerState: update.eagerState,
            next: (null: any),
          };
          // 把更新任务插入到队列末尾
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }

        // 执行每一个reducer获取newState
        // 如果每一次的reducer相同,则把eagerState赋给newState
        if (update.eagerReducer === reducer) {
          // If this update was processed eagerly, and its reducer matches the
          // current reducer, we can use the eagerly computed state.
          newState = ((update.eagerState: any): S);
        } else {
          // 否则就需要去发起更新
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      // 继续更新
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    // Mark that the fiber performed work, but only if the new state is
    // different from the current state.
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }

    // 赋值返回最新状态
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

所以,useState大致的执行流程如下:

在这里插入图片描述在这里插入图片描述

既然useReduceruseState同为状态钩子,那就来看一看userReducer的实现吧

useReducer的用法

代码语言:javascript复制
import React, {useReducer} from 'react';
const initialState = {num: 0};
const reducer = (state, action) => {
    switch(action.type){
        case 'ad': 
            return {num: state.num   1};
        case 'de': 
            return {num: state.num - 1};
        case 'ch': 
            return {num: state.num * 2};
        case 'dv': 
            return {num: state.num / 2};    
        default:
           return state
    }
}

const App = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    retrun (
        <div className="App">
          <h1>{state.num}</h1>
          <div onClick={() => dispatch({type:ad})}>ad</div>
          <div onClick={() => dispatch({type:de})}>de</div>
          <div onClick={() => dispatch({type:ch})}>ch</div>
          <div onClick={() => dispatch({type:dv})}>dv</div>
        </div>
    )
}

export default App

各位可以自行去codeSandBox上面去测试玩耍。

分析上述代码我们可以看到每一个div的点击会绑定一个dispatch,这个dispatch可以实现useState里面的setMutation的功能,维护的state值来自于initialStateactiondispatch的入参,针对于这一些,我们去看看源码。

useReducer的mount阶段

我们根据HooksDispatcherOnMount可以找到在mount阶段,useReducer调用的是mountReducer

mountReducer

代码语言:javascript复制
function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  //关联链表
  const hook = mountWorkInProgressHook();
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
  // 保存初始值
  hook.memoizedState = hook.baseState = initialState;

  //更新队列
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });
  // 处理回调
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  //返回hook.memoizedState与回调
  return [hook.memoizedState, dispatch];
}

其实这个函数与useState非常类似,只不过他的第一入参为reducer,并且附带了一个可选参数init,官网上面对其称之为惰性初始化。update阶段调用的是updateReducer,在这里不再复述。

生命周期相关的hook

生命周期相关的这里讲解两个useEffectuseLayoutEffect,面试中经常会问到这两个有什么区别,其实他们并没有什么区别,只是执行的时机不同,具体表现为:

  • useLayoutEffect是在渲染的时候同步执行的,且与componentDidMountcomponentDidUpdate执行时机一致,所以在commit阶段中,执行于commitLayoutEffec函数里。
  • useEffect执行是异步的,要等到组件渲染到屏幕上才会去执行。

操作dom性能相关问题为什么修改dom建议放在useLayoutEffect中而不是useEffect中呢?

  • 从上述表述中可以看出,只要真实dom被创建,就会执行useLayoutEffect函数,而我们知道在创建真实dom的时候,节点的属性,样式等等都会被挂到dom节点上面去,一个节点增加属性必定会引起页面的重绘重排。而useEffect执行时机实在dom渲染到屏幕上面之后,这个时候已经经历过了一次重绘重排。如果再一次的修改dom必定引起二次重绘重排,这样来说,性能开销变成了两倍。所以为了减少性能开销,才如此建议。废话少说来看看useEffect的具体实现,mount阶段对应的是mountEffect

mountEffect

代码语言:javascript复制
function mountEffect(
  // 传入的函数useEffect(()=>{},[])
  create: () => (() => void) | void,  // 依赖
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__) {
     ...
    }
  }
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}

mountEffect调用mountEffectImpl

mountEffectImpl

代码语言:javascript复制
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();// 关联链表
  const nextDeps = deps === undefined ? null : deps;//处理依赖
  currentlyRenderingFiber.flags |= fiberFlags;// 关联副作用标记
  // 加入到环形链表中
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

pushEffect

代码语言:javascript复制
function pushEffect(tag, create, destroy, deps) {
  // 入参为副作用标记,传入函数,销毁回调,依赖项
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    // 下一个副作用
    next: (null: any),
  };

  //获取函数组件更新队列
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = 
  (currentlyRenderingFiber.updateQueue: any);

  if (componentUpdateQueue === null) {
    //队列为空,创建队列
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    //与fiber更新队列关联起来
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;

  } else {
    //有更新队列
    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;
}

这里做的事情,无非就是关联componentUpdateQueueeffect,让React去调度执行。

updateEffect

updateEffect执行调用了updateEffectImpl

代码语言:javascript复制
// updateEffect
function updateEffect(
  // 传入函数
  create: () => (() => void) | void,  // 依赖项
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__) {
    ...
  }
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}

//updateEffectImpl
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();// 获取更新hook
  const nextDeps = deps === undefined ? null : deps; //依赖项

  let destroy = undefined; //销毁函数

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 比较这一次的依赖与上一次的依赖
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 即使依赖相同,也要讲effect加入链表,保证顺序
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  //在commit阶段处理effect
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

本着学习到底的精神,我们一起来看看比较两次的依赖,到底是怎么比较的:

areHookInputsEqual

代码语言:javascript复制
function areHookInputsEqual(
  nextDeps: Array<mixed>, //当前的依赖
  prevDeps: Array<mixed> | null,// 上一次的依赖
) {
  if (__DEV__) {
   ...
  }

  // 上一次为空,肯定不一样,则必须更新
  if (prevDeps === null) {
    if (__DEV__) {
     ...
    }
    return false;
  }

  if (__DEV__) {
    if (nextDeps.length !== prevDeps.length) {
     ...
  }
  // 遍历两个依赖数组,根据下标比较
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i  ) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

// import is form 'shared/objectIs'
const objectIs: (x: any, y: any) => boolean =
  // 兼容Object.is
  typeof Object.is === 'function' ? Object.is : is;

// is 
function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) 
  );
}

上述代码介绍了比较两种值是否一样,其中Object.is的浏览器适用版本比较高。做了一次兼容:

上面说了useEffectuseLayoutEffect的结构都是一样的,重点只是在于他们俩的执行时机不一样,那么我们就去深究一下useEffectuseLayoutEffect的执行时机。

生命周期相关的hook的执行时机

我们知道所谓生命周期钩子,那肯定是在某一阶段去执行的,这个阶段就是commit阶段。在commit阶段的commitLayoutEffects函数中执行一系列的生命周期钩子,但是对于函数组件来讲,会调度useEffectcreatedestroy,也就是执行schedulePassiveEffects函数。

代码语言:javascript复制
function schedulePassiveEffects(finishedWork: Fiber) {
  // 获取更新队列
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  // 获取最后一个effect
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  //如果队列没有执行完
  if (lastEffect !== null) {
    //把队列里面的最后一个作为开头
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      const {next, tag} = effect;
      if (
        (tag & HookPassive) !== NoHookEffect &&
        (tag & HookHasEffect) !== NoHookEffect
      ) {
        // 将effect中的destroy和create加入到e
        //pendingPassiveHookEffectsUnmount
        //pendingPassiveHookEffectsMount中
        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
        enqueuePendingPassiveHookEffectMount(finishedWork, effect);
      }
      effect = next;
    } while (effect !== firstEffect);
  }
}

// enqueuePendingPassiveHookEffectMount
export function enqueuePendingPassiveHookEffectMount(
  fiber: Fiber,  effect: HookEffect,
): void {
  pendingPassiveHookEffectsMount.push(effect, fiber);
  if (!rootDoesHavePassiveEffects) {
    // root上有没有副作用,默认值为false
    rootDoesHavePassiveEffects = true;
    scheduleCallback(NormalSchedulerPriority, () => {
      flushPassiveEffects();
      return null;
    });
  }
}

// enqueuePendingPassiveHookEffectUnmount
export function enqueuePendingPassiveHookEffectUnmount(
  fiber: Fiber,  effect: HookEffect,
): void {
  pendingPassiveHookEffectsUnmount.push(effect, fiber);
  if (__DEV__) {
    fiber.flags |= PassiveUnmountPendingDev;
    const alternate = fiber.alternate;
    if (alternate !== null) {
      alternate.flags |= PassiveUnmountPendingDev;
    }
  }
  if (!rootDoesHavePassiveEffects) {
    // root上有没有副作用,默认值为false
    rootDoesHavePassiveEffects = true;
    scheduleCallback(NormalSchedulerPriority, () => {
      flushPassiveEffects();
      return null;
    });
  }
}

调度执行flushPassiveEffects

调度执行flushPassiveEffect会把当前正在执行的任务优先级跟后来新增的优先级进行比较。

代码语言:javascript复制
export function flushPassiveEffects(): boolean {
    ...
    if (decoupleUpdatePriorityFromScheduler) {
      // 获得更新优先级
      const previousLanePriority = getCurrentUpdateLanePriority();
      try {
        ...
        return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
      } finally {
        ...
      }
    } else {
      return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
    }
  }
  return false;
}

flushPassiveEffectsImpl

代码语言:javascript复制
function flushPassiveEffectsImpl() {

  // First pass: Destroy stale passive effects.
  const unmountEffects = pendingPassiveHookEffectsUnmount;
  pendingPassiveHookEffectsUnmount = [];
  for (let i = 0; i < unmountEffects.length; i  = 2) {
    const effect = ((unmountEffects[i]: any): HookEffect);
    const fiber = ((unmountEffects[i   1]: any): Fiber);
    const destroy = effect.destroy;
    effect.destroy = undefined;

    ...
    if (typeof destroy === 'function') {
      ...
      } else {
        try {
          if (
            ...
          ) {
            try {
              startPassiveEffectTimer();
              destroy();//销毁回调执行
            } finally {
              recordPassiveEffectDuration(fiber);
            }
          } else {
            destroy();//销毁回调执行
          }
        } catch (error) {
          ...
        }
      }
    }
  }
  // Second pass: Create new passive effects.
  const mountEffects = pendingPassiveHookEffectsMount;
  pendingPassiveHookEffectsMount = [];
  for (let i = 0; i < mountEffects.length; i  = 2) {
    const effect = ((mountEffects[i]: any): HookEffect);
    const fiber = ((mountEffects[i   1]: any): Fiber);
    if (__DEV__) {
      ...
    } else {
      try {
        const create = effect.create;
        if (
          ...
        ) {
          try {
            startPassiveEffectTimer();
            effect.destroy = create();//本次的render的创建函数
          } finally {
            recordPassiveEffectDuration(fiber);
          }
        } else {
          effect.destroy = create();//本次的render的创建函数
        }
      } catch (error) {
        ...
      }
    }
  }
  ...
  return true;
}

在这个函数里面就是依次执行上一次的render销毁回调函数、本次的render创建回调函数。而在前面也说过commit流程是无法中断的,只有等所有节点全部commit完,浏览器才会去告知react可以执行自己的调度任务了,也正在此刻useEffect所对应的函数才会去执行,

在生命周期hook里面React帮我们做了一定的性能优化,除了这个还提供了几个手动优化hookuseMemouseCallback,那我们来一起瞧瞧吧。

性能优化相关hook

在讲这一部分之前我们应该搞明白一个问题

  • 为什么要用useMemouseCallback来做性能优化
  • useMemouseCallback应该怎么做性能优化
  • 具体场景是什么

先回答第一个问题,为什么要用他们来做性能优化: 我们要知道,React更新流程中只要组件中props或者state发生了变化,那就是必须从变化顶部更新所有的后代组件,牵一发而动全身。有些组件我们并不想要它重新渲染,它却做了一定量的性能牺牲,用useMemouseCallback就可以改变这样的局面。那么应该怎么用他们来做性能优化呢,主要体现在三个方面:

  • 避免无效的副作用
  • 避免无效的累计计算
  • 避免无效的重新渲染

因为我们前面讲到useEffect,我们可以期望在依赖发生变更的时候去做我们想要做的事情,例如接口请求

代码语言:javascript复制
const App = () => {
  // 只有当parmas发生变化的时候,我们才回去执行fetch()
  useEffect(()=>{
    fetch()
  }, [params])
}

这并不是他们俩的功能呢,没关系,我们可以变着法子让他们俩具有类似功能,不过这就有点牵强了。

那怎么去做无效的计算和无效的重复渲染呢?有这样的一道面试题:

代码语言:javascript复制
// 点击父组件里面的按钮,会不会在子组件里面打印“子组件渲染了”?如果会,该怎么优化?
import { useState } from "react";

// 父组件
export const Parent = () => {
  console.log("父组件渲染了");
  const [count, setCount] = useState(0);
  const increment = () => setCount(count   1);

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <Child />
    </div>
  );
};

// 子组件
export const Child = ({}) => {
  console.log("子组件渲染了");
  return <div>子组件</div>;
};

很明显点击父组件,改变了count,整个组件都要重新渲染。肯定会打印的,那怎么优化呢?

代码语言:javascript复制
import { useState, useMemo } from "react";

// 父组件
export const Parent = () => {
  console.log("父组件渲染了");
  const [count, setCount] = useState(0);
  const increment = () => setCount(count   1);

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      // 这种写法不太提倡,一般采用的是memo      {useMemo(()=>(<Child />),[])}    </div>
  );
};

// 子组件
export const Child = ({}) => {
  console.log("子组件渲染了");
  return <div>子组件</div>;
};

//export const Child = memo(({}) => {
//  console.log("子组件渲染了");
//  return <div>子组件</div>;
//});

那么避免无效的计算体现在哪里呢:

代码语言:javascript复制
import { useState } from "react";

const App = () => {
  const [x, setX] = useState(0);
  const [y, setY] = useState(1);

  const caculate = () => {
    console.log('正在计算中')
    //庞大的计算,跟x相关的计算
    return x
  }

  return (
     <div>
        <h4>caculate:{caculate}</h4>
        <h4>x:{x}</h4>
        <h4>y:{y}</h4>
        <div onClick={()=>setX(x   1)}>x 1</div>
        <div onClick={()=>setY(y   1)}>y 1</div>
      </div>
  )
}

上面的代码里面的函数caculate只是一个与x有关的处理逻辑,与y没有关系,但是我们知道,只要触发了render,那么整个脚本就会自上而下执行一遍,很明显,如果这里只触发setY的话,他也会重新执行一遍脚本,而在caculate里面具有大量的耗时计算,那么这个时候,再次重新触发就显得很没有必要,故而我们可以采用useMemo来进行优化。

代码语言:javascript复制
import { useState } from "react";

const App = () => {
  const [x, setX] = useState(0);
  const [y, setY] = useState(1);

  const caculate = useMemo(() => {
    console.log('正在计算中')
    //庞大的计算,跟x相关的计算
    return x
  },[x]) // 只有x变化的时候,才会重新执行caculate

  return (
     <div>
        <h4>caculate:{caculate}</h4>
        <h4>x:{x}</h4>
        <h4>y:{y}</h4>
        <div onClick={()=>setX(x   1)}>x 1</div>
        <div onClick={()=>setY(y   1)}>y 1</div>
      </div>
  )
}

useCallback适用于父子组件嵌套,父组件基于属性把方法传递给子组件,保证了每一次父组件更新不会重新创建函数堆,而是获取之前的引用,传递给子组件的属性就没有变化,例如:

代码语言:javascript复制
// 父组件
import Child from './Child'
const Parent = () => {
  console.log('父组件渲染了')
  const [info, setInfo] = useState({name:'一溪之石', age: 18});
  const [count, setCount] = useState(1);

  const tansfromChild = () => {
    //....
  }

  //采用useCallback包裹
  //const tansfromChild = useCallback(() => {
  //  //....
  //})

  const handleSome = () => {
    setCount(count   1)
  }
  return (
    <>
      <div onClick={() => handleSome}>父组件</div>
      <Child tansfromChild={tansfromChild} info={info}/>
    </>
  )
}

// 子组件
const Child = ({tansfromChild, info}) => {
  console.log('子组件渲染了')
  return (
    <div onClick={() => tansfromChild}>{info.name}{info.age}</div>
  )
}

上述代码中点击父组件的函数,必定会使得count 1,从而会重新渲染父组件,重新创建函数(不考虑info),对于子组件来说,props改变,就一定会发生重新渲染。针对这种情况,我们一般采用用useCallback包裹起来,然后你去执行点击父组件的函数,你发现他依旧是会重新渲染子组件。因为子组件是函数组件。他没有处理props浅比较,故而每一次的props他都是不一样的。针对于这种问题解决方式:

  • 子组件写成class组件,继承PureComponent(不推荐)
  • 子组件写成函数组件,用memo包裹起来。

因为PureComponent里面会有一个shouldComponentUpdate来处理props的浅比较,而memo则也是我们现在需要去探索的东西。所以对于useCallback的使用一定要配合上具有浅比较的组件使用,否则不能优化性能,反而浪费性能。

useMemo适用于大量的计算得出的结果,防止在组建更新的时候,避免不必要的重新计算

说了这么多,我们还是一起来看看他们是如何实现的吧,根据上面的经验,我们可以在HooksDispatcherOnMount里面找到mountCallbackmountMemo

mountCallback、mountMemo

代码语言:javascript复制
// mountCallback
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook(); // 获取hook链表,并关联
  const nextDeps = deps === undefined ? null : deps; // 依赖
  hook.memoizedState = [callback, nextDeps]; // 存入memoizedState
  return callback; // 返回传入的函数
}

// mountMemo
function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook(); // 获得hook,关联链表
  const nextDeps = deps === undefined ? null : deps; // 依赖
  const nextValue = nextCreate(); // 执行传入函数,把值给nextValue
  hook.memoizedState = [nextValue, nextDeps]; // 把值和依赖加入到memoizedState中
  return nextValue; // 把传入函数的执行返回值,返回
}

上述代码简单如斯,mountCallback直接把传入的函数返回出去了,而mountMemo会把传入的函数执行,把返回值返回出去。并没有什么好讲的,我们看看他们怎么更新吧。

updateCallback、updateMemo

代码语言:javascript复制
// updateCallback
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook(); // 获取更新hook
  const nextDeps = deps === undefined ? null : deps; // 依赖
  // [callback, nextDeps]上一个的hook.memoizedState
  const prevState = hook.memoizedState; 

  if (prevState !== null) { // 上一个hook.memoizedState不为空
    if (nextDeps !== null) { // 这次也不为空

      const prevDeps: Array<mixed> | null = prevState[1]; 
      // [callback, nextDeps]取出依赖赋给prevDeps

      if (areHookInputsEqual(nextDeps, prevDeps)) { 
      // 如果依赖一样,则返回上一次的函数(缓存函数)
        return prevState[0];
      }
    }
  }
  // 如果上一次没有,表示首次渲染,则记录函数与依赖项,并返回传入的函数
  hook.memoizedState = [callback, nextDeps]; 
  return callback;
}

//updateMemo
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook(); // 获取更新hook
  const nextDeps = deps === undefined ? null : deps; //依赖

  const prevState = hook.memoizedState; // 获取上一次的值(缓存值)[nextValue, nextDeps]

  if (prevState !== null) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1]; 
      // 比较两次依赖
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0]; //如果相等就返回上一次的计算结果值
      }
    }
  }
  // 首次渲染,执行会犯传入函数的结果
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];// 存入hook.memoizedState
  return nextValue; // 返回计算结果
}

两个函数的更新也是异曲同工,细节都写在了代码里面,也没什么好讲的。

useContext

开始看useContext之前,我们要明白这个hook可以用来干什么。

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

  • useContext可以帮助我们跨越组件层级直接传递变量,实现数据共享。
  • Context的作用就是对它所包含的组件树提供全局共享数据的一种技术。

听的云里雾里的?不要担心,说白了就是组件之间传值就完事了,那么你又说了,组件传值我们用props传递不就完毕了吗,为什么还要有这个,太天真。useContext提供的功能是可以跨层级的。

假如有ABCDE五个组件嵌套,你用props传值,是不是要A传到B...一直传到E,但是useContext就可以在E组件里面直接获取到A的状态,举个例子:

代码语言:javascript复制
const ComponentA = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h4>父级:{count}</h4>
      <ComponentB count={count}/>
    </div>
  )
}

const ComponentB = (count) => {
  return (
    <div>
      <h4>父级:{count}</h4>
      <ComponentC count={count} />
    </div>
  )
}

const ComponentC = (count) => {
  return (
    <div>
      <h4>父级:{count}</h4>
      <ComponentD count={count} />
    </div>
  )
}

const ComponentD = (count) => {
  return (
    <div>
      <h4>父级:{count}</h4>
      <ComponentE count={count}/>
    </div>
  )
}

const ComponentE = (count) => {
  return (
    <div>
      <h4>来自A父级的count:{count}</h4>
    </div>
  )
}

这样才能在E组件里面拿到来自A组件的count,但是useContext提供了,直接拿的功能,例如:

代码语言:javascript复制
// 用createContext创建上下文
const Content = createContext(null);

// 在父组件里面用Content.Provider包裹子组件,指定上下文范围
const ComponentA = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h4>父级:{count}</h4>
      <Content.Provider value={{count,setCount}}>
        <ComponentB />
      </Content.Provider>
    </div>
  )
}
...

const ComponentE = (count) => {
  // 需要用到的组件里面用useContext接收
  const [count, setCount] = useContext(Content)
  return (
    <div>
      <h4>来自A父级的count:{count}</h4>
    </div>
  )
}

所以useContext的用法无非就三点:

  • createContext创建上下文
  • Content.Provider指定上下文
  • useContext使用上下文

既然知道他的用法,那么一起瞧瞧他的实现吧,首先我们肯定要去关注一下createContext

createContext

代码语言:javascript复制
export function createContext<T>(
  defaultValue: T, // 传入的默认值
  calculateChangedBits: ?(a: T, b: T) => number, // 可选参数处理默认值的变化
): ReactContext<T> {
  // 没有传入处理函数
  if (calculateChangedBits === undefined) {
    calculateChangedBits = null;
  } else {
    ...
  }

  // context对象
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    _threadCount: 0,
    Provider: (null: any),
    Consumer: (null: any),
  };

  //context.Provider
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE, // element类型
    _context: context,
  };

  let hasWarnedAboutUsingNestedContextConsumers = false;
  let hasWarnedAboutUsingConsumerProvider = false;
  let hasWarnedAboutDisplayNameOnConsumer = false;

  if (__DEV__) {
    ...
    context.Consumer = Consumer; // dev环境下Consumer就是Consumer
  } else {
    context.Consumer = context;// 正常情况下,Consumer用来包裹接收状态的组件的
  }
  ...
  return context;
}

上面的代码主要关注于context.Providercontext.Consumer,先来解释一下他们两个的作用吧。

  • context.Provider的作用在于规定上下文范围,并提供组件所需要的状态。
  • context.Consumer的作用在于接收传输过来的状态。

eg:

代码语言:javascript复制
// ComponentA
const ComponentA = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h4>父级:{count}</h4>
      <Content.Provider value={{count,setCount}}>
        <ComponentB />
      </Content.Provider>
    </div>
  )
}

// ComponentB
const ComponentB = (count) => {
  // 需要用到的组件里面用useContext接收
  // const [count, setCount] = useContext(Content)
  return (
    <div>
      <context.Consumer>
        {(count)=><h4>来自A父级的count:{count}</h4>}      </context.Consumer>
    </div>
  )
}

可见不用从useContext中解构出来,而用<context.Consumer>包裹起来,也是可以达到一样的效果的,他们之间的关系大致可以这样表示:

在这里插入图片描述在这里插入图片描述

useContext的执行流程

谈到useContext这里就不得不跟react里面的context一起说一下,在react源码中存在一个valueStackvalueCursor用来记录context的历史信息和当前contextdidPerformWorkStackCursor用来表示当前的context有没有变化。

代码语言:javascript复制
//ReactFiberNewContext.old.js
const valueCursor: StackCursor<mixed> = createCursor(null);

const didPerformWorkStackCursor: StackCursor<boolean> = createCursor(false);
代码语言:javascript复制
//ReactFiberStack.old.js
const valueStack: Array<any> = [];
  • render阶段的beginWork函数里调用updateContextProvider的时候会执行pushProvider,将新的值pushvalueStack
  • render阶段的completeWork函数里调用popProvider,将栈顶context pop出来
代码语言:javascript复制
//pushProvider
function pushProvider(providerFiber, nextValue) {
  var context = providerFiber.type._context;
  {
    push(valueCursor, context._currentValue, providerFiber);
    context._currentValue = nextValue;
  }
}

//popProvider
function popProvider(providerFiber) {
  var currentValue = valueCursor.current;
  pop(valueCursor, providerFiber);
  var context = providerFiber.type._context;

  {
    context._currentValue = currentValue;
  }
}

因为在render阶段,会以深度优先的方式遍历节点,这样在每一层级都可以通过valueCursor拿到最新的值了。上面也讲了createContext的实现,那么在使用的时候useContext又在干了什么呢?我们通过调用可以看到是调用了readContext,而readContext创建了 dependencies并关联到了fiberdependencies上面。更新阶段再次调用readContext去根据context的变化执行调度更新。

代码语言:javascript复制
export function readContext<T>(
  context: ReactContext<T>,
  observedBits: void | number | boolean,
): T {
  ...

  if (lastContextWithAllBitsObserved === context) {
    // Nothing to do. We already observe everything in this context.

    // 走mount阶段的useContext逻辑
  } else if (observedBits === false || observedBits === 0) {
    // Do not observe any updates.
  } else {

    // 生成resolvedObservedBits
    let resolvedObservedBits; 
    // Avoid deopting on observable arguments or heterogeneous types.
    if (
      typeof observedBits !== 'number' ||
      observedBits === MAX_SIGNED_31_BIT_INT
    ) {
      // Observe all updates.
      lastContextWithAllBitsObserved = ((context: any): ReactContext<mixed>);
      resolvedObservedBits = MAX_SIGNED_31_BIT_INT;
    } else {
      resolvedObservedBits = observedBits;
    }

    // 生成dependencies
    const contextItem = {
      context: ((context: any): ReactContext<mixed>),
      observedBits: resolvedObservedBits,
      next: null,
    };

    if (lastContextDependency === null) {
      invariant(
        ...
      );

      // This is the first dependency for this component. Create a new list.
      lastContextDependency = contextItem;
      // fiber dependencies链表的第一个context
      currentlyRenderingFiber.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
        responders: null,
      };
    } else {
      // Append a new context item. 
      // 加入dependencies
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}

我们再来看一看render阶段react会根据不同的组件类型去执行updateContextProviderupdateContextConsumer

updateContextProvider

代码语言:javascript复制
function updateContextProvider(
  current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes,
) {
  const providerType: ReactProviderType<any> = workInProgress.type;
  const context: ReactContext<any> = providerType._context;

  const newProps = workInProgress.pendingProps; // 新的状态
  const oldProps = workInProgress.memoizedProps;// 上一次的状态

  const newValue = newProps.value;// 新状态的value
  ...

  // 把value加入到valueStack中
  pushProvider(workInProgress, newValue);

  if (oldProps !== null) {
    const oldValue = oldProps.value;

    // 计算context变化的值
    const changedBits = calculateChangedBits(context, newValue, oldValue);
    if (changedBits === 0) { // context没有变化
      // No change. Bailout early if children are the same.
      if (
        oldProps.children === newProps.children &&
        !hasLegacyContextChanged()
      ) {
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes,
        );
      }
    } else { // context有变化了
      // The context value changed. Search for matching consumers and schedule
      // them to update.
      propagateContextChange(workInProgress, context, changedBits, renderLanes);
    }
  }

  const newChildren = newProps.children;
  // context变化,要重新diff
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

上面代码可以看到如果changedBits === 0则表示当前context没有变化。

calculateChangedBits

代码语言:javascript复制
export function calculateChangedBits<T>(
  context: ReactContext<T>,
  newValue: T,
  oldValue: T,
) {
  // 两次的props是一样的
  if (is(oldValue, newValue)) {
    // No change
    return 0;
  } else {
    // 两次props不一样
    const changedBits =
      typeof context._calculateChangedBits === 'function'
        ? context._calculateChangedBits(oldValue, newValue)
        : MAX_SIGNED_31_BIT_INT;

    ...
    return changedBits | 0; // 返回0或者MAX_SIGNED_31_BIT_INT
    //  MAX_SIGNED_31_BIT_INT = 1073741823,31位调度算法
  }
}

计算完changedBits之后,如果没有变化就执行bailoutOnAlreadyFinishedWork,跳过当前的节点更新。如果有变化了则执行propagateContextChange去更新当前节点信息。

代码语言:javascript复制
// bailoutOnAlreadyFinishedWork
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    // 复用上一次的依赖
    workInProgress.dependencies = current.dependencies;
  }

 ...
  // Check if the children have any pending work.
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    ...
    return null;
  } else {
    // 复用孩子节点
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
  }
}

// propagateContextChange
export function propagateContextChange(
  workInProgress: Fiber,  context: ReactContext<mixed>,  changedBits: number,  renderLanes: Lanes,
): void {
  // 获得孩子节点信息
  let fiber = workInProgress.child;

  if (fiber !== null) {
    // 有孩子的话,就去关联他的父亲
    fiber.return = workInProgress;
  }
  while (fiber !== null) {
    let nextFiber;

    // Visit this fiber.
    const list = fiber.dependencies; // 遍历fiber

    if (list !== null) {
      nextFiber = fiber.child; // 下一个处理的就是fiber的孩子,深度优先

      let dependency = list.firstContext;
      while (dependency !== null) { // 遍历dependency链表
        if (
          // context变化了
          dependency.context === context &&
          (dependency.observedBits & changedBits) !== 0
        ) {

          if (fiber.tag === ClassComponent) {
            // 强制调度,创建更新
            const update = createUpdate(
              NoTimestamp,
              pickArbitraryLane(renderLanes),
            );
            update.tag = ForceUpdate;
            // 加入更新队列
            enqueueUpdate(fiber, update);
          }
          // 合并context更新任务的优先级与fiber渲染优先级
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate; 

          if (alternate !== null) {
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          // 更新爷爷级别的优先级
          scheduleWorkOnParentPath(fiber.return, renderLanes);

          ...
          break;
        }
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
      ...
    } else if (
      enableSuspenseServerRenderer &&
      fiber.tag === DehydratedFragment
    ) {
      ...
    }
    fiber = nextFiber;
  }
}

updateContextConsumer

updateContextConsumer中,执行prepareToReadContext判断优先级是否足够加入当前这次renderreadContext取到当前context的value

代码语言:javascript复制
function updateContextConsumer(
  current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes,
) {
  let context: ReactContext<any> = workInProgress.type;
  ...
  const newProps = workInProgress.pendingProps;
  const render = newProps.children;

  ...
  // 比一比谁的优先级高,能不能加入到渲染队列里面去
  prepareToReadContext(workInProgress, renderLanes);
  // 读取新的value
  const newValue = readContext(context, newProps.unstable_observedBits);

  let newChildren;
  ...

  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
  // diff
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

所以useContext的执行流程大致如下:

在这里插入图片描述在这里插入图片描述

属性相关的hook

对于写原生的朋友来讲,获取一个dom节点直接用document.getElementByXxx,是多么的舒服,react也提供了一种获取节点的hook -- useRef eg:

代码语言:javascript复制
const App = () => {
  const inputValue = useRef(null);

  const getValue = () => {
    console.log(inputValue.current.value)
  }

  return (
    <>
      <input ref={inputValue} type="text" />
      <button onClick={getValue}>Click</button>
    </>
  )
}

input框中输入文字,点击按钮则可以在控制台获取到输入的文字。对比于原生的,它的优势在于省略掉了那些获取操作。useRef如此简单,还是一起来看一下源码吧。根据以往经验,我们找到mountRef函数。

mountRef

代码语言:javascript复制
function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();// 获取hook链表

  const ref = {current: initialValue}; // ref是一个对象 <X ref={xxx} />

  if (__DEV__) {
    Object.seal(ref);
  }
  // 存入到hook.memoizedState中
  hook.memoizedState = ref;
  // 返回 {current: initialValue} 获取通常用x.current
  return ref;
}

更新阶段的useRef

代码语言:javascript复制
function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();// 获取更新的hook
  return hook.memoizedState; // hook返回
}

useRef的执行流程

我们一猜就知道肯定是在某个阶段对具有ref标记的属性标签,进行了某些处理,肯定是render阶段里面做的,我们去renderbeginWork里面去找,果然发现 markRef函数。

markRef

代码语言:javascript复制
const ref = workInProgress.ref;
  if (
    (current === null && ref !== null) ||
    (current !== null && current.ref !== ref)
  ) {
    // Schedule a Ref effect
    workInProgress.flags |= Ref;
  }
}

但是在查找的过程中,我们还发现位于completeWork函数中也出现了markRef的身影。

代码语言:javascript复制
function markRef(workInProgress: Fiber) {
  workInProgress.flags |= Ref;
}

在这里面做的事情就是给带有ref属性的标签打上标记,嗯?打上标记干什么呢,那肯定是在commit阶段去处理啊!那么我们再去找一下commit阶段的commitMutationEffects函数,果不其然,

代码语言:javascript复制
function commitMutationEffects(
  root: FiberRoot,  renderPriorityLevel: ReactPriorityLevel,
) {
    ...
    // flags == 'Ref' ,更新ref current
    if (flags & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current); //移除ref
      }
    }

    ...
}

在这个函数里面我们发现了对Ref的处理:

  • 如果Ref改变了,且不为null则执行commitDetachRef删掉ref,之后并在commitLayoutEffect中执行commitAttachRef更新ref的值。

commitDetachRef

代码语言:javascript复制
function commitDetachRef(current: Fiber) {
  const currentRef = current.ref; //获得当前的ref

  if (currentRef !== null) {
    // 如果ref是函数则执行
    if (typeof currentRef === 'function') {
      currentRef(null);
    } else {
      // 否则则返回{current:null}
      currentRef.current = null;
    }
  }
}

commitAttachRef

代码语言:javascript复制
function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode; // 获得ref实例

    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }

    // Moved outside to ensure DCE works with this flag
    if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
      instanceToUse = instance;
    }
    if (typeof ref === 'function') {
      // 赋值
      ref(instanceToUse);
    } else {
      ...
      ref.current = instanceToUse;
    }
  }
}

所以useRef大致执行流程如下:

在这里插入图片描述在这里插入图片描述

总结

本文主要讲解了部分hooks的使用与原理,对hook使用更加熟悉了,还有一部分React内置hook的使用请查看官网,还有基于React的扩展ahooks都是值得学习的

0 人点赞