React Hooks 源码学习

avatar

React Hooks 简介:zh-hans.reactjs.org/docs/hooks-…

一句话简介

React Hooks 添加于 React 16.8,用于 React 中函数组件的一类特殊函数,它们允许你在不编写类组件而是使用简单的函数组件(Function Component,以下称FC) 的情况下依然享受到状态、上下文、副作用等 React 特性。

Why Hooks

没有 Hooks 的函数组件太简陋,难堪大任

在没有 Hooks 的时候,FC 只是一个函数,它没有实例、没有生命周期,执行完即销毁,无法保存state,也无法使用 ref 对 FC 进行指向。这些局限使得当时的 FC 只能用于写一些静态的纯组件。 有了Hooks之后,FC也可以使用许多 React 特性,极大程度消除了以上局限。

类组件自身也存在问题

  1. 在复杂组件中,耦合的逻辑代码很难分离 React 组件的基本思想是分离 UI 和逻辑,但是这样的设计很难分离业务代码。相反的,在各个生命周期钩子中,很多没有关联的业务代码也要被迫放在一起——因为他们都需要在某个阶段被执行。而 Hooks 将组件中相互关联的部分拆分成更小的函数,在不破坏其功能的情况下让我们更好地维护每个逻辑。
  2. 对资源的管理难以做到统一 同样由于生命周期的问题,对一个资源或事件需要在Mount钩子中完成申请或监听,而在Unmount钩子中销毁。生成和销毁的逻辑不在一起,难以管理,也容易忘记同步修改。有了 Hooks,只需要在useEffect的函数中写生成逻辑,在返回的闭包中写销毁逻辑即可。
  3. 组件间逻辑复用困难 在 React 中,给组件添加一些各个组件都可以复用的逻辑比较困难。尽管 React 提供了 render props、高阶组件(HOC)等方案,但是这类方案需要重新组织组件结构,既麻烦又降低了代码的可阅读性。为了避免使用HOC带来的一些问题,React 需要为多组件共享逻辑提供更好的原生途径。Hooks 提供了自定义 Hook ,来撰写需要复用的逻辑。
  4. 学习成本高昂 与 Vue 的易于上手不同,由于类组件使用 JavaScript 中的 class 实现,开发 React 的类组件需要比较扎实的 JavaScript 基础,尤其是关于 this 的指向、闭包、bind等相关概念的理解。增加了上手门槛。 Hooks 的出现,使得上述问题得到了不同程度的解决。

State Hooks

useState

useState函数接收一个参数,可以是类型任意的一个值或返回一个任意类型值的函数。其执行结果为一个长度为2的数组,[0]是状态值,[1]是修改状态值的 setter函数。

// 创建state的两种方式
// 直接给定初始值,在渲染过程中 state 会一直保持该值
const [stateA, setStateA] = useState<Type>(initialState);
// 使用函数计算初始值,该函数只会在第一次渲染时执行一次,一般用于初始状态较为复杂的情况
const [stateB, setStateB] = useState<Type>(()=>{
    const initialState = expensiveCompute();
    return initialState;
});
//更新state的两种方式
// 直接给定新值,组件使用新值重渲染
setStateA(newStateA);
// 若新state需要依赖旧state的值,则传入一个参数为旧state的函数,返回值为新state值
setStateB((prevStateB)=>{
    const newStateB = doSth(prevStateB);
    return newStateB;
});
// 需要注意,与class的setState函数不同,useState并不会在set的时候merge所有改动。

下面来一个例子:

import React, { useState } from 'react';
const Example = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(prevcount=>(prevCount + 1))}>
        Click me
      </button>
    </div>
  );
}

