跳到主要内容React 实现 Vue 的 watch 和 computed 详解 | 极客日志JavaScript大前端
React 实现 Vue 的 watch 和 computed 详解
React 中实现 Vue 的 watch 和 computed 特性主要依赖 useMemo 和 useEffect Hook。computed 对应 useMemo 用于缓存计算结果,避免重复渲染;watch 对应 useEffect 监听数据变化执行副作用。基础场景可直接使用依赖数组,深度监听需结合 lodash.isEqual 封装 useDeepCompareEffect,立即执行利用 useEffect 默认行为或 useRef 控制。通过封装 useWatch 自定义 Hook 可进一步提升复用性,贴近 Vue 开发习惯。
随缘9 浏览 React 实现 Vue 的 watch 和 computed 详解
在前端框架生态中,Vue 的 watch 和 computed 是两大核心且极具实用性的特性:computed 用于创建基于依赖的缓存计算属性,避免无效重复计算;watch 用于监听数据变化并执行副作用操作,支撑各类业务逻辑的响应式触发。在 React 开发中,我们同样会遇到「需要缓存计算结果」和「监听数据变化执行回调」的场景,同时这也是前端面试中的高频考点。本文将详细讲解 React 生态中如何实现 Vue 这两大特性,涵盖基础写法与进阶封装,所有代码均可直接落地项目。
二、实现 Vue 的 computed(计算属性)
在开始 React 实现之前,我们先明确 Vue computed 的核心特性:基于响应式依赖进行缓存。只有当 computed 依赖的数据源发生变化时,才会重新执行计算逻辑并更新结果;如果依赖未发生变化,直接返回缓存的上一次计算结果,从而避免无效的性能消耗,这也是 computed 与普通方法的核心区别。
方式一:基础版(函数组件 + useMemo,推荐)
React 中实现 computed 核心功能的最佳选择是官方提供的 useMemo Hook,它完美匹配 Vue computed 的「缓存」核心特性,是项目开发中的推荐方案。
完整代码示例
import { useState, useMemo } from 'react';
function ComputedDemo() {
const [num1, setNum1] = useState(10);
const [num2, setNum2] = useState(20);
const [unrelatedNum, setUnrelatedNum] = useState(0);
const sum = useMemo(() => {
console.log('useMemo 计算逻辑执行了——仅依赖变化时触发');
return num1 + num2;
}, [num1, num2]);
return (
<div = '' }}>
React 实现 Vue computed(useMemo 版)
num1: {num1}
num2: {num2}
无关变量 unrelatedNum: {unrelatedNum}
计算属性 sum(num1 + num2): {sum}
{/* 3. 交互按钮:修改相关依赖 */}
setNum1(prev => prev + 1)} style={{ marginRight: '10px' }}>num1 + 1
setNum2(prev => prev + 1)} style={{ marginRight: '10px' }}>num2 + 1
{/* 4. 交互按钮:修改无关依赖 */}
setUnrelatedNum(prev => prev + 1)}>无关变量 + 1
);
}
;
style
{{
padding:
20px
<h3>
</h3>
<p>
</p>
<p>
</p>
<p>
</p>
<p style={{ color: 'blue', fontWeight: 'bold' }}>
</p>
<button onClick={() =>
</button>
<button onClick={() =>
</button>
<button onClick={() =>
</button>
</div>
export
default
ComputedDemo
核心解释
useMemo 两个核心参数的作用:
- 第一个参数:计算逻辑函数,返回值即为计算属性的结果(对应 Vue
computed 中的计算函数),该函数仅在依赖变化时执行。
- 第二个参数:依赖数组,存放当前计算逻辑依赖的所有数据源(对应 Vue
computed 自动收集的响应式依赖),只有数组中的变量发生变化时,React 才会重新执行第一个参数的计算函数,更新计算结果。
- 缓存特性验证(对比「使用 useMemo」与「直接定义变量」):
- 上述示例中,点击「num1 + 1」或「num2 + 1」(修改相关依赖),控制台会打印日志,
sum 也会同步更新,说明计算逻辑重新执行。
- 点击「无关变量 + 1」(修改不相关依赖),控制台无日志输出,
sum 保持不变,说明 useMemo 生效,直接返回了缓存的计算结果。
- 若直接定义变量
const sum = num1 + num2;,每次组件重新渲染(无论是否修改相关依赖),都会重新执行 num1 + num2 运算,当计算逻辑复杂时(如大量数据过滤、格式化),会造成不必要的性能损耗,这也是 useMemo 与普通变量定义的核心差异。
方式二:简化版(仅简单计算,无需缓存)
该方式适用于计算逻辑极简单(如简单的数值运算、模板字符串拼接),且完全不关心重复计算带来的性能损耗的场景,对应 Vue 中 computed 关闭缓存(极少使用)的场景。
简短代码示例
import { useState } from 'react';
function SimpleComputedDemo() {
const [firstName, setFirstName] = useState('Zhang');
const [lastName, setLastName] = useState('San');
const fullName = `${firstName} ${lastName}`;
return (
<div style={{ padding: '20px' }}>
<h3>React 实现 Vue computed(简化版)</h3>
<p>firstName: {firstName}</p>
<p>lastName: {lastName}</p>
<p style={{ color: 'blue', fontWeight: 'bold' }}>计算结果 fullName: {fullName}</p>
<button onClick={() => setFirstName('Li')} style={{ marginRight: '10px' }}>修改 firstName 为 Li</button>
<button onClick={() => setLastName('Si')}>修改 lastName 为 Si</button>
</div>
);
}
export default SimpleComputedDemo;
说明
该方式无需引入任何 Hook,直接通过普通变量赋值实现计算需求,写法简洁。但缺点是无缓存,每次组件渲染(无论依赖是否变化)都会重新执行计算逻辑,仅适用于计算成本极低的场景,不推荐在复杂计算中使用。
三、实现 Vue 的 watch(监听数据变化)
Vue watch 的核心特性是:监听指定的响应式数据,当数据发生变化时,执行预设的副作用函数(如发送接口请求、操作本地存储、修改其他关联数据等)。其核心应用场景包括:基础监听、深度监听(deep: true)、立即执行(immediate: true),下面我们逐一在 React 中实现这些场景。
场景一:基础监听(函数组件 + useEffect)
React 中处理副作用的核心 Hook 是 useEffect,通过控制其依赖数组,可以精准匹配 Vue 基础版 watch 的功能,这是实现数据监听的基础方案。
完整代码示例
import { useState, useEffect } from 'react';
function BasicWatchDemo() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: 'Zhang San', age: 25 });
useEffect(() => {
console.log(`[基础监听] count 发生变化,新值为:${count}`);
}, [count]);
useEffect(() => {
console.log(`[基础监听] user.name 发生变化,新值为:${user.name}`);
}, [user.name]);
return (
<div style={{ padding: '20px' }}>
<h3>React 实现 Vue watch(基础监听版)</h3>
<p>count: {count}</p>
<p>user.name: {user.name}</p>
<p>user.age: {user.age}</p>
{/* 交互按钮:修改 count */}
<button onClick={() => setCount(prev => prev + 1)} style={{ marginRight: '10px' }}>count + 1</button>
{/* 交互按钮:修改 user.name */}
<button onClick={() => setUser(prev => ({ ...prev, name: 'Li Si' }))} style={{ marginRight: '10px' }}>修改 user.name 为 Li Si</button>
{/* 交互按钮:修改 user.age(不触发 user.name 的监听) */}
<button onClick={() => setUser(prev => ({ ...prev, age: prev.age + 1 }))}>user.age + 1</button>
</div>
);
}
export default BasicWatchDemo;
核心解释
- 依赖数组与监听目标的关联:
useEffect 的副作用函数执行时机由依赖数组控制,只有当依赖数组中的变量发生「浅层次变化」时,副作用函数才会执行,这与 Vue 基础 watch 监听数据变化触发回调的逻辑完全一致。
- 上述示例中:
- 点击「count + 1」,仅触发 count 对应的监听副作用。
- 点击「修改 user.name」,仅触发 user.name 对应的监听副作用。
- 点击「user.age + 1」,由于未将 user.age 列入依赖数组,因此不会触发任何副作用函数,符合精准监听的需求。
场景二:深度监听(模拟 Vue watch 的 deep: true)
在实际开发中,我们经常需要监听复杂对象或数组的「深层属性变化」(如 user.address.province、list[0].name),此时简单的依赖数组无法实现需求,这就需要模拟 Vue watch 的 deep: true 配置,实现深度监听。
方案一:手动监听所有深层属性(适用于简单对象)
对于属性较少的简单对象,可以将所有需要监听的深层属性手动列入 useEffect 的依赖数组,实现近似的深度监听效果。
代码示例
import { useState, useEffect } from 'react';
function ManualDeepWatchDemo() {
const [user, setUser] = useState({ name: 'Zhang San', address: { province: 'Guangdong', city: 'Shenzhen' } });
useEffect(() => {
console.log(`[手动深层监听] user 深层属性发生变化,当前 user:`, user);
}, [user.name, user.address.province, user.address.city]);
return (
<div style={{ padding: '20px' }}>
<h3>React 实现 Vue watch(手动深层监听版)</h3>
<p>user.name: {user.name}</p>
<p>user.address.province: {user.address.province}</p>
<p>user.address.city: {user.address.city}</p>
<button onClick={() => setUser(prev => ({ ...prev, address: { ...prev.address, city: 'Guangzhou' } }))} style={{ marginRight: '10px' }}>修改城市为 Guangzhou</button>
<button onClick={() => setUser(prev => ({ ...prev, address: { ...prev.address, province: 'Jiangsu' } }))}>修改省份为 Jiangsu</button>
</div>
);
}
export default ManualDeepWatchDemo;
局限性说明
该方案仅适用于属性较少的简单对象,当对象结构复杂(如多层嵌套、属性数量众多)时,存在明显弊端:
- 维护成本极高:需要手动罗列所有深层属性,遗漏任何一个都可能导致监听失效。
- 代码冗余:依赖数组会变得异常冗长,降低代码可读性和可维护性。
- 无法应对动态属性:对于数组、动态添加的对象属性,无法提前手动罗列,监听效果受限。
方案二:自定义 Hook(useDeepCompareEffect,推荐)
该方案的核心思路是:通过 lodash.isEqual 实现深层数据对比,结合 useRef 缓存上一次的依赖数据,仅当深层对比发现数据变化时,才执行副作用函数,完美模拟 Vue watch 的 deep: true。
步骤 1:安装 lodash 依赖
步骤 2:自定义 Hook 封装(useDeepCompareEffect)
import { useEffect, useRef } from 'react';
import isEqual from 'lodash/isEqual';
function useDeepCompareEffect(effect, deps) {
const prevDepsRef = useRef();
const depsChanged = !isEqual(deps, prevDepsRef.current);
if (depsChanged) {
prevDepsRef.current = deps;
}
useEffect(() => {
return effect();
}, [depsChanged]);
}
export default useDeepCompareEffect;
步骤 3:使用示例(监听复杂深层对象)
import { useState } from 'react';
import useDeepCompareEffect from './useDeepCompareEffect';
function DeepWatchDemo() {
const [user, setUser] = useState({
name: 'Zhang San',
age: 25,
address: { province: 'Guangdong', city: 'Shenzhen', detail: { street: 'Nanshan Road', number: '123' } },
hobbies: ['reading', 'running']
});
useDeepCompareEffect(() => {
console.log(`[深层监听] user 发生变化(包含深层属性),当前 user:`, user);
}, [user]);
return (
<div style={{ padding: '20px' }}>
<h3>React 实现 Vue watch(深层监听版)</h3>
<p>user.name: {user.name}</p>
<p>user.address.detail.street: {user.address.detail.street}</p>
<p>user.hobbies: {user.hobbies.join(', ')}</p>
<button onClick={() => setUser(prev => ({ ...prev, address: { ...prev.address, detail: { ...prev.address.detail, street: 'Futian Road' } } }))} style={{ marginRight: '10px' }}>修改街道为 Futian Road</button>
<button onClick={() => setUser(prev => ({ ...prev, hobbies: [...prev.hobbies, 'swimming'] }))}>添加爱好 swimming</button>
</div>
);
}
export default DeepWatchDemo;
说明
该方案完美解决了复杂对象的深度监听问题,无需手动罗列深层属性,维护成本低,是项目开发中处理深度监听的推荐方案,其核心逻辑是通过 lodash.isEqual 忽略引用地址,仅对比数据的深层内容是否一致。
场景三:立即执行(模拟 Vue watch 的 immediate: true)
Vue watch 的 immediate: true 配置用于实现「监听函数在首次渲染时立即执行一次,后续依赖变化时再正常执行」。而 React 中 useEffect 的默认行为就是:首次组件渲染时执行一次副作用函数,之后仅当依赖变化时再次执行,这与 immediate: true 的逻辑完全匹配,实现起来非常简洁。
代码示例 1:默认立即执行(简洁写法)
import { useState, useEffect } from 'react';
function ImmediateWatchDemo1() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`[立即执行] count 监听触发,当前值:${count}`);
}, [count]);
return (
<div style={{ padding: '20px' }}>
<h3>React 实现 Vue watch(立即执行版)</h3>
<p>count: {count}</p>
<button onClick={() => setCount(prev => prev + 1)}>count + 1</button>
</div>
);
}
export default ImmediateWatchDemo1;
代码示例 2:取消「立即执行」(仅数据变化时执行)
有时我们需要模拟 Vue watch 的默认行为(immediate: false,仅数据变化时执行,首次渲染不执行),此时可以通过 useRef 定义一个标识位,控制首次渲染时不执行副作用逻辑。
import { useState, useEffect, useRef } from 'react';
function ImmediateWatchDemo2() {
const [count, setCount] = useState(0);
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
console.log(`[取消立即执行] count 发生变化,当前值:${count}`);
}, [count]);
return (
<div style={{ padding: '20px' }}>
<h3>React 实现 Vue watch(取消立即执行版)</h3>
<p>count: {count}</p>
<button onClick={() => setCount(prev => prev + 1)}>count + 1</button>
</div>
);
}
export default ImmediateWatchDemo2;
进阶:封装 Vue 风格的 useWatch 自定义 Hook(提升复用性)
上述方案已经实现了 Vue watch 的核心功能,但在多个组件中使用时会存在重复代码。我们可以封装一个贴近 Vue 写法的 useWatch 自定义 Hook,支持 immediate 和 deep 两个配置项,提升代码复用性。
完整封装代码
import { useEffect, useRef } from 'react';
import isEqual from 'lodash/isEqual';
function useWatch(watchSource, callback, options = { immediate: false, deep: false }) {
const { immediate, deep } = options;
const prevSourceRef = useRef();
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
if (immediate) {
callback(watchSource, undefined);
return;
}
}
const sourceChanged = deep ? !isEqual(watchSource, prevSourceRef.current) : watchSource !== prevSourceRef.current;
if (sourceChanged) {
callback(watchSource, prevSourceRef.current);
}
prevSourceRef.current = deep ? JSON.parse(JSON.stringify(watchSource)) : watchSource;
}, [watchSource, callback, immediate, deep]);
}
export default useWatch;
使用示例
import { useState } from 'react';
import useWatch from './useWatch';
function VueStyleWatchDemo() {
const [user, setUser] = useState({ name: 'Zhang San', age: 25, address: { city: 'Shenzhen' } });
useWatch(
user,
(newValue, oldValue) => {
console.log('[Vue 风格 useWatch] 监听触发');
console.log('新值:', newValue);
console.log('旧值:', oldValue);
},
{ immediate: true, deep: true }
);
return (
<div style={{ padding: '20px' }}>
<h3>React 封装 Vue 风格 useWatch</h3>
<p>user.name: {user.name}</p>
<p>user.address.city: {user.address.city}</p>
<button onClick={() => setUser(prev => ({ ...prev, address: { ...prev.address, city: 'Guangzhou' } }))}>修改城市为 Guangzhou</button>
</div>
);
}
export default VueStyleWatchDemo;
说明
该自定义 useWatch Hook 完全贴近 Vue watch 的写法,支持新旧值传递、立即执行和深度监听配置,可直接在项目中复用,同时也是前端面试中的加分项,体现了开发者的 Hook 封装能力和对 React 副作用的理解。
四、React vs Vue 核心对应关系(汇总表)
| Vue 特性 | React 实现方式 | 核心匹配点 |
|---|
| computed(带缓存) | 函数组件 + useMemo Hook | 基于依赖缓存,依赖不变时不重复计算,优化性能 |
| computed(无缓存) | 普通变量直接赋值(简单计算) | 实现基础计算需求,无缓存,写法简洁 |
| watch(基础监听) | 函数组件 + useEffect Hook | 依赖变化时执行副作用,精准监听单个/多个基础数据 |
| watch(deep: true) | 1. 手动罗列深层属性(简单对象) 2. 自定义 useDeepCompareEffect(复杂对象,依赖 lodash.isEqual) | 忽略引用地址,监听对象/数组的深层内容变化 |
| watch(immediate: true) | 函数组件 + useEffect Hook(默认行为) | 首次渲染 + 依赖变化时执行副作用 |
| watch(immediate: false) | 函数组件 + useEffect + useRef 标识位 | 仅依赖变化时执行副作用,首次渲染不执行 |
| 完整 Vue 风格 watch | 自定义 useWatch Hook(封装 useEffect + 深层对比) | 支持 deep、immediate 配置,贴近 Vue 写法,提升复用性 |
五、总结与落地说明
1. 核心要点回顾
- React 实现 Vue
computed 的核心是 useMemo Hook,其缓存特性与 computed 完全匹配,是复杂计算场景的首选方案;简单计算可直接使用普通变量赋值,兼顾简洁性。
- React 实现 Vue
watch 的基础是 useEffect Hook,通过依赖数组控制副作用执行时机,实现基础监听;深层监听需结合 lodash.isEqual 进行深层数据对比,封装自定义 Hook 提升复用性;立即执行与取消立即执行的核心是通过 useRef 标识位控制首次渲染的逻辑。
- 所有实现方案均符合 React 官方最佳实践,无额外第三方框架依赖(仅深层监听需引入 lodash),稳定性有保障。
2. 实用价值强调
本文覆盖了日常 React 开发中 99% 的「计算属性」和「数据监听」场景,从基础写法到进阶封装,代码均可直接复制落地。其中,自定义 useDeepCompareEffect 和 useWatch Hook 不仅能减少项目中的重复代码,还能在前端面试中展现个人的技术深度和工程化思维,是加分的亮点。
3. 落地补充提示
- 项目中可将
useDeepCompareEffect 和 useWatch 抽离为公共 Hook(如放在 src/hooks/ 目录下),统一管理和维护,方便所有组件引入使用。
- 若项目中已引入 lodash,可直接使用
lodash.isEqual;若不想引入完整 lodash,可单独安装 lodash.isEqual(npm install lodash.isEqual),减小打包体积。
- 对于极致性能优化的场景,可结合
useCallback 缓存回调函数,与 useMemo、useEffect 配合使用,进一步减少不必要的组件重渲染。
- 随着 React 生态的发展,也可选择成熟的第三方 Hook 库(如
react-use),其中已封装了完善的深层监听、数据监听 Hook,快速提升开发效率。
通过本文的学习,相信你已经能够在 React 项目中灵活实现 Vue watch 和 computed 的核心功能,从容应对各类响应式场景的开发需求。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online