欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

React中的?ref?及原理解析

 更新時(shí)間:2025年01月01日 08:27:21   作者:袋鼠云數(shù)棧前端  
本章深入探討了React?Ref的用法和原理,還介紹了如何使用useImperativeHandle在函數(shù)組件中暴露方法,并詳細(xì)解釋了ref的處理邏輯和原理,包括在commit階段更新ref以及在組件卸載時(shí)的處理,感興趣的朋友一起看看吧

前言

對(duì)于 ref 的理解,我們一部人還停留在用 ref 獲取真實(shí) dom 元素和獲取組件層面上,但實(shí)際 ref 除了這兩項(xiàng)功能之外,在使用上還有很多小技巧。本章我們就一起深入探討研究一下 React ref 的用法和原理;本章中所有的源碼節(jié)選來(lái)自 16.8 版本

基本概念和使用

此部分將分成兩個(gè)部分去分析,第一部分是 ref 對(duì)象的創(chuàng)建,第二部分是 React 本身對(duì) ref 的處理;兩者不要混為一談,所謂 ref 對(duì)象的創(chuàng)建,就是通過(guò) React.createRef 或者 React.useRef 來(lái)創(chuàng)建一個(gè) ref 原始對(duì)象。而 React 對(duì) ref 處理,主要指的是對(duì)于標(biāo)簽中 ref 屬性,React 是如何處理以及 React 轉(zhuǎn)發(fā) ref 。下面來(lái)仔細(xì)介紹一下。

ref 對(duì)象的創(chuàng)建

什么是 ref ?

所謂 ref 對(duì)象就是用 createRef 或者 useRef 創(chuàng)建出來(lái)的對(duì)象,一個(gè)標(biāo)準(zhǔn)的 ref 對(duì)象應(yīng)該是如下的樣子:

{
  current: null, // current指向ref對(duì)象獲取到的實(shí)際內(nèi)容,可以是dom元素,組件實(shí)例。
}

React 提供兩種方法創(chuàng)建 ref 對(duì)象

類(lèi)組件 React.createRef

class Index extends React.Component{
    constructor(props){
       super(props)
       this.currentDom = React.createRef(null)
    }
    componentDidMount(){
        console.log(this.currentDom)
    }
    render= () => <div ref={ this.currentDom } >ref對(duì)象模式獲取元素或組件</div>
}

打印

React.createRef 的底層邏輯很簡(jiǎn)單。下面一起來(lái)看一下:

react/src/ReactCreateRef.js

export function createRef() {
  const refObject = {
    current: null,
  }
  return refObject;
}

createRef 一般用于類(lèi)組件創(chuàng)建 Ref 對(duì)象,可以將 Ref 對(duì)象綁定在類(lèi)組件實(shí)例上,這樣更方便后續(xù)操作 Ref。
注意:不要在函數(shù)組件中使用 createRef,否則會(huì)造成 Ref 對(duì)象內(nèi)容丟失等情況。

函數(shù)組件

函數(shù)組件創(chuàng)建 ref ,可以用 hooks 中的 useRef 來(lái)達(dá)到同樣的效果。

export default function Index(){
    const currentDom = React.useRef(null)
    React.useEffect(()=>{
        console.log( currentDom.current ) // div
    },[])
    return  <div ref={ currentDom } >ref對(duì)象模式獲取元素或組件</div>
}

react-reconciler/ReactFiberHooks.js

function mountRef<T>(initialValue: T): {current: T} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

useRef 返回一個(gè)可變的 ref 對(duì)象,其 current 屬性被初始化為傳入的參數(shù)(initialValue)。返回的 ref 對(duì)象在組件的整個(gè)生命周期內(nèi)保持不變。
這個(gè) ref 對(duì)象只有一個(gè)current屬性,你把一個(gè)東西保存在內(nèi),它的地址一直不會(huì)變。

拓展和總結(jié)