整个 Hooks 运作过程:

  1. 函数组件 Example 第一次执行函数时 useState 进行初始化,其传入的参数 0 就是 count 的初始值;
  2. 返回的 VDOM 中使用到了 count 属性,其值为 0
  3. 通过点击按钮,触发 setCount 函数,传入修改 count的值,然后重新执行函数(就像类组件中重新执行 render 函数一样);
  4. 第二次及以后执行函数时,依旧通过 useState 来获取 count 及修改 count 的方法 setCount,只不过不会执行 count的初始化,而是使用其上一次 setCount 传入的值。 通过多次调用useState(),一个函数组件可以拥有多个状态。
function MyComponent() {
  const [state1, setState1] = useState(initial1);
  const [state2, setState2] = useState(initial2);
  const [state3, setState3] = useState(initial3);
  // ...
}
复制代码

需要注意的,要确保对useState()和其他 Hooks 中每种 Hook 的多次调用在渲染之间始终保持相同的顺序,且要放置在函数组件顶层,不能放置在循环语句、条件语句和嵌套中(原因后面会讲)。 为什么使用一个useState函数我们就可以在一个函数组件中存储状态,每次调用同一函数的时候可以获取上次的状态?接下来我们来看看 React 的源码。

// react-reconciler/src/ReactFiberHooks.js
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;
  currentlyRenderingFiber = workInProgress;
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  //注意这里,当函数初次初始化时会调用onMount,如果非初次渲染会调用onUpdate
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
  let children = Component(props, secondArg);
  // Check if there was a render phase update
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
    // Keep rendering in a loop for as long as render phase updates continue to
    // be scheduled. Use a counter to prevent infinite loops.
    let numberOfReRenders: number = 0;
    do {
      didScheduleRenderPhaseUpdateDuringThisPass = false;
      invariant(
        numberOfReRenders < RE_RENDER_LIMIT,
        "Too many re-renders. React limits the number of renders to prevent " +
          "an infinite loop."
      );
      numberOfReRenders += 1;
      // Start over from the beginning of the list
      currentHook = null;
      workInProgressHook = null;
      workInProgress.updateQueue = null;
      ReactCurrentDispatcher.current = HooksDispatcherOnRerender;
      children = Component(props, secondArg);
    } while (didScheduleRenderPhaseUpdateDuringThisPass);
  }
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;
  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;
  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);
  currentHook = null;
  workInProgressHook = null;
  didScheduleRenderPhaseUpdate = false;
  invariant(
    !didRenderTooFewHooks,
    "Rendered fewer hooks than expected. This may be caused by an accidental " +
      "early return statement."
  );
  if (enableLazyContextPropagation) {
    if (current !== null) {
      if (!checkIfWorkInProgressReceivedUpdate()) {
        const currentDependencies = current.dependencies;
        if (
          currentDependencies !== null &&
          checkIfContextChanged(currentDependencies)
        ) {
          markWorkInProgressReceivedUpdate();
        }
      }
    }
  }
  return children;
}

首先我们应该明白几个概念,这对于后续我们理解useState是很有帮助的。 current fiber树: 当完成一次渲染之后,会产生一个current树,current会在commit阶段替换成真实的Dom树。 workInProgress fiber树: 即将调和渲染的 fiber 树。在一次新的组件更新过程中,会从current复制一份作为workInProgress,更新完毕后,将当前的workInProgress树赋值给current树。 workInProgress.memoizedState: 在class组件中,memoizedState直接存放state信息,在function组件中,memoizedState在一次调和渲染过程中,以链表的形式存放hooks信息。 我们可以看到,React 通过 current 树是否存在或是current树上是否存在 memoizedState 判断是否组件为第一次渲染。如果是第一次渲染,那么使用onMount情况的Dispatcher,否则使用用onUpdate情况的Dispatcher。那么我们接下来再看看这两个Dispatcher。

const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  //对于初次渲染,调用mount方法
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useOpaqueIdentifier: mountOpaqueIdentifier,
  unstable_isNewReconciler: enableNewReconciler,
};
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  //对于非初次渲染,调用update方法
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useOpaqueIdentifier: updateOpaqueIdentifier,
  unstable_isNewReconciler: enableNewReconciler,
};

