Vue 响应式原理

目录

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

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

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

先定义一个对象

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

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

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设置为不可删除,且描述符无法被改变

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设置其描述符无法被改变

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

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

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遍历不到

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

演示get 、set。

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监听对象已有的属性,而是用它通过给对象创建属性的方式来实现监听。 

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

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效果相同,但它可以同时定义多个属性。

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,以便我们能够拦截属性的访问和修改。

function observe(obj, callback) {
    // 遍历属性,并将其转换为getter/setter
    Object.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方法来添加订阅者和通知订阅者。

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 = cb
        this.dep = dep
        this.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 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

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(反射): 对源对象的属性进行操作

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 简化代码模拟
class Reactive {
    constructor(target) {
        this.target = target
        this.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.mfbz.cn/a/881789.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

java基础知识20 Intern方法的作用

一 Intern方法作用 1.1 Intern方法 1.在jdk1.6中: intern()方法:在jdk1.6中,根据字符串对象,检查常量池中是否存在相同字符串对象 如果字符串常量池里面已经包含了等于字符串X的字符串,那么就返回常量池中这个字符…

windows安装docker 本地打包代码

参考文章1:https://gitcode.csdn.net/65ea814b1a836825ed792f4a.html 参考文章2: Windows 安装docker(详细图解)-CSDN博客 一 下载 Docker Desktop 在官网上下载 Docker Desktop,可以从以下链接下载最新版本&#x…

【大模型实战篇】关于Bert的一些实操回顾以及clip-as-service的介绍

最近在整理之前的一些实践工作,一方面是为了笔记记录,另一方面也是自己做一些温故知新,或许对于理解一些现在大模型工作也有助益。 1. 基于bert模型实现中文语句的embedding编码 首先是基于bert模型实现中文语句的embedding编码,…

鸿蒙【项目打包】- .hap 和 .app;(测试如何安装发的hap包)(应用上架流程)

#打包成.hap需要用到真机 原因是:只有用上了真机才能在项目中配置 自动签名 #步骤: ##第一步:选择真机->选择项目结构->点Sigining Configs(签名配置) ##第二步:勾选Automatically generate signature(自动签名)->点击ok ##第三步:点击构建->点击 …

接口幂等性和并发安全的区别?

目录标题 幂等性并发安全总结 接口幂等性和并发安全是两个不同的概念,虽然它们在设计API时都很重要,但侧重点不同。 幂等性 定义:幂等性指的是无论对接口进行多少次相同的操作,结果都是一致的。例如,HTTP的PUT和DELE…

QT快速安装使用指南

在Ubuntu 16.04上安装Qt可以通过多种方式进行。以下是使用Qt在线安装程序和apt包管理器的两种常见方法: 方法一:使用Qt在线安装程序 下载Qt在线安装程序 访问Qt官方网站:Try Qt | Develop Applications and Embedded Systems | Qt找到并下载…

Hadoop的安装

文章目录 一. 到Hadoop官网下载安装文件hadoop-3.4.0.tar.gz。二. 环境变量三. 配置 一. 到Hadoop官网下载安装文件hadoop-3.4.0.tar.gz。 随后点击下载即可 由于Hadoop不直接支持Windows系统,因此,需要修改一些配置才能运行 二. 环境变量 三. 配置 进…

arcgisPro地理配准

1、添加图像 2、在【影像】选项卡中,点击【地理配准】 3、 点击添加控制点 4、选择影像左上角格点,然后右击填入目标点的投影坐标 5、依次输入四个格角点的坐标 6、点击【变换】按钮,选择【一阶多项式(仿射)】变换 7…

VisualPromptGFSS

COCO-20 i ^i i太大,不建议复现

大学生必看!60万人在用的GPT4o大学数学智能体有多牛

❤️作者主页:小虚竹 ❤️作者简介:大家好,我是小虚竹。2022年度博客之星🏆,Java领域优质创作者🏆,CSDN博客专家🏆,华为云享专家🏆,掘金年度人气作者&#x1…

14年数据结构

第一题 解析: 求时间复杂度就是看程序执行了多少次。 假设最外层执行了k次,我们看终止条件是kn,则: 有, 内层是一个j1到jn的循环,显然执行了n次。 总的时间复杂度是内层外层 答案选C。 第二题 解析: 一步一…

基于协同过滤+python+django+vue的音乐推荐系统

作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于协同过滤pythondjangovue…

【前端】ES6:Class语法和Class继承

文章目录 1 Class语法1.1 类的写法1.2 getter与setter1.3 静态属性和静态方法 2 Class继承 1 Class语法 1.1 类的写法 class Person {constructor(name,age){this.name name;this.age age;}say(){console.log(this.name,this.age)} } let obj new Person("kerwin&quo…

双击热备 Electron网页客户端

安装流程: 1.下载node.js安装包进行安装 2.点击Next; 3.勾选,点击Next; 4.选择安装目录 5.选择Online 模式 6.下一步执行安装 。 7.运行cmd,执行命令 path 和 node --version,查看配置路径和版本 8.Goland安装插件node.js 9.配置运行…

【C#】内存的使用和释放

在 C# 中,内存管理主要是由 .NET 的垃圾回收器(Garbage Collector, GC)自动处理的。然而,了解如何正确地使用和释放内存对于编写高效且可靠的代码非常重要。以下是一些关键点和最佳实践: 1. 内存分配 托管资源&#x…

【我的 PWN 学习手札】House Of Karui —— tcache key 绕过手法

目录 前言 一、House of Karui 二、测试与模板 前言 早期版本的 tcachebin 由于毫无保护,导致攻击利用非常容易,成为重灾区。tcache dup,也即 tcachebin 中的 double free 利用手法,是攻击者常常选用的攻击方式。然而&#xf…

计算机网络(八) —— Udp协议

目录 一,再谈端口号 1.1 端口号 1.2 netsta命令 二,UDP协议 2.1 关于UDP 2.2 Udp协议格式 2.3 Udp协议特点 2.4 Udp的缓冲区 一,再谈端口号 http协议本质是“请求 - 响应”形式的协议,但是应用层需要先将数据交给传输层&…

机器人时代的“触觉革命”:一块小传感器如何颠覆你的认知?

你是否曾经想过,机器人也能像人类一样有“触觉”?不再是简单的机械操作,而是具备真正的感知能力,能够学会精细的任务。今天我想和你聊聊一种让机器人“长出触觉”的技术:一种小巧的触觉传感器,它的名字叫“AnySkin”。别看它小,它的潜力可一点都不小,或许能彻底改变我们…

[PICO VR眼镜]眼动追踪串流Unity开发与使用方法,眼动追踪打包报错问题解决(Eye Tracking/手势跟踪)

前言 最近在做一个工作需要用到PICO4 Enterprise VR头盔里的眼动追踪功能,但是遇到了如下问题: 在Unity里面没法串流调试眼动追踪功能,根本获取不到Device,只能将整个场景build成APK,安装到头盔里,才能在…

【机器学习(七)】分类和回归任务-K-近邻 (KNN)算法-Sentosa_DSML社区版

文章目录 一、算法概念二、算法原理(一)K值选择(二)距离度量1、欧式距离2、曼哈顿距离3、闵可夫斯基距离 (三)决策规则1、分类决策规则2、回归决策规则 三、算法优缺点优点缺点 四、KNN分类任务实现对比&am…