React Hooks 常用方法剖析

884 阅读10分钟

1. 前言

本文主要介绍了Hooks常用函数的原理以及使用场景。文中的部分示例和想法有取自官方文档, 有借鉴了部分非常优秀的文章(文章链接会在结尾处贴出)。 由于作者水平有限,源码中部分场景尚未能完整串起来,如发现表述错误的地方,望请指出。

2 useState

作为我们第一个上场的主角, useState是最具代表性的hook函数,我们将通过函数组件的装载、触发状态变更、 更新三个阶段来详细介绍它。

2.1 组件mount阶段

2.1.1 renderWithHooks

react源码执行function component的地方叫renderWithHooks方法函数, 核心代码如下:

function renderWithHooks() {
    、、、
    // 详见【2.1.2 hook函数到底调用了谁?】
    nextCurrentHook = current.memoizedState 
    if (nextCurrentHook !== null) {
      ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
    } else if (hookTypesDev !== null) {
      ReactCurrentDispatcher$1.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
    }
  }
  // 详见【2.1.3】,执行HookComp函数
  var children = Component(props, refOrContext);

  // 【2.1.4】
  //这个firstWorkInProgressHook是HookComp内部的第一个useState(0)生成的hook对象,通过它可以遍历HookComp生成的所有的hook对象。 
  renderedWork.memoizedState = firstWorkInProgressHook;
  // 标记这个组件是否有状态变更
  renderedWork.expirationTime = remainingExpirationTime;
  // useEffect生成的effect对象组成的queue,在commitHookEffectList内部被循环遍历按需执行
  renderedWork.updateQueue = componentUpdateQueue;
  renderedWork.effectTag |= sideEffectTag;
  、、、

}
2.1.2 hook函数到底调用了谁?

react提供的use系列的Hook函数调用了ReactCurrentDispatcher$1.current指向对象的内部的同名函数, 因此ReactCurrentDispatcher$1.current决定了我们hook函数的处理逻辑。

场景 ReactCurrentDispatcher$1.current指向
组件mount HooksDispatcherOnMountInDEV
组件update HooksDispatcherOnUpdateInDEV
2.1.3 运行函数组件

由【2.1.2】得知,组件第一次渲染的时候ReactCurrentDispatcher$1.current指向HooksDispatcherOnMountInDEV,所以,useState实际上调用了HooksDispatcherOnMountInDEV.useState()。


useState: function (initialState) {
    return mountState(initialState);
}

