React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了解决办法。

本文基于 React v16.8.6,本文代码地址

相关官方文档

何时使用 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 />);

打印的结果

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

<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>
    )
  }
}

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

object ref

function ObjectRef(params) {
  const r = useRef();
  // const r = createRef();
  useEffect(() => {
    console.log('ObjectRef', r);
  });
  return (
    <div ref={r}>
      ObjectRef
    </div>
  )
}

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

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>
  )
}

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

这样从父组件就可以拿到子组件了。

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。

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

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 查看调用栈。

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

renderWithHooks

本次调用时 renderWithHooks 的参数。

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

参数解释

  • Component 就是 forwardRef 中的匿名函数
  • propsReact.forwardRef 生成的组件的 props,传递 inputName 时,props 为 {inputName: xxx}
  • refOrContextReact.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 文件中

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

这里显示的非常清楚。

  • renderReact.forwardRef 返回对象的 render
  • ref 就是用 useRef 创建的对象

beginWork

再往上看,调用的是 beginWork,在 react-reconciler/src/ReactFiberBeginWork

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

shared/ReactWorkTagsexport 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 传递到返回的对象

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

注意到一个问题没有?

我们打印的 <StringRef /> 居然 ref = null

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

这是因为,我们的 ref 不在 <StringRef /> 的属性上,而是在 <div /> 上!!!

ReactElement(创建 element 的最后一个环节) 中打印

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

结果是

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

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

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

我们打个 debuger 看看。

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

注意 div...cxqa2 ,看看我们 string ref 指向的 div

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

ref 函数处理的就是我写的 string ref。

commitAttachRef

react-reconciler/src/ReactFiberCommitWork

调用栈往上看,调用了 commitAttachRef

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

看到 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

React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

这里 finishedWork.stateNode 就是 html div
React 源码系列 | ref 功能详解 | 源码 + 实战例子 | 你可能并不真正懂 ref

涉及的函数

  • forwardRef
  • renderWithHooks
  • updateForwardRef
  • beginWork
  • coerceRef
  • commitAttachRef

关于 ref 的源码分析就到这里。收获很多,这篇文章大大加深了我对 ref 的理解!!!

没做源码分析之前,总感觉非常困难, react-dom 源码就有 2.5w 行,看到了都怕!!!现在越分析越有劲,每次分析都是在不断加深理解。

相关推荐