当初次渲染时,onMount中的useState方法调用了mountState方法,非初次渲染时,则调用Update方法。再看看这两个方法的差异。

// react-reconciler/src/ReactFiberHooks.js
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

可以看到,mount方法中fiber直接将momoizedState置为initialState。而update方法中则调用updateReducer方法进一步更新。限于篇幅就不展开源码了,简要流程就是在updateReducer中会获取current树上的state,然后进行更新,最后将newState更新到memoizedState的链表末尾,达到更新state的目的。这段代码也可以说明State的底层就是使用Reducer实现的。在 useReducer 一节中会用代码阐述两者关系。 此外要注意,React默认使用浅监听模式,useState 当且仅当 state 对象本身发生变化时才会进行更新。所以更新可变对象(常见的就是 object 和 array)时如果只更新内层数据并不会引起重渲染。如果想要更新可变对象的 state 需要生成新对象,对于对象和数组一般使用对象解构和数组解构来生成。下面是例子。

const [ids,setIds] = useState<number[]>([]);
// Plan A
ids.push(1)
setIds(ids)// 错误,ids本身未发生变化,不会重渲染
// Plan B
setIds((prevIds)=>([...prevIds,1])); //正确,使用数组解构生成新数组
const [immutableVar] = useState(initialState)

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);
  • useReducer 接收三个参数:
    • 第一个参数reducer是一个 reducer 函数,reducer的参数就是state和action,返回改变后的state。用法原理和 redux 中的 reducer 一致。
    • 第二个参数initialArg是state的初始值,也是init的入参。
    • 第三个可选参数init是一个函数,同useState中的参数函数一样可以用来惰性提供初始状态。
  • useReducer 返回一个长度为2的数组
    • [0]是 state 的值
    • [1]是 用于更新的 dispatch 函数,调用时的参数会作为reducer的action传入。
function init(initialCount) { 
    return {count: initialCount}; 
} 
function reducer(state, action) { 
    switch (action.type) { 
        case 'increment': 
            return {count: state.count + 1}; 
        case 'decrement': 
            return {count: state.count - 1}; 
        case 'reset': 
            return init(action.payload); 
        default: 
            throw new Error(); 
    } 
} 
function Counter({initialCount}) { 
    const [state, dispatch] = useReducer(reducer, initialCount, init); 
    return ( 
        <> 
        Count: {state.count} 
        <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> 
            Reset 
        </button> 
        <button onClick={() => dispatch({type: 'increment'})}>+</button> 
        <button onClick={() => dispatch({type: 'decrement'})}>-</button> 
        </> 
    ); 
} 

useState 和 useRe d ucer 的实现原理:

let memoizedState
function useReducer(reducer, initialArg, init) {
    let initState = 0
    if (typeof init === 'function') {
        initState = init(initialArg)
    } else {
        initState = initialArg
    }
    function dispatch(action) {
        memoizedState = reducer(memoizedState, action)
    }
    memoizedState = memoizedState || initState
    return [memoizedState, dispatch]
}
function useState(initState) {
    return useReducer((oldState, newState) => {
        if (typeof newState === 'function') {
            return newState(oldState)
        }
        return newState
    }, initState)
}

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。) 需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

Effect Hooks

useEffect

