Vue 响应式原理

server/2024/9/24 0:53:21/

目录

1. vue 2

1.1 简单介绍 defineProperty

1.2 简单使用 defineProperty

1.3 defineProperties

1.4 数据双向绑定原理

1.41 响应式原理过程

1.42 数据劫持

1.43 发布订阅者模式

1.44 总结与补充

2. vue 3

2.1 简单介绍Proxy

2.2 简单介绍Reflect

2.3 简单使用Proxy和Reflect

2.4 数据双向绑定原理

2.41 响应式原理过程

2.42 简化代码模拟

2.43 总结与补充


1. vue 2

vue 2响应式原理主要借助于ES5的Object.defineProperty()方法来实现。


1.1 简单介绍 defineProperty

该方法允许你直接在对象上地定义新属性,或者修改现有属性,控制这些属性的特性(如是否可枚举、是否可配置、是否可写等),并返回该对象。

javascript">Object.defineProperty(obj, prop, descriptor)

它接收三个参数:

  • obj:要定义属性的对象
  • prop:属性的名称
  • descriptor:一个用于描述  “将被定义或修改属性”  的 —— 描述符对象

属性描述符(可包含以下之一或多个)

  • value:属性的值
  • writable:属性是否可被重写 ,默认为true
  • configurable:属性描述符是否可被改变,属性是否可从对象中被删除,默认为  true
  • enumerable:属性是否可被枚举到(即可以被for in遍历到),默认为  true
  • get:属性访问函数(getter),默认为undefined。执行时不传入任何参数,但是会传入this值(即被访问的对象)
  • set:属性写入函数(setter),默认为undefined。该函数将接收唯一参数,即被赋予的新值

需要注意的是,不能同时在一个描述符对象中指定 value( 或 writable)和 get( 或 set ),因为这会导致冲突。Object.defineProperty() 的一个主要用途是实现数据绑定和响应式系统,如 Vue.js 2.x 版本中的响应式原理。


1.2 简单使用 defineProperty

先定义一个对象

javascript">const count_obj = {name: 'count_obj',count_1: 1000,count_2: 1000,gap:0
}

演示 value和writable,将name的值修改为name_one,再将其设置为只读:

javascript">Object.defineProperty(count_obj, 'name', {writable: false,value: 'name_one'
})
console.log(count_obj.name) //name_one
count_obj.name = 'test'
console.log(count_obj.name) //name_one

演示 configurable,将count_1设置为不可删除,且描述符无法被改变

javascript">Object.defineProperty(count_obj, 'count_1', {configurable: false
})
delete count_obj.count_1
delete count_obj.count_2
console.log(count_obj.count_1) //1000
console.log(count_obj.count_2) //undefined

演示 configurable,将count_1设置其描述符无法被改变

javascript">Object.defineProperty(count_obj, 'count_1', {value: 10,writable: false,enumerable: false,configurable: false
})
try {Object.defineProperty(count_obj, 'count_1', {// value: 5000configurable: true,    //不能将其变回属性描述符可修改状态writable: true,        //不可将只读变回可改enumerable: true       //不可修改其是否可枚举})
} catch (error) {console.log(error) //TypeError: Cannot redefine property: count_1
}

另外,如果将writable由false变为true(只读变为可改)会报错,如果将writable由true变为false(可改变为只读)则不会报错。

javascript">Object.defineProperty(count_obj, 'count_1', {writable: true,configurable: false
})
Object.defineProperty(count_obj, 'count_1', {writable: false
})
//正常执行,且生效,count_1变为只读属性

演示 enumerable,将count_1设置为不可枚举,则for in遍历不到

javascript">Object.defineProperty(count_obj, 'count_1', {enumerable: false
})
for (const key in count_obj) {console.log(key)//输出:name, count_2
}

演示get 、set。

javascript">const test_obj = {}
let number = 1000
Object.defineProperty(test_obj, 'num', {get: function () {return number},set: function (newNum) {if (newNum > this.num) {number = newNum} else console.log('小于原数字')}
})
console.log(test_obj.num)    //1000
test_obj.num = 20    //小于原数字,修改失败test_obj.num = 2020    
console.log(test_obj.num)    //2020

