React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了解决办法。
本文基于 React v16.8.6
,本文代码地址
相关官方文档
- https://zh-hans.reactjs.org/docs/refs-and-the-dom.html Refs and the DOM
- https://zh-hans.reactjs.org/docs/forwarding-refs.html Refs 转发
何时使用 Refs
- 管理焦点,文本选择或媒体播放(显示输入框的时候自动聚焦)
- 触发强制动画
- 集成第三方 DOM 库(传递 dom 节点进去)
3 种 ref
string ref
class StringRef extends Component { componentDidMount() { console.log('this', this); console.log('this.props', this.props); console.log('this.refs', this.refs); } render() { return ( <div ref="container"> StringRef </div> ) } } console.log(<StringRef />);
打印的结果
<StringRef />
是由 React.createlElement
产生的一个对象,自身不是实例,所以它和 this
存在区别。
callback ref
class CallbackRef extends Component { componentDidMount() { console.log(this.props); } render() { return ( <div ref={r => this.container = r}> CallbackRef </div> ) } }
object ref
function ObjectRef(params) { const r = useRef(); // const r = createRef(); useEffect(() => { console.log('ObjectRef', r); }); return ( <div ref={r}> ObjectRef </div> ) }
ref 高级使用
传递回调形式的 refs
class ParentComp extends Component { componentDidMount() { setTimeout(() => { console.log('this.inner', this.inner); }, 1000); } render() { return ( <ChildComp innerRef={r => this.inner = r} /> ) } } function ChildComp({ innerRef }) { const r = createRef(); useEffect(() => { innerRef(r.current); }); return ( <div ref={r}> ChildComp </div> ) }
这样从父组件就可以拿到子组件了。
forwardRef
forward ref
class Input extends Component { focus = () => { console.log('focused'); this.input.focus(); } render() { return ( <div> <input ref={r => this.input = r} id="input" /> <button onClick={this.focus}>focus input</button> </div> ) } } function FocusInput(Comp) { class FocusInputComp extends React.Component { render() { const {forwardedRef, ...rest} = this.props; // 将自定义的 prop 属性 “forwardedRef” 定义为 ref return <Comp ref={forwardedRef} {...rest} />; } } // 注意 React.forwardRef 回调的第二个参数 “ref”。 // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef” // 然后它就可以被挂载到被 LogPros 包裹的子组件上。 return React.forwardRef((props, ref) => { return <FocusInputComp {...props} forwardedRef={ref} />; }); } function ForwardComp(params) { const input = useRef(); const ForwardInput = FocusInput(Input); useEffect(() => { console.log(input); setTimeout(() => { input.current.focus(); }, 1000); }); return <ForwardInput ref={input} inputName="ForwardInput" />; }
过 1s 之后输入框会自动 focus。
forwardRef 源码
去除 warning 代码之后,react/src/forwardRef
中的源码
// 这个 API 我也没有用过,具体文档看这里 https://reactjs.org/docs/forwarding-refs.html // 总结来说就是能把 ref 传递到函数组件上 // 其实没有这个 API 之前,你也可以通过 props 的方式传递 ref // 这个实现没啥好说的,就是让 render 函数多了 ref 这个参数 export default function forwardRef<Props, ElementType: React$ElementType>( render: (props: Props, ref: React$Ref<ElementType>) => React$Node, ) { return { $$typeof: REACT_FORWARD_REF_TYPE, render, }; }
这仅仅是构建了一种结构,渲染要交给 react dom。
打一个 debugger 查看调用栈。
renderWithHooks
本次调用时 renderWithHooks
的参数。
参数解释
Component
就是forwardRef
中的匿名函数props
是React.forwardRef
生成的组件的 props,传递inputName
时,props 为{inputName: xxx}
refOrContext
是React.forwardRef
生成的组件的 ref
在 react-reconciler/src/ReactFiberHooks
中。
// forwardRef 处理的地方 export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, Component: any, props: any, refOrContext: any, nextRenderExpirationTime: ExpirationTime, ): any { // ... // forwardRef((props, ref) => (<Comp {...props} forwardRef={ref} />)) // 将从父组件上获得的 props 和 ref 传递给匿名函数,这个匿名函数实际也是一个组件(function component) let children = Component(props, refOrContext); // ... return children; }
updateForwardRef
再看 updateForwardRef
,在 react-reconciler/src/ReactFiberBeginWork
文件中
这里显示的非常清楚。
render
是React.forwardRef
返回对象的 renderref
就是用useRef
创建的对象
beginWork
再往上看,调用的是 beginWork
,在 react-reconciler/src/ReactFiberBeginWork
。
在 shared/ReactWorkTags
中 export const ForwardRef = 11;
。再往上不是本篇文章的范围,不作讲解,囧!!!
分析 ref 是如何被挂载的
从上篇 React 源码文章 React 源码系列-Component、PureComponent、function Component 分析 ,我们知道 <StringRef>
由 babel 编译之后,是由 createElement
来生成一个对象的。函数执行过程中,会将 ref
属性从 props
中单独拿出来。
经过 function createElementWithValidation(type, props, children)
,在 function createElement(type, config, children)
中被提取出来。
if (config != null) { // 验证 ref 和 key,只在开发环境下 if (hasValidRef(config)) { ref = config.ref; } // ... }
在 ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props)
中,ref 传递到返回的对象
注意到一个问题没有?
我们打印的 <StringRef />
居然 ref = null
这是因为,我们的 ref 不在 <StringRef />
的属性上,而是在 <div />
上!!!
在 ReactElement(创建 element 的最后一个环节)
中打印
结果是
ref => refs
是在哪里实现的呢?
我们见到的组件现在都还仅仅是由 createElement
生成的,父与子、root 与 曾曾曾 child 之间的联系是 type
,type 是组件本身,class component 是 render 方法里面返回对象, function component 是直接 return 返回。
我们生成的结构,是交给 ReactDOM.render
渲染出来的,这篇文章不讲渲染部分,直达 refs 。
里面的 element._owner
就是挂载在 render
方法的class,element 是 render 方法 return 的一部分。
例子
class A extends Component{ render() { return <StrComp ref="xxx" />; }; } // A 就是 StrComp 的 owner
coerceRef
在 react-reconciler/src/ReactChildFiber
coerceRef
功能: 检查 element.ref
并返回 ref 函数或者对象
- 如果是 string ref,则返回一个函数,返回的函数主要是把该 ref 挂载在 element 的 owner 实例的 refs 上
this.refs
- 如果是其他类型的 ref,则直接返回它
function coerceRef( returnFiber: Fiber, current: Fiber | null, element: ReactElement, ) { // mixedRef 是 function 或者 object 就直接 return 它 let mixedRef = element.ref; // 如果是字符串,则返回一个函数,这个函数会将 ref 指向的 dom 挂载在 this.refs 上 if ( mixedRef !== null && typeof mixedRef !== 'function' && typeof mixedRef !== 'object' ) { // 拥有者 就是给其它组件设置 props 的那个组件。 // 更正式地说,如果组件 Y 在 render() 方法是创建了组件 X,那么 Y 就拥有 X。 // 组件不能修改自身的 props - 它们总是与它们拥有者设置的保持一致。这是保持用户界面一致性的基本不变量。 if (element._owner) { const owner: ?Fiber = (element._owner: any); let inst; // undefined // 有 owner 就提取 owner 实例 if (owner) { const ownerFiber = ((owner: any): Fiber); // owner 实例(父组件实例) inst = ownerFiber.stateNode; } // mixedRef 强制转换成字符串 const stringRef = '' + mixedRef; // Check if previous string ref matches new string ref if ( current !== null && current.ref !== null && typeof current.ref === 'function' && current.ref._stringRef === stringRef ) { return current.ref; } // 重点是这个函数 const ref = function(value) { // 拿到 owner stateNode 的 refs let refs = inst.refs; // var emptyObject = {}; // { // Object.freeze(emptyObject); // } // Component 中 this.refs = emptyObject; // export const emptyRefsObject = new React.Component().refs; if (refs === emptyRefsObject) { // This is a lazy pooled frozen object, so we need to initialize. refs = inst.refs = {}; } if (value === null) { delete refs[stringRef]; } else { // 将 dom 和 this.props.refs.xxx 绑定 refs[stringRef] = value; } }; // 给 ref 函数添加 _stringRef 属性为 stringRef ref._stringRef = stringRef; return ref; } } return mixedRef; }
ownerFiber.stateNode
就是 owner
组件的实例,可以滑到上面最上面去看 String ref 的 this
我们打个 debuger 看看。
注意 div...cxqa2
,看看我们 string ref 指向的 div
ref 函数处理的就是我写的 string ref。
commitAttachRef
在 react-reconciler/src/ReactFiberCommitWork
调用栈往上看,调用了 commitAttachRef
。
看到 commitAttachRef
的内容没有?是它来处理 dom 和 refs、function、createRef object 的挂接的。
function commitAttachRef(finishedWork: Fiber) { // finishedWork 处理好的 FiberNode, string ref 在这之前被 coerceRef 函数处理好了 const ref = finishedWork.ref; if (ref !== null) { // 获取它的实例 const instance = finishedWork.stateNode; let instanceToUse; // 下面的 switch 可能是准备加某个功能现在预留出来的 switch (finishedWork.tag) { // 原生组件,div span ... case HostComponent: // function getPublicInstance(instance) { // return instance; // } // instanceToUse === instance true instanceToUse = getPublicInstance(instance); break; default: instanceToUse = instance; } if (typeof ref === 'function') { // ref 是函数由两种情况 // 1、string ref 返回的函数,传进去 ref 本应该指向的实例,则 `refs[stringRef] = instanceToUse` // 2、ref 属性我们定义了一个函数 `r => this.xxx = r`,则 `this.xxx => instanceToUse`,这样后面就可以使用 `this.xxx` 调用该实例了 ref(instanceToUse); } else { // dev 时,检测对象是否包含 current 属性 // ... // 传进来一个对象,则把实例赋值给 `xx.current` // `React.createRef()` 返回一个对象 `{current: null}` // `React.useRef()` 返回一个对象 `{current: undefined}` // 给变量引用的对象的某个属性赋值,在其他作用域依然可以获取到该属性 ref.current = instanceToUse; } } }
finishedWork
这里 finishedWork.stateNode
就是 html div
涉及的函数
forwardRef
renderWithHooks
updateForwardRef
beginWork
coerceRef
commitAttachRef
关于 ref 的源码分析就到这里。收获很多,这篇文章大大加深了我对 ref 的理解!!!
没做源码分析之前,总感觉非常困难, react-dom 源码就有 2.5w 行,看到了都怕!!!现在越分析越有劲,每次分析都是在不断加深理解。