function mountState(initialState) {
    // 详见 【2.1.4】
  var hook = mountWorkInProgressHook();

    // 如果initialState是function,运行后重新赋值
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
    // hook对象的baseState属性保存了initialState
  hook.memoizedState = hook.baseState = initialState;
  var queue = hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  // dispatchAction绑定参数后返回给HookComp, 即setCount引用对象。
  var dispatch = queue.dispatch = dispatchAction.bind(null, 
  currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

所以当我们写成这样:
const [count, setCount] = useState(0)
通过源码看应该看成这样:
[hook.memoizedState, dispatch] = useState(0)

count值保存在hook.memoizedState上。
setCount就是以当前组件对应的fiber对象和hook.queue作为参数的dispatchAction方法, 所以我们点击increate时候回调setCount(number)时实际调用了dispatchAction方法,这个方法会触发count的变更,详细过程后面再表。

2.1.4 为每个调用的hook方法生成一个hook对象
function mountWorkInProgressHook() {
  // hook对象数据结构
  var hook = {
    memoizedState: null, //保存当前状态,不同hook方法保存的数据结构不同。
    baseState: null, // 上一次渲染的state
    queue: null, // 指向最近的一次update对象
    baseUpdate: null, // 上一次渲染中最新的update对象
    next: null // 指向下一个hook方法对应的hook对象
  };

  if (workInProgressHook === null) {
    // 这是组件的第一个hook对象,   
    // 这个firstWorkInProgressHook会在【1.3】中被保存到fiber对象上,这样下次就能通过fiber查
    // 找到组件的所有hook对象了。
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // 将hook对象添加到链表的尾部
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}

实际上,在组件被mount的时候,每调用一个hook方法(不单是useState,而是每一个React提供的hook方法),react都会帮我们生成一个的hook对象保存对应的信息,这就保证在函数组件生命周期内状态不会丢失。

2.2 触发组件状态变更

上面我们聊过const [count, setCount] = useState(0) 中的setCount实际只是dispatchAction的别名。dispatchAction核心代码如下:

function dispatchAction(fiber, queue, action) {
    ...
    var alternate = fiber.alternate;
    // 生成一个update保存action
    var _update2 = {
      expirationTime: expirationTime,
      suspenseConfig: suspenseConfig,
      action: action,
      eagerReducer: null,
      eagerState: null,
      next: null
    };
    
    var last = queue.last;

    if (last === null) {
      // This is the first update. Create a circular list.
      _update2.next = _update2;
    } else {
      var first = last.next;

      if (first !== null) {
        // Still circular.
        _update2.next = first;
      }

      last.next = _update2;
    }
    queue.last = _update2;
  }
}

这里面每次调用dispatchAction都会生成一个udpate对象来保存状态。
源码里通过last、first、next等属性来保存update之间关系。看起来可能有点晕,我们还是用下面的样例来解释下调用dispatchAction后的数据结构变化:

export function HookCountComp() {
  const [count, setCount] = useState(0);

  const increase = () => {
    setCount(1);
    setCount(2);
    setCount(3);
    setCount(4);
    setCount(5);
  };

  return (
    <div>
      {count}
      <div onClick={increase}>+</div>
    </div>
  );
}

可以看到increate函数内部会触发五次setCount,我们看下每次setCount后queue的变化。

可以看到queue.last始终指向最新的update对象,update对象之间通过next链接在一起,我们可以把这些状态叫做pendingState,即准备好的状态,待下一次渲染时使用。

上面只是某一种情形,queue.last不总是指向这么一个单向循环链表。

var first = last.next;
if (first !== null) {
    _update2.next = first;
} 
last.next = _update2;
queue.last = _update2;

如果这个first == null , _update2.next没有被指向first,那么彼此间就无法形成闭环链表了,queue.last也只是指向最新的update。 在【2.3.2】中,在baseUpdate不为空的情况下,queue.last.next就会被置成null。

那么这个hook在下次渲染前有多次状态变更怎么表示出来了? 答案是last.next = _update2;, 即上一个update.next会指向最新update,而hook.baseUpdate.next会指向update链表上的first update。

2.3 界面重新渲染

组件状态发生改变后就会触发下一次渲染,【1.1】,【1.2】也会被重新执行,只是这个时候ReactCurrentDispatcher$1.current 已经指向HooksDispatcherOnUpdateInDEV。

HooksDispatcherOnUpdateInDEV.useState的主要逻辑在updateReducer中,我们看下源码:

function updateReducer(reducer, initialArg, init) {
  var hook = updateWorkInProgressHook();
  var queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  var last = queue.last;// last指向最新的update对象
  var baseUpdate = hook.baseUpdate;
  var baseState = hook.baseState; 
  // first指向第一个update对象
  var first;
  if (baseUpdate !== null) {
    if (last !== null) {
      last.next = null;
    }
    first = baseUpdate.next; 
  } else {
    first = last !== null ? last.next : null;//指向第一个update对象
  }

  if (first !== null) {
    var _newState = baseState;
    var newBaseState = null;
    var newBaseUpdate = null;
    var prevUpdate = baseUpdate;
    var _update = first;
    var didSkip = false;
    // 循环获取最新的值
    do {
        if (_update.eagerReducer === reducer) {
          _newState = _update.eagerState;
        } else {
          var _action = _update.action;
          _newState = reducer(_newState, _action);// 在useState中,reducer返回的就是action
        }
      }

      prevUpdate = _update;
      _update = _update.next;
    } while (_update !== null && _update !== first);
   
    newBaseUpdate = prevUpdate;
    newBaseState = _newState;
   
    if (!is$1(_newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }

    hook.memoizedState = _newState; // _newState是最新状态
    hook.baseUpdate = newBaseUpdate;// baseUpdate指向最新的update
    hook.baseState = newBaseState; // newBaseState是最新状态,和上面的baseUpdate对应
    queue.lastRenderedState = _newState; 
  }

我们在 【2.2.1 dispatchAction】 分析到所有变更的状态(update)可以通过 hook.queue 或者 hook.baseUpdate 找到,这些变更的状态会在updateReducer函数中依次执行 reducer(_newState, _action) , 这个默认的reducer函数就是这样:

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

可以看到默认reducer函数会把传入的action当做最新state返回,这个state最终作为useState函数的返回值返回给了函数组件,函数组件就完成了最新状态的渲染。

2.4 保证顺序一致性的必要性

hook有一条规则:只在最顶层使用 Hook,为啥会有这么条规则了?

假如FunctionCompont有调用了4个hook函数,相应地,react内部就为这个component生成了4个hook对象,每个hook对象保存对应的hook函数的状态信息。

那么下次函数组件渲染重新调用这4个hook函数,这些hook函数怎么找到对应的hook对象取出状态了?

也就是它们之间是如何映射的了?答案是hook函数在函数组件内的位置顺序。
第一个useState(0)取到的是Fiber内的first hook对象,第二个hook函数就会取到上一个hook的hook.next,以此类推。

可见如果在一个函数组件内部Hook函数位置不是固定的,那么就不能保证每次渲染时同一个位置hook函数能取到保存它状态的hook对象了。

所以只在最顶层使用 Hook,而不是在循环、条件语句中。

3 useReducer

这个是不是很熟悉,redux里面的reduer函数,通过action和preState生成新的state, useReducer()可谓是官方实现版了吧。看个demo加深下印象:

//useReducer版本  
const initialState = {count: 0};
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
  }
}
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

// useState版本
function Counter() {
  const [count, dispatch] = useState(count);

  const increase = () => {
    setCount(count + 1);
  };

  const decrease = () => {
    setCount(count - 1);
  };

  return (
    <>
      Count: {state.count}
      <button onClick={decrease}>-</button>
      <button onClick={increase}>+</button>
    </>
  );
}

看一下useReducer和useState是不是很类似,都是返回一个state和一个改变state的方法, 本质区别就是useState内部使用了一个默认的reduer,这个reduer实现如下:

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

可以看出useState会把传入的action会作为state返回回去。
useReducer需要我们传入一个自定义的reducer。
通过上面代码也可以看出,合理使用useReducer会使逻辑更加内聚,更好管控,后期维护也更加方便。

4 useEffect

useEffect为组件提供操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

当你在调用useEffect时,就是在告诉React在完成对DOM的更改后运行你的'副作用'函数,副作用函数还可以通过返回一个函数来指定如何’清楚‘副作用, 这样我们就能把组件内相关的副作用组织在一起了(如创建订阅和取消订阅),而不需要把他们放到不同的声明周期函数里面。

function Example() {
  const [count, setCount] = useState(0);

  // 相当于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 使用浏览器的 API 更新页面标题
    document.title = `You clicked ${count} times`;
    //组件被销毁时调用,相当于componetWillUnmount
    return () => {
        console.log('game over');
    }
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

我们通过mount 和 destroy两个阶段来分析useEffect;

4.1. mount

4.1.1 mountEffectImpl

在组件mount阶段,useEffect最终会调用mountEffectImpl,我们看下代码:

// create是我们传入的回调函数; deps是函数的依赖,可写可不写,但会影响create函数是否被调用。
function mountEffect(create, deps) {
  return mountEffectImpl(Update | Passive, UnmountPassive | MountPassive, create, deps);
}
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
    // 详见[2.1.4]中为每个Hook函数生成对应的hook对象。
  var hook = mountWorkInProgressHook();
  // 记录当前依赖
  var nextDeps = deps === undefined ? null : deps;
  // sideEffectTag会影响fiber.effectTag, 后面再聊
  sideEffectTag |= fiberEffectTag;
  // 详见[4.1.2]
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
4.1.2 pushEffect
function pushEffect(tag, create, destroy, deps) {
    // 为这个useEffect生成effect对象保存信息
  var effect = {
    tag: tag,
    create: create,
    destroy: destroy,
    deps: deps,
    // Circular
    next: null
  };

    // 收集所有effect对象到componentUpdateQueue上, componentUpdateQueue会保存到fiber.updateQueue上。
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    var lastEffect = componentUpdateQueue.lastEffect;

    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      var firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

这个componentUpdateQueue最终是由update对象组成的环状链表:

4.1.3 finishSyncRender

渲染的入口函数performSyncWorkOnRoot包含两个部分:

performSyncWorkOnRoot() {
    workLoopSync();
    finishSyncRender(root, workInProgressRootExitStatus, expirationTime);
}

workLoopSync()内部循环遍历虚拟dom,完成dom节点的mount、update、unmount的处理流程,我们函数组件的处理函数renderWithHook也只是workLoopSync内部处理函数组件的一个分支。

finishSyncRender()就是在dom渲染完毕后执行函数,通过相关调用到flushPassiveEffects()函数来判断是否需要执行副作用函数。

4.1.4 flushPassiveEffects
function flushPassiveEffects() {
  if (pendingPassiveEffectsRenderPriority !== NoPriority) {
    var priorityLevel = pendingPassiveEffectsRenderPriority > NormalPriority ? NormalPriority : pendingPassiveEffectsRenderPriority;
    pendingPassiveEffectsRenderPriority = NoPriority;
    return runWithPriority$2(priorityLevel, flushPassiveEffectsImpl);
  }
}

要进入到条件语句内部比如要满足pendingPassiveEffectsRenderPriority !== NoPriority, 那么pendingPassiveEffectsRenderPriority在什么时候被赋值了? 全局搜了下,在commitRootImpl()内部有段赋值逻辑:

var rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
  if (rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = false;
    rootWithPendingPassiveEffects = root;
    pendingPassiveEffectsExpirationTime = expirationTime;
    pendingPassiveEffectsRenderPriority = renderPriorityLevel;
  } 

就是说rootDoesHavePassiveEffects === true的情况下pendingPassiveEffectsRenderPriority会被赋值,那么rootDoesHavePassiveEffects又在什么时候为true了?搜索下,在commitBeforeMutationEffects()存在这样一段逻辑:

function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    var effectTag = nextEffect.effectTag;
    if ((effectTag & Passive) !== NoEffect) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        scheduleCallback(NormalPriority, function () {
          flushPassiveEffects();
          return null;
        });
      }
    }

    nextEffect = nextEffect.nextEffect;
  }
}