注意:不要用Object.defineProperty监听对象已有的属性,而是用它通过给对象创建属性的方式来实现监听。 

假设监听已有的对象,如下:

javascript">const test_obj = {name:'test'
}
Object.defineProperty(test_obj, 'test', {get: function () {return this.name}
})
console.log(test_obj.name)

提问:什么情况下会触发get ?答案是,当test_obj.name被访问时会触发。而get内又访问了test_obj.name!!!所以运行上面的代码会报调用栈溢出的错误。(get同理)


1.3 defineProperties

defineProperties 与 defineProperty效果相同,但它可以同时定义多个属性。

javascript">const student = {}
let sex = '男'
Object.defineProperties(student, {name: {writable: false,value: 'kunkun'},age: {writable: true,value: 22},sex: {get() {return sex},set(v) {sex = v}}
})
console.log(`${student.name}: ${student.age}岁`)
student.sex = '女'
console.log(student.sex)

1.4 数据双向绑定原理
1.41 响应式原理过程

① 初始化:在vue实例初始化时,使用Object.defineProperty来实现数据劫持。同时,会为每个属性创建一个Dep(依赖管理器)实例,用于存储依赖这个属性的Watcher。

② 依赖收集:在模板编译过程中,Vue会解析模板中的指令和数据绑定,并为它们创建Watcher实例。当模板中的某个属性被访问时(如在模板中使用{{ someData }}),会触发该属性的getter函数,此时Watcher会被添加到该属性的Dep实例的依赖数组中。

③ 数据变化:当数据发生变化时(如通过Vue实例的data属性直接修改数据),会触发setter函数。setter函数会通知Dep实例,Dep实例随后会遍历其依赖数组中的所有Watcher,并调用它们的更新方法。

④视图更新:Watcher的更新方法会执行回调函数,这些回调函数通常会重新渲染视图或执行其他逻辑,从而实现数据的响应式更新。


1.42 数据劫持

Vue 2使用Object.defineProperty方法来实现数据劫持。在Vue实例初始化时,Vue会遍历data中的每一个属性,并使用Object.defineProperty将它们转换为getter/setter。这样做的目的是在访问和修改这些属性时,能够执行一些额外的操作:

① getter属性:依赖收集,记录当前有哪些Watcher(订阅者)正在观察这个属性

② setter属性:派发更新,通知所有依赖这个属性的Watcher(订阅者),告诉它们属性已经更新,需要执行更新操作

简单的代码模拟

首先,我们创建一个简单的observe函数,它接受一个对象,并使用Object.defineProperty来定义属性的getter和setter,以便我们能够拦截属性的访问和修改。

javascript">function observe(obj, callback) {// 遍历属性,并将其转换为getter/setterObject.keys(obj).forEach((key) => {let internalValue = obj[key]Object.defineProperty(obj, key, {enumerable: true,configurable: true,get() {console.log(`访问属性${key}`)// 依赖收集等操作...//....略return internalValue},set(newValue) {console.log(`更新属性${key}`)internalValue = newValue// 通知订阅者进行更新....callback(key, newValue) }})})
}
// 使用示例
let data = { name: 'Vue', age: 2 }
function update(key, newValue) {console.log(`更新依赖于 ${key}: ${newValue}的视图`)
}
observe(data, update) //将data转化为响应式对象
data.name = 'React' // 输出: 更新属性name 和 更新依赖于 name: React的视图
data.name //输出:访问属性name

1.43 发布订阅者模式

Vue 2通过发布订阅者模式来实现数据的响应式更新。在这个模式中,有三个核心组件:Observer(观察者)、Dep(依赖管理器)和Watcher(订阅者):

① Observer:负责观察数据对象,并将它们转换成响应式对象。当数据发生变化时,Observer会通知Dep。(实际上,就是借助Object.defineProperty实现数据劫持)

② Dep:是一个依赖管理器,它内部维护了一个数组,用于存储所有依赖当前属性的Watcher。当数据发生变化时,Dep会通知这些Watcher执行更新操作。

③ Watcher:是订阅者,它会在数据变化时收到通知,并执行相应的回调函数来更新视图

简单的代码模拟

