React Hooks source code learning

Introduction to React Hooks: zh-hans.reactjs.org/docs/hooks-…

One sentence introduction

React Hooks, added in React 16.8, are a special class of functions for functional components in React. They allow you to still enjoy state, React features like context, side effects, etc.

Why Hooks

Function components without Hooks are too rudimentary and embarrassing

When there are no Hooks, FC is just a function. It has no instance and no life cycle . It is destroyed after execution, cannot save state, and cannot use ref to point to FC. These limitations made FC at that time only used to write some static pure components. With Hooks, FC can also use many React features, which greatly eliminates the above limitations.

The class component itself also has problems

  1. In complex components, coupled logic code is difficult to separate. The basic idea of ​​React components is to separate UI and logic, but such a design is difficult to separate business code. On the contrary, in each life cycle hook, a lot of unrelated business code is also forced to be put together - because they all need to be executed at a certain stage. Hooks, on the other hand, split the interconnected parts of a component into smaller functions, allowing us to better maintain each logic without breaking its functionality.
  2. It is difficult to unify the management of resources. Also due to life cycle problems, a resource or event needs to be applied or monitored in the Mount hook, and destroyed in the Unmount hook. The logic of generation and destruction is not together, it is difficult to manage, and it is easy to forget to synchronize changes. With Hooks, you only need to write the generation logic in the function of useEffect, and write the destruction logic in the returned closure.
  3. Difficulty in reusing logic between components In React, it is difficult to add some logic that can be reused by each component. Although React provides solutions such as render props and higher-order components ( HOC ), such solutions need to reorganize the component structure, which is troublesome and reduces the readability of the code. In order to avoid some of the problems that come with using HOC, React needs to provide a better native way for multiple components to share logic. Hooks provides custom Hooks to write logic that needs to be reused.
  4. The high cost of learning is different from the ease of use of Vue. Since class components are implemented using classes in JavaScript, developing React class components requires a solid JavaScript foundation, especially the understanding of related concepts such as this pointing, closure, and bind. The threshold for getting started has been increased. The emergence of Hooks has solved the above problems to varying degrees.

State Hooks

useState

The useState function accepts a parameter, which can be a value of any type or a function that returns a value of any type. The execution result is an array of length 2, [0] is the state value, and [1] is the setterfunction .

// 创建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所有改动。

Here's an example:

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>
  );
}

The entire Hooks operation process:

  1. The function Examplecomponent useStateis initialized when the function is executed for the first time, and the parameter passed in 0is countthe initial value of ;
  2. The countproperty , and its value is 0;
  3. By clicking the button, trigger the setCountfunction , pass countin the modified value, and then re-execute the function (just like re-executing the render function in a class component);
  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-… A custom Hook is a function whose name starts with "use", and other Hooks can be called inside the function.

// 下面自定义了一个获取窗口长宽值的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

Custom Hooks solve the problem of previously not being able to share logic flexibly in React components. You can create custom Hooks covering various scenarios such as form handling, animations, subscription declarations, timers, etc.

Use caution

  • Hooks are called at the top level only: useState() cannot be called in loops, conditions, nested functions, etc. In multiple useState() calls, the order of calls must be the same between renders.
  • Only call Hooks from React functions: useState() must be called only inside function components or custom hooks.
// 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.',
  );
}
  • Custom Hooks must start with use so that React can check whether custom Hooks comply with Hooks-related specifications.

Guess you like

Origin juejin.im/post/7114491826694389768