亚洲国产日韩欧美一区二区三区,精品亚洲国产成人av在线,国产99视频精品免视看7,99国产精品久久久久久久成人热,欧美日韩亚洲国产综合乱

目錄
1. ref 的理解與使用
1.1.2. useRef
1.2. React 對標(biāo)籤中ref 屬性的處理
:::
%%PRE_BLOCK_1%%
#ref 屬性傳遞函數(shù)時,會在commit 階段建立真實DOM 時執(zhí)行ref 指定的函數(shù),並將元素作為第一個參數(shù)傳入,此時我們就可以利用它進(jìn)行賦值以取得DOM 元素或元件實例%%PRE_BLOCK_2%%
2.1.3. 高階組件轉(zhuǎn)發(fā) ref
2.2. ref 實現(xiàn)組件通信
2.2.1. 類組件 ref 暴露組件實例
2.2.2. 函數(shù)組件 ref 暴露指定方法
2.3. 函數(shù)組件緩存數(shù)據(jù)
3. 通過 callback ref 探究 ref 原理
3.1. ref 的底層原理
3.1.1. commitDetachRef
3.1.2. commitAttachRef
3.2. 為什么 string ref 也是以函數(shù)的方式調(diào)用?
3.3. ref 的執(zhí)行時機(jī)
3.3.1. commitDetachRef 執(zhí)行時機(jī)
3.3.2. commitAttachRef 執(zhí)行時機(jī)
3.3.3. fiber 何時打上 Ref tag?
3.3.4. 為什么每次點擊按鈕 callback ref 都會執(zhí)行?
3.3.5. 如何解決?
4. 總結(jié)
首頁 web前端 js教程 深入詳解React中的ref

深入詳解React中的ref

Jan 05, 2023 pm 09:13 PM
react ref

深入詳解React中的ref

對於Ref 理解與使用,有些讀者可能還會停留在用ref 取得真實DOM 元素和取得類別元件實例層面上

其實ref 除了這兩項常用功能之外,還有很多別的小技巧

透過本篇文章的學(xué)習(xí),你將收穫React ref 的基本和進(jìn)階用法,並且能夠明白React 內(nèi)部是如何處理ref 的,並通過一個小Demo 提問的方式帶你更深刻地理解ref 的底層原理

1. ref 的理解與使用

對於Ref 的理解,要從兩個角度去分析:

  • Ref 物件的建立:使用createRefuseRef 建立Ref 物件【相關(guān)推薦:Redis影片教學(xué)程式設(shè)計影片

  • React 本身對Ref 的處理:對於標(biāo)籤中的ref 屬性,React 是如何處理的

1.1. ref 物件的建立

1.1.1. createRef在類別元件中,我們會透過createRef

去建立一個Ref 對象,會被儲存在類別元件實例上,它的實作很簡單

packages/react/src/ReactCreateRef.js#

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

  return refObject
}

可以看到,就是創(chuàng)建了一個包含

current

屬性的對象,僅此而已

1.1.2. useRef

這也意味著我們不能在函數(shù)元件中使用

createRef,因為每次函數(shù)元件渲染都是一次新的函數(shù)執(zhí)行,每次執(zhí)行createRef

得到的都是一個新的對象,無法保留其原來的參考

所以在函數(shù)元件中,我們會使用

useRef 建立Ref 對象,React 會將useRef 和函數(shù)元件對應(yīng)的fiber 物件關(guān)聯(lián),將useRef建立的ref 物件掛載到對應(yīng)的fiber 物件上

這樣一來每次函數(shù)元件執(zhí)行,只要函數(shù)元件不被銷毀,那麼對應(yīng)的fiber 物件實例也會一直存在,所以ref 也能夠被保留下來

1.2. React 對標(biāo)籤中ref 屬性的處理

首先要明確一個結(jié)論,在React 中取得DOM 元素或元件實例並不是只能透過ref 物件取得! ! !

也就是說不是只能透過先呼叫createRef

建立ref 對象,然後將它賦值到要取得的元素或元件實例的ref 屬性上,實際上還有別的方式

深入詳解React中的ref:::tip

只有類別元件才有取得元件實例這一說法,函數(shù)元件沒有實例,不能被ref 標(biāo)記,但是可以透過

forwardRef

結(jié)合

useImperativeHandle

給予函數(shù)元件ref 標(biāo)記的

:::

1.2.1. string ref

#當(dāng)我們給元素或類別元件標(biāo)籤中的ref 屬性傳遞字串時,能夠在元件實例的深入詳解React中的refthis.refs

中存??取到

class Child extends React.Component<PropsWithChildren> {
  render(): React.ReactNode {
    const { children } = this.props

    return (
      <div>
        <p>Child</p>
        {children}
      </div>
    )
  }
}