nextEffect是在这次渲染中发生update的fiber节点,nextEffect.effectTag是不是有点印象,我们在【 4.1.1 mountEffectImpl 】分析中可以看到了调用useEffect函数会把 Update | Passive赋值给fiber.effectTag, 这样就满足了 (effectTag & Passive) !== NoEffect这个条件,rootDoesHavePassiveEffects就会被赋值为true, 接着flushPassiveEffects()会被放到scheduleCallback中等待被调度执行。

4.1.5 flushPassiveEffectsImpl
function flushPassiveEffectsImpl() {
    ...
  var effect = root.current.firstEffect;

  while (effect !== null) {
    {
      setCurrentFiber(effect);
      // 详见[ 4.1.6 commitPassiveHookEffects ]
      invokeGuardedCallback(null, commitPassiveHookEffects, null, effect);
    }

    var nextNextEffect = effect.nextEffect; 
    effect.nextEffect = null;
    effect = nextNextEffect;
  }
    ...
  return true;
}
4.1.6 commitPassiveHookEffects
function commitPassiveHookEffects(finishedWork) {
  if ((finishedWork.effectTag & Passive) !== NoEffect) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent:
        {
          commitHookEffectList(UnmountPassive, NoEffect$1, finishedWork);
          commitHookEffectList(NoEffect$1, MountPassive, finishedWork);
          break;
        }

      default:
        break;
    }
  }
}

