中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

React中的ref怎么使用

發布時間:2023-01-06 09:42:15 來源:億速云 閱讀:99 作者:iii 欄目:web開發

這篇文章主要介紹“React中的ref怎么使用”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“React中的ref怎么使用”文章能幫助大家解決問題。

1. ref 的理解與使用

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

  • Ref 對象的創建:使用 createRefuseRef 創建 Ref 對象 

  • React 本身對 Ref 的處理:對于標簽中的 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
}

可以看到,就是創建了一個包含 current 屬性的對象,僅此而已

1.1.2. useRef

這也就意味著我們不能在函數組件中使用 createRef,因為每次函數組件渲染都是一次新的函數執行,每次執行 createRef 得到的都是一個新的對象,無法保留其原來的引用

所以在函數組件中,我們會使用 useRef 創建 Ref 對象,React 會將 useRef 和函數組件對應的 fiber 對象關聯,將 useRef 創建的 ref 對象掛載到對應的 fiber 對象上

這樣一來每次函數組件執行,只要函數組件不被銷毀,那么對應的 fiber 對象實例也會一直存在,所以 ref 也能夠被保留下來

1.2. React 對標簽中 ref 屬性的處理

首先要明確一個結論,在 React 中獲取 DOM 元素或者組件實例并不是只能通過 ref 對象獲取!!!

也就是說并不是只能通過先調用 createRef 創建 ref 對象,然后將它賦值到要獲取的元素或組件實例的 ref 屬性上,實際上還有別的方式

:::tip

只有類組件才有獲取組件實例這一說法,函數組件沒有實例,不能被 ref 標記,但是可以通過 forwardRef 結合 useImperativeHandle 給函數組件賦予 ref 標記的

:::

1.2.1. string ref

當我們給元素或類組件標簽中的 ref 屬性傳遞字符串時,能夠在組件實例的 this.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('RefDemo1')

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

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

React中的ref怎么使用

:::warning

這種方式已經被 React 官方廢棄,盡量不要使用

:::

1.2.2. callback ref

ref 屬性傳遞函數時,會在 commit 階段創建真實 DOM 時執行 ref 指定的函數,并將元素作為第一個參數傳入,此時我們就可以利用它進行賦值以獲取 DOM 元素或組件實例

/** @description ref 屬性傳遞函數 */
class RefDemo2 extends React.Component {
  logger = createLoggerWithScope('RefDemo2')

  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 屬性傳遞函數獲取 DOM 元素
        </div>

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

React中的ref怎么使用

1.2.3. object ref

這種方式就是我們最常用的方式了,使用 createRef 或者 useRef 創建 Ref 對象,并將其傳給標簽的 ref 屬性即可

這種方式獲取到的 ref 需要先調用 current 屬性才能獲取到對應的 DOM 元素或組件實例

/** @description ref 屬性傳遞對象 */
class RefDemo3 extends React.Component {
  logger = createLoggerWithScope('RefDemo3')

  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 轉發 ref

2.1.1. 跨層級獲取

想要在爺組件中通過在子組件中傳遞 ref 獲取到孫組件的某個元素,也就是在爺組件中獲取到了孫組件的元素,是一種跨層級獲取

/** @description 孫組件 */
const Child: React.FC<{ grandRef: LegacyRef<HTMLDivElement> }> = (props) => {
  const { grandRef } = props

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

/**
 * @description 父組件
 *
 * 第一個泛型參數是 ref 的類型
 * 第二個泛型參數是 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. 合并轉發自定義 ref

forwardRef 不僅可以轉發 ref 獲取 DOM 元素和組件實例,還可以轉發合并后的自定義 ref

什么是“合并后的自定義 ref”呢?通過一個場景來看看就明白了

:::info{title=場景}

通過給 Foo 組件綁定 ref,獲取多個內容,包括:

  • 子組件 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. 高階組件轉發 ref

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

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

/** @description 不使用 forwardRef 轉發 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('RefHOCWithoutForwardRefDemo')
  const wrapRef = useRef(null)

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

  return <HOCComponent1 ref={wrapRef} />
}

React中的ref怎么使用

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

這時候就可以用 forwardRef 進行轉發了

/** @description HOC 中使用 forwardRef 轉發 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('RefHOCWithForwardRefDemo')
  const hocComponent2Ref = useRef(null)

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

  return <HOCComponent2 ref={hocComponent2Ref} />
}

React中的ref怎么使用

2.2. ref 實現組件通信

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

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

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

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

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

/**
 * 父 -> 子 使用 ref
 * 子 -> 父 使用 props 回調
 */
class CommunicationDemoFather extends React.Component<
  {},
  CommunicationDemoFatherState
> {
  state: Readonly<CommunicationDemoFatherState> = {
    fatherToChildMessage: '',
    childToFatherMessage: '',
  }

  childRef = React.createRef<CommunicationDemoChild>()

  /** @description 提供給子組件修改父組件中的狀態 */
  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}>
        <h4>父組件</h4>
        <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,
              )
            }
          >
            發送
          </button>
        </div>

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

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