useEffect(effect, dependenciesArray);
  • useEffect 接受两个参数。
    • 第一个参数 effect 接受一个类型是 ()=> void | (()=>void) 的副作用函数。该函数将在相对于componentDidMount 时无条件触发和 componentDidUpdate 时有条件地触发(该条件为第二个参数 dependenciesArray)。该函数允许返回一个清除函数,返回的函数将在componentWillUnmount 时无条件触发及 componentDidUpdate 时有条件地先于 effect 触发。
    • 第二个参数dependenciesArray是可选参数,一个存有变量或函数的数组。
      • 如果省略,则每次 componentDidUpdate 时都会先触发清除函数(如果存在),再触发 effect。
      • 如果为空数组[],componentDidUpdate 时不会触发清除函数和 effect。
      • 如果数组内有变量或函数,在指定变量或函数改变时触发清除函数和 effect。 dependenciesArray的存在给我们提供了一种非常简便的方法来自动关注某些的值变化而触发对应逻辑。我们需要该段逻辑在什么值变化时触发,就将其加入依赖列表中。但是也要注意,忘记写依赖数组或者 注意: useEffect 与类组件中的 componentDidMount 和 componentDidUpdate 不同之处是,effect 函数触发时间为在浏览器完成渲染之后,它不会阻塞浏览器更新视图。 如果需要在渲染之前触发,需要使用 useLayoutEffect。

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。 那么useEffect又是怎么实现的呢?我们继续来看源码

//省略了 React 开发调试的_DEV相关逻辑
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
    return mountEffectImpl(
      PassiveEffect | PassiveStaticEffect,
      HookPassive,
      create,
      deps
    );
}
// mountEffect的真正逻辑
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
  );
}
function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  let componentUpdateQueue: null | FunctionComponentUpdateQueue =
    (currentlyRenderingFiber.updateQueue: any); //这是一个循环链表
    // 如果是第一个Effect
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    //向循环链表中压入当前effect
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else { //存在多个Effect
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      //将当前effect加到循环链表末尾
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}
function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;
  //没有Effect的情况流程和mountEffect一模一样
  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 注意:这里比较的deps使用Object.is比较依赖数组内的依赖
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    // 如果依赖项有变,会将HookHasEffect加入effect的tag中
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps
  );
}

在渲染时renderWithHooks也会判断useEffect是否初次执行,如果是,执行mount逻辑,否则执行update逻辑。但与State有区别的是,effect不会在mount或者update的时候主动执行,而是会放到一个循环队列中等待fiber渲染时再根据时点执行整个队列中的effect。

// react-reconciler/src/ReactFiberCommitWork.new.js 
function commitHookEffectListUnmount(
  flags: HookFlags,
  finishedWork: Fiber,
  nearestMountedAncestor: Fiber | null
) {
  const updateQueue: FunctionComponentUpdateQueue | null =
    (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null =
    (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & tag) === tag) {
        // Mount
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

useLayoutEffect

useLayoutEffect(effect, dependenciesArray);

与 useEffect 使用方法一样,只是执行回调函数的时机有着略微区别。 useEffect执行顺序: 组件更新挂载完成 -> 浏览器 dom 绘制完成 -> 执行 useEffect 回调。 useLayoutEffect 执行顺序: 组件更新挂载完成 -> 执行 useLayoutEffect 回调-> 浏览器dom绘制完成。 所以说 useLayoutEffect 代码可能会阻塞浏览器的绘制 。我们写的 effectuseLayoutEffectReact在底层会被分别打上PassiveEffectHookLayout,在commit阶段区分出,在什么时机执行。 ⚠️注意:useLayoutEffect 在 development 模式下进行 SSR 会有警告⚠️ 通常情况下 useLayoutEffect 会用在做动效和记录 layout 的一些特殊场景。一般我们不需要使用 useLayoutEffect。

Memorized Hooks

memo

对于类组件来说,使用 PureComponent 可以通过判断父组件传入的 props 是否进行改变来优化渲染性能。在函数式组件中,也有一个类似 PureComponent 功能的高阶组件 memo,效果与 PureComponent 相同,都会判断父组件传入的 props 是否发生改变来重新渲染当前组件。

import React, { memo } from 'react'
function Demo(props){
    return (
     <div>{props.name}</div>
    )
}
export default memo(Demo)

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

useMemo接受两个参数,第一个参数是一个函数,返回值用于产生保存值。 第二个参数是一个数组,作为dep依赖项,数组里面的依赖项发生变化,重新执行第一个函数,产生新的值。 使用场景:

  1. 充当缓存,避免重复进行大量运算导致性能下降
const number = useMemo(()=>{
 /** ....大量的逻辑运算 **/ 
 return number },[ props.number ]); // 只有 props.number 改变的时候,重新计算number的值。
  1. 减少不必要的DOM循环
 /* 用 useMemo包裹的list可以限定当且仅当list改变的时候才更新此list,这样就可以避免selectList重新循环 */
 {useMemo(() => (
      <div>{
          selectList.map((i, v) => (
              <span
                  className={style.listSpan}
                  key={v} >
                  {i.patentName} 
              </span>
          ))}
      </div>
), [selectList])}
  1. 减少子组件渲染
 /* 只有当props中,list列表改变的时候,子组件才渲染 */
const goodListChild = useMemo(
  () => <GoodList list={props.list} />,
  [props.list]
);

其实现源码也非常简单:

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;
}
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps; // 新的 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]; // 之前保存的 deps 值
      if (areHookInputsEqual(nextDeps, prevDeps)) { //判断前后的 deps 值是否相等
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

初始化useMemo会创建一个hook,然后执行useMemo的第一个参数,得到需要缓存的值,然后将值和deps记录下来,赋值给当前hookmemoizedState。 更新时执行useMemo会判断两次 deps是否相等,如果不相等,证明依赖项发生改变,那么执行 useMemo的第一个函数,得到新的值,然后重新赋值给hook.memoizedState,如果相等证明没有依赖项改变,那么直接获取缓存的值。 也要注意,nextCreate()的执行如果里面引用了 useState 等信息,变量也会被引用,形成闭包,无法GC。所以存在可能访问的值不是最新的的情况,在这种情况下就需要我们把引用的值也添加到deps数组中,每次引用的值改变时都重新执行就不会出错了。

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b)
  },
  [a, b],
)