由上面的分析得知,(finishedWork.effectTag & Passive) !== NoEffect这个条件语句成立,并且我们是个FunctionComponent,所以接着会调用两次参数不同的commitHookEffectList。

4.1.7 commitHookEffectList
function commitHookEffectList(unmountTag, mountTag, finishedWork) {
  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;

    do {
      if ((effect.tag & unmountTag) !== NoEffect$1) {
        var destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }

      if ((effect.tag & mountTag) !== NoEffect$1) {
        var create = effect.create;
        effect.destroy = create();
      }

      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

这个才是执行副作用的地方。函数有三个参数,前两个分别是unmountTag和 mountTag,顾名思义,分别代表组件卸载tag和组件装载tag。结合【4.1.6】传入的参数得总结如下:
1 ) 组件mount:

unmountTag mountTag effect.tag 执行情况
UnmountPassive NoEffect$1 UnmountPassive | MountPassive destroy为undefined,不执行destroy()
NoEffect$1 MountPassive$1 MountPassive 执行create(),并将返回值赋值给effect.destroy

2 ) 组件update时, 依赖没有变化,此时effect.tag会置成NoEffect$1 :

unmountTag mountTag effect.tag 执行情况
UnmountPassive NoEffect$1 NoEffect$1 跳过
NoEffect$1 MountPassive$1 NoEffect$1 跳过