下面是一个简单的发布订阅系统。我们创建一个Dep类来管理依赖(即订阅者),并提供subscribe和notify方法来添加订阅者和通知订阅者。

javascript">class Dep {constructor() {this.subscribers = new Set()}//添加订阅subscribe(watcher) {this.subscribers.add(watcher)}//发布订阅notify(newValue) {//通知所有订阅者更新this.subscribers.forEach((watcher) => {watcher.update(newValue)})}
}
class Watcher {constructor(dep, cb) {this.cb = cbthis.dep = depthis.dep.subscribe(this)}update(newValue) {this.cb(newValue)}
}
// 使用示例
let dep = new Dep()//创建依赖管理器//更新视图的回函数,由watcher调用
function updateView(newValue) {console.log(`视图数据更新: ${newValue}`)
}let watcher = new Watcher(dep, updateView)dep.notify('Hello, Vue!') // 控输出: 视图数据更新: Hello, Vue!

注意,上面的Dep和Watcher示例并没有直接集成到数据劫持中。如果感兴趣:

你可以将Dep实例与通过observe函数处理的对象属性关联起来,并在setter中调用dep.notify来通知所有订阅者。

这需要在observe函数中为每个属性创建一个Dep实例,并在setter中调用它的notify方法。然后,你可以在组件或其他地方创建Watcher实例来订阅这些属性的变化。


1.44 总结与补充

① Vue 2的响应式原理主要基于数据劫持和发布订阅者模式,通过这两个机制实现数据的自动更新和视图的响应式渲染。

②  上述代码是对vue 2响应式系统的一个简化版模拟,在实际实现中,其过程更为复杂,有兴趣者建议阅读 Vue 3 的官方文档或源代码,以了解其完整的实现细节。

③ 基于上述原理的vue 2响应式系统也存在一定的缺点——对于数据新增和删除的检测

由于它是通过,在组件实例初始化时遍历data中的属性,并使用Object.defineProperty将它们转换为getter/setter,从而实现对属性变化的监听的。

添加属性:所以,它只能对初始化时已经存在的属性进行拦截,对于后续动态添加到对象上的新属性,默认是不会对其进行拦截的。因此,如果开发者在组件的实例创建后向data对象或其嵌套对象中添加了新的属性,Vue 2将不会检测到这些新属性的变化,从而导致视图不会自动更新。

虽然官方提供了Vue.set或实例的$set方法来解决这个问题,但这种方法需要开发者手动调用,增加了代码的复杂性和出错的可能性。

删除属性:同样,它也无法自动检测到对象属性的删除。当开发者使用delete操作符或其他方式删除对象的属性时,Vue 2不会触发视图更新。这是因为Object.defineProperty只能拦截属性的读取和设置操作,而无法拦截属性的删除操作。

虽然开发者可以通过将属性的值设置为null或undefined来间接实现 “伪删除” 的效果,并通过条件渲染等方式在视图中进行相应的处理。但这种方法并不是真正的删除属性,且在某些情况下可能并不适用。


2. vue 3

Vue 3的响应式原理相比Vue 2有了显著的提升,主要基于ES6的Proxy对象和Reflect API来实现。这一改进不仅提高了性能,还解决了Vue 2中一些无法处理的问题,如数据新增和删除的检测。


2.1 简单介绍Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

javascript">let proxy = new Proxy(target, handler)

targetd对象

要代理包装的对象,可以是任何类型的对象,包括原生数组,函数,甚至另一个代理

handler对象

一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 P 的行为

  • get(target, propKey, receiver):拦截对象属性的读取操作

  • set(target, propKey, value, receiver):拦截对象属性的赋值操作,返回一个布尔值

  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,并返回一个布尔值

  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值

  • 等等...

参数介绍:target目标对象。propertyKey目标属性。value要设置的值。receiver(略)

参考:handler.defineProperty() - JavaScript |MDN 系列 (mozilla.org)


2.2 简单介绍Reflect

Reflect 对象提供了一套用于拦截和操作 JS对象的方法。这些方法与 Proxy 对象处理程序中的方法相对应,允许你以编程方式拦截和定义对象的基本操作,如属性查找、赋值、枚举、函数调用等。所以它常用于与Proxy配合使用,用于执行对象的默认操作。