  /** @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}>
        <h4>子組件</h4>
        <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 回調完成組件通信 */}
          <button onClick={() => emitChildToFather(childToFatherMessage)}>
            發送
          </button>
        </div>
      </div>
    )
  }
}

React中的ref怎么使用

2.2.2. 函數組件 ref 暴露指定方法

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

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

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

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

/**
 * 父 -> 子 使用 ref
 * 子 -> 父 使用 props 回調
 */
const CommunicationDemoFunctionComponentFather: React.FC = () => {
  const [fatherToChildMessage, setFatherToChildMessage] = useState('')
  const [childToFatherMessage, setChildToFatherMessage] = useState('')

  const childRef = useRef<ChildRef>(null)

  return (
    <div className={s.father}>
      <h4>父組件</h4>
      <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)
          }
        >
          發送
        </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

  // 子組件自己維護狀態 不依賴于父組件 props
  const [fatherToChildMessage, setFatherToChildMessage] = useState('')
  const [childToFatherMessage, setChildToFatherMessage] = useState('')

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

  return (
    <div className={s.child}>
      <h4>子組件</h4>
      <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 回調完成組件通信 */}
        <button onClick={() => emitChildToFather(childToFatherMessage)}>
          發送
        </button>
      </div>
    </div>
  )
})

2.3. 函數組件緩存數據

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

為什么 useRef 可以對數據進行緩存?

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

React 會將 useRef 和函數組件對應的 fiber 對象關聯,將 useRef 創建的 ref 對象掛載到對應的 fiber 對象上,這樣一來每次函數組件執行,只要函數組件不被銷毀,那么對應的 fiber 對象實例也會一直存在,所以 ref 也能夠被保留下來

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

:::warning{title=注意}

一定要看清前提,只適用于與視圖無關的數據

:::

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

假設我有一個 todoList 列表,視圖上會把這個列表渲染出來,并且有一個數據 activeTodoItem 是控制當前選中的是哪個 todoItem

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

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

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

interface TodoItem {
  id: number
  name: string
}

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

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

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

  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 原理

首先先看一個關于 callback ref 的小 Demo 來引出我們后續的內容

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('this.el -- ', this.el)
          }}
        >
          ref element
        </div>
        <button
          onClick={() => this.setState({ counter: this.state.counter + 1 })}
        >
          add
        </button>
      </div>
    )
  }
}

React中的ref怎么使用

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

3.1. ref 的底層原理

還記得 React 底層是有 render 階段和 commit 階段的嗎?關于 ref 的處理邏輯就在 commit 階段進行的

React 底層有兩個關于 ref 的處理函數 -- commitDetachRefcommitAttachRef

上面的 Demo 中 callback ref 執行了兩次正是對應著這兩次函數的調用,大致來講可以理解為 commitDetachRef 在 DOM 更新之前執行,commitAttachRef 在 DOM 更新之后執行

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

3.1.1. commitDetachRef

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

packages/react-reconciler/src/ReactFiberCommitWork.js

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

  if (currentRef !== null) {
    if (typeof currentRef === 'function') {
      // callback ref 和 string ref 執行時機
      currentRef(null)
    } else {
      // object ref 處理時機
      currentRef.current = null
    }
  }
}

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

并且也能看到 commitDetachRef 主要是將 ref 置為 null,這也就是為什么 RefDemo8 中第一次執行的 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 === 'function') {
      // callback ref 和 string ref
      ref(instanceToUse)
    } else {
      // object ref
      ref.current = instanceToUse
    }
  }
}

3.2. 為什么 string ref 也是以函數的方式調用?

從上面的核心源碼中能看到,對于 callback refstring ref,都是統一以函數的方式調用,將 nullinstanceToUse 傳入

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

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

packages/react-reconciler/src/ReactChildFiber.js

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

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

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

3.3. ref 的執行時機

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

那么我們就得來看看這兩個函數的執行時機才能行

3.3.1. commitDetachRef 執行時機

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 執行時機

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?

可以看到,只有當 fiber 被打上了 Ref 這個 flag tag 時才會去執行 commitDetachRef/commitAttachRef

那么什么時候會標記 Ref tag 呢?

packages/react-reconciler/src/ReactFiberBeginWork.js

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

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

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

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

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

<div
  ref={(el) => {
    this.el = el
    console.log('this.el -- ', this.el)
  }}
>
  ref element
</div>

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

3.3.5. 如何解決?

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

const logger = createLoggerWithScope('RefDemo9')

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('this.el -- ', 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怎么使用

關于“React中的ref怎么使用”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

新野县| 万年县| 晋江市| 任丘市| 桦川县| 滨州市| 长宁区| 桃源县| 奉贤区| 浠水县| 江川县| 布拖县| 满城县| 汉阴县| 八宿县| 东兴市| 沂水县| 霸州市| 同仁县| 庆云县| 博野县| 隆安县| 东乌| 克东县| 阿巴嘎旗| 新河县| 鸡西市| 莱州市| 罗山县| 景宁| 关岭| 蚌埠市| 清远市| 宜都市| 台山市| 东明县| 枣强县| 蛟河市| 浮梁县| 青神县| 娱乐|