useRef 底層邏輯是和 createRef 差不多,就是 ref 保存位置不相同,類(lèi)組件有一個(gè)實(shí)例 instance 能夠維護(hù)像 ref 這種信息,但是由于函數(shù)組件每次更新都是一次新的開(kāi)始,所有變量重新聲明,所以 useRef 不能像 createRef 把 ref 對(duì)象直接暴露出去,如果這樣每一次函數(shù)組件執(zhí)行就會(huì)重新聲明 ref,此時(shí) ref 就會(huì)隨著函數(shù)組件執(zhí)行被重置,這就解釋了在函數(shù)組件中為什么不能用 createRef 的原因。
為了解決這個(gè)問(wèn)題,hooks 和函數(shù)組件對(duì)應(yīng)的 fiber 對(duì)象建立起關(guān)聯(lián),將 useRef 產(chǎn)生的 ref 對(duì)象掛到函數(shù)組件對(duì)應(yīng)的 fiber 上,函數(shù)組件每次執(zhí)行,只要組件不被銷(xiāo)毀,函數(shù)組件對(duì)應(yīng)的 fiber 對(duì)象一直存在,所以 ref 等信息就會(huì)被保存下來(lái)。

react 對(duì) ref 屬性的處理-標(biāo)記 ref

首先我們先明確一個(gè)問(wèn)題就是 DOM 元素和組件實(shí)例必須用 ref 來(lái)獲取嗎?答案肯定是否定的,比如 react 還提供了一個(gè) findDOMNode 方法可以獲取 dom 元素,有興趣的可以私下去了解一下。不過(guò)通過(guò) ref 的方式來(lái)獲取還是最常用的一種方式。

類(lèi)組件獲取 ref 二種方式

因?yàn)?ref 屬性是字符串的這種方式,react 高版本已經(jīng)舍棄掉,這里就不再介紹了。

ref 屬性是個(gè)函數(shù)

class Children extends React.Component{  
    render=()=><div>hello,world</div>
}
/* TODO: Ref屬性是一個(gè)函數(shù) */
export default class Index extends React.Component{
    currentDom = null
    currentComponentInstance = null
    componentDidMount(){
        console.log(this.currentDom)
        console.log(this.currentComponentInstance)
    }
    render=()=> <div>
        <div ref={(node)=> this.currentDom = node }  >Ref屬性是個(gè)函數(shù)</div>
        <Children ref={(node) => this.currentComponentInstance = node  }  />
    </div>
}

打印

當(dāng)用一個(gè)函數(shù)來(lái)標(biāo)記 ref 的時(shí)候,將作為 callback 形式,等到真實(shí) DOM 創(chuàng)建階段,執(zhí)行 callback ,獲取的 DOM 元素或組件實(shí)例,將以回調(diào)函數(shù)第一個(gè)參數(shù)形式傳入,所以可以像上述代碼片段中,用組件實(shí)例下的屬性 currentDom 和 currentComponentInstance 來(lái)接收真實(shí) DOM 和組件實(shí)例。

ref 屬性是一個(gè) ref 對(duì)象

class Children extends React.Component{  
    render=()=><div>hello,world</div>
}
export default class Index extends React.Component{
    currentDom = React.createRef(null)
    currentComponentInstance = React.createRef(null)
    componentDidMount(){
        console.log(this.currentDom)
        console.log(this.currentComponentInstance)
    }
    render=()=> <div>
         <div ref={ this.currentDom }  >Ref對(duì)象模式獲取元素或組件</div>
        <Children ref={ this.currentComponentInstance }  />
   </div>
}

函數(shù)組件獲取 ref 的方式(useRef)

const Children = () => {  
  return <div>hello,world</div>
}
const Index = () => {
  const currentDom = React.useRef(null)
  const currentComponentInstance = React.useRef(null)
  React.useEffect(()=>{
    console.log( currentDom ) 
    console.log( currentComponentInstance ) 
  },[])
  return (
    <div>
      <div ref={ currentDom }  >通過(guò)useRef獲取元素或者組件</div>
      <Children ref={ currentComponentInstance }  />
   </div>
  )
}

ref 的拓展用法

不得不說(shuō)的 forwardRef