注意:Reflect 不是一个函数对象,因此它不可被构造( new Reflect())。

常用的静态方法

  • Reflect.get(target, propertyKey): 获取对象上属性的值
  • Reflect.set(target, propertyKey, value): 在对象上设置属性的值
  • Reflect.deleteProperty(target, propertyKey): 删除对象上的属性
  • Reflect.has(target, propertyKey): 判断一个对象是否存在某个属性,类似于 in 操作符
  • 等等....

参数介绍:target目标对象。propertyKey目标属性。value要设置的值。

更多方法参考:Reflect - JavaScript |MDN 系列 (mozilla.org)


2.3 简单使用Proxy和Reflect

① 通过Proxy(代理): 拦截对象中任意属性的变化,包括:属性值的读写,属性的增加,属性的删除等

② 通过Reffect(反射): 对源对象的属性进行操作

javascript">function popFun(target, prop) {if (prop in target) return '更新了'return '新增了'
}
const counter = {name: 'test_obj',count: 1
}
const counter_proxy = new Proxy(counter, {//拦截读取属性值get(target, prop) {console.log(`访问了${prop}`)return Reflect.get(target, prop)},//拦截设置属性值或添加新属性set(target, prop, value) {let pop = popFun(target, prop)console.log(`${pop}${prop}:${value}`)return Reflect.set(target, prop, value)},//拦截删除属性deleteProperty(target, prop) {console.log(`删除了${prop}`)return Reflect.deleteProperty(target, prop)}
})
console.log(counter) //{name: 'test_obj', count: 1}
counter_proxy.count = 1000 //输出:更新了count:1000
counter_proxy.doubleCount = 2 * counter_proxy.count //输出:
console.log(counter) //{name: 'test_obj', count: 1000, doubl
delete counter_proxy.doubleCount
console.log(counter) //{name: 'test_obj', count: 1000}

不论是通过counter_proxy对象对counter对象进行增删改查中的任何一种操作,都能监测到,同时数据也能得到更新,对比起Vue2来说,确实真正意义上实现了数据的完全式响应。


2.4 数据双向绑定原理
2.41 响应式原理过程

① 响应式对象创建:Vue 3提供了reactive函数来创建响应式对象。当调用reactive函数并传入一个普通JS对象时,它会使用Proxy来拦截该对象的所有属性访问和修改操作。这样,每当对象的属性被读取或修改时,Vue 3都能感知到这些变化,并据此执行相应的依赖收集和更新操作。

②依赖收集与触发更新

依赖收集:在Vue 3中,当响应式对象的属性被访问时,会触发getter函数的执行。getter函数内部会收集当前正在执行的副作用函数(如组件的渲染函数)作为依赖。这些依赖会被存储在一个与响应式对象相关联的依赖集合中。

触发更新:当响应式对象的属性被修改时,会触发setter函数的执行。setter函数内部会通知所有依赖于该属性的副作用函数执行更新操作。这通常会导致组件的重新渲染,从而确保视图与数据的同步。


2.42 简化代码模拟
javascript">class Reactive {constructor(target) {this.target = targetthis.handler = {get(target, key) {// 依赖收集等逻辑....略const result = Reflect.get(target, key, receiver)if (typeof result === 'object' && result !== null) {return reactive(result) // 对于对象类型的结果,递归地使其也变成响}return result // 返回结果},set(target, key, value) {//发布更新等逻辑....略return Reflect.set(target, key, value)}}return new Proxy(target, this.handler) // 使用 Proxy 包裹目标对象}
}
function reactive(target) {return new Reactive(target)
}
// 使用示例
const state = reactive({count: 0
})
console.log(state.count) // 0
state.count = 1
console.log(state.count) // 1

上述代码是对vue 3响应式系统的一个简化版模拟,缺少依赖收集和派发更新等重要部分,而在实际实现中,这些是通过更复杂的数据结构和算法来完成的。

如果你对 Vue 3 的响应式系统有更深入的兴趣,建议阅读 Vue 3 的官方文档或源代码,以了解其完整的实现细节。


2.43 总结与补充

