React常用hooks源码解析

(一)useCallback

总的思路:根据输入的deps,也就是一个数组其内部的内容是否有变化,决定是返回存储的老方法,还是返回新的方法并记录

挂载组件阶段

在这个mount阶段中,主要完成hook实例的创建,以及缓存当前的callback及其依赖列表到hook的memorizedState中,返回回调函数。

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
    
    
  // 创建一个hook实例
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 缓存当前的callback及其依赖列表
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

组件更新阶段

在update阶段进行依赖比较,如果新依赖与旧依赖一致(其比较方式采用的是Object.is比较方法),返回原callback。如果不一致,返回新的callback,并缓存新callback以及新依赖列表。

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
    
    
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 从hook的memorizedState中获取上次保存的值[callback, deps]
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    
    
    if (nextDeps !== null) {
    
    
      const prevDeps: Array<mixed> | null = prevState[1];
      // 比较新旧依赖列表
      if (areHookInputsEqual(nextDeps, prevDeps)) {
    
    
        // 如果相等,返回memorized的callback
        return prevState[0];
      }
    }
  }
  // 如果deps发生变化,更新hook的memorizedState,并返回最新的callback
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

(二)useMemo

挂载组件阶段

在mount阶段,执行创建函数获得返回值,
保存到hook的memorizedState中。

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
    
    
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

可以看到,与mountCallback相比这两个唯一的区别是
● mountMemo会将回调函数(nextCreate)的执行结果作为value保存
● mountCallback会将回调函数作为value保存

组件更新阶段

在组件更新阶段,获取新的deps,
从memorizedState中获得上次保存的值,比较新deps和旧deps是否相等,如果两者相等,返回旧的创建函数的返回值。
如果deps发生改变,hook中保存新的返回值和deps,并返回新的创建函数的返回值。

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) {
    
    
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
    
    
      const prevDeps: Array<mixed> | null = prevState[1];
       // 判断update前后value是否变化
      if (areHookInputsEqual(nextDeps, prevDeps)) {
    
    
        // 未变化
        return prevState[0];
      }
    }
  }
  // 变化,重新计算value
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

可见,对于update,这useCallback和useMemo两个hook的唯一区别也是是回调函数本身还是回调函数的执行结果作为value。

(三)useRef

挂载组件阶段

在这个阶段,useRef和其他Hook一样创建一个Hook对象,然后创建一个{current: initialValue}的值,缓存到Hook的memoizedState属性,并返回该值

function mountRef<T>(initialValue: T): {
    
    |current: T|} {
    
    
  // 获取当前useRef hook
  const hook = mountWorkInProgressHook();
  // 创建ref
  const ref = {
    
    current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

组件更新阶段

这个阶段直接从Hook实例中返回之前缓存的值。

function updateRef<T>(initialValue: T): {
    
    |current: T|} {
    
    
  // 获取当前useRef hook
  const hook = updateWorkInProgressHook();
  // 返回保存的数据
  return hook.memoizedState;
}

(四) useState

根据下面的源码,可见useState不过就是个语法糖,本质其实就是useReducer,或者说,useState只是预置了reducer的useReducer。
当调用setState方法(对应的就是useReducer返回的dispatch函数)时,触发basicStateReducer函数,第一个参数接收oldState,第二个参数是setState被调用时传入的参数newState,但是因为newState不是函数类型,直接返回newState,完成state的更新。

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
    
    
  return typeof action === 'function' ? action(state) : action;
}

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
    
    
  return useReducer(
    basicStateReducer,
    // useReducer has a special case to support lazy useState initializers
    (initialState: any),
  );
}

(五)useReducer

useReducer是比较重要的hook,笔者详细解说一下,注意下面笔者展示的代码都经过简化,去掉了静态类型和fiber有关的逻辑,保留最本质的Hook有关逻辑。
对于useReducer Hook,考虑如下例子:

export default function App() {
    
    
    const reducer = (state,action)=> {
    
    
        if(action === 'add'){
    
    
            return state + 1;
        }
        return state;
    }
    const [count, dispatch] = useReducer(reducer, 0);
    return (
        <div>
            <h1 className="title">{
    
    count}</h1>
            <button className="btn is-primary"
                onClick={
    
    ()=> dispatch('add')}
                >Increment</button>
        </div>
    )
}

我们可以将它的实现分为三个部分:
1.通过一些途径产生更新,更新会造成组件render。
(1)调用ReactDOM.render会产生mount的更新,更新内容为useReducer的initialValue(即0)。
(2)点击button标签触发dispatch会产生一次update的更新,更新内容为reducer。
2. 组件render时useReducer返回的count为更新后的结果

更新是什么?

首先我们要明确更新是什么。React是如何在每次重新渲染之后都能返回最新的状态? Class Component因为自身的特点可以将私有状态持久化的挂载到类实例上,每时每刻保存的都是最新的值。而 Function Component 由于本质就是一个函数,并且每次渲染都会重新执行。所以React必定拥有某种机制去记住每一次的dispatch更新操作,并最终得出最新的count值返回。
实际上,更新就是如下数据结构:

const update = {
    
    
  // 更新执行的函数
  action,
  // 与同一个Hook的其他更新形成链表
  next: null
}

对于App来说,点击button标签产生的update的action为reducer。

update数据结构

如果我们点击button标签三次,会执行三次dispatch,产生三次update,那么这些dispatch是如何组合到一起的呢?
答案是他们会形成环状单向queue链表。queue链表会存放每一次的更新,以便后面的update阶段可以返回最新的状态。
每次我们调用dispatch的时候,实际上我们调用的是dispatchAction.bind(null, hook.queue),当调用dispatchAction方法的时候,会形成一个新的update对象,添加到queue环状单向链表上。
我们先来了解下dispatchAction这个函数:

function dispatchAction(fiber,queue,action,) {
    
    
    const update = {
    
    
      action,
      next: null,
    };
    // 将update对象添加到循环链表中
    const last = queue.last;
    if (last === null) {
    
    
      // 链表为空,将当前更新作为第一个,并保持循环
      update.next = update;
    } else {
    
    
      const first = last.next;
      if (first !== null) {
    
    
        // 在最新的update对象后面插入新的update对象
        update.next = first;
      }
      last.next = update;
    }
    // 将表头保持在最新的update对象上
    queue.last = update;
   // 进行调度工作
    scheduleWork();
}

下面我们举例解释一下dispatchAction:
当产生第一个update(我们叫他u0),此时queue.last === null。
update.next = update;即u0.next = u0,他会和自己首尾相连形成单向环状链表。
然后queue.pending = update;即queue.last = u0

queue.last    = u0 ---> u0
                ^       |
                |       |
                ---------

当产生第二个update(我们叫他u1),update.next = queue.last.next;,此时queue.last.next === u0, 即u1.next = u0。
queue.last.next = update;,即u0.next = u1。
然后queue.last = update;即queue.last = u1

queue.last    = u1 ---> u0   
                ^       |
                |       |
                ---------

照着这个例子模拟插入多个update的情况,会发现queue.last始终指向最后一个插入的update。
这样做的好处是,当我们要遍历update时,queue.last.next指向第一个插入的update。

React 如何管理区分Hooks

如果App组件中有多个useReducer,那么React 如何管理区分这些Hooks呢?
这里我们可以从 mountState 里的 mountWorkInProgressHook方法和Hook的类型定义中找到答案

export type Hook = {
    
    
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any, any> | null,
   // 保存update的queue,即上文介绍的queue
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,  // 指向下一个Hook, 与下一个Hook连接形成单向无环链表
};

每个useReducer都对应一个hook对象,Hook是无环的单向链表。
这些Hooks节点是怎么利用链表数据结构串联在一起的呢?相关逻辑就在每个具体mount 阶段 Hooks函数调用的 mountWorkInProgressHook方法里:

function mountWorkInProgressHook(): Hook {
    
    
  const hook: Hook = {
    
    
    memoizedState: null,
    baseState: null,
    queue: null,
    baseUpdate: null,
    next: null,
  };
  if (workInProgressHook === null) {
    
    
    // 当前workInProgressHook链表为空的话,
    // 将当前Hook作为第一个Hook
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    
    
    // 否则将当前Hook添加到Hook链表的末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

如上,workInProgressHook我们可以理解为是一个按Hooks的执行顺序依次将Hook节点添加到链表中的辅助指针。

状态如何保存

好的,现在我们已经了解了React 通过链表来管理 Hooks,同时也是通过一个循环链表来存放每一次的更新操作,得以在每次组件更新的时候可以计算出最新的状态返回给我们。那么我们这个Hooks链表又存放在那里呢?理所当然的我们需要将它存放到一个跟当前组件相对于的地方。这个与组件一一对应的地方就是我们的FiberNode,组件构建的Hooks链表会挂载到FiberNode节点的memoizedState上面去。

mounted阶段源码:

mount 阶段 Hooks函数会调用mountReducer方法,它首先通过调用mountWorkInProgressHook,创建每个useReducer对应的hook对象,并且利用workInProgressHook指针,将存储在currentlyRenderingFiber.memoizedState上的hook对象们都串联起来,最后返回
[hook.memoizedState, dispatch],也就是对应的调用useReducer返回的[count,distpatch]

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
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    
    
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
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: UpdateQueue<S, A> = {
    
    
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

调用dispatch阶段源码:

当我们点击按钮,触发dispatch函数的时候,如上面update数据结构小节中所讲,实际上我们调用的是dispatchAction.bind(null, hook.queue),当调用dispatchAction方法的时候,会形成一个新的update对象,添加到queue环状单向链表上,之后会调用 scheduleWork方法进行调度工作。过程可以概括为:
创建update,将update加入queue.last中,并开启调度。

update阶段源码:

// react-reconciler/src/ReactFiberHooks.js
// 去掉与fiber有关的逻辑

function updateReducer(reducer,initialArg,init) {
    
    
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // 拿到更新列表的表头
  const last = queue.last;

  // 获取最早的那个update对象
  first = last !== null ? last.next : null;

  if (first !== null) {
    
    
    let newState;
    let update = first;
    do {
    
    
      // 执行每一次更新,去更新状态
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = newState;
  }
  const dispatch = queue.dispatch;
  // 返回最新的状态和修改状态的方法
  return [hook.memoizedState, dispatch];
}

组件render时,执行到useReducer的时候,会遍历update对象循环链表,执行每一次更新去计算出最新的状态来返回,以保证我们每次刷新组件都能拿到当前最新的状态。useReducer 的reducer是用户定义的reducer,所以会根据传入的action和每次循环得到的newState逐步计算出最新的状态。总结下来,这个阶段做的事情是
找到对应的hook,根据update计算该hook的新state并返回。
由这个阶段的源码,我们可以得知useReducer如何在每次渲染时,返回最新的值都原因如下:
● 每个Hook节点通过循环链表记住所有的更新操作
● 在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回

附件:手写简易useState

如下是摘自《React技术揭秘》一书的极简useState的实现代码,笔者认为对于理解useState和useReducer的关键源码有较大的帮助,遂附上供大家参考。
手写简易useState代码

猜你喜欢

转载自blog.csdn.net/m0_57307213/article/details/126993340