一文弄懂 React ref

news2024/12/23 7:22:47

前言

对于 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.propsreturn (<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></>)}
} 

:::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 = nullrefDemo2Component: Child | null = nullcomponentDidMount(): 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></>)}
} 

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 } = propsreturn (<><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 = nulluseEffect(() => {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: BarbarButton: HTMLButtonElementbaz: Baz
}

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

class Bar extends React.Component<{customRef: ForwardedRef<CustomRef>
}> {buttonEl: HTMLButtonElement | null = nullbazInstance: Baz | null = nullcomponentDidMount(): void {const { customRef } = this.propsif (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} />
} 

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} />
} 

如果我们希望 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.propsreturn (<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} />
} 

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.statereturn (<div className={s.father}><h3>父组件</h3><p>子组件对我说:{childToFatherMessage}</p><div className={s.messageInputBox}><section><label htmlFor="to-father">我对子组件说:</label><inputtype="text"id="to-child"onChange={(e) =>this.setState((state) => ({...state,fatherToChildMessage: e.target.value,}))}/></section>{/* 父 -> 子 -- 使用 ref 完成组件通信 */}<buttononClick={() =>this.childRef.current?.setFatherToChildMessage(fatherToChildMessage,)}>发送</button></div><CommunicationDemoChildref={this.childRef}onChildToFather={this.handleChildToFather}/></div>)}
}

interface CommunicationDemoChildProps {onChildToFather: (message: string) => void
}
// 子组件自己维护状态 不依赖于父组件 props
interface CommunicationDemoChildState {fatherToChildMessage: stringchildToFatherMessage: 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.propsconst { fatherToChildMessage, childToFatherMessage } = this.statereturn (<div className={s.child}><h3>子组件</h3><p>父组件对我说:{fatherToChildMessage}</p><div className={s.messageInputBox}><section><label htmlFor="to-father">我对父组件说:</label><inputtype="text"id="to-father"onChange={(e) =>this.setState((state) => ({...state,childToFatherMessage: e.target.value,}))}/></section>{/* 子 -> 父 -- 使用 props 回调完成组件通信 */}<button onClick={() => emitChildToFather(childToFatherMessage)}>发送</button></div></div>)}
} 

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><inputtype="text"id="to-child"onChange={(e) => setFatherToChildMessage(e.target.value)}/></section>{/* 父 -> 子 -- 使用 ref 完成组件通信 */}<buttononClick={() =>childRef.current?.setFatherToChildMessage(fatherToChildMessage)}>发送</button></div><CommunicationDemoFunctionComponentChildref={childRef}onChildToFather={(message) => setChildToFatherMessage(message)}/></div>)
}

interface CommunicationDemoFunctionComponentChildProps {onChildToFather: (message: string) => void
}
const CommunicationDemoFunctionComponentChild = forwardRef<ChildRef,CommunicationDemoFunctionComponentChildProps
>((props, ref) => {const { onChildToFather: emitChildToFather } = props// 子组件自己维护状态 不依赖于父组件 propsconst [fatherToChildMessage, setFatherToChildMessage] = useState('')const [childToFatherMessage, setChildToFatherMessage] = useState('')// 定义暴露给外界的 APIuseImperativeHandle(ref, () => ({ setFatherToChildMessage }))return (<div className={s.child}><h3>子组件</h3><p>父组件对我说:{fatherToChildMessage}</p><div className={s.messageInputBox}><section><label htmlFor="to-father">我对父组件说:</label><inputtype="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: numbername: 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) => (<divkey={todoItem.id}className={s.item}onClick={() => (activeTodoItem.current = todoItem)}><p>{todoItem.name}</p></div>))}</div><button onClick={() => logger.log(activeTodoItem.current)}>控制台输出最新的 activeTodoItem</button></div>)
} 

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 = nullrender(): React.ReactNode {return (<div><divref={(el) => {this.el = elconsole.log('this.el -- ', this.el)}}>ref element</div><buttononClick={() => this.setState({ counter: this.state.counter + 1 })}>add</button></div>)}
} 

