• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

ref 是 React 里常用的特性,我们会用它来拿到 dom 的引用,或者用来保存渲染过程中不变的数据。

我们创建个项目试一下:

npx create-vite

去掉 index.css 和 StrictMode

改下 App.tsx

import { useRef, useEffect } from "react";

export default function App() {
    const inputRef = useRef < HTMLInputElement > null;

    useEffect(() => {
        inputRef.current?.focus();
    }, []);

    return <input ref={inputRef} type="text" />;
}

把开发服务跑起来:

npm run dev

创建个调试配置:

{
    "type": "chrome",
    "request": "launch",
    "name": "Launch Chrome against localhost",
    "url": "http://localhost:5173",
    "webRoot": "${workspaceFolder}"
}

可以看到,useRef 可以拿到 dom 的引用:

此外,useRef 还可以保存渲染中不变的一些值:

import { useEffect, useRef, useState } from "react";

export default function App() {
    const [num, setNum] = useState(0);
    const timerRef = useRef<number>();

    useEffect(() => {
        timerRef.current = setInterval(() => {
            setNum((num) => num + 1);
        }, 100);
    }, []);

    return (
        <div>
            {num}
            <button
                onClick={() => {
                    clearInterval(timerRef.current!);
                }}>
                停止
            </button>
        </div>
    );
}

当传入 null 时,返回的是 RefObject 类型,用来保存 dom 引用:

传其他值返回的是 MutableRefObject,可以修改 current,保存其它值:

而在 class 组件里用 createRef:

import React from "react";

export default class App extends React.Component {
    constructor() {
        super();
        this.inputRef = React.createRef();
    }

    componentDidMount() {
        this.inputRef.current.focus();
    }

    render() {
        return <input ref={this.inputRef} type="text" />;
    }
}

如果想转发 ref 给父组件,可以用 forwardRef:

import React, {
    useRef,
    forwardRef,
    useImperativeHandle,
    useEffect,
} from "react";

const ForwardRefMyInput = forwardRef<HTMLInputElement>((props, ref) => {
    return <input {...props} ref={ref} type="text" />;
});

export default function App() {
    const inputRef = useRef < HTMLInputElement > null;

    useEffect(() => {
        inputRef.current?.focus();
    }, []);

    return (
        <div className="App">
            <ForwardRefMyInput ref={inputRef} />
        </div>
    );
}

而且还可以使用 useImperativeHandle 自定义传给父元素的 ref:

import React, {
    useRef,
    forwardRef,
    useImperativeHandle,
    useEffect,
} from "react";

interface RefType {
    aaa: Function;
}

const ForwardRefMyInput = forwardRef<RefType>((props, ref) => {
    const inputRef = useRef<HTMLInputElement>(null);

    useImperativeHandle(ref, () => {
        return {
            aaa() {
                inputRef.current?.focus();
            },
        };
    });
    return <input {...props} ref={inputRef} type="text" />;
});

export default function App() {
    const apiRef = useRef<RefType>(null);

    useEffect(() => {
        apiRef.current?.aaa();
    }, []);

    return (
        <div className="App">
            <ForwardRefMyInput ref={apiRef} />
        </div>
    );
}

这就是我们平时用到的所有的 ref api 了。

小结一下:

  • 函数组件里用 useRef 保存 dom 引用或者自定义的值,而在类组件里用 createRef
  • forwardRef 可以转发子组件的 ref 给父组件,还可以用 useImperativeHandle 来修改转发的 ref 的值

相信开发 React 项目,大家或多或少会用到这些 api。

那这些 ref api 的实现原理是什么呢?

下面我们就从源码来探究下:

我们通过 jsx 写的代码,最终会编译成 React.createElement 等 render function,执行之后产生 vdom:

所谓的 vdom 就是这样的节点对象:

vdom 是一个 children 属性连接起来的树。

react 会先把它转成 fiber 链表:

vdom 树转 fiber 链表树的过程就叫做 reconcile,这个阶段叫 render。

render 阶段会从根组件开始 reconcile,根据不同的类型做不同的处理,拿到渲染的结果之后再进行 reconcileChildren,这个过程叫做 beginWork:

比如函数组件渲染完产生的 vom 会继续 renconcileChildren:

beginWork 只负责渲染组件,然后继续渲染 children,一层层的递归。

全部渲染完之后,会递归回来,这个阶段会调用 completeWork:

这个阶段会创建需要的 dom,然后记录增删改的 tag 等,同时也记录下需要执行的其他副作用到 fiber 上。

之后 commit 阶段才会遍历 fiber 链表根据 tag 来执行增删改 dom 等 effect。

commit 阶段也分了三个小阶段,beforeMutation、mutation、layout:

它们都是消费的同一条 fiber 链表,但是每个阶段做的事情不同