useMemouseCallback 接收的参数都是一样的,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于 useMemo 返回的是函数运行的结果, useCallback 返回的是函数。 返回的callback可以作为props回调函数传递给子组件。

useMemo和useCallback可以相互转化。下面的代码是等价的:

const memoCallback = useCallback((...args) => {
  // DO SOMETHING
}, [...deps]);
const memoCallback = useMemo(() => (...args) => {
  // DO SOMETHING
}, [...deps]);

Reference Hooks

useRef

const xxxRef = useRef<Type>(initialRef);

createRef 使用方法和 useRef 一致,返回的是一个 ref 对象,该对象下面有一个 current 属性指向被引用对象的实例,一般用于操作dom:

import { React, createRef, useRef } from 'react'
const FocusInput = () => {
  const inputElement = createRef()
  // const inputElement = useRef()
  const handleFocusInput = () => {
    inputElement.current.focus()
  }
  return (
    <>
      <input type='text' ref={inputElement} />
      <button onClick={handleFocusInput}>Focus Input</button>
    </>
  )
}
export default FocusInput

但是,这两者对应 ref 的引用其实是有着本质区别的:createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。 当需要存放一个数据,需要无论在哪里都取到最新状态时,需要使用 useRef。

function SomeComponent() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  countRef.current = count;
  // 这里 useEffect 表示在第一次渲染完成后,执行回调函数,具体 useEffect 用法下面讲
  useEffect(() => {
    const id = setInterval(() => {
        console.log(countRef.current);
        setCount(currentCount => currentCount + 1);
    });
    return () => { clearInterval(id); }
  }, []);
  return <h1>See what's printed in console.</h1>
}

useRef 生成的可变对象,因为使用起来就跟普通对象一样,赋值时候 React 是无法感知到值变更的,所以也不会触发组件重绘。利用其与 useState 的区别,我们一般这样区分使用:

  • 维护与 UI 相关的状态,使用 useState

确保更改时刷新 UI

  • 值更新不需要触发重绘时,使用 useRef
  • 不需要变更的数据、函数,使用 useState

