「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」
react 版本:v17.0.3
1、useRef 简介
useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。
我们通常会使用 useRef 来绑定DOM
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
如果我们想在组件中存储某个状态值,却又不想引起组件重新渲染,那么我们可以使用 useRef 。因为 useRef 可以很方便地保存任何可变值,当 ref 对象发生变化时,不会引发组件重新渲染。
那么,useRef 是如何实现 ref 对象发生变化而不引发组件重新渲染的呢?下面,我们来看看 useRef 的源码。
2、Hook 入口
在 React Hooks 源码解读之Hook入口 一文中,我们介绍了 Hooks 的入口及hook处理函数的挂载,从 hook 处理函数的挂载关系我们可以得到这样的等式:
-
挂载阶段:
useRef = ReactCurrentDispatcher.current.useRef = HooksDispatcherOnMount.useRef = mountRef;
-
更新阶段:
useRef = ReactCurrentDispatcher.current.useRef = HooksDispatcherOnUpdate.useRef = updateRef;
因此,组件在挂载阶段,执行 useRef,其实执行的是 mountRef,而在更新阶段时,则执行的是 updateRef 。
3、挂载阶段
组件在挂载阶段,执行 useRef,实际上执行的是 mountRef,下面我们来看看 mountRef 的实现。
3.1 mountRef
// packages/react-reconciler/src/ReactFiberHooks.new.js
function mountRef<T>(initialValue: T): {| current: T |} {
// 创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表
const hook = mountWorkInProgressHook();
if (enableUseRefAccessWarning) {
if (__DEV__) {
// dev 部分代码可忽略不看
// Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变
// 将创建的 ref 对象密封起来,阻止向对象添加新属性
Object.seal(ref);
// 将 ref 对象缓存到 hook 对象的 memoizedState 属性上
hook.memoizedState = ref;
// 返回一个可变的 ref 对象,其属性 current 发生变化时,不会引发组件重新渲染
return ref;
} else {
// 创建 ref 对象,其 current 属性初始化为传入的参数(initialValue)
const ref = { current: initialValue };
// 将 ref 对象缓存到 hook 对象的 memoizedState 属性上
hook.memoizedState = ref;
// 返回一个可变的 ref 对象,其属性 current 发生变化时,不会引发组件重新渲染
return ref;
}
} else {
// 创建 ref 对象,其 current 属性初始化为传入的参数(initialValue)
const ref = { current: initialValue };
// 将 ref 对象缓存到 hook 对象的 memoizedState 属性上
hook.memoizedState = ref;
// 返回一个可变的 ref 对象,其属性 current 发生变化时,不会引发组件重新渲染
return ref;
}
}
可以看到, mountRef 的实现十分简单。首先会创建一个 hook 对象,该 hook 对象将会被添加到 workInProgressHook 单向链表中,接下来将要创建的 ref 对象将会被缓存到该 hook 对象上:
const hook = mountWorkInProgressHook();
接着创建一个 ref 对象,其 current 属性初始化为传入的参数(initialValue):
const ref = { current: initialValue };
然后将 ref 对象缓存到 hook 对象的 memoizedState 属性上:
hook.memoizedState = ref;
最后返回一个可变的 ref 对象,其属性 current 发生变化时,不会引发组件重新渲染:
return ref;
3.2 mountWorkInProgressHook
在 mountRef() 函数中,使用 mountWorkInProgressHook() 创建了一个新的 hook 对象,我们来看看它是如何被创建的:
// packages/react-reconciler/src/ReactFiberHooks.new.js
// 创建一个新的 hook 对象,并返回当前的 workInProgressHook 对象
// workInProgressHook 对象是全局对象,在 mountWorkInProgressHook 中首次初始化
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
// Hooks are stored as a linked list on the fiber's memoizedState field
// 将新建的 hook 对象以链表的形式存储在当前的 fiber 节点memoizedState属性上
// 只有在第一次打开页面的时候,workInProgressHook 为空
if (workInProgressHook === null) {
// This is the first hook in the list
// 链表上的第一个 hook
// currentlyRenderingFiber: The work-in-progress fiber. I've named it differently to distinguish it fromthe work-in-progress hook.
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
// 已经存在 workInProgressHook 对象,则将新创建的这个 Hook 接在 workInProgressHook 的尾部,形成链表
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
可以看到,在新建一个 hook 对象时,如果全局的 workInProgressHook 对象不存在 (值为 null),即组件在首次渲染时,将新建的 hook 对象赋值给 workInProgressHook 对象,也同时将 hook 对象赋值给 currentlyRenderingFiber 的 memoizedState 属性,如果 workInProgressHook 对象已经存在,则将 hook 对象接在 workInProgressHook 的尾部,从而形成一个单向链表。
4、更新阶段
组件在更新阶段,执行 useRef,实际上执行的是 updateRef,下面我们来看看 updateRef 的实现。
4.1 updateRef
// packages/react-reconciler/src/ReactFiberHooks.new.js
function updateRef<T>(initialValue: T): {| current: T |} {
// 获取该 useRef 对应的hook 对象
const hook = updateWorkInProgressHook();
// 返回在挂载阶段缓存在 hook 对象上的 ref 对象
return hook.memoizedState;
}
可以看到,在更新阶段,仅仅只是返回了在挂载阶段挂载在 hook 对象的 memoizedState 属性上的 ref 对象,因此当 ref 对象内容发生变化,即 current 属性发生变更时,不会引发组件重新渲染。
4.2 updateWorkInProgressHook
在 updateRef() 函数中,通过 updateWorkInProgressHook() 函数获取到了当前正在工作中的 Hook,即 workInProgressHook,我们来看看 updateWorkInProgressHook 的实现:
// packages/react-reconciler/src/ReactFiberHooks.new.js
function updateWorkInProgressHook(): Hook {
// This function is used both for updates and for re-renders triggered by a
// render phase update. It assumes there is either a current hook we can
// clone, or a work-in-progress hook from a previous render pass that we can
// use as a base. When we reach the end of the base list, we must switch to
// the dispatcher used for mounts.
// 获取 当前 hook 的下一个 hook
let nextCurrentHook: null | Hook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
// 取下一个 hook 为当前的hook
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
// 拷贝当前的 hook,作为当前正在工作中的 workInProgressHook
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
这里分两种情况:
- 如果是在 render 阶段,则会取下一个 hook 作为当前的hook,并返回 workInProgressHook;
- 如果是在 re-render 阶段,则在当前处理周期中,继续取当前的 workInProgressHook 做更新处理,最后再返回 workInProgressHook。