mutation 阶段会根据标记增删改 dom,也就是这样的:

所以这个阶段叫做 mutation,它之前的一个阶段叫做 beforeMutation,而它之后的阶段叫做 layout。

小结下 react 的流程:

通过 jsx 写的代码会编译成 render function,执行产生 vdom,也就是 React Element 对象的树。

react 分为 render 和 commit 两个阶段:

render 阶段会递归做 vdom 转 fiber,beginWork 里递归进行 reconcile、reconcileChildren,completeWork 里创建 dom,记录增删改等 tag 和其他 effect

commit 阶段遍历 fiber 链表,做三轮处理,这三轮分别叫做 before mutation、mutation、layout,mutation 阶段会根据 tag 做 dom 增删改。

ref 的实现同样是在这个流程里的。

首先,我们 ref 属性一般是加在原生标签上的,比如 input、div、p 这些,所以看 HostComponent 的分支就可以了,HostComponent 就是原生标签。

可以看到处理原生标签的 fiber 节点时,beginWork 里会走到这个分支:

里面调用 markRef 打了个标记:

前面说的 tag 就是指这个 flags。

然后就到了 commit 阶段,开始根据 flags 做不同处理:

在 layout 阶段,这时候已经操作完 dom 了,就会遍历 fiber 链表,给 HostComponent 设置新的 ref。

ref 的元素就是在 fiber.stateNode 属性上保存的在 render 阶段就创建好了的 dom,:

这样,在代码里的 ref.current 就能拿到这个元素了:

而且我们可以发现,他只是对 ref.current 做了赋值,并不管你是用 createRef 创建的、useRef 创建的,还是自己创建的一个普通对象。

我们试验一下:

我创建了一个普通对象,current 属性依然被赋值为 input 元素。

那我们用 createRef、useRef 的意义是啥呢?

看下源码就知道了:

createRef 也是创建了一个这样的对象,只不过 Object.seal 了,不能增删属性。

用自己创建的对象其实也没啥问题。

那 useRef 呢?

useRef 也是一样的,只不过是保存在了 fiber 节点 hook 链表元素的 memoizedState 属性上。

只是保存位置的不同,没啥很大的区别。

同样,用 forwardRef 转发的 ref 也很容易理解,只是保存的位置变了,变成了从父组件传过来的 ref:

那 forwardRef 是怎么实现这个 ref 转发的呢?

我们再看下源码:

forwarRef 函数其实就是创建了个专门的 React Element 类型:

然后 beginWork 处理到这个类型的节点会做专门的处理:

也就是把它的 ref 传递给函数组件:

渲染函数组件的时候专门留了个后门来传第二个参数:

所以函数组件里就可以拿到 ref 参数了:

这样就完成了 ref 从父组件到子组件的传递:

那 useImperativeHandle 是怎么实现的修改 ref 的值呢?

源码里可以看到 useImperativeHandle 底层就是 useEffect,只不过是回调函数是把传入的 ref 和 create 函数给 bind 到 imperativeHandleEffect 这个函数了:

而这个函数里就是更新 ref.current 的逻辑:

我们知道,useEffect 是在 commit 阶段异步调度的,在 layout 更新 dom 之后了,自然可以拿到新的 dom:

更新了 ref 的值:

这样,useImperativeHandle 就成功修改了 forwardRef 传过来的 ref。

总结

我们平时会用到 createRef、useRef、forwardRef、useImperativeHandle 这些 api,而理解它们的原理需要熟悉 react 的运行流程,也就是 render(beginWork、completeWork) + commit(before mutation、mutation、layout)的流程。

render 阶段处理到原生标签的也就是 HostComponent 类型的时候,如果有 ref 属性会在 fiber.flags 里加一个标记。

commit 阶段会在 layout 操作完 dom 后遍历 fiber 链表更新 HostComponent 的 ref,也就是把 fiber.stateNode 赋值给 ref.current。

react 并不关心 ref 是哪里创建的,用 createRef、useRef 创建的,或者 forwardRef 传过来的都行,甚至普通对象也可以,createRef、useRef 只是把普通对象 Object.seal 了一下。

forwarRef 是创建了单独的 React Element 类型,在 beginWork 处理到它的时候做了特殊处理,也就是把它的 ref 作为第二个参数传递给了函数组件,这就是它 ref 转发的原理。

useImperativeHandle 的底层实现就是 useEffect,只不过执行的函数是它指定的,bind 了传入的 ref 和 create 函数,这样在 layout 阶段调用 hook 的 effect 函数的时候就可以更新 ref 了。

理解了 react 渲染流程之后,很多特性只是其中多一个 switch case 的分支而已,就比如 ref。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
65.React18的并发机制是怎么实现的
Next
67.低代码编辑器:核心数据结构、全局store