《深入理解react》之hooks原理(下)

在前面的文章中我们学习了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的信息,因为它们互相引用着

然后在这里面会调用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结构就应该是这一个样子的

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

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

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都已经了解了

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

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

相关推荐
余生H17 分钟前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍19 分钟前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai24 分钟前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默36 分钟前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_857297911 小时前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_1 小时前
meta标签作用/SEO优化
前端·javascript·html
Ink1 小时前
从底层看 path.resolve 实现
前端·node.js
金灰1 小时前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5
茶卡盐佑星_1 小时前
说说你对es6中promise的理解?
前端·ecmascript·es6
Promise5201 小时前
总结汇总小工具
前端·javascript