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提供了更丰富的响应式数据管理方式,使得开发者可以根据实际需求选择最适合的响应式解决方案。


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值