Skip to Content
全部文章万论前端Vue3响应式 Vs React显式依赖:设计哲学的差异

Vue3响应式 Vs React显式依赖:设计哲学的差异

发布时间: 2026-01-23

前言

今天我对Vue3和React做了一个总结,这两个MVVM框架现在在前端开发中受欢迎程度最高的两个,Angular尽管还有,但是不得不说国内真的比较少,比较重,我在上家公司的时候 采用angular2,没想到现在都版本发布到21了,它给我的感觉就是重型框架,但是对于大型的企业级应用来说能够归类得比较到位。

来看看这3个框架的下载量情况。

首先是react的周下载量,7000w次断档的领先

Hello

然后第二名是vue的周下载量,大约830多w次

Hello

第三名是angular的周下载量,大约480w次

Hello

当然今天主要是想聊一下Vue3的响应式和React的显式依赖出发,根据我自己的经验聊聊它们的设计哲学差异点。

Vue3响应式

国内很多公司比较喜欢用Vue,很多人都说它入门快,如果是React反而面临招聘难,工资成本高。这其实不是乱说,确实存在的现实情况,Vue是尤雨溪创造的,我觉得中国人在做it这方面 还是很有特色的,就是想尽可能的包办和使用简单,做保姆式的服务,你看电商领域,老外做电商多少年了,全球电商亚马逊的APP哪怕是放到10年前和现在对比,依然没有什么提升,对比国内 的京东、淘宝的购物体验那也是输了十万八千里,不过好的是Vue它尽管在设计上尽量去帮你简化使用,但是它没有为这样的设计付出过大的代价,不像京东淘宝一个APP比亚马逊大了很多倍。

Vue我是从2.x才开始接触的,它的双向绑定一度让我认为这就是前端框架开发应有的样子,方便到极致,你根本不需要考虑任何依赖的问题,你只管改数据,它就像魔法一样帮你同步到页面,你 改页面输入组件,数据也同步被修改。这样的设计让国内很多公司都采用了它,腾讯一度是vue的重度用户,直到最近这几年开始逐步采用React开发,但是你看微信小程序、uniapp都借鉴了vue2的 语法,由此可见vue在国内的占比其实并不像开头提到的那样,React7000万下载量,Vue的只有800多万的下载量这么大的差距,反而是 Vue 的市占比好像更高一些。

自动依赖收集

Vue是如何收集到依赖关系,在值变化时通知依赖该值的地方响应变化的?

响应式的原理, Vue2 中使用 Object.defineProperty 拦截对象单个属性的读写操作,由于defineProperty对数组等的一些支持不如ES6新增的 Proxy,于是Vue3开始使用Proxy来 代理整个对象的各类操作。当在effect副作用函数 中访问响应式对象的属性(如 ref.value 或 reactive 对象中的某个属性)时,Vue 会通过 track 函数将当前活跃的副作用函数(当前正在执行的effect函数)标记为该属性的依赖。

副作用函数,类似React手动写的useEffect,在Vue中很多函数会被自动使用effect包裹,例如setup、模板中引用响应数据、watch、computed,以此来达到可以响应对象的变化

这里举个例子,比如我的组件渲染,渲染它是会自动被包裹在effect函数中的,我执行渲染的时候,内部读取了ref.value,读取某个响应式对象的value时, 则会将当前正在执行的副作用函数(渲染函数)标记为它依赖了ref.value。

当你给响应式对象的属性赋值时,Vue 会调用 trigger 函数通知所有依赖该属性的 effect(如组件渲染、计算属性、监听器) 重新执行,从而触发视图或逻辑的更新。

现在我手动写一个effect函数来测试一下副作用函数的自动收集依赖,代码如下,有一个reactive对象,有2个属性,我在effect副作用函数中读取了user响应对象的name属性, 并且模板上有2个按钮,一个是增加年龄,一个是修改名称:

<template> <div> <p>姓名:{{ user.name }}</p> <p>年龄:{{ user.age }}</p> <button @click="user.age++">增加年龄</button> <button @click="user.name = '李四'">修改姓名</button> </div> </template> <script setup> import { reactive, effect } from 'vue' // 定义对象类型响应式数据 const user = reactive({ name: '张三', age: 20 }) // effect 追踪 user 的所有属性 effect(() => { console.log(`用户信息变化:${user.name}`) }) </script>