forwardRef 的初衷就是解決 ref 不能跨層級(jí)捕獲和傳遞的問(wèn)題。 forwardRef 接受了父級(jí)元素標(biāo)記的 ref 信息,并把它轉(zhuǎn)發(fā)下去,使得子組件可以通過(guò) props 來(lái)接受到上一層級(jí)或者是更上層級(jí)的 ref 。

跨層級(jí)獲取

我們把上面的例子改一下

獲取子元素的dom

const Children = React.forwardRef((props, ref) => {
  return <div ref={ref}>hello,world</div>
}) 
const Index = () => {
  const currentDom = React.useRef(null)
  const currentComponentInstance = React.useRef(null)
  React.useEffect(()=>{
    console.log( currentDom ) 
    console.log( currentComponentInstance ) 
  },[])
  return (
    <div>
      <div ref={ currentDom }  >通過(guò)useRef獲取元素或者組件</div>
      <Children ref={ currentComponentInstance }  />
   </div>
  )

想要在 GrandFather 組件通過(guò)標(biāo)記 ref ,來(lái)獲取孫組件 Son 的組件實(shí)例。

// 孫組件
function Son (props){
  const { grandRef } = props
  return <div>
      <div> i am alien </div>
      <span ref={grandRef} >這個(gè)是想要獲取元素</span>
  </div>
}
// 父組件
class Father extends React.Component{
  render(){
      return <div>
          <Son grandRef={this.props.grandRef}  />
      </div>
  }
}
const NewFather = React.forwardRef((props,ref)=> <Father grandRef={ref}  {...props} />)
// 爺組件
class GrandFather extends React.Component{
  node = null 
  componentDidMount(){
      console.log(this.node) // span #text 這個(gè)是想要獲取元素
  }
  render(){
      return <div>
          <NewFather ref={(node)=> this.node = node } />
      </div>
  }
}

合并轉(zhuǎn)發(fā)ref

通過(guò) forwardRef 轉(zhuǎn)發(fā)的 ref 不要理解為只能用來(lái)直接獲取組件實(shí)例,DOM 元素,也可以用來(lái)傳遞合并之后的自定義的 ref

場(chǎng)景:想通過(guò) Home 綁定 ref ,來(lái)獲取子組件 Index 的實(shí)例 index ,dom 元素 button ,以及孫組件 Form 的實(shí)例

// 表單組件
class Form extends React.Component{
  render(){
     return <div>{...}</div>
  }
}
// index 組件
class Index extends React.Component{ 
  componentDidMount(){
      const { forwardRef } = this.props
      forwardRef.current={
          form:this.form,      // 給form組件實(shí)例 ,綁定給 ref form屬性 
          index:this,          // 給index組件實(shí)例 ,綁定給 ref index屬性 
          button:this.button,  // 給button dom 元素,綁定給 ref button屬性 
      }
  }
  form = null
  button = null
  render(){
      return <div   > 
        <button ref={(button)=> this.button = button }  >點(diǎn)擊</button>
        <Form  ref={(form) => this.form = form }  />  
    </div>
  }
}
const ForwardRefIndex = React.forwardRef(( props,ref )=><Index  {...props} forwardRef={ref}  />)
// home 組件
const Home = () => {
  const ref = useRef(null)
   useEffect(()=>{
       console.log(ref.current)
   },[])
  return <ForwardRefIndex ref={ref} />
}

高階組件轉(zhuǎn)發(fā)

如果通過(guò)高階組件包裹一個(gè)原始類(lèi)組件,就會(huì)產(chǎn)生一個(gè)問(wèn)題,如果高階組件 HOC 沒(méi)有處理 ref ,那么由于高階組件本身會(huì)返回一個(gè)新組件,所以當(dāng)使用 HOC 包裝后組件的時(shí)候,標(biāo)記的 ref 會(huì)指向 HOC 返回的組件,而并不是 HOC 包裹的原始類(lèi)組件,為了解決這個(gè)問(wèn)題,forwardRef 可以對(duì) HOC 做一層處理。

function HOC(Component){
  class Wrap extends React.Component{
     render(){
        const { forwardedRef ,...otherprops  } = this.props
        return <Component ref={forwardedRef}  {...otherprops}  />
     }
  }
  return  React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> ) 
}
class Index1 extends React.Component{
  state={
    name: '222'
  }
  render(){
    return <div>hello,world</div>
  }
}
const HocIndex =  HOC(Index1)
const AppIndex = ()=>{
  const node = useRef(null)
  useEffect(()=>{
    console.log(node.current)  /* Index 組件實(shí)例  */ 
  },[])
  return <div><HocIndex ref={node}  /></div>
}