3 ) 组件update时, 依赖发生变化:

unmountTag mountTag effect.tag 执行情况
UnmountPassive MountPassive$1 UnmountPassive | MountPassive 上一个destroy不为undefined的话,则先执行destroy(), 再执行create(), 并将最新的返回值赋值给effect.destroy

4.2 unmount

当组件被销毁时,会被标记为Deletion。commitUnmount函数内部会有副作用函数的处理逻辑:

function commitUnmount(finishedRoot, current$$1, renderPriorityLevel) {
  onCommitUnmount(current$$1);

  switch (current$$1.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent:
      {
        var updateQueue = current$$1.updateQueue;

        if (updateQueue !== null) {
          var lastEffect = updateQueue.lastEffect;
          if (lastEffect !== null) {
            ...
            runWithPriority$2(priorityLevel, function () {
              var effect = firstEffect;
              do {
                var destroy = effect.destroy;

                if (destroy !== undefined) {
                  safelyCallDestroy(current$$1, destroy);
                }

                effect = effect.next;
              } while (effect !== firstEffect);
            });
          }
        }

        break;
      }
      ...
  }
}

5 useCallback

调用useCallback返回一个 memoized 回调函数。我们看下代码实现:

// 1.组件mount阶段
function mountCallback(callback, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;//直接返回当前的callback回到函数。
}