运行上面这段代码

Hello

刷新页面,控制台输出了用户信息变化:张三,每一个effect副作用函数会默认执行一次,执行到user.name的时候,会通过 track 函数将当前活跃的副作用函数(当前正在执行的effect函数)标记为该属性的依赖, 也就是当前这个effect函数依赖了user.name的值,只要后续user.name的值变化,这个副作用函数就会重新执行。然后我点了一次增加年龄按钮,结果如图:

Hello

看右下角控制台输出,没有变化,说明effect函数没有执行,然后我点击一次修改姓名,结果如图:

Hello

可以看到effect里的匿名函数执行了,对照上面我说的自动收集依赖的原理,都能一一对应上。

这就是Vue收集依赖关系的关键原理,现在我们捡两个比较常用的来看看内部的关键代码是不是和上面说的一致。

Ref的关键实现

Vue3的Ref它是一个封装的类,并不是指针Ref这样的意思,虽然Vue3是通过Proxy实现对象代理,监听对象操作来标记track和trigger,但是Proxy它是代理对象的,那我们平时用的String、Number、Boolean 类型的值怎么监听呢?所以Vue3提供了一个Ref类,通过给这个类添加getter和setter访问属性器,对外提供了.value获取值和设置值的方式,

当你执行xxxxx = ref.value获取ref的值时它就调用dep的track来让”当前活跃的副作用函数”把这个ref标记为依赖。

当你执行ref.value = xxxxxxx的时候,他就调用dep的trigger来让所以依赖这个ref的副作用函数重新执行。 请看Ref类源码get value()set value()

