Skip to content

在前面的文章中我们学习了useStateuseEffectuseLayoutEffect的基本原理,并看了源码了解了它的执行过程,而本篇文章我们继续学习react常用的hooks

一、useMemo & useCallback

这两个 hook 的原理基本上是差不多的,我们可以一起来介绍,和前面我们介绍的 hooks 一样,分为初始化和更新两种场景

初始化

useMemo的初始化会调用mountMemo

js
function mountMemo(nextCreate, deps) {
  var hook = mountWorkInProgressHook(); // 创建当前的hook对象,并且接在fiber的hook链表后面
  var nextDeps = deps === undefined ? null : deps;
  var nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

mountWorkInProgressHook在上一篇已经分析过了,大部分的hook初始化时都要调用这个来创建自己的hook对象,但是也会有例外的情况,比如useContext,我们后面再说;第一次执行useMemo都要调用用户提供的函数,得到需要缓存的值,将依赖和值都放在hookmemoizedState身上

useCallback的初始化会调用mountCallback

js
function mountCallback(callback, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

可以看到唯一的区别就是useCallback会把传递进来的函数直接缓存起来,而不进行调用求值,经过初始化后组件对应的 fiber 节点上就保存着对应的hook信息,而缓存的函数和值也会被保存在这个hook

更新

useMemo在更新时实际上会调用updateMemo,它的实现如下:

js
function updateMemo(nextCreate, deps) {
  var hook = updateWorkInProgressHook(); // 基于current创建workInProgress的hook对象
  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对象中
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

每次更新的时候,都会通过areHookInputsEqual来判断依赖是否发生了变化,areHookInputsEqual会比较这个数组中的每一项,看是否与原来的保持一致,有任何一个不同都会返回false,导致重新计算。

js
function areHookInputsEqual(nextDeps, prevDeps) {
  for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (objectIs(nextDeps[i], prevDeps[i]) /*判断是否相等*/) {
      continue;
    }
    return false;
  }
  return true;
}

缓存的核心原理就是workInProgress的 hook 对象中的memoizedState是直接复用的原来的hook对象,因此相关的信息得以被完整的保存下来,只有在需要更新的时候才进行替换 ,useCallback的更新逻辑和useMemo的逻辑是一样的,在这里就不多花更多的篇幅去介绍了

二、useRef

接下来我们来看一下useRef的基本原理,我们先来回顾一下useRef的作用,它是一个用于保存数据的引用,可以作为基本类型、复杂类型、DOM 元素、类组件实例等数据的引用,用于存储的值,在组件更新过程中始终保持一致,因此非常适合用于保存需要持久化的数据。

初始化

初始化时会通过mountRef来创建引用对象

js
function mountRef(initialValue) {
  var hook = mountWorkInProgressHook(); // 创建hook对象
  {
    var _ref2 = {
      // 创建ref对象
      current: initialValue,
    };
    hook.memoizedState = _ref2; //将其保存在hook的memoizedState上
    return _ref2; // 返回
  }
}

初始化的逻辑很简单,创建一个ref对象,将其保存在对应hookmemoizedState属性身上。

更新时

js
function updateRef(initialValue) {
  var hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

ref的更新就更加简单了,直接返回原来的引用就好,因为hook的信息都是基于老的hook直接复用的,因此信息还是原来的信息,所以在整个 react 运行时过程中,这个引用就像一个静态的变量一样,永远被持久的存储了下来。

DOM 元素&类组件实例

在我们专栏的《深入理解 react》之 commit 阶段 这篇文章中我们有分析过 ref 在有些特殊情况下会将一些特殊信息存储下来,例如 DOM 元素或者类组件实例的情况

js
...
const ref = React.useRef();

<h1 id="h1" ref={ref}>hello</h1>
或者
<ClassComponent ref={ref}/>
或者
<FunctionComponent ref={ref}/>
...

创建 Ref 引用的过程发生在render阶段,以上几种情况都会给当前的组件的 fiber 上打上Ref的标签,等到commit阶段处理,处理的逻辑就是将相关的信息赋值到对应的 ref 引用上达到持久存储的目的。

在 commit 阶段会通过commitAttachRef来将fiber身上的stateNode属性的信息赋值给引用对象上,对于类式组件来说就是实例对象;对于原生元素来说,就是 DOM 元素。

当然对于函数式组件来说,就是useImperativeHandle返回的对象,我们后面再去了解它是如何做到的

三、useContext

useContext相信大家在工作中经常用到,它可以很方便的将状态提升到更上层,然后在任意子孙组件都可以消费状态信息,避免层层传递props而导致的尴尬境地,接下来我们就来研究它是如何实现的吧!

在使用useContext之前我们得有一个context吧,因此先来看一下React.createContext()做了什么吧!

js
function createContext(defaultValue) {
    var context = { // 创建一个context对象,就是长下面这个样子
      $$typeof: REACT_CONTEXT_TYPE,
      _currentValue: defaultValue,
      _currentValue2: defaultValue,
      _threadCount: 0,
      Provider: null,
      Consumer: null,
      _defaultValue: null,
      _globalName: null,
    };
    context.Provider = { // Provider类型的组件,提供者
      $$typeof: REACT_PROVIDER_TYPE,
      _context: context,
    };

    {
      var Consumer = { // Context类型的组件,消费者
        $$typeof: REACT_CONTEXT_TYPE,
        _context: context,
      };
      // 给Consumer绑定一些属性
      Object.defineProperties(Consumer, {
        Provider: {
          get: function () {
            return context.Provider;
          },
          set: function (_Provider) {
            context.Provider = _Provider;
          },
        },
        ...
        Consumer: {
          get: function () {
            return context.Consumer;
          },
        },
      });
      context.Consumer = Consumer;
    }
    // 返回这个context
    return context;
}

我保留了核心的 context 创建过程, 可以看的出来还是比较容易理解的,在context的内部有ProviderConsumer,它们都是ReactElement类型的对象,可以直接在用户层使用 JSX 来消费,根据逻辑我们可以看的出来contextProvider以及Consumer都是互相引用着的

一般来说这个创建 context 的过程是最先发生的,紧接着会先触发Providerrender阶段,最后再触发useContext,因为我们知道useContext需要在renderWithHooks中执行,而renderWithHooks是发生在beginWork过程的,因此它是自上而下的这么一个顺序

Provider

Provider是一个ReactElement类型的元素,它拥有属于一类的 fiber 类型,在它的父节点被调和的时候,它对应的 fiber 节点也会被创建出来,对应的tag类型是10

js
export const ContextProvider = 10;

我们在使用Provider的时候,同时也会将自定义信息注入进来

js
<Provider value={{... }}>
  <.../>
</Provider>

此时也会被保存在Provider类型的fiberpendingProps身上,在真正调和这个Provider的时候会进入updateContextProvider进行处理

js
function updateContextProvider(current, workInProgress, renderLanes) {
    var providerType = workInProgress.type; // 就是context信息 { _context:context , $$typeof: xxx }
    var context = providerType._context;
    var newProps = workInProgress.pendingProps;
    var newValue = newProps.value; // 用户给定的
    pushProvider(workInProgress, context, newValue);
    ...
    return workInProgress.child;
}

Provider身上会有context的信息,因为它们互相引用着

image.png

然后在这里面会调用pushProvider(workInProgress, context, newValue);,这里面会将用户给定的值赋值给context中的_currentValue保存起来

js
function pushProvider(providerFiber, context, nextValue) {
   ...
   context._currentValue = nextValue;
}

自此之后提供者任务完成,将一个上层的状态和方法保存在了context这个公共区域之中,接下来就是下层如何进行消费

useContext

我们可以使用useContext来消费上层的状态和其他 hook 不同的一点是,无论初始化还是更新阶段,都是调用的readContext来获取相关的信息

js
function readContext(context) {
    var value =  context._currentValue ; // 直接取出context
    ...
    {
      var contextItem = {
        context: context,
        memoizedValue: value,
        next: null
      };

      if (lastContextDependency === null) {
        // 如果是第一个 useContext
        lastContextDependency = contextItem;
        currentlyRenderingFiber.dependencies = { // context 信息是放在dependencies属性上的
          lanes: NoLanes,
          firstContext: contextItem
        };
      } else {
        // 如果有多个,形成单向链表
        lastContextDependency = lastContextDependency.next = contextItem;
      }
    }
    return value;
}

通过上面的分析我们可以知道,useContext并非和之前的hook一样会在fibermemoizedState上形成一个链表,而是会在dependencies属性上形成一个链表,假设我们用了两个useContext来获取上层的信息

js
function App (){
  const context1 = useContext(Context1);
  const context2 = useContext(Context2);

  return (...)
}

那么对应的 Fiber 结构就应该是这一个样子的

image.png

由于beginWork是自上而下的,因此在reactContext获取状态时,值早已在祖先节点上被更新为了最新的状态,因此在使用useContext时消费的也是最新的状态

如果从useContext的地方触发了更新,由于触发的更新的setXXX是由祖先节点提供的,实际上会从祖先节点开始发起更新,从祖先组件的整棵子树都会被重新reder,如下图所示:

image.png

Consumer

当然除了使用useContext我们还可以通过Consumer这样的方式来进行消费,用法如下:

js
import AppContext from "xxx";

const Consumer = AppContext.Consumer;

function Child() {
  return <Consumer>{(value) => xxx}</Consumer>;
}

render阶段中当beginWork来到了Consumer类型的节点时,会触发updateContextConsumer

js
function updateContextConsumer(current, workInProgress, renderLanes) {
  var context = workInProgress.type; //Consumer类型的fiber将context信息存贮在type属性上
  context = context._context;
  var newProps = workInProgress.pendingProps; // 获取porps
  var render = newProps.children;

  {
    if (typeof render !== "function") {
      // 意味着被Consumer包括的必须是个函数
      报错;
    }
  }

  var newValue = readContext(context); // 依然是调用readContext
  var newChildren;

  newChildren = render(newValue); // 这样就把最新的状态交给下层去消费了

  reconcileChildren(current, workInProgress, newChildren, renderLanes); // 继续调和子节点
  return workInProgress.child;
}

可以看到实际上Consumer内部依然是通过readContext来获取context信息的,原理和useContext一致

小结
通过上面的分析我们可以得出一个结论,context最基本的原理就是利用beginWork自上而下进行这样的特点,将状态通过上层先存贮第三方,然后下层的节点因为后进行beginWork就可以无忧的消费提存存贮在第三方的状态了,而这个第三方实际上就是我们的context

四、useImpertiveHandle

useImpertiveHandle这个 hook 的作用想必大家都知道,函数式组件本身是没有实例的,但是这个hook可以让用户自定义一些方法暴露给上层的组件使用,我们来看看它是怎么做的

初始化时

初始化时useImpertiveHandle执行的是mountImperativeHandle

js
function mountImperativeHandle(ref, create, deps) {
  // 这个ref实际上就是上层组件的一个ref引用{ current:xxx }
  // 其实本质上调用的是mountEffectImpl
  var effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;
  var fiberFlags = Update;
  //因为传入的是Layout, 所以实际上和useLayoutEffect的执行时机一样
  return mountEffectImpl(
    fiberFlags,
    Layout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps
  );
}

在上一篇中我们有分析effect类型的 hook 的执行时机以及原理等,如果忘了可以复习一下 《深入理解 react》之 hooks 原理(上),我们可以看到这个实际上和上一篇文章中提到的useLayoutEffect执行时机是一样的,都是在Mutation阶段同步执行,唯一的区别就是useLayoutEffect执行的是用户自定义的函数,而useImpertiveHandle执行的是imperativeHandleEffect.bind(null, create, ref)

js
function imperativeHandleEffect(create, ref) {
  var refObject = ref;
  {
    if (!refObject.hasOwnProperty("current")) {
      // 引用必须具有 current属性
      error("报错");
    }
  }

  var _inst2 = create(); // 调用用户提供的函数,得到的是一个对象,用户可以在这个对象上绑定一些子组件的方法 { fun1, fun2 ,... }

  refObject.current = _inst2; // 赋值给父组件的引用
  return function () {
    // 并且提供销毁函数,方便删除这个引用
    refObject.current = null;
  };
}

可以看到,整体还是比较好理解的,本质上就是把父组件传下来的 ref 引用赋个值而已,这样父组件的 ref 就能够使用子组件的方法或者状态了,实际上通过上面的分析如果你不想要使用imperativeHandleEffect,使用下面的降级方式,效果完全相同

js
function Child(props , ref){
  useLayoutEffect(()=>{

    ref.current = { // 当deps发生改变的时候,直接给ref.current赋新值就好了

    }

  } , [deps])

  return (...)
}

更新时

更新时执行的是updateImperativeHandle

js
function updateImperativeHandle(ref, create, deps) {
  // 将ref的引用添加为依赖
  var effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;
  // updateEffectImpl 和 imperativeHandleEffect 我们都分析过了
  return updateEffectImpl(
    Update,
    Layout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps
  );
}

在上一篇中我们提到过updateEffectImpl在依赖不变时会传入不同标识,方便commit阶段区分出来然后跳过执行,这里也是一样的

当依赖未产生变化时 imperativeHandleEffect 便不会执行,ref还是原来的信息;只有当依赖变化才会重新赋最新的值

五、最后的话

本篇文章我们学习了useMemouseCallbackuseContextuseImperativeHandleuseRef , 加上前面的文章,这么算下来我们已经把react目前发布了的hooks学了一大半了,而且基本常用的hook都已经了解了

image.png

当然还有一部分我们还没有学习,我们将在后面的文章中将其作为新特性来进行剖析,毕竟相信大家和笔者一样,剩下的hook用的频率并不高,所以一起期待后续的文章吧!

后面的文章我们会依然会深入剖析 react 的源码,学习 react 的设计思想,如果你也对 react 相关技术感兴趣请订阅我的《深入理解 react》专栏,笔者争取至少月更一篇,我们一起进步,有帮助的话希望朋友点个赞支持下,多谢多谢!

遵循MIT开源协议