源碼位置 react/src/forwardRef.js

export default function forwardRef<Props, ElementType: React$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
  return {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
}

ref 實(shí)現(xiàn)組件通信

如果有種場(chǎng)景不想通過(guò)父組件 render 改變 props 的方式,來(lái)觸發(fā)子組件的更新,也就是子組件通過(guò) state 單獨(dú)管理數(shù)據(jù)層,針對(duì)這種情況父組件可以通過(guò) ref 模式標(biāo)記子組件實(shí)例,從而操縱子組件方法,這種情況通常發(fā)生在一些數(shù)據(jù)層托管的組件上,比如  表單,經(jīng)典案例可以參考 antd 里面的 form 表單,暴露出對(duì)外的 resetFields , setFieldsValue 等接口,可以通過(guò)表單實(shí)例調(diào)用這些 API 。

/* 子組件 */
class Son extends React.PureComponent{
  state={
     fatherMes:'',
     sonMes:'我是子組件'
  }
  fatherSay=(fatherMes)=> this.setState({ fatherMes  }) /* 提供給父組件的API */
  render(){
      const { fatherMes, sonMes } = this.state
      return <div className="sonbox" >
          <p>父組件對(duì)我說(shuō):{ fatherMes }</p>
          <button className="searchbtn" onClick={ ()=> this.props.toFather(sonMes) }  >to father</button>
      </div>
  }
}
/* 父組件 */
function Father(){
  const [ sonMes , setSonMes ] = React.useState('') 
  const sonInstance = React.useRef(null) /* 用來(lái)獲取子組件實(shí)例 */
  const toSon =()=> sonInstance.current.fatherSay('我是父組件') /* 調(diào)用子組件實(shí)例方法,改變子組件state */
  return <div className="box" >
      <div className="title" >父組件</div>
      <p>子組件對(duì)我說(shuō):{ sonMes }</p>
      <button className="searchbtn"  onClick={toSon}  >to son</button>
      <Son ref={sonInstance} toFather={setSonMes} />
  </div>
}

子組件暴露方法 fatherSay 供父組件使用,父組件通過(guò)調(diào)用方法可以設(shè)置子組件展示內(nèi)容。
父組件提供給子組件 toFather,子組件調(diào)用,改變父組件展示內(nèi)容,實(shí)現(xiàn)父 <-> 子 雙向通信。

函數(shù)組件 forwardRef + useImperativeHandle

對(duì)于函數(shù)組件,本身是沒(méi)有實(shí)例的,但是 React Hooks 提供了,useImperativeHandle 一方面第一個(gè)參數(shù)接受父組件傳遞的 ref 對(duì)象,另一方面第二個(gè)參數(shù)是一個(gè)函數(shù),函數(shù)返回值,作為 ref 對(duì)象獲取的內(nèi)容。一起看一下 useImperativeHandle 的基本使用。

useImperativeHandle 接受三個(gè)參數(shù):

第一個(gè)參數(shù) ref : 接受 forWardRef 傳遞過(guò)來(lái)的 ref 。
第二個(gè)參數(shù) createHandle :處理函數(shù),返回值作為暴露給父組件的 ref 對(duì)象。
第三個(gè)參數(shù) deps : 依賴項(xiàng) deps,依賴項(xiàng)更改形成新的 ref 對(duì)象。