/** @description ref 屬性傳遞字符串 */
class RefDemo1 extends React.Component {
  logger = createLoggerWithScope(&#39;RefDemo1&#39;)

  componentDidMount(): void {
    this.logger.log(this.refs)
  }

  render(): React.ReactNode {
    return (
      <>
        <div ref="refDemo1DOM">ref 屬性傳遞字符串獲取 DOM 元素</div>
        <Child ref="refDemo1Component">ref 屬性傳遞字符串獲取類組件實例</Child>
      </>
    )
  }
}

##::: warning

這種方式已經(jīng)被React 官方廢棄,盡量不要使用##:::

1.2.2. callback ref

#ref 屬性傳遞函數(shù)時,會在commit 階段建立真實DOM 時執(zhí)行ref 指定的函數(shù),並將元素作為第一個參數(shù)傳入,此時我們就可以利用它進(jìn)行賦值以取得DOM 元素或元件實例
/** @description ref 屬性傳遞函數(shù) */
class RefDemo2 extends React.Component {
  logger = createLoggerWithScope(&#39;RefDemo2&#39;)

  refDemo2DOM: HTMLElement | null = null
  refDemo2Component: Child | null = null

  componentDidMount(): void {
    this.logger.log(this.refDemo2DOM)
    this.logger.log(this.refDemo2Component)
  }

  render(): React.ReactNode {
    return (
      <>
        <div ref={(el) => (this.refDemo2DOM = el)}>
          ref 屬性傳遞函數(shù)獲取 DOM 元素
        </div>

        <Child ref={(child) => (this.refDemo2Component = child)}>
          ref 屬性傳遞函數(shù)獲取類組件實例
        </Child>
      </>
    )
  }
}

1.2.3. object ref

這種方式就是我們最常用的方式了,使用

createRef

或###useRef### 建立Ref 對象,並將其傳給標(biāo)籤的ref 屬性即可######這種方式取得到的ref 需要先呼叫###current### 屬性才能取得對應(yīng)的DOM 元素或元件實例###
/** @description ref 屬性傳遞對象 */
class RefDemo3 extends React.Component {
  logger = createLoggerWithScope(&#39;RefDemo3&#39;)

  refDemo3DOM = React.createRef<HTMLDivElement>()
  refDemo3Component = React.createRef<Child>()

  componentDidMount(): void {
    this.logger.log(this.refDemo3DOM)
    this.logger.log(this.refDemo3Component)
  }

  render(): React.ReactNode {
    return (
      <>
        <div ref={this.refDemo3DOM}>ref 屬性傳遞對象獲取 DOM 元素</div>

        <Child ref={this.refDemo3Component}>
          ref 屬性傳遞對象獲取類組件實例
        </Child>
      </>
    )
  }
}
######2. ref 高階用法############2.1. forwardRef 轉(zhuǎn)送ref######### ####2.1.1. 跨層級獲取#########想要在爺組件中透過在子組件中傳遞ref 獲取到孫組件的某個元素,也就是在爺組件中獲取到了孫組件的元素,是一種跨層級取得###
/** @description 孫組件 */
const Child: React.FC<{ grandRef: LegacyRef<HTMLDivElement> }> = (props) => {
  const { grandRef } = props

  return (
    <>
      <p>Child</p>
      <div ref={grandRef}>要獲取的目標(biāo)元素</div>
    </>
  )
}

/**
 * @description 父組件
 *
 * 第一個泛型參數(shù)是 ref 的類型
 * 第二個泛型參數(shù)是 props 的類型
 */
const Father = forwardRef<HTMLDivElement, {}>((props, ref) => {
  return (
    <div>
      <Child grandRef={ref} />
    </div>
  )
})

/** @description 爺組件 */
const GrandFather: React.FC = () => {
  let grandChildDiv: HTMLDivElement | null = null

  useEffect(() => {
    logger.log(grandChildDiv)
  }, [])

  return (
    <div>
      <Father ref={(el) => (grandChildDiv = el)} />
    </div>
  )
}
######2.1.2. 合併轉(zhuǎn)送自訂ref#########forwardRef 不僅可以轉(zhuǎn)送ref 取得DOM 元素與元件實例,還可以轉(zhuǎn)送合併後的自訂ref######什麼是「合併後的自訂ref」呢?透過一個場景來看看就明白了######:::info{title=場景}######透過給 Foo 元件綁定 ref,取得多個內(nèi)容,包括:###
  • 子組件 Bar 的組件實例

  • Bar 組件中的 DOM 元素 button