① Vue 3的响应式原理主要基于Proxy对象和Reflect API来实现。通过拦截对象的读取和设置操作,Vue 3能够感知到数据的变化,并据此执行相应的依赖收集和更新操作

② Vue 3响应式系统进行了许多优化,以提高性能,减少内存占用。如,Vue 3使用了WeakMap来存储依赖关系,以避免内存泄漏;同时,Vue 3还引入了“惰性观察” 和 “标记-清除”算法来优化依赖收集和清理过程。

③ 除了reactive函数外,Vue 3还提供了其他几个与响应式相关的API,如ref、computed、watch等。这些API提供了更丰富的响应式数据管理方式,使得开发者可以根据实际需求选择最适合的响应式解决方案。


 若有错误或描述不当的地方,烦请评论或私信指正,万分感谢 😃


http://www.ppmy.cn/server/121074.html

相关文章

openssl 生成多域名 多IP 的数字证书

openssl.cnf 文件内容: [req] default_bits 2048 distinguished_name req_distinguished_name copy_extensions copy req_extensions req_ext x509_extensions v3_req prompt no [req_distinguished_name] countryName CN stateOrProvinceName GuangDong l…

2.《DevOps》系列K8S部署CICD流水线之部署NFS网络存储与K8S创建StorageClass

架构 服务器IP服务名称硬件配置192.168.1.100k8s-master8核、16G、120G192.168.1.101k8s-node18核、16G、120G192.168.1.102k8s-node28核、16G、120G192.168.1.103nfs2核、4G、500G操作系统:Rocky9.3 后续通过K8S部署GitLab、Harbor、Jenkins 一、环境准备 #关闭防火墙开机自…

迈入IT世界:技术趋势、职业选择与未来展望

迈入IT世界:技术趋势、职业选择与未来展望 1. 引言 随着科技的飞速发展,信息技术(IT)已经成为当今社会的中坚力量。无论是智能设备、互联网服务,还是数据分析与人工智能,IT技术驱动着各行各业的创新与进步…

Element Plus图片上传组件二次扩展

Element Plus 的图片上传组件主要通过 <el-upload> 实现&#xff0c;该组件支持多种配置和功能&#xff0c;如文件类型限制、文件大小限制、自动上传、手动上传、预览、删除等。以下是对 Element Plus 图片上传组件的详细介绍和使用示例&#xff1a; 功能概述 文件类型限…

python爬虫初体验(一)

文章目录 1. 什么是爬虫&#xff1f;2. 为什么选择 Python&#xff1f;3. 爬虫小案例3.1 安装python3.2 安装依赖3.3 requests请求设置3.4 完整代码 4. 总结 1. 什么是爬虫&#xff1f; 爬虫&#xff08;Web Scraping&#xff09;是一种从网站自动提取数据的技术。简单来说&am…

【已解决】如何使用JAVA 语言实现二分查找-二分搜索折半查找【算法】手把手学会二分查找【数据结构与算法】

文章目录 前言任务描述编程要求 输出样例:未查找到11元素&#xff01; 二、代码实现总结理解不了考试的时候直接背下来就好了。 前言 [TOC]二分搜索 任务描述 折半查找&#xff08;二分搜索&#xff09; 设a[low..high]是当前的查找区间&#xff0c;首先确定该区间的中点位置…

【洛谷】AT_abc371_e [ABC371E] I Hate Sigma Problems 的题解

【洛谷】AT_abc371_e [ABC371E] I Hate Sigma Problems 的题解 洛谷传送门 AT传送门 题解 I Hate Sigma Problems!!! 意思很简单就是求序列中每一个子区间内含有不同数字的个数之和。 暴力的话时间复杂度是 O ( n 2 ) O(n ^ 2) O(n2)&#xff0c;是肯定不行的&#xff0…

基于SpringBoot+Vue+MySQL的教学资料管理系统

系统展示 管理员后台界面 教师后台界面 系统背景 在当今信息化高速发展的时代&#xff0c;教育机构面临着日益增长的教学资料管理需求。为了提升教学管理的效率&#xff0c;优化资源的配置与利用&#xff0c;开发一套高效、便捷的教学资料管理系统显得尤为重要。基于SpringBoot…