本文正在参加「金石方案 . 分割 6 万现金大奖」

前言

关于 Ref 了解与运用,一些读者或许还逗留在用 ref 获取实在 DOM 元素和获取类组件实例层面上

其实 ref 除了这两项常用功能之外,还有很多其他小技巧

经过本篇文章的学习,你将收成 React ref 的根本和进阶用法,而且能够了解 React 内部是怎么处理 ref 的,并经过一个小 Demo + 发问的办法带你愈加深刻地了解 ref 的底层原理

1. ref 的了解与运用

关于 Ref 的了解,要从两个视点去剖析:

  1. Ref 方针的创立:运用 createRefuseRef 创立 Ref 方针
  2. 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,获取多个内容,包括:

  1. 子组件 Bar 的组件实例
  2. Bar 组件中的 DOM 元素 button
  3. 孙组件 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}>
        <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,
              )
            }
          >
            发送
          </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}>
        <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 回调完结组件通讯 */}
          <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}>
      <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)
          }
        >
          发送
        </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}>
      <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 回调完结组件通讯 */}
        <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

这样就完美处理啦,既修复了 bug,又搞懂了 ref 的底层原理,一举两得!

4. 总结

本篇文章咱们学习到了:

  • ref 的了解与运用,包括怎么创立 ref 方针,以及除了 object ref 之外的 string ref 和 callback ref 的办法去运用 ref
  • ref 的高阶用法,包括 forwardRef 转发 ref、ref 完结组件通讯、运用 ref 在函数组件中缓存数据等
  • 经过一个简略的 callback ref 的 Demo 研讨 ref 的底层原理,string ref 为何也是以函数的办法被调用,以及 ref 的履行机遇