function createRef(rawValue: unknown, shallow: boolean) { if (isRef(rawValue)) { return rawValue } return new RefImpl(rawValue, shallow) } class RefImpl<T = any> { _value: T private _rawValue: T dep: Dep = new Dep() public readonly [ReactiveFlags.IS_REF] = true public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false constructor(value: T, isShallow: boolean) { this._rawValue = isShallow ? value : toRaw(value) this._value = isShallow ? value : toReactive(value) this[ReactiveFlags.IS_SHALLOW] = isShallow } get value() { if (__DEV__) { this.dep.track({ target: this, type: TrackOpTypes.GET, key: 'value', }) } else { // 如果在副作用函数中调用了这个get访问属性器,它会把这个值跟对应的副作用函数绑定为依赖关系 // 而如果你在自己的一些函数内调用,则当前正在执行的并不是副作用函数,所以并不会触发最终关系 this.dep.track() } return this._value } set value(newValue) { const oldValue = this._rawValue const useDirectValue = this[ReactiveFlags.IS_SHALLOW] || isShallow(newValue) || isReadonly(newValue) newValue = useDirectValue ? newValue : toRaw(newValue) if (hasChanged(newValue, oldValue)) { this._rawValue = newValue this._value = useDirectValue ? newValue : toReactive(newValue) if (__DEV__) { this.dep.trigger({ target: this, type: TriggerOpTypes.SET, key: 'value', newValue, oldValue, }) } else { // 如果你在哪里调用这个set访问属性器,它会通过trigger去通知所有依赖了此ref的副作用函数都更新 this.dep.trigger() } } } }

Reactive的关键实现

Reactive底层就是使用Proxy来代理一个对象,拦截对象内的某个属性的读写时执行和上面Ref一样的track和trigger逻辑

export function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. if (isReadonly(target)) { return target } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap, ) } function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any>, ) { // ... // 这里使用了Proxy,handler使用的是另一个类中的逻辑,根据监听的类型,来使用两个handlers const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers, ) proxyMap.set(target, proxy) return proxy } // 可以看到Object和Array使用的是baseHandlers,如果是Map Set使用的是collectionHandlers function targetTypeMap(rawType: string) { switch (rawType) { case 'Object': case 'Array': return TargetType.COMMON case 'Map': case 'Set': case 'WeakMap': case 'WeakSet': return TargetType.COLLECTION default: return TargetType.INVALID } }

单独看看baseHandlers里面是什么逻辑,collectionHandlers的源码在'./collectionHandlers'中,我就不贴出来了, baseHandlers里面有两个类,在Reactive类中使用的是mutableHandlers,mutableHandlers继承了BaseReactiveHandler, 所以我现在把这两个类都贴出来,看你面的get和set内部的track和trigger即可。

class BaseReactiveHandler implements ProxyHandler<Target> { constructor( protected readonly _isReadonly = false, protected readonly _isShallow = false, ) {} get(target: Target, key: string | symbol, receiver: object): any { if (key === ReactiveFlags.SKIP) return target[ReactiveFlags.SKIP] const isReadonly = this._isReadonly, isShallow = this._isShallow if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if (key === ReactiveFlags.IS_SHALLOW) { return isShallow } else if (key === ReactiveFlags.RAW) { if ( receiver === (isReadonly ? isShallow ? shallowReadonlyMap : readonlyMap : isShallow ? shallowReactiveMap : reactiveMap ).get(target) || // receiver is not the reactive proxy, but has the same prototype // this means the receiver is a user proxy of the reactive proxy Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver) ) { return target } // early return undefined return } const targetIsArray = isArray(target) if (!isReadonly) { let fn: Function | undefined if (targetIsArray && (fn = arrayInstrumentations[key])) { return fn } if (key === 'hasOwnProperty') { return hasOwnProperty } } const res = Reflect.get( target, key, // if this is a proxy wrapping a ref, return methods using the raw ref // as receiver so that we don't have to call `toRaw` on the ref in all // its class methods isRef(target) ? target : receiver, ) if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res } if (!isReadonly) { // 这里在标记track,和前面的Ref是类似的逻辑,为什么这里要多传一个key呢?因为它是在标记对象中当前正在被读取的prop,比如 // 对象obj = reactive({a:1,b:2}),此时我访问const a = obj.b; 这里的key就是'b' track(target, TrackOpTypes.GET, key) } if (isShallow) { return res } if (isRef(res)) { // ref unwrapping - skip unwrap for Array + integer key. const value = targetIsArray && isIntegerKey(key) ? res : res.value return isReadonly && isObject(value) ? readonly(value) : value } if (isObject(res)) { // Convert returned value into a proxy as well. we do the isObject check // here to avoid invalid value warning. Also need to lazy access readonly // and reactive here to avoid circular dependency. return isReadonly ? readonly(res) : reactive(res) } return res } } class MutableReactiveHandler extends BaseReactiveHandler { constructor(isShallow = false) { super(false, isShallow) } set( target: Record<string | symbol, unknown>, key: string | symbol, value: unknown, receiver: object, ): boolean { let oldValue = target[key] const isArrayWithIntegerKey = isArray(target) && isIntegerKey(key) if (!this._isShallow) { const isOldValueReadonly = isReadonly(oldValue) if (!isShallow(value) && !isReadonly(value)) { oldValue = toRaw(oldValue) value = toRaw(value) } if (!isArrayWithIntegerKey && isRef(oldValue) && !isRef(value)) { if (isOldValueReadonly) { if (__DEV__) { warn( `Set operation on key "${String(key)}" failed: target is readonly.`, target[key], ) } return true } else { oldValue.value = value return true } } } else { // in shallow mode, objects are set as-is regardless of reactive or not } const hadKey = isArrayWithIntegerKey ? Number(key) < target.length : hasOwn(target, key) const result = Reflect.set( target, key, value, isRef(target) ? target : receiver, ) // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { // 这里在调用trigger,和前面的Ref是类似的逻辑,多传一个key的原因我在上面的get中已经说明了 if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result } deleteProperty( target: Record<string | symbol, unknown>, key: string | symbol, ): boolean { const hadKey = hasOwn(target, key) const oldValue = target[key] const result = Reflect.deleteProperty(target, key) if (result && hadKey) { // 删除属性也需要调用 trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) } return result } has(target: Record<string | symbol, unknown>, key: string | symbol): boolean { const result = Reflect.has(target, key) if (!isSymbol(key) || !builtInSymbols.has(key)) { track(target, TrackOpTypes.HAS, key) } return result } ownKeys(target: Record<string | symbol, unknown>): (string | symbol)[] { track( target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY, ) return Reflect.ownKeys(target) } }

Dep的实现

这是Dep类的track和trigger的逻辑,这部分我没有深入的去看,结合前面的源码大概知道它做了什么,响应式最核心的逻辑就在dep和effect这两个源文件中, 它们实现了依赖关系的管理。

// 这部分就是在把值和当前正在执行的副作用关联依赖关系 track(debugInfo?: DebuggerEventExtraInfo): Link | undefined { if (!activeSub || !shouldTrack || activeSub === this.computed) { return } let link = this.activeLink if (link === undefined || link.sub !== activeSub) { link = this.activeLink = new Link(activeSub, this) // add the link to the activeEffect as a dep (as tail) if (!activeSub.deps) { activeSub.deps = activeSub.depsTail = link } else { link.prevDep = activeSub.depsTail activeSub.depsTail!.nextDep = link activeSub.depsTail = link } addSub(link) } else if (link.version === -1) { // reused from last run - already a sub, just sync version link.version = this.version // If this dep has a next, it means it's not at the tail - move it to the // tail. This ensures the effect's dep list is in the order they are // accessed during evaluation. if (link.nextDep) { const next = link.nextDep next.prevDep = link.prevDep if (link.prevDep) { link.prevDep.nextDep = next } link.prevDep = activeSub.depsTail link.nextDep = undefined activeSub.depsTail!.nextDep = link activeSub.depsTail = link // this was the head - point to the new head if (activeSub.deps === link) { activeSub.deps = next } } } if (__DEV__ && activeSub.onTrack) { activeSub.onTrack( extend( { effect: activeSub, }, debugInfo, ), ) } return link } trigger(debugInfo?: DebuggerEventExtraInfo): void { this.version++ globalVersion++ this.notify(debugInfo) } // 这个函数就是通知各个依赖的副作用你依赖的值变化了,要重新执行,这部分我没细看,看得出来依赖时一个链表 notify(debugInfo?: DebuggerEventExtraInfo): void { startBatch() try { if (__DEV__) { // subs are notified and batched in reverse-order and then invoked in // original order at the end of the batch, but onTrigger hooks should // be invoked in original order here. for (let head = this.subsHead; head; head = head.nextSub) { if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) { head.sub.onTrigger( extend( { effect: head.sub, }, debugInfo, ), ) } } } for (let link = this.subs; link; link = link.prevSub) { if (link.sub.notify()) { // if notify() returns `true`, this is a computed. Also call notify // on its dep - it's called here instead of inside computed's notify // in order to reduce call stack depth. ;(link.sub as ComputedRefImpl).dep.notify() } } } finally { endBatch() } }

到这里,Vue3的响应式就已经弄明白了。

设计哲学差异

React显式依赖

React的设计和Vue3完全不一样,Vue3的响应式通过拦截对象的读写来建立依赖关系,自动通知依赖变化。React却要显示指定依赖,你看各个hooks都需要明确指定依赖的对象。

把上面的vue effect例子拿来改成react的实现方法,不得不说看起来就是比vue要麻烦不少。

import { useState, useEffect } from 'react'; function UserInfo() { const [user, setUser] = useState({ name: '张三', age: 20 }); useEffect(() => { console.log(`用户信息变化:${user.name}`); }, [user]); const incrementAge = () => { setUser(prev => ({ ...prev, age: prev.age + 1 })); }; const changeName = () => { setUser(prev => ({ ...prev, name: '李四' })); }; return ( <div> <p>姓名:{user.name}</p> <p>年龄:{user.age}</p> <button onClick={incrementAge}>增加年龄</button> <button onClick={changeName}>修改姓名</button> </div> ); } export default UserInfo;

为什么?

React设计原则就是显式是更好的,显式就表示行为更可控,逻辑更清晰,它没有Vue便利,依赖控制完全交给程序员,对于构建更大型的应用,可控性更好, 相应的对程序员的要求就更高,如果用不好依赖,反而是会导致性能不佳。

还有一个很重要的是,他们俩的设计上完全不一样,Vue3通过trigger触发重新执行对应的副作用函数,这样来保持最小粒度的更新。 React本质是显式触发更新后,Fiber架构按优先级调度组件重渲染(构建 workInProgress 树),靠 可中断、双缓存树对比来减少变更Dom,而非细粒度更新。

简单总结就是,Vue是只变化有改动的地方,React是检查(双缓存树)有改动的地方更新。这样导致了Vue在细碎范围Dom更新性能更优,但是React的Fiber架构在大型组件树、复杂渲染上, 这种整体对比差异更新,可中断的设计性能会更优,这也是为什么大家流传着React更适合大型项目的原因。

最后编辑于

hi