前言
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返回的始终都是同一个引用对象,从而不会状态丢失。