在 render 阶段标记 ref 标记,commit 阶段挂载 dom 节点时通过标记获取 dom 节点。主要实现流程如下:
mount 时:存在 ref update 时:ref 引用变化
标记的时机包括:
beginWork
包括两类操作:
Mount
mount 阶段主要是构建 hook 对象,并将 hook 对象添加到 fiber 节点的 hook 链表中。这也是所有 hook 在 mount 阶段的基本操作。
最后创建一个 current 对象,用来保存数据,这也就是 useRef 函数的返回值。
function mountRef(initialValue) {
// 构建hook对象并添加到链表中
const hook = mountWorkInProgressHook();
// 创建current对象
const ref = { current: initialValue };
// 保存current对象,这也是useRef函数返回的对象结果
hook.memoizedState = ref;
return ref;
}
在构建 hook 对象时会有两种情况,如果是第一个 hook 函数,需要将其保存在 fiber 节点的 memoizedState 属性中,如果是后续的 hook 函数,直接通过 next 指针连接
function mountWorkInProgressHook() {
const hook = {
memoizedState: null,
updateQueue: null,
next: null,
baseQueue: null,
baseState: null,
};
if (workInProgressHook === null) {
// mount时 第一个hook
if (currentlyRenderingFiber === null) {
throw new Error("请在函数组件内调用hook");
} else {
workInProgressHook = hook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
// 后续的hook
// 通过next指针连接
workInProgressHook.next = hook;
workInProgressHook = hook;
}
return workInProgressHook;
}
Update
更新阶段由于并没有其他操作,所以只需要通过双缓存树的机制更新 hook 对象。
function updateRef(initialValue) {
// 更新hook对象
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
更新 hook 对象的本质是复用,通过 current 树中对应 fiber 节点的 hook 链表,来创建本次更新新的 hook 对象并更新链表
currentHook 属性保存 current 树中上一个 hook 对应的对象,所以本次更新如果有值,通过 next 指针获取本次处理的 hook 对应的旧 hook 对象。如果没有值,说明当前 hook 函数为函数组件的第一个。
如果这两种情况都没有取到值,说明存在动态 hook 函数(本次更新比上一次更新 hooks 数量对应不上)。这种情况是 react 不被允许的,所以报错“本次执行时的 Hook 比上次执行时多”。
function updateWorkInProgressHook() {
let nextCurrentHook;
if (currentHook === null) {
// 这是这个函数组件update时的第一个hook
const current = currentlyRenderingFiber?.alternate;
if (current !== null) {
// 获取对应的memoizedState属性
nextCurrentHook = current?.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 这个函数组件update时 后续的hook
nextCurrentHook = currentHook.next;
}
if (nextCurrentHook === null) {
throw new Error(
`组件${currentlyRenderingFiber?.type}本次执行时的Hook比上次执行时多`
);
}
currentHook = nextCurrentHook;
// 复用current树的旧hook对象属性
const newHook = {
memoizedState: currentHook.memoizedState,
updateQueue: currentHook.updateQueue,
next: null,
baseQueue: currentHook.baseQueue,
baseState: currentHook.baseState,
};
if (workInProgressHook === null) {
// mount时 第一个hook
if (currentlyRenderingFiber === null) {
throw new Error("请在函数组件内调用hook");
} else {
workInProgressHook = newHook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
// 后续的hook
workInProgressHook.next = newHook;
workInProgressHook = newHook;
}
return workInProgressHook;
}
坑
关于把 ref 对象作为 useCallback 依赖的做法
正常的话,ref 是不会变的,一直指向同一个对象。依然把它传递给 useCallback 的原因是,让 useCallback 的逻辑与变量 counterRef 的逻辑解耦。
意思就是对于 useCallback 的意义是:我不管 counterRef 会不会变,反正我依赖了它,为了我能正常运行,我必须把它声明出来。
你也可以这样理解,万一 counterRef 会变化,那么 useCallback 是不会受到影响的,这就是解耦合的意义所在,比如
const oneRef = useRef(); const twoRef = useRef(); const counterRef = ? oneRef : twoRef;
ref 想象成类的属性变量,它的值在渲染周期之间可以保持,并且,和 state 的一个重要区别是,它的变化不会触发组件重新渲染。
useRef 可以传入初始值,你这个代码没有传入,因此直接当作函数来调用肯定报错的。
同步不同渲染周期需要共用的数据, 只能设置在 useRef 的 current 属性上吗 ?
取决于这个数据的变更要不要触发重渲染,如果不重渲染,那么放 ref 是比较建议的做法;否则,就只能用 state 了。
export default function App() {
const [count, setCount] = useState(0);
const preCountRef = useRef(count);
useEffect(() => {
preCountRef.current = count;
});
const preCount = preCountRef.current;
return (
<div className="App">
<div
onClick={() => {
setCount((count) => count + 1);
}}
>
<h1>
Now:{count} preCount: {preCount}
</h1>
</div>
</div>
);
}
useEffect 它没有依赖参数,说明每一次组件的渲染,都会运行一次这个副作用。而 useEffect 是在每次的渲染后来执行的。因而我们又把本次渲染所使用的 count 保存在了 preCountRef 中 现在我们执行下一次渲染,先运行的代码是:
const [count, setCount] = useState(0);
const prevCountRef = useRef();
const prevCount = prevCountRef.current;
这个就是典型的先渲染 jsx, 在执行 useEffect
ref 有个限制,那就是 ref 的 current 值改变的时候,并不会重新触发渲染,所以我们不能完全抛弃 useState 而只用 useRef。
const [count, setCount] = useState(3);
const countRef = useRef(3);
const countDown = () => {
var timer = setInterval(() => {
// setCount((pre) => {
// if (pre === 0) {
// clearInterval(timer);
// }
// console.log("pre", pre);
// });
countRef.current -= 1;
console.log("ref", countRef.current);
if (countRef.current === 0) {
clearInterval(timer);
}
}, 1000);
};