{
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
React Hooks: useRef & Refs
说明
本文将分析以下两点
阅读本文需先阅读 React Hooks: hooks 链表
分析基于以下代码
TLNR
useRef解析
mount 场景下的 useRef
在 mount 场景下对 useRef 进行打点会进入到 mountRef 这一函数内。
东西少的可怜,简直没什么好分析的。
update 场景下的 useRef
与其他 hooks 一样,它也存在 update 阶段,因为代码也很简单,就直接粘过来了。
一目了然的代码。当然我们对 useRef 内的值的改动不像 useState 一样需要调用 dispatch,因此也不会调用 scheduleUpdateOnFiber,那么这个改动也不会触发 react 的渲染。
总结
useRef 内传入的值会被赋值给 ref.current 上,且 ref 会被挂载到 useRef 对应的 hook 对象上的 memoizedState 属性上,每一次 update 阶段创建新 hook 对象时都会把老 hook 对象上的 memoizedState 赋值到新 hook 对象上,因此可以保证 ref 的引用地址不变从而保证存储的值不变(也就是说每次渲染返回的 ref 对象都是同一个对象)。且对 ref 内存储的值的改变不会引起 react 的渲染。
Refs
由于 useRef 的内容过少,因此我想再聊一聊例子中的 ref。
不过关于 ref 的调试思路比较复杂,由于 JSX 会被 Babel 转译为 React.createElement。这个函数存在于
packages/react/src/ReactElement.js
中。该函数返回的类型声明为很显然可以看到 ref 是被单独分离出来的,但如果需要进行调试,则需要将目光放到根据 ReactElement 创建 fiber 的过程中,也就是 beginWork 内。不过这里分为两个阶段 —— 1. UseRef 的 beginWork;2. div#refTest 的 beginWork。
UseRef 的 beginWork
在进入 UseRef 的 beginWork 时,会运行 mountIndeterminateComponent
reconcileChildren 这个函数已经是面试的老面孔了,diff 的入口就是它。不过这里不讲 diff,就直接看涉及到 ref 的部分(reconcileChildren -> ChildReconciler -> reconcileSingleElement)
很显然在创建完新 fiber 节点后,会在该 fiber.ref 上赋值( coerceRef 返回的其实就是 ReactElement 上的 ref )。
div#refTest 的 beginWork
根据断点的记录会进入 updateHostComponent,该函数内与 ref 相关的代码存在于 markRef。
markRef 和它的名称一样,主要是为 fiber 打上存在 ref 的标记。
阶段性总结1
beginWork 内与 ref 相关的逻辑其实就做了两件事
refs 如何与 dom 绑定
很显然 refs 的逻辑到这里还没结束,我们完成了对 refs 的初始化,但是赋值呢?我们该怎么把对应的 dom 绑定到 refs 上呢?
解释这个问题我们需要参考官网关于 refs 的描述
既然描述和生命周期有关系,那只能写一个 class 的例子看看了。
对 componentDidMount 打上断点,可以看到这样的调用栈
可以很明显的看到,componentDidMount 的调用发生在 commit 阶段( commit 阶段的入口就是 commitRootImpl ),而 commit 阶段又分为三个子阶段
在这三个阶段中,只有在 layout 阶段可以拿到完全处理完之后的 dom 结构。那么 refs 与 dom 绑定的步骤也很适合放在此处处理。
事实上也确实如此
对于 refs 的赋值就藏在 commitLayoutEffects 的 commitLayoutEffectOnFiber 内
还记得在 beginWork 时给 fiber 打上的标记吗?这个函数就用到了。当 flags 包含 Ref的标记时便执行 commitAttachRef
阶段性总结2
refs 与 dom 的绑定发生在 commit 的 layout 阶段。