- 为什么 React 不将 ref 存在 fiber 的 props 中,这样在组件中就能通过 props.ref 获取到值
- ref 的值什么时候设置,什么时候被释放?
React Ref 用法可以看这篇文章
React.createElement 对 ref 属性进行特殊处理
我们知道在构建时,JSX 经过 babel 编译为一系列 React.createElement,比如下面的代码
<div ref={this.domRef} id="counter" name="test">
dom ref
</div>
经过 babel 编译,变成下面的函数调用
React.createElement(
"div",
{
ref: this.domRef,
id: "counter",
name: "test",
},
"dom ref"
);
React.createElement 最终返回的是一个 react element 对象
var RESERVED_PROPS = {
key: true,
ref: true,
};
function createElement(type, config, children) {
var propName;
var props = {};
var key = null;
var ref = null;
if (config != null) {
if (config.ref) {
ref = config.ref;
}
if (config.key) {
key = "" + config.key;
}
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
return ReactElement(type, key, ref, ReactCurrentOwner.current, props);
}
var ReactElement = function (type, key, ref, owner, props) {
return {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
};
可以看出,ref 属性和 key 属性一样都是比较特殊的,不会被添加到 props 中,这也是为什么我们通过 props.ref 或者 props.key 获取到的永远是 undefined 的原因
ref 和 key 都是直接添加到 fiber 的属性当中的。为什么 React 不将 ref 存储在 props 中?
我们在使用 ref,必须显示的调用 React.createRef 或者 React.useRef 方法创建一个 ref 对象(回调 ref 不需要调用这两个方法)
这两个函数都比较简单,都是用于创建 ref 对象,比如:
function createRef() {
return {
current: null,
};
}
// 在函数组件初次渲染阶段,useRef就是mountRef
function mountRef(initialValue) {
var hook = mountWorkInProgressHook();
var ref = {
current: initialValue,
};
hook.memoizedState = ref;
return ref;
}
// 在函数组件更新阶段,useRef就是updateRef
function updateRef(initialValue) {
var hook = updateWorkInProgressHook();
return hook.memoizedState;
}
为什么 React 要采用对象保存 ref?这是因为对象是引用类型,方便存值,比如下面的例子中,我们给 div 传递了 ref 属性
<div ref={this.domRef} id="counter" name="test">
dom ref
</div>
this.domRef 是一个对象:
this.domRef = { current };
在 render 阶段为 div 创建 fiber 节点时,会将 ref 设置给 fiber.ref,即fiber.ref = this.domRef
,然后在 commit 阶段,React 会给 fiber.ref.current 设置 dom 实例,此时 this.domRef.current 也就可以访问到 dom 节点
前面说过,React.createElement 在创建 react element 对象时,会将 ref 单独放在 element 对象的属性中,而不是放在 element.props 属性中,element 对象属性如下所示:
{
$$typeof: Symbol(react.element),
key: null,
props: { id: "counter", name: "test", children: "dom ref:0", onClick },
ref: { current: null },
type: "div",
};
在 render 阶段,React 会为当前的 fiber 协调子元素,即将当前 fiber 节点的子节点和新的子 element 节点比较,以创建新的 workInProgress 节点。其中,在协调时,会将 element 上的 ref 属性赋值给 fiber ref 属性,fiber ref 属性就是在协调阶段设置的。以下面的例子为例:
<div id="container">
<div ref={this.domRef} id="counter" name="test">
dom ref
</div>
</div>
在 beginWork 阶段,div#container
执行 reconcileChildren 工作,为 div#counter
创建子 fiber 节点,然后给新的 div#counter
fiber 节点设置 ref 属性。伪代码如下:
// returnFiber即 div#container,element即是新的div#counter对应的react element对象
// currentFirstChild是returnFiber的第一个子节点
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
if (!currentFirstChild) {
// 第一次渲染
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);
_created4.ref = element.ref;
_created4.return = returnFiber;
return _created4;
} else {
var _existing3 = useFiber(child, element.props);
_existing3.ref = element.ref;
_existing3.return = returnFiber;
return _existing3;
}
}
从上面的代码可以看出,在 reconcile 阶段,无论是第一次渲染还是更新阶段,都会使用 element.ref 重新赋值给新的 fiber。区别在于,第一次渲染时,会调用 createFiberFromElement 创建新的 fiber 节点,而在更新阶段,会调用 useFiber 复用旧的 fiber 节点。
因此,fiber ref 属性是在父节点的 reconcile 阶段被设置的
render 阶段如果满足下面两个条件之一,会为 fiber 节点添加一个 Ref 副作用标记:
- 第一次渲染,并且 ref 有值,即 current === null && ref !== null
- 更新阶段,即第二次或者后续的渲染中,如果 ref 发生了变化,即 current !== null && current.ref !== workInProgress.ref
下面是 HTML 元素和类组件的场景
function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case ClassComponent: {
return updateClassComponent(current, workInProgress);
}
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
}
}
function updateClassComponent(current, workInProgress) {
//....
var nextUnitOfWork = finishClassComponent(current, workInProgress);
return nextUnitOfWork;
}
function finishClassComponent(current, workInProgress) {
// 即使是shouldComponentUpdate返回了false,Ref也要更新
markRef(current, workInProgress);
//...
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
//...
return workInProgress.child;
}
function updateHostComponent(current, workInProgress, renderLanes) {
//...
markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function markRef(current, workInProgress) {
var ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// 添加一个 Ref 副作用(effect)
workInProgress.flags |= Ref;
}
}
从上面的代码可以看出,不管是类组件还是 HTML 元素的 fiber,在为他们调用 reconcileChildren 协调子元素之前,都会调用 markRef 判断是否为它们添加 Ref 副作用
在 render 阶段,会调用 markRef 为 fiber 节点添加 Ref 副作用。在 commit 阶段,React 会判断 fiber 是否具有 Ref 副作用,如果有,则为 fiber.ref 设置 current 值。
在深入概述 React 初次渲染及状态更新主流程中介绍过,commit 分为三个小阶段:
- commitBeforeMutationEffects
- commitMutationEffects
- commitLayoutEffects
与 Ref 操作有关的阶段只有commitMutationEffects
以及commitLayoutEffects
function commitRootImpl(root, renderPriorityLevel) {
//...
commitBeforeMutationEffects();
//...
commitMutationEffects(root, renderPriorityLevel);
//...
commitLayoutEffects(root, lanes);
//...
}
commitMutationEffects
主要是执行节点的增删改操作,在执行这些操作之前,会先调用 commitDetachRef 重置 ref。
function commitDetachRef(current) {
var currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === "function") {
currentRef(null);
} else {
currentRef.current = null;
}
}
}
function commitMutationEffects(root, renderPriorityLevel) {
while (nextEffect !== null) {
var flags = nextEffect.flags;
//...
if (flags & Ref) {
var current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
//...
case Deletion: {
commitDeletion(root, nextEffect);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
这里,删除节点(commitDeletion)的操作比较特殊,commitDeletion 调用 unmountHostComponents 卸载节点,而 unmountHostComponents 最终又会调用 commitUnmount 卸载节点,在 commitUnmount 中会调用 safelyDetachRef 小心的重置 ref 为 null
function safelyDetachRef(current) {
var ref = current.ref;
if (ref !== null) {
if (typeof ref === "function") {
try {
ref(null);
} catch (refError) {
captureCommitPhaseError(current, refError);
}
} else {
ref.current = null;
}
}
}
function commitUnmount(finishedRoot, current, renderPriorityLevel) {
onCommitUnmount(current);
switch (current.tag) {
//...
case ClassComponent: {
safelyDetachRef(current);
var instance = current.stateNode;
if (typeof instance.componentWillUnmount === "function") {
safelyCallComponentWillUnmount(current, instance);
}
return;
}
case HostComponent: {
safelyDetachRef(current);
return;
}
}
}
commitLayoutEffects 会判断 fiber 是否具有 Ref 副作用,如果有,则调用 commitAttachRef 设置 ref 的值
function commitLayoutEffects(root, committedLanes) {
while (nextEffect !== null) {
var flags = nextEffect.flags;
//...
if (flags & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
commitAttachRef 主要就是设置 ref 的值,这里会判断 ref 属性是否是函数,如果是函数,则执行。否则直接设置 ref.current 属性
function commitAttachRef(finishedWork) {
var ref = finishedWork.ref;
if (ref !== null) {
var instance = finishedWork.stateNode;
if (typeof ref === "function") {
ref(instance);
} else {
ref.current = instance;
}
}
}
在 render 阶段,执行函数调用 useImperativeHandle 时,React 会为 forwardRef 创建一个 imperativeHandle 类型的 Effect 对象,并添加到 updateQueue 队列中,如下:
function imperativeHandleEffect(create, ref) {
if (typeof ref === "function") {
var refCallback = ref;
var _inst = create();
refCallback(_inst);
return function () {
// 注意这里会返回一个函数!!!
refCallback(null);
};
} else if (ref !== null && ref !== undefined) {
var refObject = ref;
var _inst2 = create();
refObject.current = _inst2;
return function () {
refObject.current = null;
};
}
}
const imperativeEffect = {
create: imperativeHandleEffect,
deps: null,
destroy: undefined,
next: null,
tag: 3,
};
imperativeEffect.next = imperativeEffect;
fiber.updateQueue = {
lastEffect: imperativeEffect,
};
以下面的代码为例:
const FunctionCounter = (props, ref) => {
const createInst = () => ({
focus: () => {
console.log("focus...");
},
});
useImperativeHandle(ref, createInst);
return <div>{`计数器:${props.count}`}</div>;
};
const ForwardRefCounter = React.forwardRef(FunctionCounter);
imperativeHandleEffect(create, ref)
中的第一个参数create
对应useImperativeHandle(ref, createInst);
中的第二个参数createInst
。
imperativeHandleEffect(create, ref)
中的第二个参数ref
对应useImperativeHandle(ref, createInst);
中的第一个参数ref
。
注意,这里我们用 React.forwardRef 包裹 FunctionCounter,React 会为 forwardRef 创建一个 fiber 节点,但不会为 FunctionCounter 创建一个 fiber 节点。因此 render 阶段执行的工作是针对 forwardRef 类型的 fiber 节点
commitLayoutEffects 阶段调用 commitLifeCycles。注意,在 commitHookEffectListMount 中会遍历 fiber.updateQueue 的 effect 队列,然后执行 effect.create 方法,就是我们前面说过的 imperativeHandleEffect 方法。
function commitLifeCycles(current, finishedWork) {
switch (finishedWork.tag) {
case ForwardRef: {
commitHookEffectListMount(Layout | HasEffect, finishedWork);
return;
}
}
}
function commitHookEffectListMount(tag, finishedWork) {
var updateQueue = finishedWork.updateQueue;
var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
var firstEffect = lastEffect.next;
var effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Mount
var create = effect.create;
effect.destroy = create(); // 调用effect.create
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
在执行 imperativeHandleEffect 方法时,会返回一个函数:
function imperativeHandleEffect(create, ref) {
if (typeof ref === "function") {
var refCallback = ref;
var _inst = create();
refCallback(_inst);
return function () {
// 注意这里会返回一个函数!!!
refCallback(null);
};
} else if (ref !== null && ref !== undefined) {
var refObject = ref;
var _inst2 = create();
refObject.current = _inst2;
return function () {
refObject.current = null;
};
}
}
这个函数就是用来重置 ref.current 属性为 null 的。返回函数会在 commitMutationEffects 阶段执行
commitMutationEffects 阶段调用 commitWork
function commitWork(current, finishedWork) {
switch (finishedWork.tag) {
case ForwardRef:
commitHookEffectListUnmount(Layout | HasEffect, finishedWork);
return;
}
}
function commitHookEffectListUnmount(tag, finishedWork) {
var updateQueue = finishedWork.updateQueue;
var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
var firstEffect = lastEffect.next;
var effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Unmount
var destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
从这个过程也可以看出,如果 ref 是一个函数,会被执行两次,第一次在 commitMutationEffects 阶段执行,用于重置 ref.current 为 null,第二次在 commitLayoutEffects 阶段执行,用于设置 ref.current 为最新的值