为什么会执行两次?为什么第一次 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.refif (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.refif (ref !== null) {const instance = finishedWork.stateNodelet instanceToUse// 处理 ref 来源switch (finishedWork.tag) {// HostComponent 代表 DOM 元素类型的 tagcase HostComponent:instanceToUse = getPublicInstance(instance)break// 类组件使用组件实例default:instanceToUse = instance}if (typeof ref === 'function') {// callback ref 和 string refref(instanceToUse)} else {// object refref.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.refsif (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.alternateconst flags = finishedWork.flagsif (flags & Ref) {if (current !== null) {// 也就是 commitDetachRefsafelyDetachRef(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.flagsif (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.refif (// current === null 意味着是初次挂载,fiber 首次调和时会打上 Ref tag(current === null && ref !== null) ||// current !== null 意味着是更新,此时需要 ref 发生了变化才会打上 Ref tag(current !== null && current.ref !== ref)) {// Schedule a Ref effectworkInProgress.flags |= Ref}
} 

3.3.4. 为什么每次点击按钮 callback ref 都会执行?

那么现在再回过头来思考 RefDemo8 中为什么每次点击按钮都会执行 commitDetachRefcommitAttachRef 呢?

注意我们使用 callback ref 的时候是如何使用的

<divref={(el) => {this.el = elconsole.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 = nullconstructor(props: RefDemo9Props) {super(props)this.setElRef = this.setElRef.bind(this)}setElRef(el: HTMLDivElement | null) {this.el = ellogger.log('this.el -- ', this.el)}render(): React.ReactNode {return (<div><div ref={this.setElRef}>ref element</div><buttononClick={() => this.setState({ counter: this.state.counter + 1 })}>add</button></div>)}
} 

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

4. 总结

本篇文章我们学习到了:

  • ref 的理解与使用,包括如何创建 ref 对象,以及除了 object ref 之外的 string ref 和 callback ref 的方式去使用 ref
  • ref 的高阶用法,包括 forwardRef 转发 ref、ref 实现组件通信、利用 ref 在函数组件中缓存数据等
  • 通过一个简单的 callback ref 的 Demo 研究 ref 的底层原理,string ref 为何也是以函数的方式被调用,以及 ref 的执行时机

最后

为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/113891.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode HOT 100 —— 621. 任务调度器

题目 给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行&#xff0c;并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间&#xff0c;CPU 可以完成一个任务&#xff0c;或者处于待命状态。 然…

使用 SwiftUI 布局协议构建六边形网格,如何制作在六边形网格中显示子视图的通用 SwiftUI 容器

我们将要制作的组件可以作为Swift 包使用。 SwiftUI 非常擅长构建矩形框架的层次结构。随着最近的加入,Grid它变得更好了。然而,今天我们要构建一个疯狂的六边形布局。当然,没有专门的布局类型。所以我们用协议建立我们自己的Layout! 绘制一个六边形 让我们首先为我们的…

在linux中配置redis去中心化集群

目录 前情回顾 一、集群配置 二、启动redis集群 三、检验是否成功 成功&#xff01; 前情回顾 linux中配置redis主从复制及开启哨兵模式 一、集群配置 查看所有的redis服务进程 ps -ef | grep redis 关闭所有的redis服务&#xff08;6379,6380,6381) kill -9 99168 kill …

第十章:数据库恢复技术

1、【多选题】下列哪些属于事务的特征&#xff1a; 正确答案&#xff1a; AD 2、【多选题】下列关于故障恢复的说法正确的是&#xff1a; 正确答案&#xff1a; BC 3、【多选题】下列说法错误的是&#xff1a; 正确答案&#xff1a; AB

无线通信网络优化的自动路测系统设计(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 无线通信网络是一个动态的网络&#xff0c;无线网络优化是一项贯穿于整个网络发展全过程的长期工程。在网络建成投入运营以后,…

学习笔记 - Word、WPS分别设置背景色

学习笔记 - Word、WPS分别设置背景色前言实现原理实现步骤模拟背景色1. 插入矩形形状2. 调整矩形&#xff1a;位置、文字环绕、大小。3. 调整颜色实现按节分别设置1. 插入分节符2. 取消“同前节”3. 矩形入进页眉建议场景参考资料前言 Word、WPS 都没有自带此功能。只能统一设…

node.js+uni计算机毕设项目基于微信小程序的校园快递代取平台(程序+小程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等…

第十一章:并发控制

1、【多选题】二级封锁协议能够避免数据库哪些一致性问题 正确答案&#xff1a; AB 2、【多选题】下列说法正确的是&#xff1a; 正确答案&#xff1a; ABC 3、【多选题】下列哪些是解决死锁的方法&#xff1a; 正确答案&#xff1a; ABC

node.js+uni计算机毕设项目互联网教育系统小程序(程序+小程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等…

k8s-dashboard布署

k8s-dashboard布署Kubernetes dashboard作用获取Kubernetes dashboard资源清单文件修改并部署kubernetes dashboard资源清单文件访问Kubernetes dashboardKubernetes dashboard作用 通过dashboard能够直观了解Kubernetes集群中运行的资源对象通过dashboard可以直接管理&#x…

标准的晋升 PPT 长什么样子?互联网职场晋升内幕!想升职加薪?得这么干……...

如果你真的想顺利地升职加薪&#xff0c;就得从现在开始&#xff0c;重新系统地理解晋升到底是怎么回事。面评技巧导学你可能会认为&#xff0c; “是金子总会发光的”&#xff0c;只要自己能力达到了&#xff0c; 晋升就是“水到 渠成”的事情。毕竟评委的眼睛都是雪亮的&am…

node中,path.join和path.resolve的区别

1. path.join和path.resolve的区别 path.join 拼接路径能够识别 \ path.resolve 从当前的执行路径&#xff0c;解析出绝对路径不能识别 \ &#xff0c;会被当成根路径 注意下面的例子&#xff0c;是从当前的执行路径&#xff0c;解析出绝对路径 使用 process.cwd() 可以获取…

2023年中国博士后科学基金资助指南发布

今日&#xff0c;博管会发布了2023年中国博士后科学基金资助指南&#xff0c;内容上有较大的变化&#xff1a;1.面上资助不再分设一等、二等资助&#xff0c;资助标准统一调整为自然科学资助标准8万元&#xff0c;社会科学资助标准5万元&#xff1b;2.申请人申报周期从2个月调整…

AiFlow大数据框架应用简介

文章大纲1. 平台定位2. 平台特点一站建模智能分析交互分析通用部署3. 项目案例ETL 过程样例物料分类业务规则建模合作1. 平台定位 数据挖掘平台在此起到数据运营的承上启下的环节&#xff0c;主要负责数据的挖掘分析、ETL、数据检测。 平台支持自动建模、可视化交互建模、嵌入…

JavaScript:队列的封装及面试题击鼓传花队列方法实现案例

队列的定义&#xff1a;队列简称队。是一种操作受限的线性表&#xff0c;只允许在表的一端进行插入&#xff0c;而在表的另一端进行删除。向队列中插入元素称为入队或进队&#xff1b;删除元素称为出队或离队。其操作特性为先进先出&#xff08;First In First Out&#xff0c;…

【金猿人物展】天云数据雷涛:从数据湖到湖仓一体再到数据编织,完成的是燃油车到油电混再到纯电技术的改造...

‍雷涛本文由天云数据CEO雷涛撰写并投递参与“数据猿年度金猿策划活动——2022大数据产业趋势人物榜单及奖项”评选。‍数据智能产业创新服务媒体——聚焦数智 改变商业这一两年&#xff0c;北美以Facebook、谷歌为驱动的存算分离的虚拟数仓架构&#xff0c;正在非常快速的洗牌…

【408篇】C语言笔记-第十七章(考研必会的排序算法(下))

文章目录第一节&#xff1a;选择排序1. 选择排序原理解析2. 选择排序代码实战3. 时间复杂度与空间复杂度第二节&#xff1a;堆排序1. 堆排序原理解析2. 堆排序代码实战3. 时间复杂度与空间复杂度第三节&#xff1a;归并排序1. 归并排序原理解析2. 归并排序代码实战3. 时间复杂度…

5、前端笔记-JS-运算符

运算符也被称为操作符&#xff0c;用于实现赋值、比较和执行算数运算等功能的符号 常用的运算符&#xff1a; 算数运算符、递增和递减运算符、比较运算符、逻辑运算符、赋值运算符 1、算术运算符 -*/% - * / %&#xff08;1&#xff09;%取余可以用来判断一个数是否可以被整除…

家政清洁服务系统设计与实现

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 修改意见&#xff1a;题目改为“家政清洁服务系统设计与实现” 提供服务&#xff1a;钟点打扫卫生&#xff08;按小时收…

node.js+uni计算机毕设项目基于微信平台的大学生时间规划管理小程序设计(程序+小程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等…