const Son = React.forwardRef((props, ref) => {
  const state = {
      sonMes:'我是子組件'
  }
  const [fatherMes, setFatherMes] = React.useState('')
    useImperativeHandle(ref,()=>{
      const handleRefs = {
        fatherSay(fatherMss){            
          setFatherMes(fatherMss)
        }
      }
      return handleRefs
  },[])
  const { sonMes } = state
  return (
    <div>
      <p>父組件對(duì)我說(shuō): {fatherMes}</p>
      <button  onClick={ ()=> props.toFather(sonMes) }  >to father</button>
    </div>
  ) 
})
/* 父組件 */
function Father(){
  const [ sonMes , setSonMes ] = React.useState('') 
  const sonInstance = React.useRef(null) /* 用來(lái)獲取子組件實(shí)例 */
  const toSon = () => {
    sonInstance.current.fatherSay('我是父組件')
  }
  return <div className="box" >
      <div className="title" >父組件</div>
      <p>子組件對(duì)我說(shuō):{ sonMes }</p>
      <button className="searchbtn"  onClick={toSon}  >to son</button>
      <Son ref={sonInstance} toFather={setSonMes} />
  </div>
}

forwardRef + useImperativeHandle 可以完全讓函數(shù)組件也能流暢的使用 Ref 通信。其原理圖如下所示:

函數(shù)組件緩存數(shù)據(jù)

函數(shù)組件每一次 render ,函數(shù)上下文會(huì)重新執(zhí)行,那么有一種情況就是,在執(zhí)行一些事件方法改變數(shù)據(jù)或者保存新數(shù)據(jù)的時(shí)候,有沒(méi)有必要更新視圖,有沒(méi)有必要把數(shù)據(jù)放到 state 中。如果視圖層更新不依賴想要改變的數(shù)據(jù),那么 state 改變帶來(lái)的更新效果就是多余的。這時(shí)候更新無(wú)疑是一種性能上的浪費(fèi)。

這種情況下,useRef 就派上用場(chǎng)了,上面講到過(guò),useRef 可以創(chuàng)建出一個(gè) ref 原始對(duì)象,只要組件沒(méi)有銷(xiāo)毀,ref 對(duì)象就一直存在,那么完全可以把一些不依賴于視圖更新的數(shù)據(jù)儲(chǔ)存到 ref 對(duì)象中。這樣做的好處有兩個(gè):

第一個(gè)能夠直接修改數(shù)據(jù),不會(huì)造成函數(shù)組件冗余的更新作用。
第二個(gè) useRef 保存數(shù)據(jù),如果有 useEffect ,useMemo 引用 ref 對(duì)象中的數(shù)據(jù),無(wú)須將 ref 對(duì)象添加成 dep 依賴項(xiàng),因?yàn)?useRef 始終指向一個(gè)內(nèi)存空間,所以這樣一點(diǎn)好處是可以隨時(shí)訪問(wèn)到變化后的值。

清除定時(shí)器

const App = () => {
  const [count, setCount] = useState(0)
  let timer;
  useEffect(() => {
    timer = setInterval(() => {
      console.log('觸發(fā)了');
    }, 1000);
  },[]);
  const clearTimer = () => {
    clearInterval(timer);
  }
  return (
    <>
     <button onClick={() => {setCount(count + 1)}}>點(diǎn)了{(lán)count}次</button>
      <button onClick={clearTimer}>停止</button>
    </>)
}

但是上面這個(gè)寫(xiě)法有個(gè)巨大的問(wèn)題,如果這個(gè) App 組件里有state變化或者他的父組件重新 render 等原因?qū)е逻@個(gè) App 組件重新 render 的時(shí)候,我們會(huì)發(fā)現(xiàn),點(diǎn)擊按鈕停止,定時(shí)器依然會(huì)不斷的在控制臺(tái)打印,定時(shí)器清除事件無(wú)效了。
為什么呢?因?yàn)榻M件重新渲染之后,這里的 timer 以及 clearTimer 方法都會(huì)重新創(chuàng)建,timer 已經(jīng)不是定時(shí)器的變量了。
所以對(duì)于定時(shí)器,我們都會(huì)使用 useRef 來(lái)定義變量。

