Appearance
Ref
对于 React 中 ref 的作用,按照其数据来源可以区分为三大类: 1. 访问 DOM; 2. 访问组件。 3. 访问实时数据。
按照其使用方法又可以为两大类 : 1. Class 组件 2. 函数式组件
其中 Class 组件中借助于
this.ref = createRef(null)去申明 ref 的对象, 通过<input ref={this.ref}>,<ClassComponent ref={(instance) => this.ref.current= instance }>等方式去进行赋值对于函数式组件,确需要借助于特定的两个方法去实现 1.
React.forwardRef(() => {})去将 ref 与函数式组件关联 , 2.React.useImperativeHandle(ref , () => { })去将 ref 与 其具体暴露的方法进行关联。
1. Ref 的理解
Ref 的理解可以可以分为以下三个部分:
- Ref 的创建流程,如通过
React.createRef或者React.useRef来创建一个 ref 对象。 - Ref 的赋值流程,如通过
ref={xxx}来给 ref 对象赋值。 - Ref 的使用流程,如通过
ref.current来获取 ref 对象的值。
下面我们就按照这三个部分来详细介绍 Ref 的使用。
2. Ref 的创建流程
Ref 的创建流程主要分为以下两个部分:
- 函数式组件中 创建 Ref 对象。
- 类组件中 创建 Ref 对象。
在这两个组件中对于 Ref 的创建流程是不一样的。
2.1. createRef 创建流程
在Class 组件中,我们会通过 createRef 去创建一个 Ref 对象,其会被保存在类组件实例上,它的实现很简单。
如下:
js
export function createRef(): RefObject {
const refObject = {
current: null,
};
if (__DEV__) {
Object.seal(refObject);
}
return refObject;
}2.2. useRef 的创建流程
对于函数式组件中,我们会通过 useRef 去创建一个 Ref 对象,它的实现也很简单。但是还是按照 Hook 的通用流程来分析
2.2.1. 初次渲染阶段
useRef 在初次渲染阶段实现的逻辑和 createRef 是一样的,主要是生成一个 {current: initialValue}类型的对象,其存放地址不一样,函数式组件存放到 hook.memoizedState。
js
/**
* useRef 在初次渲染阶段实现
* 主要是生成一个 {current: initialValue} 存放到 hook.memoizedState
* @param {*} initialValue
*/
function mountRef<T>(initialValue: T): {| current: T |} {
// 创建一个 hook 对象
const hook = mountWorkInProgressHook();
if (enableUseRefAccessWarning) {
} else {
// 创建一个 { current: initialValue } 对象
const ref = { current: initialValue };
// 并将其赋值给 hook.memoizedState
hook.memoizedState = ref;
return ref;
}
}2.2.2. 更新渲染阶段
useRef 在更新渲染阶段实现的逻辑更简单,就是去取存放在函数式组件 FiberNode 对象上的 hook.memoizedState中的数据,即初次渲染阶段存放的{current: initialValue}类型的对象。
js
function updateRef<T>(initialValue: T): {| current: T |} {
// 复用初次渲染阶段生成的 hook
const hook = updateWorkInProgressHook();
// 直接返回 hook.memoizedState
return hook.memoizedState;
}3. Ref 的赋值流程
Ref 的赋值流程主也要也在两种组件类型中(函数式组件 和 类组件),但是其值可以分为以下几种类型
- 自定义类型的 Ref 对象
- 绑定 DOM 元素
- 绑定函数式组件
- 绑定类组件
下面我们分别对这四种类型的 Ref 对象进行分析。
3.1. 自定义类型的 Ref 对象
自定义类型的 Ref 对象就是通过 React.createRef 或者 React.useRef 来创建的 Ref 对象,其值为 {current: null} 类型的对象。然后通过 this.ref.current = xxx 来给 Ref 对象赋值。
其特点就是 值是实时更新的,不像 state 类型的数据是异步更新的。
3.2. 绑定 DOM 元素
绑定 DOM 元素类型的 Ref 对象就是直接将 DOM 元素赋值给 Ref 对象。
js
this.inputRef = React.createRef(); // 类组件
<input ref={this.inputRef} />; // 就可以使用 this.inputRef.current 来获取 DOM 元素
const inputRef = React.useRef(); // 函数组件
<input ref={inputRef} />; // 就可以使用 inputRef.current 来获取 DOM 元素其核心在 HostComponent 组件的 render 和 commit 阶段。在 render 阶段的 compeleteWork 流程中会根据是否可以复用 和 是否需要删除等需要更新状态去创建一个 Ref 类型的副作用添加到 FiberNode.flags 中, 在 commit 阶段根据 flags & Ref知道存在重新赋值的 ref,那么就通过 safelyDetachRef()去更新 ref 的值
所以其涉及到两个阶段:
1. compeleteWork(render 阶段)
js
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
switch (workInProgress.tag) {
case HostComponent: {
// 元素节点可以复用的流程
// 1. DOM 元素节点复用流程
if (current !== null && workInProgress.stateNode != null) {
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// 2. DOM 元素节点创建流程
if (workInProgress.ref !== null) {
markRef(workInProgress);
}
}
return null;
}
}
}通过 current !== null && workInProgress.stateNode != null 来判断 原生节点是进入复用流程还是创建流程。
- 对于复用流程,其可能只是 DOM 元素的属性更新,也有可能是 DOM 元素的子元素发生了变化。
- 对于创建流程,其就是 DOM 元素的创建流程。
其实还缺少了一个 DOM 元素的删除流程(ref.current = DOM 转变成 ref.current = null ),对于这种情况不需要标记存在 Ref 副作用,而是直接根据 Deletion 类型的副作用在 commitDeletionEffectsOnFiber 中去处理。 具体是在 commitMutationEffects 的 处理 parentFiber.deletions的流程中,
2. Ref 的解绑 (commitMutationEffects)
在 render 阶段主要是标记 更新、创建 状态的 DOM 元素存在 Ref 类型的副作用,在 commitMutationEffects 阶段主要是去执行 Ref 类型的副作用的旧节点的 ref 解绑工作。
具体如下
- 创建、更新类型的 Ref 副作用
js
// 如果存在 ref 属性,那就执行更新
if (flags & Ref) {
if (current !== null) {
// 存在旧节点
// 执行旧节点的 ref 解绑
safelyDetachRef(current, current.return);
}
}- 删除类型的 Ref 副作用
刚才说对于删除类型的 DOM 元素在 render 阶段没有处理,但是其 DOM 元素本身存在 Deletion 类型的副作用,所以在 commit 阶段也会去处理。
具体如下
js
function commitDeletionEffectsOnFiber(
// 根节点
finishedRoot: FiberRoot,
// 父节点
nearestMountedAncestor: Fiber,
// 待删除的节点
deletedFiber: Fiber
) {
switch (deletedFiber.tag) {
// 元素节点类型的 处理 ref
case HostComponent: {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
}
}
}所以对于 Ref 类型的副作用处理,都是通过 safelyDetachRef 来处理的。 其实现具体如下
js
/**
* 执行定义了ref的待删除DOM节点的ref回调
* - 如果是函数类型,执行回调函数 ref(null)
* - 如果是 useRef 类型,将 ref.current 赋值为 null
*
* @param {*} current
* @param {*} nearestMountedAncestor
*/
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
// 获取 ref 属性
const ref = current.ref;
if (ref !== null) {
// 如果是函数类型,执行函数
if (typeof ref === "function") {
let retVal;
try {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
current.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
retVal = ref(null);
} finally {
recordLayoutEffectDuration(current);
}
} else {
retVal = ref(null);
}
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
}
} else {
// 不是函数类型的 将其赋值为 null
ref.current = null;
}
}
}3. Ref 的绑定(commitLayoutEffects)
js
/**
* 将最终的 fiber 数有关的 ref 提交给 ref.current 或者 ref(instanceToUse)
* @param {*} finishedWork
*/
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
// 获取 stateNode
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
// 元素类型的 那就是 DOM 元素
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
// Moved outside to ensure DCE works with this flag
if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
instanceToUse = instance;
}
// 2. 根据 ref 值的类型,执行 ref(instanceToUse) 或者 ref.current = instanceToUse
if (typeof ref === "function") {
let retVal;
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
retVal = ref(instanceToUse);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
retVal = ref(instanceToUse);
}
} else {
ref.current = instanceToUse;
}
}
}从绑定 Ref 的流程看出,其不是仅仅只作用于 DOM 类型元素的绑定工作,其对于函数式组件、类组件等类型的元素也会去绑定 Ref。只是其绑定的值不同
- 对于 DOM 元素类型的,其绑定的值 通过
getPublicInstance(instance)来获取的 DOM 元素的值。 - 对于函数式组件、类组件等类型的,其绑定的值 就是
instance即FiberNode.stateNode。
3.3. 绑定 Class 组件
对于类组件的绑定 Ref 流程,其主要是在 commitLayoutEffects 中去处理的。
1. render 阶段
js
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderLanes: Lanes
) {
markRef(current, workInProgress);
}
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// Schedule a Ref effect
workInProgress.flags |= Ref;
if (enableSuspenseLayoutEffectSemantics) {
workInProgress.flags |= RefStatic;
}
}
}可见对于 类组件 Ref 的流程中也是在 beginWork 阶段去标记存在 Ref 副作用的。
2. Ref 的解绑 (commitMutationEffects)
在 render 阶段主要是标记 更新、创建 状态的 DOM 元素存在 Ref 类型的副作用,在 commitMutationEffects 阶段主要是去执行 Ref 类型的副作用的旧节点的 ref 解绑工作。
具体如下
- 创建、更新类型的 Ref 副作用
js
// 如果存在 ref 属性,那就执行更新
if (flags & Ref) {
if (current !== null) {
// 存在旧节点
// 执行旧节点的 ref 解绑
safelyDetachRef(current, current.return);
}
}- 删除类型的 Ref 副作用
刚才说对于删除类型的 DOM 元素在 render 阶段没有处理,但是其 DOM 元素本身存在 Deletion 类型的副作用,所以在 commit 阶段也会去处理。
具体如下
js
function commitDeletionEffectsOnFiber(
// 根节点
finishedRoot: FiberRoot,
// 父节点
nearestMountedAncestor: Fiber,
// 待删除的节点
deletedFiber: Fiber
) {
switch (deletedFiber.tag) {
case ClassComponent: {
// Ref 的 解绑
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
return;
}
}
}所以对于 Ref 类型的副作用处理,都是通过 safelyDetachRef 来处理的。
3. Ref 的绑定(commitLayoutEffects)
具体看 DOM 元素的绑定流程
3.4. 绑定 函数组件
对于在函数式组件上绑定 Ref 的流程,其不像类组件那样存在一个 实例对象 instace , 所以也无法直接访问函数式组件中的 实例方法,所以对于函数式组件的 Ref 绑定流程,其需要借助于一个单独的函数 forwardRef(FunctionalComponent , ref ) 去完成 ref 的绑定工作
下面我们看一下这个方法 forwardRef 的实现
forwardRef
例子
jsx
const FunctionalRefComponent = React.forwardRef((props, ref) => {
// 使用 ref 来绑定 DOM 元素
React.useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
}));
return (
<div>
<input ref={inputRef} />
</div>
);
});其主要分为以下几个阶段的处理工作
执行阶段
js
/**
* React.forwardRef 接收一个 render 函数作为参数,返回一个 REACT_FORWARD_REF_TYPE 类型的内置组件对象
* 重点: 创建一个 REACT_FORWARD_REF_TYPE 类型的内置组件对象
* @param {*} render
* @returns
*/
export function forwardRef<Props, ElementType: React$ElementType>(
render: (props: Props, ref: React$Ref<ElementType>) => React$Node
) {
if (__DEV__) {
if (render != null && render.$$typeof === REACT_MEMO_TYPE) {
console.error(
"forwardRef requires a render function but received a `memo` " +
"component. Instead of forwardRef(memo(...)), use " +
"memo(forwardRef(...))."
);
} else if (typeof render !== "function") {
console.error(
"forwardRef requires a render function but was given %s.",
render === null ? "null" : typeof render
);
} else {
if (render.length !== 0 && render.length !== 2) {
console.error(
"forwardRef render functions accept exactly two parameters: props and ref. %s",
render.length === 1
? "Did you forget to use the ref parameter?"
: "Any additional parameter will be undefined."
);
}
}
if (render != null) {
if (render.defaultProps != null || render.propTypes != null) {
console.error(
"forwardRef render functions do not support propTypes or defaultProps. " +
"Did you accidentally pass a React component?"
);
}
}
}
// forwardRef 返回的是一个 REACT_FORWARD_REF_TYPE 类型的内置组件对象
// 该对象的 render 属性指向传入的 render 函数
const elementType = {
$$typeof: REACT_FORWARD_REF_TYPE,
render,
};
if (__DEV__) {
let ownName;
Object.defineProperty(elementType, "displayName", {
enumerable: false,
configurable: true,
get: function () {
return ownName;
},
set: function (name) {
ownName = name;
if (!render.name && !render.displayName) {
render.displayName = name;
}
},
});
}
return elementType;
}可以看出对于 forwardRef 方法,其主要是创建一个 REACT_FORWARD_REF_TYPE 类型的内置组件对象,该对象的 render 属性指向传入的 render 函数。
同时看其 dev 环境下的一些错误处理。可以看出我们使用 forwardRef 方法时,其传入的函数必须
- 不是一个
React.memo()包裹的组件
- 不是一个
- 必须是一个函数,且通过
render.length来判断其参数存在的情况下必须要 2 个入参,即props和ref
- 必须是一个函数,且通过
- 函数不能存在
propTypes和defaultProps不能是 Class 组件类型的
- 函数不能存在
同时通过 Object.defineProperty 来给 REACT_FORWARD_REF_TYPE 类型的内置组件对象添加 displayName 属性。并将其直接指向 render 函数。
beginWork 阶段
forwardRef 函数创建的组件类型为 REACT_FORWARD_REF_TYPE, 然后通过 createFiberFromTypeAndProps创建 Fiber 对象的时候其组件类型转换成 ForwordRef 类型,那么在 beginWork 阶段对于 ForwordRef 类型 的 处理流程跟 FunctionalComponent 组件的流程基本差不多。
其主要区别是什么?
- 函数式组件的函数不一样
FuntionalComponent或者IndeterminateComponent类型的render是直接通过createFiberFromTypeAndProps获取的ForwardRef组件的render是直接通过Component.render获取的
- 函数组件的 ref 获取不一样
FuntionalComponent或者IndeterminateComponent类型的ref是直接通过createFiberFromTypeAndProps获取的ForwardRef组件的render是直接通过workInProgress.ref获取的
js
function updateForwardRef(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes
) {
const render = Component.render;
const ref = workInProgress.ref;
nextChildren = renderWithHooks(
current,
workInProgress,
render, // 传入的render
nextProps,
ref, // 传入的 ref
renderLanes
);
return workInProgress.child;
}那么怎么去给函数式组件的 ref 提供实例对象呐? 这就靠 forwradRef一起使用的另外一个方法 useImperativeHandle 其本身是一个 钩子函数
useImperativeHandle
初次渲染阶段
js
function mountImperativeHandle<T>(
ref: {| current: T | null |} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null
): void {
// 开发状态 检查第二参数必须是函数类型,提供 ref 绑定的实例函数数据
// 生成 deps 依赖更新的对象数组
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
return mountEffectImpl(
fiberFlags,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps
);
}useImperativeHandle 初次渲染的 执行函数,其主要是通过 useEffect 去维护 ref 和 提供数据的绑定关系的,所以其也包含 3 个关键数据
- 副作用
deps: 即[ref , ...deps]ref 和自定义依赖 - 处理函数 create :
imperativeHandleEffect - 销毁回调函数:
imperativeHandleEffect提供的refCallback(null)或refCallback.current = null
所以其核心就是 imperativeHandleEffect方法
imperativeHandleEffect
js
function imperativeHandleEffect<T>(
create: () => T,
ref: {| current: T | null |} | ((inst: T | null) => mixed) | null | void
) {
// function 类型的 ref
if (typeof ref === "function") {
const refCallback = ref;
// 生成 函数式组件提供方法对象
const inst = create();
// 回调 ref()
refCallback(inst);
return () => {
refCallback(null);
};
} else if (ref !== null && ref !== undefined) {
const refObject = ref;
const inst = create();
refObject.current = inst;
return () => {
refObject.current = null;
};
}
}按照 useEffect 的处理函数的方式去创建 函数式组件 ref 和 函数式组件提供方法对象 的绑定流程
- 按照 ref 是 函数 和 ref 对象 类型两种方式去将 create()的结果绑定到 ref.current 的结果上
- 提供对应的 effect 副作用 destroy 的销毁函数 如: refCallback(null) 或 refObject.current = null;
更新渲染阶段
js
function updateImperativeHandle<T>(
ref: {| current: T | null |} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null
): void {
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
return updateEffectImpl(
UpdateEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps
);
}更新渲染阶段处理, 没有什么特殊的逻辑,其跟初次渲染差不多,还是借助于 useEffect 更新阶段的实现方法 updateEffectImpl 绑定处理函数 imperativeHandleEffect
4. 总结
通过上面源码分析可以得出 Ref 主要在 3 个流程中进行处理
- completeWork 阶段: 收集 Ref 类型的副作用
- commitMutationEffects 阶段 : 将存在 Ref 副作用的与旧节点进行解绑
- commitLayoutEffects 阶段 : 将存在 Ref 副作用的与新节点进行绑定
当然对于特殊的函数式组件,因为其不存在生命周期对象,也不存在什么实例对象的概念, 所以其 ref 的处理需要通过 forwordRef 和 useImperativeHandle 两个方法进行处理
- 对于
forwordRef:
其创建了一个新的内置组件类型 REACT_FORWARD_REF_TYPE的组件,从而可以访问组件属性上的 ref 属性,然后通过 FunctionalComponent的流程去处理传入的第一个参数(函数式组件),这样就将 ref 的绑定与函数解耦。
- 对于
useImperativeHandle
其主要借助于 useEffect的实现方式去维护和管理 ref 与 其create()数据 的关系
