1. 为什么我们需要关注 ref 的差异
在 Vue 和 React 的日常开发中,ref 是一个高频使用的特性,但很多开发者在使用时常常混淆两者的区别。上周我在团队代码审查时就发现,有同事在 Vue 项目中直接照搬 React 的 ref 用法,导致组件行为异常。这促使我决定系统梳理两者的差异。
ref 的核心作用是获取 DOM 元素或组件实例的引用。虽然概念相似,但 Vue 和 React 的实现机制和使用方式存在本质区别。理解这些差异不仅能避免踩坑,还能帮助我们根据项目需求选择更合适的方案。
从底层实现来看,Vue 的 ref 是基于响应式系统实现的,而 React 的 ref 则是 Fiber 架构下的产物。这种根本性的架构差异导致了 API 设计和使用模式的不同。接下来,我将从 6 个维度详细对比两者的区别。
2. 基础用法对比
2.1 Vue 中的 ref 使用
在 Vue 3 的 Composition API 中,我们通过 ref() 函数创建响应式引用:
javascript复制import { ref, onMounted } from 'vue'
export default {
setup() {
const myRef = ref(null)
onMounted(() => {
console.log(myRef.value) // 获取DOM元素
myRef.value.style.color = 'red'
})
return { myRef }
}
}
模板中使用时,只需在元素上添加 ref 属性:
html复制<div ref="myRef">这是一个DOM引用</div>
Vue 2 的 Options API 中使用方式略有不同:
javascript复制export default {
mounted() {
console.log(this.$refs.myRef)
}
}
注意:Vue 3 中如果同时使用 Composition API 和 Options API 的 ref,Composition API 的 ref 会覆盖 Options API 的同名 ref。
2.2 React 中的 ref 使用
React 提供了三种创建 ref 的方式:
- 回调函数形式(传统方式):
javascript复制class MyComponent extends React.Component {
componentDidMount() {
console.log(this.myRef)
}
render() {
return <div ref={(el) => { this.myRef = el }}>回调ref</div>
}
}
- createRef API(React 16.3+):
javascript复制class MyComponent extends React.Component {
constructor(props) {
super(props)
this.myRef = React.createRef()
}
componentDidMount() {
console.log(this.myRef.current)
}
render() {
return <div ref={this.myRef}>createRef</div>
}
}
- useRef Hook(函数组件):
javascript复制function MyComponent() {
const myRef = React.useRef(null)
useEffect(() => {
console.log(myRef.current)
}, [])
return <div ref={myRef}>useRef</div>
}
3. 核心差异解析
3.1 响应式行为的区别
Vue 的 ref 是响应式的,这意味着:
- 当 ref 值变化时,会自动触发组件更新
- 可以在模板中直接绑定和使用
- 适用于任何值类型(基本类型、对象、函数等)
javascript复制const count = ref(0)
// 自动触发更新
count.value++
React 的 ref 是非响应式的:
- ref 值变化不会触发组件重新渲染
- 主要用于存储不会影响渲染的"可变值"
- 通常只用于 DOM 引用或保存实例
javascript复制const count = useRef(0)
// 不会触发重新渲染
count.current++
3.2 模板/JSX 中的使用差异
Vue 的模板中可以直接使用 ref 值:
html复制<div>{{ myRef }}</div>
React 的 JSX 中不能直接渲染 ref 对象:
javascript复制// 错误!会渲染[object Object]
<div>{myRef}</div>
// 正确做法
<div>{myRef.current}</div>
3.3 组件引用时的行为差异
引用子组件时,Vue 的 ref 会得到组件实例:
javascript复制// 子组件
const Child = {
methods: {
sayHello() {
console.log('Hello')
}
}
}
// 父组件中
const childRef = ref(null)
onMounted(() => {
childRef.value.sayHello() // 可以直接调用子组件方法
})
React 默认不允许直接访问子组件实例:
javascript复制// 需要使用forwardRef
const Child = React.forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
sayHello: () => console.log('Hello')
}))
return <div>Child</div>
})
// 父组件中
const childRef = useRef(null)
useEffect(() => {
childRef.current?.sayHello()
}, [])
4. 性能与优化考量
4.1 Vue 的 ref 性能特点
Vue 的 ref 基于响应式系统,需要注意:
- 每个 ref 都会创建一个 Proxy 对象,有一定内存开销
- 频繁修改 ref 值会触发依赖追踪,可能影响性能
- 对于大量静态引用,可以考虑使用 shallowRef
javascript复制const heavyArray = shallowRef([...]) // 只对.value变化响应
4.2 React 的 ref 性能优化
React 的 ref 优化策略包括:
- 避免在渲染函数中创建新的 ref 回调(会导致不必要的重新挂载)
- 对于动态 ref,使用 useCallback 缓存回调函数
- 使用 useImperativeHandle 限制暴露的方法
javascript复制// 不好的做法 - 每次渲染创建新函数
<div ref={(el) => { this.myRef = el }}></div>
// 优化做法
const refCallback = useCallback((el) => {
myRef.current = el
}, [])
<div ref={refCallback}></div>
5. 高级用法对比
5.1 动态 ref 名称
Vue 中实现动态 ref:
javascript复制const refs = {}
const setRef = (name) => (el) => {
refs[name] = el
}
<div :ref="setRef('dynamic')"></div>
React 的动态 ref:
javascript复制const refs = useRef({})
<div ref={(el) => refs.current.dynamic = el}></div>
5.2 ref 转发机制
Vue 的 ref 转发:
javascript复制// 子组件
const Child = {
setup(props, { expose }) {
const internalRef = ref(null)
expose({ internalRef })
return { internalRef }
}
}
// 父组件
const childRef = ref(null)
onMounted(() => {
console.log(childRef.value.internalRef)
})
React 的 ref 转发:
javascript复制const Child = forwardRef((props, ref) => {
const internalRef = useRef(null)
useImperativeHandle(ref, () => ({
internalRef: internalRef.current
}))
return <div ref={internalRef}>Child</div>
})
const parentRef = useRef(null)
useEffect(() => {
console.log(parentRef.current.internalRef)
}, [])
6. 实际场景中的选择建议
6.1 何时选择 Vue 的 ref
- 需要响应式引用值时
- 要在模板中直接使用 ref 值时
- 需要简单访问子组件实例时
- 处理表单输入等需要双向绑定的场景
javascript复制// Vue 表单输入示例
const inputRef = ref(null)
const inputValue = ref('')
onMounted(() => {
inputRef.value.focus()
})
<input ref="inputRef" v-model="inputValue">
6.2 何时选择 React 的 ref
- 只需要存储不会影响渲染的值时
- 需要手动控制 DOM 元素时
- 需要与第三方 DOM 库集成时
- 需要更精细的性能控制时
javascript复制// React 与动画库集成示例
const animationRef = useRef(null)
useEffect(() => {
const anim = new Animation(animationRef.current)
return () => anim.dispose()
}, [])
<div ref={animationRef}></div>
7. 常见问题与解决方案
7.1 Vue 中 ref 的常见问题
问题1:ref 在 setup 中返回但模板访问不到
- 检查 setup 中是否正确 return 了 ref
- 确保模板中的 ref 名称与返回的名称一致
问题2:动态组件 ref 失效
- 使用 Vue 的 nextTick 等待组件更新完成
javascript复制const currentComponent = ref('ComponentA')
const compRef = ref(null)
watch(currentComponent, async () => {
await nextTick()
console.log(compRef.value) // 现在可以正确获取
})
7.2 React 中 ref 的常见问题
问题1:函数组件不能直接使用 ref
- 解决方案:使用 forwardRef + useImperativeHandle
javascript复制const MyComponent = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
doSomething: () => {...}
}))
return <div>...</div>
})
问题2:ref 在渲染期间为 null
- 这是因为 ref 在组件挂载后才会设置
- 解决方案:在 useEffect/useLayoutEffect 中访问 ref
javascript复制useEffect(() => {
// 这里可以安全访问 ref.current
}, [])
8. 测试策略差异
8.1 Vue ref 的测试方法
在测试 Vue 组件时,可以通过 wrapper.vm.$refs 访问 ref:
javascript复制test('测试 ref', async () => {
const wrapper = mount(MyComponent)
await wrapper.vm.$nextTick()
expect(wrapper.vm.$refs.myRef.textContent).toBe('...')
})
对于 Composition API 的 ref,可以通过组件实例访问:
javascript复制test('测试 composition ref', () => {
const wrapper = mount(MyComponent)
expect(wrapper.vm.myRef.value).toBeDefined()
})
8.2 React ref 的测试方法
React 测试中需要使用 act 包裹 ref 操作:
javascript复制test('测试 ref', () => {
const ref = React.createRef()
act(() => {
render(<MyComponent ref={ref} />, container)
})
expect(ref.current).not.toBeNull()
})
对于函数组件的 ref 测试:
javascript复制test('测试 forwardRef', async () => {
const ref = React.createRef()
render(<MyComponent ref={ref} />)
await act(async () => {
ref.current.doSomething()
})
// 断言...
})
9. 与状态管理的协作
9.1 Vue ref 与 Pinia/Vuex
Vue 的 ref 可以与状态管理库良好配合:
- 将 ref 作为 store 的状态
- 在 actions 中修改 ref 值
- 保持响应式特性
javascript复制// store 中使用 ref
export const useStore = defineStore('main', {
state: () => ({
count: ref(0)
}),
actions: {
increment() {
this.count.value++
}
}
})
9.2 React ref 与 Redux/MobX
React 的 ref 通常不直接与状态管理交互:
- ref 主要用于 DOM 操作和组件通信
- 状态管理应该使用 state 或 context
- 避免将 ref 存储在全局状态中
javascript复制// 不推荐的做法
const store = configureStore({
reducer: {
// 不要这样做!
domRef: (state = { current: null }, action) => {...}
}
})
10. 未来演进方向
10.1 Vue ref 的发展
Vue 团队正在探索:
- 更轻量级的 ref 实现(如 shallowRef)
- 更好的 TypeScript 支持
- 与 Suspense 等新特性的集成
10.2 React ref 的演进
React 的未来方向包括:
- 更简单的 ref 转发 API
- 与并发渲染的更好兼容
- 可能引入响应式 ref 概念
在实际项目中,我通常会根据团队的技术栈和项目需求选择适合的 ref 使用方式。对于需要大量 DOM 操作的项目,React 的 ref 提供了更直接的控制;而对于需要响应式特性的场景,Vue 的 ref 则更加方便。理解这些差异有助于我们在跨框架开发时避免常见的陷阱。
