React 源碼系列 | ref 功能詳解 | 源碼 + 實戰例子 | 你可能並不真正懂 ref

Refs 提供了一種方式,容許咱們訪問 DOM 節點或在 render 方法中建立的 React 元素。
在典型的 React 數據流中,props 是父組件與子組件交互的惟一方式。要修改一個子組件,你須要使用新的 props 來從新渲染它。可是,在某些狀況下,你須要在典型數據流以外強制修改子組件。被修改的子組件多是一個 React 組件的實例,也多是一個 DOM 元素。對於這兩種狀況,React 都提供瞭解決辦法。

本文基於 React v16.8.6,本文代碼地址html

相關官方文檔react

什麼時候使用 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 />);

打印的結果git

<StringRef /> 是由 React.createlElement 產生的一個對象,自身不是實例,因此它和 this 存在區別。github

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 refbabel

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。dom

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 查看調用棧。post

renderWithHooks

本次調用時 renderWithHooks 的參數。

參數解釋

  • 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 文件中

這裏顯示的很是清楚。

  • renderReact.forwardRef 返回對象的 render
  • ref 就是用 useRef 建立的對象

beginWork

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

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 傳遞到返回的對象

注意到一個問題沒有?

咱們打印的 <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 是函數由兩種狀況
      // 一、string ref 返回的函數,傳進去 ref 本應該指向的實例,則 `refs[stringRef] = instanceToUse`
      // 二、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 行,看到了都怕!!!如今越分析越有勁,每次分析都是在不斷加深理解。

相關文章
相關標籤/搜索