React Hooks的闭包陷阱

前言

React有两种组件形式:class组件和函数组件,class组件代表面向对象的编程思想,函数组件代表着函数式编程的思想,React Hooks就是带有Hook的函数组件,Hook机制赋予函数组件具有状态变量以及生命周期的功能。

使用React Hooks常见的问题就是闭包陷阱,从表现形式来看就是Hook内部使用的State数据不是最新状态而是旧的状态。本文旨在梳理清楚闭包陷阱产生的原因,React源码版本18.3.0.

闭包陷阱产生的原因

React Hooks本质就是函数,Hooks的处理逻辑按照阶段的不同而不同,大体分为挂载阶段和更新阶段,根据源码分析的结果逻辑如下:

  • 挂载阶段:Hooks的主要逻辑就是在创建hook对象并且将其挂载到对应的Fiber节点上
  • 更新阶段:Hooks的主要逻辑是否更新hook对象

Hooks底层创建的hook对象非常重要,hook对象的结构如下:

var hook = {
  memoizedState: null,
  baseState: null,
  baseQueue: null,
  queue: null,
  next: null
};

memoizedState属性是非常重要的,它缓存这上一次渲染时的相关数据,这里以useCallback实例为例来说明,下面是一种闭包陷阱的实例:

function App() {
  const [timeState, setTimeState] = useState(0)
  const handleClick = useCallback(() => {
    setTimeState(Date.now() + timeState === 0 ? 1000 : 0)
  }, [])
  
  return <h1>Hello {{timeState}}</h1>;
}

useCallback对应生成的hook对象的memoizedState属性值就是如此:

hook.memoizedState = [callback, nextDeps];

以上面实例做说明,当点击页面元素时:

  • 首先就会触发setTimeState从而更改App函数组件
  • React内部流程就会去更新函数组件,即调用updateFunctionComponent,从而会导致App函数组件会再次运行
  • 当执行useState时,发现timeState更新了值,就会在内部创建新的hook对象
  • 当执行到useCallback时,此时是更新阶段在源码中就会调用updateCallback,这里是闭包陷阱真正产生的逻辑处理

updateCallback源码如下:

  function updateCallback(callback, deps) {
    var hook = updateWorkInProgressHook();
    var nextDeps = deps === undefined ? null : deps;
    var prevState = hook.memoizedState;

    if (nextDeps !== null) {
      var prevDeps = prevState[1];

      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }

    hook.memoizedState = [callback, nextDeps];
    return callback;
  }

逻辑还是很清晰的:对于依赖项没有发生变更的就返回memoizedState保存的旧的回调函数,如果发生的变更就会创建返回当前传入的新的回调函数,并且更新当前hook的memoizedState值。

上面逻辑就是闭包陷阱产生的根源,由于依赖项对比没有变更导致useCallback返回的还是旧的回调处理函数,这里涉及到JavaScript闭包和作用域链等相关概念:

  • 函数组件本质就是函数,每一次函数执行都会创建执行上下文环境
  • 闭包是什么?闭包是函数和周围词法环境的结合

函数组件每一次执行内部都有一份独立的变量集(执行上下文+作用域链),当State更新后函数组件重新执行,由于依赖项对比没有变更导致useCallback返回的还是旧的回调处理函数,其内部引用的timeState还是旧的词法环境中的变量,而当前的词法环境中timeState则是变更后的值,仅此而已。

由于React的成功以及新概念的层出不穷,有时容易给人误解闭包陷阱是不是内部又实现了啥高大上的逻辑,实际上内部并没有做什么复杂的处理,只不过利用Fiber架构保存旧State数据状态而已。

React Hooks闭包陷阱就是Hooks缓存机制下JavaScript语言的闭包特性导致的变量值不对的问题,本质还是对闭包的应用,Fiber架构部分的复杂性并不影响对其的理解。

useRef

闭包陷阱对于useRef Hook来说是不存在的,该API也是常常用于解决闭包陷阱以及性能优化的方式之一。为什么useRef没有闭包陷阱?答案很简单,因为useRef没有缓存机制并且其hook对象的memoizedState属性值也是特别的,具体代码如下:

 // 挂载阶段处理
 function mountRef(initialValue) {
    var hook = mountWorkInProgressHook();

    {
      var _ref2 = {
        current: initialValue
      };
      hook.memoizedState = _ref2;
      return _ref2;
    }
  }
  // 更新阶段处理
  function updateRef() {
    var hook = updateWorkInProgressHook();
    return hook.memoizedState;
  }

memoizedState是一个包含current属性的引用对象,而且需要注意的是每次函数组件执行时,useRef返回的始终都是同一个引用对象,从而不会状态丢失。

你可能感兴趣的:(React相关,react.js,javascript,前端)