const App = () => {
  const [count, setCount] = useState(0)
  const timer = useRef();
  useEffect(() => {
    timer.current = setInterval(() => {
      console.log('觸發(fā)了');
    }, 1000);
  },[]);
  const clearTimer = () => {
    clearInterval(timer.current);
  }
  return (
    <>
      <button onClick={() => {setCount(count + 1)}}>點(diǎn)了{(lán)count}次</button>
      <button onClick={clearTimer}>停止</button>
    </>)
}

ref 原理探究

對(duì)于 ref 標(biāo)簽引用,React 是如何處理的呢? 接下來(lái)先來(lái)看看一段 demo 代碼

class DomRef extends React.Component{
  state={ num:0 }
  node = null
  render(){
      return <div >
          <div ref={(node)=>{
             this.node = node
             console.log('此時(shí)的參數(shù)是什么:', this.node )
          }}  >ref元素節(jié)點(diǎn)</div>
          <button onClick={()=> this.setState({ num: this.state.num + 1  }) } >點(diǎn)擊</button>
      </div>
  }
}

控制臺(tái)輸出結(jié)果

提問(wèn): 第一次打印為 null ,第二次才是 div ,為什么會(huì)這樣呢? 這樣的意義又是什么呢?

ref 執(zhí)行的時(shí)機(jī)和處理邏輯

根據(jù) React 的生命周期可以知道,更新的兩個(gè)階段 render 階段和 commit 階段,對(duì)于整個(gè) ref 的處理,都是在 commit 階段發(fā)生的。之前了解過(guò) commit 階段會(huì)進(jìn)行真正的 Dom 操作,此時(shí) ref 就是用來(lái)獲取真實(shí)的 DOM 以及組件實(shí)例的,所以需要 commit 階段處理。
但是對(duì)于 ref 處理函數(shù),React 底層用兩個(gè)方法處理:commitDetachRef 和 commitAttachRef ,上述兩次 console.log 一次為 null,一次為 div 就是分別調(diào)用了上述的方法。

這兩次正好,一次在 DOM 更新之前,一次在 DOM 更新之后。

第一階段:一次更新中,在 commit 的 mutation 階段, 執(zhí)行commitDetachRef,commitDetachRef 會(huì)清空之前ref值,使其重置為 null。
源碼先來(lái)看一下

react-reconciler/src/ReactFiberCommitWork.js

function commitDetachRef(current: Fiber) {
  const currentRef = current.ref;
  if (currentRef !== null) {
    if (typeof currentRef === 'function') { /* function獲取方式。 */
      currentRef(null); 
    } else {   /* Ref對(duì)象獲取方式 */
      currentRef.current = null;
    }
  }
}

第二階段:DOM 更新階段,這個(gè)階段會(huì)根據(jù)不同的 effect 標(biāo)簽,真實(shí)的操作 DOM 。
第三階段:layout 階段,在更新真實(shí)元素節(jié)點(diǎn)之后,此時(shí)需要更新 ref

react-reconciler/src/ReactFiberCommitWork.js

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent: //元素節(jié)點(diǎn) 獲取元素
        instanceToUse = getPublicInstance(instance);
        break;
      default:  // 類(lèi)組件直接使用實(shí)例
        instanceToUse = instance;
    }
    if (typeof ref === 'function') {
      ref(instanceToUse);  //* function 和 字符串獲取方式。 */
    } else {
      ref.current = instanceToUse; /* ref對(duì)象方式 */
    }
  }
}

這一階段,主要判斷 ref 獲取的是組件還是 DOM 元素標(biāo)簽,如果 DOM 元素,就會(huì)獲取更新之后最新的 DOM 元素。上面流程中講了二種獲取 ref 的方式。 如果是 函數(shù)式 ref={(node)=> this.node = node } 會(huì)執(zhí)行 ref 函數(shù),重置新的 ref 。如果是 ref 對(duì)象方式,會(huì)更新 ref 對(duì)象的 current 屬性。達(dá)到更新 ref 對(duì)象的目的。

ref 的處理特性