  • 孫組件 Baz 的組件實例

:::

這種在一個 ref 里能夠訪問多個元素和實例的就是“合并后的自定義 ref”

/** @description 自定義 ref 的類型 */
interface CustomRef {
  bar: Bar
  barButton: HTMLButtonElement
  baz: Baz
}

class Baz extends React.Component {
  render(): React.ReactNode {
    return <div>Baz</div>
  }
}

class Bar extends React.Component<{
  customRef: ForwardedRef<CustomRef>
}> {
  buttonEl: HTMLButtonElement | null = null
  bazInstance: Baz | null = null

  componentDidMount(): void {
    const { customRef } = this.props

    if (customRef) {
      ;(customRef as MutableRefObject<CustomRef>).current = {
        bar: this,
        barButton: this.buttonEl!,
        baz: this.bazInstance!,
      }
    }
  }

  render() {
    return (
      <>
        <button ref={(el) => (this.buttonEl = el)}>Bar button</button>
        <Baz ref={(instance) => (this.bazInstance = instance)} />
      </>
    )
  }
}
const FowardRefBar = forwardRef<CustomRef>((props, ref) => (
  <Bar {...props} customRef={ref} />
))

const Foo: React.FC = () => {
  const customRef = useRef<CustomRef>(null)

  useEffect(() => {
    logger.log(customRef.current)
  }, [])

  return <FowardRefBar ref={customRef} />
}

深入詳解React中的ref

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

如果我們在高階組件中直接使用 ref,它會直接指向 WrapComponent

class TestComponent extends React.Component {
  render(): React.ReactNode {
    return <p>TestComponent</p>
  }
}

/** @description 不使用 forwardRef 轉(zhuǎn)發(fā) HOC 中的 ref */
const HOCWithoutForwardRef = (Component: typeof React.Component) => {
  class WrapComponent extends React.Component {
    render(): React.ReactNode {
      return (
        <div>
          <p>WrapComponent</p>
          <Component />
        </div>
      )
    }
  }

  return WrapComponent
}

const HOCComponent1 = HOCWithoutForwardRef(TestComponent)
const RefHOCWithoutForwardRefDemo = () => {
  const logger = createLoggerWithScope(&#39;RefHOCWithoutForwardRefDemo&#39;)
  const wrapRef = useRef(null)

  useEffect(() => {
    // wrapRef 指向的是 WrapComponent 實例 而不是 HOCComponent1 實例
    logger.log(wrapRef.current)
  }, [])

  return <HOCComponent1 ref={wrapRef} />
}

深入詳解React中的ref

如果我們希望 ref 指向的是被包裹的 TestComponent 而不是 HOC 內(nèi)部的 WrapComponent 時該怎么辦呢?

這時候就可以用 forwardRef 進(jìn)行轉(zhuǎn)發(fā)了

/** @description HOC 中使用 forwardRef 轉(zhuǎn)發(fā) ref */
const HOCWithForwardRef = (Component: typeof React.Component) => {
  class WrapComponent extends React.Component<{
    forwardedRef: LegacyRef<any>
  }> {
    render(): React.ReactNode {
      const { forwardedRef } = this.props

      return (
        <div>
          <p>WrapComponent</p>
          <Component ref={forwardedRef} />
        </div>
      )
    }
  }

  return React.forwardRef((props, ref) => (
    <WrapComponent forwardedRef={ref} {...props} />
  ))
}

const HOCComponent2 = HOCWithForwardRef(TestComponent)
const RefHOCWithForwardRefDemo = () => {
  const logger = createLoggerWithScope(&#39;RefHOCWithForwardRefDemo&#39;)
  const hocComponent2Ref = useRef(null)

  useEffect(() => {
    // hocComponent2Ref 指向的是 HOCComponent2 實例
    logger.log(hocComponent2Ref.current)
  }, [])

  return <HOCComponent2 ref={hocComponent2Ref} />
}

深入詳解React中的ref

2.2. ref 實現(xiàn)組件通信

一般我們可以通過父組件改變子組件 props 的方式觸發(fā)子組件的更新渲染完成組件間通信

但如果我們不希望通過這種改變子組件 props 的方式的話還能有別的辦法嗎?

可以通過 ref 獲取子組件實例,然后子組件暴露出通信的方法,父組件調(diào)用該方法即可觸發(fā)子組件的更新渲染

對于函數(shù)組件,由于其不存在組件實例這樣的說法,但我們可以通過 useImperativeHandle 這個 hook 來指定 ref 引用時得到的屬性和方法,下面我們分別用類組件和函數(shù)組件都實現(xiàn)一遍

2.2.1. 類組件 ref 暴露組件實例

/**
 * 父 -> 子 使用 ref
 * 子 -> 父 使用 props 回調(diào)
 */
class CommunicationDemoFather extends React.Component<
  {},
  CommunicationDemoFatherState
> {
  state: Readonly<CommunicationDemoFatherState> = {
    fatherToChildMessage: &#39;&#39;,
    childToFatherMessage: &#39;&#39;,
  }

  childRef = React.createRef<CommunicationDemoChild>()

  /** @description 提供給子組件修改父組件中的狀態(tài) */
  handleChildToFather = (message: string) => {
    this.setState((state) => ({
      ...state,
      childToFatherMessage: message,
    }))
  }

  constructor(props: {}) {
    super(props)
    this.handleChildToFather = this.handleChildToFather.bind(this)
  }

  render(): React.ReactNode {
    const { fatherToChildMessage, childToFatherMessage } = this.state

    return (
      <div className={s.father}>
        <h3>父組件</h3>
        <p>子組件對我說:{childToFatherMessage}</p>
        <div className={s.messageInputBox}>
          <section>
            <label htmlFor="to-father">我對子組件說:</label>
            <input
              type="text"
              id="to-child"
              onChange={(e) =>
                this.setState((state) => ({
                  ...state,
                  fatherToChildMessage: e.target.value,
                }))
              }
            />
          </section>

          {/* 父 -> 子 -- 使用 ref 完成組件通信 */}
          <button
            onClick={() =>
              this.childRef.current?.setFatherToChildMessage(
                fatherToChildMessage,
              )
            }
          >
            發(fā)送
          </button>
        </div>

        <CommunicationDemoChild
          ref={this.childRef}
          onChildToFather={this.handleChildToFather}
        />
      </div>
    )
  }
}

interface CommunicationDemoChildProps {
  onChildToFather: (message: string) => void
}
// 子組件自己維護(hù)狀態(tài) 不依賴于父組件 props
interface CommunicationDemoChildState {
  fatherToChildMessage: string
  childToFatherMessage: string
}
class CommunicationDemoChild extends React.Component<
  CommunicationDemoChildProps,
  CommunicationDemoChildState
> {
  state: Readonly<CommunicationDemoChildState> = {
    fatherToChildMessage: &#39;&#39;,
    childToFatherMessage: &#39;&#39;,
  }

  /** @description 暴露給父組件使用的 API -- 修改父到子的消息 fatherToChildMessage */
  setFatherToChildMessage(message: string) {
    this.setState((state) => ({ ...state, fatherToChildMessage: message }))
  }

  render(): React.ReactNode {
    const { onChildToFather: emitChildToFather } = this.props
    const { fatherToChildMessage, childToFatherMessage } = this.state

    return (
      <div className={s.child}>
        <h3>子組件</h3>
        <p>父組件對我說:{fatherToChildMessage}</p>
        <div className={s.messageInputBox}>
          <section>
            <label htmlFor="to-father">我對父組件說:</label>
            <input
              type="text"
              id="to-father"
              onChange={(e) =>
                this.setState((state) => ({
                  ...state,
                  childToFatherMessage: e.target.value,
                }))
              }
            />
          </section>

          {/* 子 -> 父 -- 使用 props 回調(diào)完成組件通信 */}
          <button onClick={() => emitChildToFather(childToFatherMessage)}>
            發(fā)送
          </button>
        </div>
      </div>
    )
  }
}

深入詳解React中的ref

2.2.2. 函數(shù)組件 ref 暴露指定方法

使用 useImperativeHandle hook 可以讓我們指定 ref 引用時能獲取到的屬性和方法,個人認(rèn)為相比類組件的 ref,使用這種方式能夠更加好的控制組件想暴露給外界的 API

而不像類組件那樣直接全部暴露出去,當(dāng)然,如果你想在類組件中只暴露部分 API 的話,可以用前面說的合并轉(zhuǎn)發(fā)自定義 ref 的方式去完成

接下來我們就用 useImperativeHandle hook 改造上面的類組件實現(xiàn)的 demo 吧

interface ChildRef {
  setFatherToChildMessage: (message: string) => void
}

/**
 * 父 -> 子 使用 ref
 * 子 -> 父 使用 props 回調(diào)
 */
const CommunicationDemoFunctionComponentFather: React.FC = () => {
  const [fatherToChildMessage, setFatherToChildMessage] = useState(&#39;&#39;)
  const [childToFatherMessage, setChildToFatherMessage] = useState(&#39;&#39;)

  const childRef = useRef<ChildRef>(null)

  return (
    <div className={s.father}>
      <h3>父組件</h3>
      <p>子組件對我說:{childToFatherMessage}</p>
      <div className={s.messageInputBox}>
        <section>
          <label htmlFor="to-father">我對子組件說:</label>
          <input
            type="text"
            id="to-child"
            onChange={(e) => setFatherToChildMessage(e.target.value)}
          />
        </section>

        {/* 父 -> 子 -- 使用 ref 完成組件通信 */}
        <button
          onClick={() =>
            childRef.current?.setFatherToChildMessage(fatherToChildMessage)
          }
        >
          發(fā)送
        </button>
      </div>

      <CommunicationDemoFunctionComponentChild
        ref={childRef}
        onChildToFather={(message) => setChildToFatherMessage(message)}
      />
    </div>
  )
}

interface CommunicationDemoFunctionComponentChildProps {
  onChildToFather: (message: string) => void
}
const CommunicationDemoFunctionComponentChild = forwardRef<
  ChildRef,
  CommunicationDemoFunctionComponentChildProps
>((props, ref) => {
  const { onChildToFather: emitChildToFather } = props

  // 子組件自己維護(hù)狀態(tài) 不依賴于父組件 props
  const [fatherToChildMessage, setFatherToChildMessage] = useState(&#39;&#39;)
  const [childToFatherMessage, setChildToFatherMessage] = useState(&#39;&#39;)

  // 定義暴露給外界的 API
  useImperativeHandle(ref, () => ({ setFatherToChildMessage }))

  return (
    <div className={s.child}>
      <h3>子組件</h3>
      <p>父組件對我說:{fatherToChildMessage}</p>
      <div className={s.messageInputBox}>
        <section>
          <label htmlFor="to-father">我對父組件說:</label>
          <input
            type="text"
            id="to-father"
            onChange={(e) => setChildToFatherMessage(e.target.value)}
          />
        </section>

        {/* 子 -> 父 -- 使用 props 回調(diào)完成組件通信 */}
        <button onClick={() => emitChildToFather(childToFatherMessage)}>
          發(fā)送
        </button>
      </div>
    </div>
  )
})

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

當(dāng)我們在函數(shù)組件中如果數(shù)據(jù)更新后不希望視圖改變,也就是說視圖不依賴于這個數(shù)據(jù),這個時候可以考慮用 useRef 對這種數(shù)據(jù)進(jìn)行緩存

為什么 useRef 可以對數(shù)據(jù)進(jìn)行緩存?

還記得之前說的 useRef 在函數(shù)組件中的作用原理嗎?

React 會將 useRef 和函數(shù)組件對應(yīng)的 fiber 對象關(guān)聯(lián),將 useRef 創(chuàng)建的 ref 對象掛載到對應(yīng)的 fiber 對象上,這樣一來每次函數(shù)組件執(zhí)行,只要函數(shù)組件不被銷毀,那么對應(yīng)的 fiber 對象實例也會一直存在,所以 ref 也能夠被保留下來

利用這個特性,我們可以將數(shù)據(jù)放到 useRef 中,由于它在內(nèi)存中一直都是同一塊內(nèi)存地址,所以無論如何變化都不會影響到視圖的改變

:::warning{title=注意}

一定要看清前提,只適用于與視圖無關(guān)的數(shù)據(jù)

:::

我們通過一個簡單的 demo 來更清楚地體會下這個應(yīng)用場景

假設(shè)我有一個 todoList 列表,視圖上會把這個列表渲染出來,并且有一個數(shù)據(jù) activeTodoItem 是控制當(dāng)前選中的是哪個 todoItem

點擊 todoItem 會切換這個 activeTodoItem,但是并不需要在視圖上作出任何變化,如果使用 useState 去保存 activeTodoItem,那么當(dāng)其變化時會導(dǎo)致函數(shù)組件重新執(zhí)行,視圖重新渲染,但在這個場景中我們并不希望更新視圖

相對的,我們希望這個 activeTodoItem 數(shù)據(jù)被緩存起來,不會隨著視圖的重新渲染而導(dǎo)致其作為 useState 的執(zhí)行結(jié)果重新生成一遍,因此我們可以改成用 useRef 實現(xiàn),因為其在內(nèi)存中一直都是同一塊內(nèi)存地址,這樣就不會因為它的改變而更新視圖了

同理,在 useEffect 中如果使用到了 useRef 的數(shù)據(jù),也不需要將其聲明到 deps 數(shù)組中,因為其內(nèi)存地址不會變化,所以每次在 useEffect 中獲取到的 ref 數(shù)據(jù)一定是最新的

interface TodoItem {
  id: number
  name: string
}

const todoList: TodoItem[] = [
  {
    id: 1,
    name: &#39;coding&#39;,
  },
  {
    id: 2,
    name: &#39;eating&#39;,
  },
  {
    id: 3,
    name: &#39;sleeping&#39;,
  },
  {
    id: 4,
    name: &#39;playing&#39;,
  },
]

const CacheDataWithRefDemo: React.FC = () => {
  const activeTodoItem = useRef(todoList[0])

  // 模擬 componentDidUpdate -- 如果改變 activeTodoItem 后組件沒重新渲染,說明視圖可以不依賴于 activeTodoItem 數(shù)據(jù)
  useEffect(() => {
    logger.log(&#39;檢測組件是否有更新&#39;)
  })

  return (
    <div className={s.container}>
      <div className={s.list}>
        {todoList.map((todoItem) => (
          <div
            key={todoItem.id}
            className={s.item}
            onClick={() => (activeTodoItem.current = todoItem)}
          >
            <p>{todoItem.name}</p>
          </div>
        ))}
      </div>

      <button onClick={() => logger.log(activeTodoItem.current)}>
        控制臺輸出最新的 activeTodoItem
      </button>
    </div>
  )
}

深入詳解React中的ref

3. 通過 callback ref 探究 ref 原理

首先先看一個關(guān)于 callback ref 的小 Demo 來引出我們后續(xù)的內(nèi)容

interface RefDemo8State {
  counter: number
}
class RefDemo8 extends React.Component<{}, RefDemo8State> {
  state: Readonly<RefDemo8State> = {
    counter: 0,
  }

  el: HTMLDivElement | null = null

  render(): React.ReactNode {
    return (
      <div>
        <div
          ref={(el) => {
            this.el = el
            console.log(&#39;this.el -- &#39;, this.el)
          }}
        >
          ref element
        </div>
        <button
          onClick={() => this.setState({ counter: this.state.counter + 1 })}
        >
          add
        </button>
      </div>
    )
  }
}

深入詳解React中的ref

為什么會執(zhí)行兩次?為什么第一次 this.el === null?為什么第二次又正常了?

3.1. ref 的底層原理

還記得 React 底層是有 render 階段和 commit 階段的嗎?關(guān)于 ref 的處理邏輯就在 commit 階段進(jìn)行的

React 底層有兩個關(guān)于 ref 的處理函數(shù) -- commitDetachRefcommitAttachRef

上面的 Demo 中 callback ref 執(zhí)行了兩次正是對應(yīng)著這兩次函數(shù)的調(diào)用,大致來講可以理解為 commitDetachRef 在 DOM 更新之前執(zhí)行,commitAttachRef 在 DOM 更新之后執(zhí)行

這也就不難理解為什么會有上面 Demo 中的現(xiàn)象了,但我們還是要結(jié)合源碼來看看,加深自己的理解

3.1.1. commitDetachRef

在新版本的 React 源碼中它改名為了 safelyDetachRef,但是核心邏輯沒變,這里我將核心邏輯簡化出來供大家閱讀:

packages/react-reconciler/src/ReactFiberCommitWork.js

function commitDetachRef(current: Fiber) {
  // current 是已經(jīng)調(diào)和完了的 fiber 對象
  const currentRef = current.ref

  if (currentRef !== null) {
    if (typeof currentRef === &#39;function&#39;) {
      // callback ref 和 string ref 執(zhí)行時機(jī)
      currentRef(null)
    } else {
      // object ref 處理時機(jī)
      currentRef.current = null
    }
  }
}

可以看到,就是從 fiber 中取出 ref,然后根據(jù) callback ref、string ref、object ref 的情況進(jìn)行處理

并且也能看到 commitDetachRef 主要是將 ref 置為 null,這也就是為什么 RefDemo8 中第一次執(zhí)行的 callback ref 中看到的 this.el 是 null 了

3.1.2. commitAttachRef

核心邏輯代碼如下:

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref
  if (ref !== null) {
    const instance = finishedWork.stateNode
    let instanceToUse

    // 處理 ref 來源
    switch (finishedWork.tag) {
      // HostComponent 代表 DOM 元素類型的 tag
      case HostComponent:
        instanceToUse = getPublicInstance(instance)
        break

      // 類組件使用組件實例
      default:
        instanceToUse = instance
    }

    if (typeof ref === &#39;function&#39;) {
      // callback ref 和 string ref
      ref(instanceToUse)
    } else {
      // object ref
      ref.current = instanceToUse
    }
  }
}

3.2. 為什么 string ref 也是以函數(shù)的方式調(diào)用?

從上面的核心源碼中能看到,對于 callback refstring ref,都是統(tǒng)一以函數(shù)的方式調(diào)用,將 nullinstanceToUse 傳入

callback ref 這樣做還能理解,但是為什么 string ref 也是這樣處理呢?

因為當(dāng) React 檢測到是 string ref 時,會自動綁定一個函數(shù)用于處理 string ref,核心源碼邏輯如下:

packages/react-reconciler/src/ReactChildFiber.js

// 從元素上獲取 ref
const mixedRef = element.ref
const stringRef = &#39;&#39; + mixedRef
const ref = function (value) {
  // resolvedInst 就是組件實例
  const refs = resolvedInst.refs

  if (value === null) {
    delete refs[stringRef]
  } else {
    refs[stringRef] = value
  }
}

這樣一來 string ref 也變成了一個函數(shù)了,從而可以在 commitDetachRefcommitAttachRef 中被執(zhí)行,并且也能印證為什么 string ref 會在類組件實例的 refs 屬性中獲取到

3.3. ref 的執(zhí)行時機(jī)

為什么在 RefDemo8 中我們每次點擊按鈕時都會觸發(fā) commitDetachRefcommitAttachRef 呢?這就需要聊聊 ref 的執(zhí)行時機(jī)了,而從上文也能夠了解到,ref 底層實際上是由 commitDetachRefcommitAttachRef 在處理核心邏輯

那么我們就得來看看這兩個函數(shù)的執(zhí)行時機(jī)才能行

3.3.1. commitDetachRef 執(zhí)行時機(jī)

packages/react-reconciler/src/ReactFiberCommitWork.js

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate
  const flags = finishedWork.flags

  if (flags & Ref) {
    if (current !== null) {
      // 也就是 commitDetachRef
      safelyDetachRef(current, current.return)
    }
  }
}

3.3.2. commitAttachRef 執(zhí)行時機(jī)

packages/react-reconciler/src/ReactFiberCommitWork.js

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
) {
  const flags = finishedWork.flags

  if (flags & Ref) {
    safelyAttachRef(finishedWork, finishedWork.return)
  }
}

3.3.3. fiber 何時打上 Ref tag?

可以看到,只有當(dāng) fiber 被打上了 Ref 這個 flag tag 時才會去執(zhí)行 commitDetachRef/commitAttachRef

那么什么時候會標(biāo)記 Ref tag 呢?

packages/react-reconciler/src/ReactFiberBeginWork.js

function markRef(current: Fiber | null, workInProgress: Fiber) {
  const ref = workInProgress.ref

  if (
    // current === null 意味著是初次掛載,fiber 首次調(diào)和時會打上 Ref tag
    (current === null && ref !== null) ||
    // current !== null 意味著是更新,此時需要 ref 發(fā)生了變化才會打上 Ref tag
    (current !== null && current.ref !== ref)
  ) {
    // Schedule a Ref effect
    workInProgress.flags |= Ref
  }
}

3.3.4. 為什么每次點擊按鈕 callback ref 都會執(zhí)行?

那么現(xiàn)在再回過頭來思考 RefDemo8 中為什么每次點擊按鈕都會執(zhí)行 commitDetachRefcommitAttachRef 呢?

注意我們使用 callback ref 的時候是如何使用的

<div
  ref={(el) => {
    this.el = el
    console.log(&#39;this.el -- &#39;, this.el)
  }}
>
  ref element
</div>

是直接聲明了一個箭頭函數(shù),這樣的方式會導(dǎo)致每次渲染這個 div 元素時,給 ref 賦值的都是一個新的箭頭函數(shù),盡管函數(shù)的內(nèi)容是一樣的,但內(nèi)存地址不同,因而 current.ref !== ref 這個判斷條件會成立,從而每次都會觸發(fā)更新

3.3.5. 如何解決?

那么要如何解決這個問題呢?既然我們已經(jīng)知道了問題的原因,那么就好說了,只要讓每次賦值給 ref 的函數(shù)都是同一個就可以了唄~

const logger = createLoggerWithScope(&#39;RefDemo9&#39;)

interface RefDemo9Props {}
interface RefDemo9State {
  counter: number
}
class RefDemo9 extends React.Component<RefDemo9Props, RefDemo9State> {
  state: Readonly<RefDemo9State> = {
    counter: 0,
  }

  el: HTMLDivElement | null = null

  constructor(props: RefDemo9Props) {
    super(props)
    this.setElRef = this.setElRef.bind(this)
  }

  setElRef(el: HTMLDivElement | null) {
    this.el = el
    logger.log(&#39;this.el -- &#39;, this.el)
  }

  render(): React.ReactNode {
    return (
      <div>
        <div ref={this.setElRef}>ref element</div>
        <button
          onClick={() => this.setState({ counter: this.state.counter + 1 })}
        >
          add
        </button>
      </div>
    )
  }
}

深入詳解React中的ref

這樣就完美解決啦,既修復(fù)了 bug,又搞懂了 ref 的底層原理,一舉兩得!

4. 總結(jié)

本篇文章我們學(xué)習(xí)到了:

  • ref 的理解與使用,包括如何創(chuàng)建 ref 對象,以及除了 object ref 之外的 string ref 和 callback ref 的方式去使用 ref
  • ref 的高階用法,包括 forwardRef 轉(zhuǎn)發(fā) ref、ref 實現(xiàn)組件通信、利用 ref 在函數(shù)組件中緩存數(shù)據(jù)等
  • 通過一個簡單的 callback ref 的 Demo 研究 ref 的底層原理,string ref 為何也是以函數(shù)的方式被調(diào)用,以及 ref 的執(zhí)行時機(jī)

【推薦學(xué)習(xí):javascript視頻教程

以上是深入詳解React中的ref的詳細(xì)內(nèi)容。更多資訊請關(guān)注PHP中文網(wǎng)其他相關(guān)文章!

本網(wǎng)站聲明
本文內(nèi)容由網(wǎng)友自願投稿,版權(quán)歸原作者所有。本站不承擔(dān)相應(yīng)的法律責(zé)任。如發(fā)現(xiàn)涉嫌抄襲或侵權(quán)的內(nèi)容,請聯(lián)絡(luò)admin@php.cn

熱AI工具

Undress AI Tool

Undress AI Tool

免費(fèi)脫衣圖片

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅(qū)動的應(yīng)用程序,用於創(chuàng)建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費(fèi)的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費(fèi)的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強(qiáng)大的PHP整合開發(fā)環(huán)境

Dreamweaver CS6

Dreamweaver CS6

視覺化網(wǎng)頁開發(fā)工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Laravel 教程
1597
29
PHP教程
1488
72
React與Vue:Netflix使用哪個框架? React與Vue:Netflix使用哪個框架? Apr 14, 2025 am 12:19 AM

NetflixusesAcustomFrameworkcalled“ Gibbon” BuiltonReact,notReactorVuedIrectly.1)TeamSperience:selectBasedonFamiliarity.2)ProjectComplexity:vueforsimplerprojects:reactforforforproproject,reactforforforcompleplexones.3)cocatizationneedneeds:reactoffipicatizationneedneedneedneedneedneeds:reactoffersizationneedneedneedneedneeds:reactoffersizatization needefersmoreflexibleise.4)

React的生態(tài)系統(tǒng):庫,工具和最佳實踐 React的生態(tài)系統(tǒng):庫,工具和最佳實踐 Apr 18, 2025 am 12:23 AM

React生態(tài)系統(tǒng)包括狀態(tài)管理庫(如Redux)、路由庫(如ReactRouter)、UI組件庫(如Material-UI)、測試工具(如Jest)和構(gòu)建工具(如Webpack)。這些工具協(xié)同工作,幫助開發(fā)者高效開發(fā)和維護(hù)應(yīng)用,提高代碼質(zhì)量和開發(fā)效率。

Netflix的前端:React(或VUE)的示例和應(yīng)用 Netflix的前端:React(或VUE)的示例和應(yīng)用 Apr 16, 2025 am 12:08 AM

Netflix使用React作為其前端框架。 1)React的組件化開發(fā)模式和強(qiáng)大生態(tài)系統(tǒng)是Netflix選擇它的主要原因。 2)通過組件化,Netflix將復(fù)雜界面拆分成可管理的小塊,如視頻播放器、推薦列表和用戶評論。 3)React的虛擬DOM和組件生命週期優(yōu)化了渲染效率和用戶交互管理。

反應(yīng):JavaScript庫用於Web開發(fā)的功能 反應(yīng):JavaScript庫用於Web開發(fā)的功能 Apr 18, 2025 am 12:25 AM

React是由Meta開發(fā)的用於構(gòu)建用戶界面的JavaScript庫,其核心是組件化開發(fā)和虛擬DOM技術(shù)。 1.組件與狀態(tài)管理:React通過組件(函數(shù)或類)和Hooks(如useState)管理狀態(tài),提升代碼重用性和維護(hù)性。 2.虛擬DOM與性能優(yōu)化:通過虛擬DOM,React高效更新真實DOM,提升性能。 3.生命週期與Hooks:Hooks(如useEffect)讓函數(shù)組件也能管理生命週期,執(zhí)行副作用操作。 4.使用示例:從基本的HelloWorld組件到高級的全局狀態(tài)管理(useContext和

React的未來:Web開發(fā)的趨勢和創(chuàng)新 React的未來:Web開發(fā)的趨勢和創(chuàng)新 Apr 19, 2025 am 12:22 AM

React的未來將專注於組件化開發(fā)的極致、性能優(yōu)化和與其他技術(shù)棧的深度集成。 1)React將進(jìn)一步簡化組件的創(chuàng)建和管理,推動組件化開發(fā)的極致。 2)性能優(yōu)化將成為重點,特別是在大型應(yīng)用中的表現(xiàn)。 3)React將與GraphQL和TypeScript等技術(shù)深度集成,提升開發(fā)體驗。

React的前端開發(fā):優(yōu)勢和技術(shù) React的前端開發(fā):優(yōu)勢和技術(shù) Apr 17, 2025 am 12:25 AM

React的優(yōu)勢在於其靈活性和高效性,具體表現(xiàn)在:1)組件化設(shè)計提高了代碼重用性;2)虛擬DOM技術(shù)優(yōu)化了性能,特別是在處理大量數(shù)據(jù)更新時;3)豐富的生態(tài)系統(tǒng)提供了大量第三方庫和工具。通過理解React的工作原理和使用示例,可以掌握其核心概念和最佳實踐,從而構(gòu)建高效、可維護(hù)的用戶界面。

反應(yīng),vue和Netflix前端的未來 反應(yīng),vue和Netflix前端的未來 Apr 12, 2025 am 12:12 AM

Netflix主要使用React作為前端框架,輔以Vue用於特定功能。 1)React的組件化和虛擬DOM提升了Netflix應(yīng)用的性能和開發(fā)效率。 2)Vue在Netflix的內(nèi)部工具和小型項目中應(yīng)用,其靈活性和易用性是關(guān)鍵。

React與後端框架:比較 React與後端框架:比較 Apr 13, 2025 am 12:06 AM

React是前端框架,用於構(gòu)建用戶界面;後端框架用於構(gòu)建服務(wù)器端應(yīng)用程序。 React提供組件化和高效的UI更新,後端框架提供完整的後端服務(wù)解決方案。選擇技術(shù)棧時需考慮項目需求、團(tuán)隊技能和可擴(kuò)展性。

See all articles