forwardRef

基本用法:

forwardRef((props, ref) => {
    // dosomething
    return (
     <div ref={ref}></div>
    )
})

forwardRef 准确来说不是 hooks 中的内容,但是如果我们要使用 useImperativeHandle,就需要使用它来进行配合使用。 该方法的作用是:引用父组件的 ref 实例,成为子组件的一个参数,可以引用父组件的 ref 绑定到子组件自身的节点上。 该方法可以看做是一个高阶组件,本身 props 只带有 children 这个参数,它能将从父组件拿到的 ref 和 props 传入给子组件,由子组件来调用父组件传入的 ref。 传入的组件会接收到两个参数,一个是父组件传递的 props,另一个就是 ref 的引用。

useImperativeHandle

useImperativeHandle(ref, () => ({
    a:1,
    b:2,
    c:3
}))

官方建议 useImperativeHandle 和 forwardRef 同时使用,减少暴露给父组件的属性,避免使用 ref 这样的命令式代码。 useImperativeHandle 有三个参数:

  • 第一个参数,接收一个通过 forwardRef 引用父组件的 ref 实例
  • 第二个参数一个回调函数,返回一个对象,对象里面存储需要暴露给父组件的属性或方法
  • 第三个参数为一个可选参数,该参数是一个依赖项数组,就像 useEffect 那样
function Example(props, ref) {
    const inputRef = useRef()
    useImperativeHandle(ref, () => ({
        // 父组件可以通过this.xxx.current.focus的方式使用子组件传递出去的focus方法
        focus: () => {
            inputRef.current.focus()
        }
    }))
    return <input ref={inputRef} />
}
export default forwardRef(Example)
class App extends Component {
  constructor(props){
      super(props)
      this.inputRef = createRef()
  }
  render() {
    return (
        <>
            <Example ref={this.inputRef}/>
            <button onClick={() => {this.inputRef.current.focus()}}>Click</button>
        </>
    )
  }
}

自定义Hooks

zh-hans.reactjs.org/docs/hooks-… 自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

// 下面自定义了一个获取窗口长宽值的hooks
import React, { useState, useEffect, useCallback } from 'react'
function useWinSize() {
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  })
  const onResize = useCallback(() => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    })
  }, [])
  useEffect(() => {
    window.addEventListener('resize', onResize)
    return () => {
      window.removeEventListener('reisze', onResize)
    }
  }, [onResize])
  return size
}
export const useWinSize
// index.js
import { useWinSize } from './myhooks'
function MyHooksComponent() {
  const size = useWinSize()
  return (
    <div>
      页面Size:{size.width}x{size.height}
    </div>
  )
}
export default MyHooksComponent

自定义 Hook 解决了以前在 React 组件中无法灵活共享逻辑的问题。你可以创建涵盖各种场景的自定义 Hook,如表单处理、动画、订阅声明、计时器等。

使用注意

  • 仅顶层调用 Hook :不能在循环,条件,嵌套函数等中调用useState()。在多个useState()调用中,渲染之间的调用顺序必须相同。
  • 仅从 React 函数调用 Hook:必须仅在函数组件或自定义钩子内部调用useState()。
// react-reconciler/src/ReactFiberHooks.new.js 
// 在useState节中 renderWithHooks 函数有对于此处的调用。
export const ContextOnlyDispatcher: Dispatcher = {
  readContext,
  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  useMemo: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  useDebugValue: throwInvalidHookError,
  useDeferredValue: throwInvalidHookError,
  useTransition: throwInvalidHookError,
  useMutableSource: throwInvalidHookError,
  useOpaqueIdentifier: throwInvalidHookError,
  unstable_isNewReconciler: enableNewReconciler,
};
function throwInvalidHookError() {
  invariant(
    false,
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
  );
}
  • 自定义Hook必须以use开头,以便于React检查自定义Hooks是否遵守Hooks相关规范。