接下來(lái)看一下 ref 的一些特性,首先來(lái)看一下,上述沒(méi)有提及的一個(gè)問(wèn)題,React 被 ref 標(biāo)記的 fiber,那么每一次 fiber 更新都會(huì)調(diào)用 commitDetachRef 和 commitAttachRef 更新 Ref 嗎 ?
答案是否定的,只有在 ref 更新的時(shí)候,才會(huì)調(diào)用如上方法更新 ref ,究其原因還要從如上兩個(gè)方法的執(zhí)行時(shí)期說(shuō)起

更新 ref

在 commit 階段 commitDetachRef 和 commitAttachRef 是在什么條件下被執(zhí)行的呢 ? 來(lái)一起看一下:
commitDetachRef 調(diào)用時(shí)機(jī)

react-reconciler/src/ReactFiberWorkLoop.js

function commitMutationEffects(){
     if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current);
      }
    }
}

commitAttachRef 調(diào)用時(shí)機(jī)

function commitLayoutEffects(){
     if (effectTag &amp; Ref) {
      commitAttachRef(nextEffect);
    }
}

從上可以清晰的看到只有含有 ref tag 的時(shí)候,才會(huì)執(zhí)行更新 ref,那么是每一次更新都會(huì)打 ref tag 嗎? 跟著我的思路往下看,什么時(shí)候標(biāo)記的 ref .

react-reconciler/src/ReactFiberBeginWork.js

function markRef(current: Fiber | null, workInProgress: Fiber) {
  const ref = workInProgress.ref;
  if (
    (current === null && ref !== null) ||      // 初始化的時(shí)候
    (current !== null && current.ref !== ref)  // ref 指向發(fā)生改變
  ) {
    workInProgress.effectTag |= Ref;
  }
}

首先 markRef 方法執(zhí)行在兩種情況下:
第一種就是類(lèi)組件的更新過(guò)程
第二種就是更新 HostComponent 的時(shí)候
markRef 會(huì)在以下兩種情況下給 effectTag 標(biāo)記 ref,只有標(biāo)記了 ref tag 才會(huì)有后續(xù)的 commitAttachRef 和 commitDetachRef 流程。( current 為當(dāng)前調(diào)和的 fiber 節(jié)點(diǎn) )

第一種 current === null && ref !== null:就是在 fiber 初始化的時(shí)候,第一次 ref 處理的時(shí)候,是一定要標(biāo)記 ref 的。
第二種 current !== null && current.ref !== ref:就是 fiber 更新的時(shí)候,但是 ref 對(duì)象的指向變了。

所以回到最初的那個(gè) DomRef 組件,為什么每一次按鈕,都會(huì)打印 ref ,那么也就是 ref 的回調(diào)函數(shù)執(zhí)行了,ref 更新了。每一次更新的時(shí)候,都給 ref 賦值了新的函數(shù),那么 markRef 中就會(huì)判斷成 current.ref !== ref,所以就會(huì)重新打 Ref 標(biāo)簽,那么在 commit 階段,就會(huì)更新 ref 執(zhí)行 ref 回調(diào)函數(shù)了。
要想解決這個(gè)問(wèn)題,我們把 DomRef 組件做下修改:

 class DomRef extends React.Component{
    state={ num:0 }
    node = null
    getDom= (node)=>{
        this.node = node
        console.log('此時(shí)的參數(shù)是什么:', this.node )
     }
    render(){
        return <div >
            <div ref={this.getDom}>ref元素節(jié)點(diǎn)</div>
            <button onClick={()=> this.setState({ num: this.state.num + 1  })} >點(diǎn)擊</button>
        </div>
    }
}

function DomRef () {
  const [num, setNum] = useState(0)
  const node = useRef(null)
      return (
      <div >
          <div ref={node}>ref元素節(jié)點(diǎn)</div>
          <button onClick={()=> {
            setNum(num + 1)
            console.log(node)
          }} >點(diǎn)擊</button>
      </div>
    )
}

卸載 ref

上述講了 ref 更新階段的特點(diǎn),接下來(lái)分析一下當(dāng)組件或者元素卸載的時(shí)候,ref 的處理邏輯是怎么樣的。

react-reconciler/src/ReactFiberCommitWork.js

