React 实现 Vue 的 watch 和 computed 详解
文章目录
- [React 实现 Vue 的 watch 和 computed 详解](#React 实现 Vue 的 watch 和 computed 详解)
-
- [二、实现 Vue 的 computed(计算属性)](#二、实现 Vue 的 computed(计算属性))
-
- [方式一:基础版(函数组件 + useMemo,推荐)](#方式一:基础版(函数组件 + useMemo,推荐))
- 方式二:简化版(仅简单计算,无需缓存)
- [三、实现 Vue 的 watch(监听数据变化)](#三、实现 Vue 的 watch(监听数据变化))
-
- [场景一:基础监听(函数组件 + useEffect)](#场景一:基础监听(函数组件 + useEffect))
- [场景二:深度监听(模拟 Vue watch 的 deep: true)](#场景二:深度监听(模拟 Vue watch 的 deep: true))
-
- 方案一:手动监听所有深层属性(适用于简单对象)
- [方案二:自定义 Hook(useDeepCompareEffect,推荐)](#方案二:自定义 Hook(useDeepCompareEffect,推荐))
-
- [步骤 1:安装 lodash 依赖](#步骤 1:安装 lodash 依赖)
- [步骤 2:自定义 Hook 封装(useDeepCompareEffect)](#步骤 2:自定义 Hook 封装(useDeepCompareEffect))
- [步骤 3:使用示例(监听复杂深层对象)](#步骤 3:使用示例(监听复杂深层对象))
- 说明
- [场景三:立即执行(模拟 Vue watch 的 immediate: true)](#场景三:立即执行(模拟 Vue watch 的 immediate: true))
-
- [代码示例 1:默认立即执行(简洁写法)](#代码示例 1:默认立即执行(简洁写法))
- [代码示例 2:取消「立即执行」(仅数据变化时执行)](#代码示例 2:取消「立即执行」(仅数据变化时执行))
- [进阶:封装 Vue 风格的 useWatch 自定义 Hook(提升复用性)](#进阶:封装 Vue 风格的 useWatch 自定义 Hook(提升复用性))
- [四、React vs Vue 核心对应关系(汇总表)](#四、React vs Vue 核心对应关系(汇总表))
- 五、总结与落地说明
-
- [1. 核心要点回顾](#1. 核心要点回顾)
- [2. 实用价值强调](#2. 实用价值强调)
- [3. 落地补充提示](#3. 落地补充提示)
在前端框架生态中,Vue 的 watch 和 computed 是两大核心且极具实用性的特性: computed 用于创建基于依赖的缓存计算属性,避免无效重复计算; watch 用于监听数据变化并执行副作用操作,支撑各类业务逻辑的响应式触发。在 React 开发中,我们同样会遇到「需要缓存计算结果」和「监听数据变化执行回调」的场景,同时这也是前端面试中的高频考点。本文将详细讲解 React 生态中如何实现 Vue 这两大特性,涵盖基础写法与进阶封装,所有代码均可直接落地项目。
二、实现 Vue 的 computed(计算属性)
在开始 React 实现之前,我们先明确 Vue computed 的核心特性:基于响应式依赖进行缓存 。只有当 computed 依赖的数据源发生变化时,才会重新执行计算逻辑并更新结果;如果依赖未发生变化,直接返回缓存的上一次计算结果,从而避免无效的性能消耗,这也是 computed 与普通方法的核心区别。
方式一:基础版(函数组件 + useMemo,推荐)
React 中实现 computed 核心功能的最佳选择是官方提供的 useMemo Hook,它完美匹配 Vue computed 的「缓存」核心特性,是项目开发中的推荐方案。
完整代码示例
jsx
import { useState, useMemo } from 'react';
function ComputedDemo() {
// 1. 定义数据源(模拟 Vue 的 data 选项)
const [num1, setNum1] = useState(10);
const [num2, setNum2] = useState(20);
const [unrelatedNum, setUnrelatedNum] = useState(0); // 与计算逻辑无关的变量
// 2. 使用 useMemo 实现计算属性(模拟 Vue computed)
const sum = useMemo(() => {
console.log('useMemo 计算逻辑执行了------仅依赖变化时触发');
return num1 + num2;
}, [num1, num2]); // 依赖数组:仅 num1、num2 变化时,重新计算 sum
return (
<div style={{ padding: '20px' }}>
<h3>React 实现 Vue computed(useMemo 版)</h3>
<p>num1: {num1}</p>
<p>num2: {num2}</p>
<p>无关变量 unrelatedNum: {unrelatedNum}</p>
<p style={{ color: 'blue', fontWeight: 'bold' }}>计算属性 sum(num1 + num2): {sum}</p>
{/* 3. 交互按钮:修改相关依赖 */}
<button onClick={() => setNum1(prev => prev + 1)} style={{ marginRight: '10px' }}>
num1 + 1
</button>
<button onClick={() => setNum2(prev => prev + 1)} style={{ marginRight: '10px' }}>
num2 + 1
</button>
{/* 4. 交互按钮:修改无关依赖 */}
<button onClick={() => setUnrelatedNum(prev => prev + 1)}>
无关变量 + 1
</button>
</div>
);
}
export default ComputedDemo;
核心解释
-
useMemo两个核心参数的作用:- 第一个参数:计算逻辑函数 ,返回值即为计算属性的结果(对应 Vue
computed中的计算函数),该函数仅在依赖变化时执行。 - 第二个参数:依赖数组 ,存放当前计算逻辑依赖的所有数据源(对应 Vue
computed自动收集的响应式依赖),只有数组中的变量发生变化时,React 才会重新执行第一个参数的计算函数,更新计算结果。
- 第一个参数:计算逻辑函数 ,返回值即为计算属性的结果(对应 Vue
-
缓存特性验证(对比「使用 useMemo」与「直接定义变量」):
- 上述示例中,点击「num1 + 1」或「num2 + 1」(修改相关依赖),控制台会打印日志,
sum也会同步更新,说明计算逻辑重新执行。 - 点击「无关变量 + 1」(修改不相关依赖),控制台无日志输出,
sum保持不变,说明useMemo生效,直接返回了缓存的计算结果。 - 若直接定义变量
const sum = num1 + num2;,每次组件重新渲染(无论是否修改相关依赖),都会重新执行num1 + num2运算,当计算逻辑复杂时(如大量数据过滤、格式化),会造成不必要的性能损耗,这也是useMemo与普通变量定义的核心差异。
- 上述示例中,点击「num1 + 1」或「num2 + 1」(修改相关依赖),控制台会打印日志,
方式二:简化版(仅简单计算,无需缓存)
该方式适用于计算逻辑极简单 (如简单的数值运算、模板字符串拼接),且完全不关心重复计算带来的性能损耗的场景,对应 Vue 中 computed 关闭缓存(极少使用)的场景。
简短代码示例
jsx
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 的功能,这是实现数据监听的基础方案。
完整代码示例
jsx
import { useState, useEffect } from 'react';
function BasicWatchDemo() {
// 1. 定义数据源(模拟 Vue data)
const [count, setCount] = useState(0); // 单个基础类型值
const [user, setUser] = useState({ name: 'Zhang San', age: 25 }); // 对象类型
// 子场景 1:监听单个基础类型值(对应 Vue 监听单个基础数据)
useEffect(() => {
console.log(`[基础监听] count 发生变化,新值为:${count}`);
// 此处可执行副作用逻辑:如接口请求、本地存储等
}, [count]); // 依赖数组:仅 count 变化时,执行副作用函数
// 子场景 2:监听对象的某个具体属性(对应 Vue 监听对象单个属性)
useEffect(() => {
console.log(`[基础监听] user.name 发生变化,新值为:${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监听数据变化触发回调的逻辑完全一致。 - 上述示例中:
场景二:深度监听(模拟 Vue watch 的 deep: true)
在实际开发中,我们经常需要监听复杂对象或数组的「深层属性变化」(如 user.address.province、list[0].name),此时简单的依赖数组无法实现需求,这就需要模拟 Vue watch 的 deep: true 配置,实现深度监听。
方案一:手动监听所有深层属性(适用于简单对象)
对于属性较少的简单对象,可以将所有需要监听的深层属性手动列入 useEffect 的依赖数组,实现近似的深度监听效果。
代码示例
jsx
import { useState, useEffect } from 'react';
function ManualDeepWatchDemo() {
// 定义深层对象数据源
const [user, setUser] = useState({
name: 'Zhang San',
address: {
province: 'Guangdong',
city: 'Shenzhen'
}
});
// 手动监听深层属性:user.name、user.address.province、user.address.city
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 依赖
bash
npm install lodash
# 或
yarn add lodash
步骤 2:自定义 Hook 封装(useDeepCompareEffect)
jsx
import { useEffect, useRef } from 'react';
import isEqual from 'lodash/isEqual';
// 自定义深层对比 useEffect Hook(模拟 deep: true)
function useDeepCompareEffect(effect, deps) {
// 1. 使用 useRef 缓存上一次的依赖数据
const prevDepsRef = useRef();
// 2. 深层对比当前依赖与上一次依赖是否一致
const depsChanged = !isEqual(deps, prevDepsRef.current);
// 3. 更新缓存的依赖数据(无论是否变化,都更新为当前最新依赖)
if (depsChanged) {
prevDepsRef.current = deps;
}
// 4. 调用 useEffect,依赖数组为 [depsChanged](仅当深层对比发现变化时,执行 effect)
useEffect(() => {
return effect();
}, [depsChanged]);
}
export default useDeepCompareEffect;
步骤 3:使用示例(监听复杂深层对象)
jsx
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 实现深度监听
useDeepCompareEffect(() => {
console.log(`[深层监听] user 发生变化(包含深层属性),当前 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:默认立即执行(简洁写法)
jsx
import { useState, useEffect } from 'react';
function ImmediateWatchDemo1() {
const [count, setCount] = useState(0);
// useEffect 默认行为:首次渲染 + count 变化时执行(对应 Vue immediate: true)
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 定义一个标识位,控制首次渲染时不执行副作用逻辑。
jsx
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;
}
// 非首次渲染(仅 count 变化时),执行副作用逻辑
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 两个配置项,提升代码复用性。
完整封装代码
jsx
import { useEffect, useRef } from 'react';
import isEqual from 'lodash/isEqual';
// 自定义 Vue 风格 useWatch Hook
function useWatch(watchSource, callback, options = { immediate: false, deep: false }) {
const { immediate, deep } = options;
const prevSourceRef = useRef(); // 缓存上一次的监听源数据
const isFirstRender = useRef(true); // 标记是否为首次渲染
// 处理监听逻辑
useEffect(() => {
// 1. 处理首次渲染立即执行
if (isFirstRender.current) {
isFirstRender.current = false;
if (immediate) {
callback(watchSource, undefined); // 首次执行:新值为当前监听源,旧值为 undefined
return;
}
}
// 2. 处理深度对比 vs 浅对比
const sourceChanged = deep
? !isEqual(watchSource, prevSourceRef.current)
: watchSource !== prevSourceRef.current;
// 3. 监听源变化时,执行回调函数
if (sourceChanged) {
callback(watchSource, prevSourceRef.current); // 传递新值和旧值
}
// 4. 更新缓存的监听源数据
prevSourceRef.current = deep ? JSON.parse(JSON.stringify(watchSource)) : watchSource;
}, [watchSource, callback, immediate, deep]);
}
export default useWatch;
使用示例
jsx
import { useState } from 'react';
import useWatch from './useWatch';
function VueStyleWatchDemo() {
// 定义监听源
const [user, setUser] = useState({
name: 'Zhang San',
age: 25,
address: {
city: 'Shenzhen'
}
});
// 使用封装的 useWatch(支持 deep: true + immediate: true)
useWatch(
user, // 监听源(对应 Vue watch 的监听目标)
(newValue, oldValue) => { // 回调函数(对应 Vue watch 的处理函数)
console.log('[Vue 风格 useWatch] 监听触发');
console.log('新值:', newValue);
console.log('旧值:', oldValue);
},
{ immediate: true, deep: true } // 配置项(对应 Vue watch 的 immediate 和 deep)
);
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的核心是useMemoHook,其缓存特性与computed完全匹配,是复杂计算场景的首选方案;简单计算可直接使用普通变量赋值,兼顾简洁性。 - React 实现 Vue
watch的基础是useEffectHook,通过依赖数组控制副作用执行时机,实现基础监听;深层监听需结合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 的核心功能,从容应对各类响应式场景的开发需求。