Vue2与Vue3响应式原理源码解析
本文深入解析Vue2与Vue3响应式系统的实现原理。Vue2基于Object.defineProperty劫持对象属性,通过Dep类管理依赖;Vue3采用ES6的Proxy和Reflect,解决数组监听及新增属性限制。通过源码级封装演示Watcher收集与更新机制,帮助开发者理解数据驱动视图的核心逻辑。
前言
在开发Vue2和Vue3应用时,开发者通常会在data选项中定义数据,并在模板中多处引用。通过操作这些数据,可以自动更新视图,实现数据驱动视图的效果。
利用Mustache语法,可以将data中的数据渲染到页面。当data中的数据发生变化时,视图会自动同步更新。
这种响应式机制在Vue2和Vue3中是如何具体实现的?
Vue2响应式操作
响应式函数的封装
在实现响应式之前,需要先封装一个响应式函数。该函数接收一个函数作为参数,传入的函数将被标记为需要响应式处理,而默认定义的普通函数则不需要。
我们需要使用一个数组来收集这些函数(目前暂用数组,后续优化为Set),代码如下:
// 封装一个响应式的函数
let reactiveFns = []
function watchFn(fn) {
reactiveFns.push(fn)
}当需要执行这些函数时(具体触发时机稍后说明),可以遍历该数组并执行每个函数:
reactiveFns.forEach(fn => {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->
fn()
})Depend类的封装
我们需要封装一个Depend类,用于管理某个对象特定属性的所有响应式函数。由于一个对象可能包含多个属性,且每个属性都可能被依赖,因此需要为每个被使用的属性创建一个独立的Depend实例,以管理对该属性的所有依赖函数。
为了获取响应式函数中传入的函数,我们可以使用activeReactiveFn变量暂时保存当前正在执行的函数。
基于此,我们对响应式函数的封装进行重构,如下所示:
// 保存当前需要收集的响应式函数
let activeReactiveFn = null
// 封装一个响应式的函数
function watchFn(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}由于某个属性可能被多个函数依赖,我们在Depend类内部定义一个Set集合reactiveFns = new Set()。使用Set而非数组是因为Set自动去重,从而避免重复执行依赖函数。
这里定义了一个depend方法,当activeReactiveFn存在时,将其加入reactiveFns集合中;notify函数则负责遍历并执行所有收集到的函数。
class Depend {
constructor() {
this.reactiveFns = new Set()
}
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}监听对象的变化
在Vue2中,监听对象变化主要依赖Object.defineProperty方法。
我们可以封装一个reactive函数,接收一个对象作为参数。该函数遍历对象的所有属性及其值,并对每个属性使用Object.defineProperty进行监听。在defineProperty的第三个参数中,通过get和set方法实现劫持。在set方法中,当值被修改为新值后,需要获取该属性对应的Depend对象。
这里存在一个问题:如果有多个对象,且每个对象包含多个属性,该如何管理这些Depend对象?
可以定义一个WeakMap来存储各个对象,将对象保存为Map形式。在每个单一对象内部,使用Map形式保存其属性的Depend类实例,如图所示:

那么如何根据对象名和属性名获取对应的depend呢?可以通过getDepend函数实现,该函数接收对象和属性名作为参数,代码如下:
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
// 根据target对象获取map的过程
let map = targetMap.get(target)
if (!map) {
map = new Map()
targetMap.set(target, map)
}
// 根据key获取depend对象
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}获取到属性特定的depend后,回到set方法。当值被修改为新值后,获取该属性对应的depend,并调用其notify方法,从而执行所有对该属性有依赖的函数,实现数据更新。
在get方法中,在返回属性值之前,先获取该属性对应的depend,并调用其depend方法,将当前依赖的函数保存下来。
代码如下:
function reactive(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
const depend = getDepend(obj, key)
depend.depend()
return value
},
set: function(newValue) {
value = newValue
const depend = getDepend(obj, key)
depend.notify()
}
})
})
return obj
}至此,Vue2的响应式操作原理已实现。
所有代码以及测试代码如下:
// 保存当前需要收集的响应式函数
let activeReactiveFn = null
class Depend {
constructor() {
this.reactiveFns = new Set()
}
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
// 封装一个响应式的函数
function watchFn(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
// 根据target对象获取map的过程
let map = targetMap.get(target)
if (!map) {
map = new Map()
targetMap.set(target, map)
}
// 根据key获取depend对象
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}
function reactive(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
const depend = getDepend(obj, key)
depend.depend()
return value
},
set: function(newValue) {
value = newValue
const depend = getDepend(obj, key)
depend.notify()
}
})
})
return obj
}
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
name: "cy", // depend对象
age: 18 // depend对象
})
const infoProxy = reactive({
address: "安徽省",
height: 1.88
})
watchFn(() => {
console.log(infoProxy.address)
})
infoProxy.address = "北京市"
const foo = reactive({
name: "foo"
})
watchFn(() => {
console.log(foo.name)
})
foo.name = "aaa"
foo.name = "bbb"
// 安徽省
// 北京市
// foo
// aaa
// bbbVue3响应式操作
Proxy、Reflect
Proxy: 在Vue2中,使用Object.defineProperty来监听对象的变化,但这种方法存在哪些局限性?
首先,Object.defineProperty的设计初衷并非用于监听一个对象的所有属性。
在定义某些属性时,初衷是定义普通属性,但后来强行将其转换为数据属性描述符。
其次,如果想监听更丰富的操作,如新增属性或删除属性,Object.defineProperty无法做到。
因此,我们需要明确,数据描述符的设计初衷并不是为了监听完整对象的所有变化。
在ES6中,新增了一个Proxy类,从名称即可看出,它用于创建代理对象:
也就是说,如果我们希望监听对象的相关操作,可以先创建一个代理对象(Proxy对象);
之后对该对象的所有操作都通过代理对象完成,代理对象可以监听我们对原对象进行的操作;
如果我们想要侦听某些具体操作,可以在handler中添加对应的捕捉器(Trap): set函数有四个参数:
target:目标对象(被监听的对象); property:将被设置的属性key; value:新属性值; receiver:调用的代理对象;
get函数有三个参数:
target:目标对象(被监听的对象); property:被获取的属性key; receiver:调用的代理对象
实例代码如下:
const obj = {
name: "cy",
age: 18
}
const objProxy = new Proxy(obj, {
// 获取值时的捕获器
get: function(target, key) {
console.log(`监听到对象的${key}属性被访问了`, target)
return target[key]
},
// 设置值时的捕获器
set: function(target, key, newValue) {
console.log(`监听到对象的${key}属性被设置值`, target)
target[key] = newValue
}
})
console.log(objProxy.name)
console.log(objProxy.age)
objProxy.name = "kobe"
objProxy.age = 30
console.log(obj.name)
console.log(obj.age)
// 监听到对象的name属性被访问了 { name: 'cy', age: 18 }
// cy
// 监听到对象的age属性被访问了 { name: 'cy', age: 18 }
// 18
// 监听到对象的name属性被设置值 { name: 'cy', age: 18 }
// 监听到对象的age属性被设置值 { name: 'kobe', age: 18 }
// kobe
// 30Reflect:
Reflect也是ES6新增的一个API,它是一个对象,字面意思是反射。
那么Reflect有什么用呢?
它主要提供了许多操作JavaScript对象的方法,类似于Object中操作对象的方法;
例如Reflect.getPrototypeOf(target)类似于Object.getPrototypeOf();
例如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty();
既然已有Object可以执行这些操作,为什么还需要新增Reflect对象?
这是因为在早期的ECMA规范中,未充分考虑对对象本身的操作如何设计更为规范,因此将这些API放置在了Object上;
但Object作为构造函数,这些操作放在其上并不合适;
此外,还包含一些类似in、delete等操作符,使得JS代码看起来有些奇怪;
因此在ES6中新增了Reflect,将这些操作集中到Reflect对象上;
Reflect中常见的方法:

我们可以将之前Proxy案例中对原对象的操作,都修改为使用Reflect来执行;
在使用getter和setter时,有一个receiver参数,它的作用是什么?
如果源对象(obj)包含setter和getter访问器属性,可以通过receiver来改变内部的this指向。
Vue3响应式
Vue3响应式使用Proxy,我们需要在Vue2的reactive函数基础上进行一些调整:
function reactive(obj) {
return new Proxy(obj, {
get: function(target, key, receiver) {
// 根据target.key获取对应的depend
const depend = getDepend(target, key)
// 给depend对象中添加响应函数
depend.depend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// depend.notify()
const depend = getDepend(target, key)
depend.notify()
}
})
}其他方面的代码与Vue2基本无异,Vue3的响应式操作即已实现。
所有代码以及测试代码如下:
// 保存当前需要收集的响应式函数
let activeReactiveFn = null
class Depend {
constructor() {
this.reactiveFns = new Set()
}
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
// 封装一个响应式的函数
function watchFn(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
// 根据target对象获取map的过程
let map = targetMap.get(target)
if (!map) {
map = new Map()
targetMap.set(target, map)
}
// 根据key获取depend对象
let depend = map.get(key)
if (!depend) {
depend = new Depend()
map.set(key, depend)
}
return depend
}
function reactive(obj) {
return new Proxy(obj, {
get: function(target, key, receiver) {
// 根据target.key获取对应的depend
const depend = getDepend(target, key)
// 给depend对象中添加响应函数
depend.depend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
// depend.notify()
const depend = getDepend(target, key)
depend.notify()
}
})
}
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
name: "cy", // depend对象
age: 18 // depend对象
})
const infoProxy = reactive({
address: "安徽省",
height: 1.88
})
watchFn(() => {
console.log(infoProxy.address)
})
infoProxy.address = "北京市"
const foo = reactive({
name: "foo"
})
watchFn(() => {
console.log(foo.name)
})
foo.name = "bar"
// 安徽省
// 北京市
// foo
// bar以上就是一篇搞懂Vue2、Vue3响应式源码的原理的详细内容,更多关于Vue2、Vue3响应式源码的原理的资料请关注本站其它相关文章!
Vue.js官网地址是多少?区分国际站与中文文档入口
2026-11-05 16:29:53
Vue/React 项目中怎么封装复用 flv.js 播放器组件
2026-11-03 19:42:36
Vue.js 动态路由权限控制与菜单生成实战方案
2026-11-03 13:46:47
Vue.js 企业级项目架构搭建与目录规范实战
2026-11-03 12:25:32
Vue 或 React 项目中集成 Swiper.js 的最佳实践
2026-11-02 10:29:42