function safelyDetachRef(current) {
  const ref = current.ref;
  if (ref !== null) {
    if (typeof ref === 'function') {  // 函數(shù)式 
        ref(null)
    } else {
      ref.current = null;  // ref 對(duì)象
    }
  }
}

被卸載的 fiber 會(huì)被打成 Deletion effect tag ,然后在 commit 階段會(huì)進(jìn)行 commitDeletion 流程。對(duì)于有 ref 標(biāo)記的 ClassComponent (類(lèi)組件) 和 HostComponent (元素),會(huì)統(tǒng)一走 safelyDetachRef 流程,這個(gè)方法就是用來(lái)卸載 ref。

總結(jié)

  • ref 對(duì)象的二種創(chuàng)建方式
  • 兩種獲取 ref 方法
  • 介紹了一下forwardRef 用法
  • ref 組件通信-函數(shù)組件和類(lèi)組件兩種方式
  • useRef 緩存數(shù)據(jù)的用法
  • ref 的處理邏輯原理

到此這篇關(guān)于React中的 ref 及原理淺析的文章就介紹到這了,更多相關(guān)React ref 原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • React Native提供自動(dòng)完成的下拉菜單的方法示例

    React Native提供自動(dòng)完成的下拉菜單的方法示例

    這篇文章主要為大家介紹了React Native提供自動(dòng)完成的下拉菜單的方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-10-10
  • 為react組件庫(kù)添加typescript類(lèi)型提示的方法

    為react組件庫(kù)添加typescript類(lèi)型提示的方法

    這篇文章主要介紹了為react組件庫(kù)添加typescript類(lèi)型提示,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-06-06
  • React開(kāi)發(fā)進(jìn)階redux saga使用原理詳解

    React開(kāi)發(fā)進(jìn)階redux saga使用原理詳解

    這篇文章主要為大家介紹了React開(kāi)發(fā)進(jìn)階redux saga使用原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-11-11
  • webpack4+react多頁(yè)面架構(gòu)的實(shí)現(xiàn)

    webpack4+react多頁(yè)面架構(gòu)的實(shí)現(xiàn)

    webpack在單頁(yè)面打包上應(yīng)用廣泛,以create-react-app為首的腳手架眾多。這篇文章主要介紹了webpack4+react多頁(yè)面架構(gòu)的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-10-10
  • React事件綁定的方式詳解

    React事件綁定的方式詳解

    react事件綁定時(shí)。this并不會(huì)指向當(dāng)前DOM元素。往往使用bind來(lái)改變this指向,今天通過(guò)本文給大家介紹React事件綁定的方式,感興趣的朋友
    2021-07-07
  • 淺談react受控組件與非受控組件(小結(jié))

    淺談react受控組件與非受控組件(小結(jié))

    本篇文章主要介紹了淺談react受控組件與非受控組件(小結(jié)),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-02-02
  • React Native使用Modal自定義分享界面的示例代碼

    React Native使用Modal自定義分享界面的示例代碼

    本篇文章主要介紹了React Native使用Modal自定義分享界面的示例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-10-10
  • 在create-react-app中使用sass的方法示例

    在create-react-app中使用sass的方法示例

    這篇文章主要介紹了在create-react-app中使用sass的方法示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-10-10
  • React實(shí)現(xiàn)導(dǎo)出excel文件的操作步驟

    React實(shí)現(xiàn)導(dǎo)出excel文件的操作步驟

    在React項(xiàng)目的TypeScript文件中,因?yàn)樵腏avaScript或TypeScript并沒(méi)有提供直接的Excel導(dǎo)出功能,常用的Excel導(dǎo)出方法通常涉及使用第三方庫(kù),本文介紹了React實(shí)現(xiàn)導(dǎo)出excel文件的操作步驟,需要的朋友可以參考下
    2024-12-12
  • 使用useMutation和React Query發(fā)布數(shù)據(jù)demo

    使用useMutation和React Query發(fā)布數(shù)據(jù)demo

    這篇文章主要為大家介紹了使用useMutation和React Query發(fā)布數(shù)據(jù)demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12

最新評(píng)論