// 2.0 组件udpate阶段
function updateCallback(callback, deps) {
  var hook = updateWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      var prevDeps = prevState[1];
      // 2.1如果依赖数组没有发生变化,则返回缓存中的callback回调函数
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  // 2.2 依赖发生变化,则返回新的callback.
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

如官方所说,当你把回调函数传递给经过优化并且使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它会非常有用。我们看下对比就能理解的更加清楚了:

// 未优化前
function Parent() {
  const [count, setCount] = useState(0);
  const changeCount = () => {
    setCount(c => c + 10);
  };

  return (
    <Child changeCount={changeCount}/>
  );
}

class Child extends Component {

    shouldComponentUpdate(nextProps, nextState) {
      if (this.props.changeCount != nextProps.changeCount) {
        return true;
      } else {
        return false;
      }
  }

  render() {
    console.log("child render");
    const { changeCount } = this.props;
    return (
      <div
        onClick={() => {
          changeCount();
        }}
      >
        changeCount
      </div>
    );
  }
}

每次点击changeCount, Child都会打印'child render',虽然我们在shouldComponentUpdate里面比较了前后props的参数来避免不必要渲染, 但是每次Parent渲染时, changeCount指向的都是一个新的函数,导致Child的shouldComponentUpdate不能发挥作用。
下面我们稍微改动下Parent:

function Parent() {
  const [count, setCount] = useState(0);
  const changeCount = useCallback(() => {
    setCount(c => c + 10);
  }, []);

  return (
    <Child changeCount={changeCount}/>
  );
}

改动后后,点击changeCount, Child不会再打印了。
这是因为Parent中的changeCount始终指向的都是同一个引用, 这样Child的shouldComponentUpdate就能发挥我们给它的职责了。

6 useMemo

官方给了这样一个样例:

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

第一眼看上去和useCallbakc很像:

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

都是传入一个fn和依赖列表作为参数, 区别是useMemo返回的是运行fn函数后的返回值,而useCallback返回的就是fn,也就是说:
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

我们看下源码是不是这样:

// 1.组件mount时
function mountMemo(nextCreate, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var nextValue = nextCreate(); // 运行传入的函数参数,并保存值
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue; // 返回函数的运行结果。
}

// 2.组件update时
function updateMemo(nextCreate, deps) {
  var hook = updateWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var prevState = hook.memoizedState;

  if (prevState !== null) {
    if (nextDeps !== null) {
      var prevDeps = prevState[1];
      // 依赖没有发生变化时,直接返回上次运行的结果
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  // 依赖发生变化,重新运行函数,并返回结果
  var nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

useMemo()内部会执行传入的nextCreate函数,保存并返回函数的返回值。组件下次渲染的时候,如果依赖没有发生变化,则直接返回上次计算的值,而不会重新计算,我们可以利用useMemo这个特性来避免每次渲染时都进行高开销的计算。

在ClassComponent中我们可以利用shouldComponentUpdate(nextProps, nextState)来避免组件的不必要渲染。对于FunctionComponent我们可以用React.memo()也可以达到同样的效果。下面的例子就是向React.memo传入了一个FunctionComponent和一个compare函数,来避免无效渲染:

const CountButton = React.memo(
  ({ onClick, count }) => {
    console.log("CountButton");
    return <button onClick={onClick}>{count}</button>;
  },
  (preProps, nextProps) => {
    if (
      preProps.onClick === nextProps.onClick &&
      preProps.count === nextProps.count
    ) {
      return true;// true代表无需重新渲染, 和shouldComponentUpdate相反。
    }
    return false; // false 代表需要重新渲染
  }
);

只要onClick的引用和count值没有改变,CountButton都不会进行无效渲染。

我们用useMemo也能实现React.memo()效果:

const CountButtonNoMemo = ({ onClick, count }) => {
  console.log("CountButton");
  return <button onClick={onClick}>{count}</button>;
};

function DualCounter() {
  const [count1, setCount1] = React.useState(0);
  const increment1 = useCallback(() => setCount1(c => c + 1), [count1]);

  const [count2, setCount2] = React.useState(0);
  const increment2 = useCallback(() => setCount2(c => c + 1), [count2]);

  return (
    <>
      {React.useMemo(() => {
        return <CountButtonNoMemo count={count1} onClick={increment1} />;
      }, [count1])}
      <CountButtonNoMemo count={count2} onClick={increment2} />
    </>
  );
}

用React.useMemo()包裹的CountButtonNoMemo组件只有在count1发生变化时,才会重新发生渲染。 作为对比的没有用React.useMemo包裹的CountButtonNoMemo只要上层组件DualCounter发生渲染,它都会重新渲染,这显然不是太好。
我们通过源码看看差异在什么地方的:

function beginWork$1(current$$1, workInProgress, renderExpirationTime) {
  var updateExpirationTime = workInProgress.expirationTime;
  // current$$1 不为空,进入内部逻辑
  if (current$$1 !== null) {
    var oldProps = current$$1.memoizedProps;
    var newProps = workInProgress.pendingProps;
    
    if (oldProps !== newProps || hasContextChanged() || ( 
    workInProgress.type !== current$$1.type)) {
      // [ 6.1 ] 
      didReceiveUpdate = true;
    } else if (updateExpirationTime < renderExpirationTime) {
      // [ 6.2 ] 
      didReceiveUpdate = false; 
      return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);
    } else {
      didReceiveUpdate = false;
    }
  } else {
    didReceiveUpdate = false;
  } 

  workInProgress.expirationTime = NoWork;
  // 【6.3 Component重新渲染逻辑】
  switch (workInProgress.tag) {
    ...
    case FunctionComponent:
      {
        var _Component = workInProgress.type;
        var unresolvedProps = workInProgress.pendingProps;
        var resolvedProps = workInProgress.elementType === _Component ? unresolvedProps : resolveDefaultProps(_Component, unresolvedProps);
        return updateFunctionComponent(current$$1, workInProgress, _Component, resolvedProps, renderExpirationTime);
      }
    ...
}

这个判断fiber是否需要执行,我们解释下每一个条件分支.

6.1 判断组件的props、context、type是否变化,我们重点关注props
if (oldProps !== newProps || hasContextChanged() || ( 
    workInProgress.type !== current$$1.type)) {
      didReceiveUpdate = true;
    } 

第一个判断条件oldProps !== newProps什么时候成立了?
我们通常写React组件用的都是JSX:

function() {
  return (
    <CountButtonNoMemo count={count1} onClick={increment1}/>
  )
}

JSX实际就是一种语法糖,翻译过来应该是这样:

function() {
  return { type: CountButtonNoMemo, props: { count: count1, onClick: increment1} };
}

所以当CountButtonNoMemo的上层组件重新渲染时, CountButtonNoMemo的props都会被赋值为一个新对象,oldProps !== newProps就成立了,didReceiveUpdate会被置成true, 接着会走上面【6.3 Component重新渲染逻辑】,CountButtonNoMemo组件就会被重新渲染了。

当我们用React.useMemo包裹了下CountButtonNoMemo,

function() {
  return (
    {React.useMemo(() => {
        return <CountButtonNoMemo count={count1} onClick={increment1} />;
      }, [count1])}
    )
}

翻译后(这个memo变量只是为了表达对象被保存起来这个概念,在源码中并不存在这个变量)

var memo = { type: CountButtonNoMemo, props: { count: count1, onClick: increment1}};
function() {
  return memo;
}

可以看到CountButtonNoMemo会被保存到缓存memo中,只要依赖的count1没有发生变化,memo对象都会保持不变,属性props自然不会发生变化。
所以用React.useMemo包裹的CountButtonNoMemo在依赖count1没有发生变化的情况下,oldProps !== newProps不成立,也就不会发生重新渲染。

6.2 当前组件不需要重新渲染
if (updateExpirationTime < renderExpirationTime) {
      didReceiveUpdate = false; 
      return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);
} 

// bailoutOnAlreadyFinishedWork
function bailoutOnAlreadyFinishedWork(current$$1, workInProgress,           renderExpirationTime) {
  var updateExpirationTime = workInProgress.expirationTime;

  var childExpirationTime = workInProgress.childExpirationTime;
  
  if (childExpirationTime < renderExpirationTime) {
    // 后代没有状态变更,直接返回null。
    return null;
  } else {
    // 后代里面有发生了状态变更,返回后代。
    cloneChildFibers(current$$1, workInProgress);
    return workInProgress.child;
  }
}

组件状态发生变化时,组件对应的fiber的expirationTime会被赋值为SYNC (【2.1.1】源码中有涉及), 同时该组件的所有上层组件的对应的fiber的childExpirationTime也会被赋值为SYNC,这样在遍历过程中React就知道哪条路径下有组件需要重新渲染了。

updateExpirationTime < renderExpirationTime, 说明此组件没有状态变化,不需要重新渲染, 接着就会走到bailoutOnAlreadyFinishedWork函数里面。

在ailoutOnAlreadyFinishedWork函数里面, 会判断下后代是否有状态变更,确定是否需要遍历后代节点重新渲染。

7 参考文章

zh-hans.reactjs.org/docs/hooks-…

juejin.cn/post/684490…

jancat.github.io/post/2019/t…