ref 的作用是获取实例,可能是 DOM 实例,也可能是 ClassComponent 的实例。
但会碰到以下问题:
① 如果目标组件是一个FunctionComponent
的话,是没有实例的(PureComponent),此时用 ref 去传递会报错:
② 如果你是一个库的开发者的话,使用该库的人是不知道库的组件类别的,那么当库组件类别是FunctionComponent
的时候,使用者想用 ref 获取库组件,怎么办?
③ redux 中的connect
方法将组件包装成高阶组件(HOC),那么我如何通过 ref 去获取包装前的组件实例?
④ props 不能传递 ref
React 官方也表述了 ref 的使用条件:
React.forwardRef
存在的意义就是为了解决以上问题。
关于React.forwardRef
的使用,请参考:
https://zh-hans.reactjs.org/docs/react-api.html#reactforwardref
接下来我们讲下React.forwardRef
的源码
作用: 创建一个 React 组件,该组件能够将其接收的 ref 属性转发到内部的一个组件中
源码:
export default function forwardRef<Props, ElementType: React$ElementType>(
render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
//删除了 dev 代码
return {
//被forwardRef包裹后,组件内部的$$typeof是REACT_FORWARD_REF_TYPE
$$typeof: REACT_FORWARD_REF_TYPE,
//render即包装的FunctionComponent,ClassComponent是不用forwardRef的
render,
};
}
解析: (1) 返回的是一个对象,即下面的变量 Child:
const Child = React.forwardRef((props, ref) => (
<button ref={ref}>
{props.children}
</button>
));
console.log(Child,'Child29')
(2) 返回的对象里面的$$typeof
,并不说明Child
组件类型是REACT_FORWARD_REF_TYPE
我之前写的 React源码解析之React.createElement()和ReactElement() 中有提到ReactElement
的type
为REACT_ELEMENT_TYPE:
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// This tag allows us to uniquely identify this as a React Element
//标识element的类型
//因为jsx都是通过createElement创建的,所以ReactElement的类型固定:为REACT_ELEMENT_TYPE
//重要!因为react最终渲染到DOM上时,需要判断$$typeof===REACT_ELEMENT_TYPE
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
//设置元素的内置属性
type: type,
};
}
注意:
① 变量Child
是对象:
{
$$typeof: REACT_FORWARD_REF_TYPE,
render,
};
Child
组件是ReactElement
对象,两者不一样:
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
};
当把Child
转变为ReactElement
的时候,Child
对象作为Child
组件的type
属性:
const element = {
$$typeof: REACT_ELEMENT_TYPE,
//注意!!!
type: {
$$typeof: REACT_FORWARD_REF_TYPE,
render,
},
};
不要认为被forwardRef
包裹后,React 组件的$$typeof
的值会改变成REACT_FORWARD_REF_TYPE
!
② 只有FunctionComponent
才用得到forwardRef
,ClassComponent
`不需要
作用:
更新ref
指向及被React.forwardRef
包裹的FunctionComponent
源码:
//更新被React.forwardRef包裹的 FunctionComponent
function updateForwardRef(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderExpirationTime: ExpirationTime,
) {
// TODO: current can be non-null here even if the component
// hasn't yet mounted. This happens after the first render suspends.
// We'll need to figure out if this is fine or can cause issues.
//删除了 dev 代码
//Component:{
// $$typeof: REACT_FORWARD_REF_TYPE,
// render,
// }
//FunctionComponent
const render = Component.render;
// 开发层面上不允许FunctionComponent,但你打印 props 的话是有的,
// 因为是 React 只允许内部通过 props 传进来 ref
const ref = workInProgress.ref;
// The rest is a fork of updateFunctionComponent
let nextChildren;
//context 相关的可跳过
prepareToReadContext(workInProgress, renderExpirationTime);
prepareToReadEventComponents(workInProgress);
if (__DEV__) {
//删除了 dev 代码
} else {
//渲染的过程中,对里面用到的 hook函数做一些操作
//关于renderWithHooks的讲解,请看:https://www.jianshu.com/p/959498695e83
//注意:在updateFunctionComponent()中传的参数不是 ref,
//而是 context:nextChildren = renderWithHooks(
// current,
// workInProgress,
// Component,
// nextProps,
// 传的是 context 而不是 ref
// context,
// renderExpirationTime,
// );
nextChildren = renderWithHooks(
current,
workInProgress,
render,
nextProps,
ref,
renderExpirationTime,
);
//renderWithHooks 内部通过let children = Component(props, refOrContext)来更新 ref 或 context
}
//如果 props 相同,并且 ref 也相同的话,就不需要更新
if (current !== null && !didReceiveUpdate) {
//跳过hooks更新
//关于bailoutHooks的讲解,请看:https://www.jianshu.com/p/959498695e83
bailoutHooks(current, workInProgress, renderExpirationTime);
//跳过该节点及所有子节点的更新
//关于bailoutOnAlreadyFinishedWork的讲解,请看:https://www.jianshu.com/p/06b18db8b5d4
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
//将 ReactElement 变成 fiber对象,并更新,生成对应 DOM 的实例,并挂载到真正的 DOM 节点上
//关于reconcileChildren的讲解,请看:https://www.jianshu.com/p/959498695e83
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
解析:
(1) 逻辑比较简单,里面大部分函数在之前的文章里已解析过:
① 关于 renderWithHooks
、bailoutHooks
、reconcileChildren
的讲解,请看:
React源码解析之FunctionComponent(上)
② 关于 bailoutOnAlreadyFinishedWork
的讲解,请看:
(2) ref 的更新是在renderWithHooks
中:
let children = Component(props, refOrContext);
这里的Component
是「二、React.forwardRef
」中 Child 对象的render
属性,也就是执行渲染FunctionComponent
的方法
refOrContext
在这里是workInProgress.ref
也就是说Component(props, refOrContext)
的参数即React.forwardRef
中的参数(props, ref)
:
React.forwardRef((props, ref) => (
xxx
));
小进进还没开通留言功能,觉得不错的话,点「在看」、转发朋友圈都是一种支持 (●'◡'●)ノ 。