入口
下篇文章有入口源码详解,想看的可以跳转过去,这里就不放源码了,简单梳理一下流程吧
流程
beginWork
判断组件类型,指挥交通,各行其道,函数组件走函数组件的道 updateFunctionComponent
updateFunctionComponent
起作用的重点函数就是我们常谈的 renderWithHooks
renderWithHooks
主要做了两件事:
- 用变量
currentlyRenderingFiber
记录当前的fiber node
。使得 hook 能拿到当前node
的状态。 - 判断
hook api
挂载在那个对象上。首次渲染和后期的更新,挂载的对象是不同的 => 解耦。
useRef 源码解析
声明阶段
获取并返回useRef
函数
export function useRef<T>(initialValue: T): {current: T} {
// 通过resolveDispatcher获取当前的Dispatcher
const dispatcher = resolveDispatcher();
return dispatcher.useRef(initialValue);
}
mount阶段
调用 mountRef
,创建hook
和ref
对象,仅用四步完成ref
对象的创建,并把hook
添加到链上
function mountRef<T>(initialValue: T): {current: T} {
// 第一步:创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表
const hook = mountWorkInProgressHook();
// 第二步:创建 ref 对象,其 current 属性初始化为传入的参数(initialValue)
const ref = {current: initialValue};
// 第三步:将 ref 对象缓存到 hook 对象的 memoizedState 属性上
// 例如useRef(0),memoizedState即{current: 0}
hook.memoizedState = ref;
// 第四步:返回一个可变的 ref 对象,其属性 current 发生变化时,不会引发组件重新渲染
return ref;
}
mountWorkInProgressHook
内部逻辑也是很清晰的(在删除DEV环境逻辑后)
// workInProgressHook 是全局对象,在 mountWorkInProgressHook 中首次初始化;
// workInProgressHook 将被添加到 work-in-progress fiber 中的 hook 链表
// workInProgressHook hook 链表中的一个重要指针 => 它通过记录当前生成(更新)的 hook 对象,
// 可以间接反映在组件中当前调用到哪个 hook 函数了。每调用一次 hook 函数,就将这个指针的指向移到该 hook 函数产生的 hook 对象上。
let workInProgressHook: Hook | null = null;
/**
* 创建一个新的 hook
* @returns 返回当前 WorkInProgressHook
*/
function mountWorkInProgressHook(): Hook {
// 创建hook对象
const hook: Hook = {
memoizedState: null, // 指向当前渲染节点fiber,存储上一次更新后的最终状态
baseState: null, // 初始化状态,每次dispatch后的newState
baseQueue: null, // Update<any, any> 当前需要更新的,每次更新完,会赋值上一个update,方便react在渲染错误的边缘,进行错误回溯
queue: null, // UpdateQueue<any, any> 缓存的更新队列,存储多次更新行为
next: null, // link到下一个hook,通过next串联所有的hooks
};
// 只有在第一次打开页面的时候,workInProgressHook 为空
if (workInProgressHook === null) {
// 链表上无hook, 初始化链表的第一个hook
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 链表上有hook,将新创建的这个hook加在链表尾部
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
workInProgressHook
:hook
链表中的一个重要指针 => 它通过记录当前生成(更新)的hook
对象,可以间接反映在组件中当前调用到哪个hook
函数了。每调用一次hook
函数,就将这个指针的指向移到该hook
函数产生的hook
对象上。
update阶段、Rerender阶段
调用 updateRef
获取获取当前 useRef
,然后返回 hook
链表上缓存下来的值。
也就是无论函数组件怎么执行,执行多少次,
hook.memoizedState
内存中都指向了一个对象,所以也解释了在useEffect
,useMemo
等函数中,为什么useRef
不需要依赖注入,就能访问到可变且不刷新页面的最新值。
/**
* 仅仅返回在挂载阶段挂载在 hook.memoizedState 上的 ref 对象
* 因此当 ref 对象内容发生变化,即 current 属性发生变更时,不会引发组件重新渲染
* @param {*} initialValue
* @returns
*/
function updateRef<T>(initialValue: T): {current: T} {
// 通过 updateWorkInProgressHook() 函数获取该 useRef 对应的当前正在工作的 Hook
const hook = updateWorkInProgressHook();
// 返回在挂载阶段缓存在 hook 对象上的 ref 对象
return hook.memoizedState;
}
/**
* 取出current fiber中的hooks链表中对应的hook节点,挂载到workInProgress上的hooks链表
* 用于在 updates 和 re-renders 阶段触发的重新渲染
* @returns 返回当前 workInProgressHook
*/
function updateWorkInProgressHook(): Hook {
// 迭代 current fiber 链表
let nextCurrentHook: null | Hook;
if (currentHook === null) {
// 通过alternate备份指针,复用 老的Fiber Hook链表
const current = currentlyRenderingFiber.alternate; // 备份指针
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 获取 当前hook 的下一个 hook
nextCurrentHook = currentHook.next;
}
// 迭代 workInProgress fiber 链表
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
// workInProgressHook === null 说明是首次创建
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 非首次创建,取下一个 workInProgress Hook 为 当前hook
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// 只有 re-render 的情况下,nextWorkInProgressHook 不为 null,
// 因为在之前的 render 过程中已经创建过 workInProgress hook了,此时直接复用
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// 防患于未然
if (nextCurrentHook === null) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
// update阶段,currentFiber不可能为null,正常情况下应该走不到这个branch
throw new Error(
'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
);
} else {
// update阶段,nextCurrentHook不可能为null,正常情况下应该走不到这个branch
throw new Error('Rendered more hooks than during the previous render.');
}
}
// 重新定位 正在工作中的workInProgressHook
currentHook = nextCurrentHook;
// 同mountWorkInProgressHook主要逻辑
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
因为renderWithHooks
函数中会执行如下代码: workInProgress.memoizedState = null
,所以在执行上述函数时,有两种情况
-
currentlyRenderingFiber.memoizedState
为 null,需要从 current fiber 对应的节点中取 clone 对应的 hook,再挂载到 workInProgress fiber 的 memoizedState 链表上; -
re-render 的情况下,由于已经创建过了 hooks,会复用已有的 workInProgress fiber 的 memoizedState。
这里正好提到,为什么 hook 不能用在条件语句中?
因为如果前后两次渲染的条件判断不一致时,会导致 current fiber 和 workInProgress fiber 的 hooks 链表结点无法对齐。
在这个函数中通过
currentlyRenderingFiber.alternate
备用指针复用currentFiber
的memorizedState
, 老的 Fiber Hook 链表,并且按照严格的对应顺序来复用 老的 Fiber Hook 链表中的 Hook,通过尽可能的复用来创建新的 Hook 对象,构建新的 Hook 链表。
useRef 特点
1. 渲染周期之间共享数据,即在组件重新渲染之间,引用的对象是同一个
2. 更新引用不会触发组件重渲染
3. 同步更新,更新后立即可用
用途:存储组件的基础数据,存储不需要展示在页面上的数据
最佳实践
场景1:访问dom元素 <= 最常见的使用场景
// 与浏览器监听相结合
import React, { useRef } from 'react';
function Example() {
const divElement = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = () => {
console.log('Page is scrolling');
};
divElement.current?.addEventListener('scroll', handleScroll, true);
return () => {
divElement.current?.removeEventListener('scroll', handleScroll, true);
}
}, [handleScroll]);
return (
<div ref={divElement}>
<p>Scroll down to see the event log.</p>
</div>
);
}
场景2:存储定时器变量
import { useRef } from 'react';
function Example() {
const timer = useRef<NodeJS.Timeout>();
const onOpenMenu = () => {
timer.current = setTimeout(() => { /* ... */ }, 100);
};
const onCloseMenu = () => {
clearTimeout(timer.current);
};
return <div>...</div>;
}
场景3:解决 useState 异步更新问题
useState
存储直接呈现在屏幕上的信息
1. 更新会触发组件的重渲染,不能存储跨渲染周期的数据
2. 异步更新
- 更新后立即使用可能出错
- 解决方案:`setCount(prevCount => prevCount + 1)}`
- ahooks ` useLatest `和 `useMemoizedFn` 即是依赖 useRef 实现的
- 在闭包内使用可能出错【闭包陷阱】
- 存储数据,在定时器内使用
- 问题原因:定时器一直都没有被清除,上下文环境都是第一次创建本函数式组件的上下文,
因此获取到的是定时器被创建时的state
- 解决方案:用useRef保存一份state数据,定时器里使用ref.current
import { useRef, useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
const lastCount = useRef(count);
useEffect(() => {
const timer = setInterval(() => {
console.log('last count: ', lastCount.current);
}, 1000);
return ()=>{
clearInterval(timer);
}
}, []);
const addCount = () => {
setCount((val) => val + 1);
lastCount.current += 1;
};
return <button onClick={addCount}>Add Count</button>;
}
场景四:🔥🔥🔥一定要注意ref的第一个特点🔥🔥🔥
=即在组件重新渲染之间,引用的对象是同一个=
如果给ref赋值了一个对象initObj
,并且后面对ref进行了修改,那么initObj
也会跟随变化
导致initObj
已经不是最初的initObj
了,这里要注意哦,很容易写bug
最好的写法是👇👇👇
const initData = useRef<>({...initObj}); // 解构一下,让ref不直接引用 initObj
最后
交流:你从 useRef 源码中发现了哪些设计思想或原则?
结束语:希望本次分享能带给你新的视野,或者帮你回忆起被遗忘在角落的记忆
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
一个有意思的变量,感兴趣可以研究一下