简单理解Vue2和Vue3响应式原理

Vue是前端比较火的开发框架,Vue修改数据后能够立马在页面上反应出最新的修改结果,这种就叫数据响应式,本文讲解一下Vue响应式原理,理解Vue背后的逻辑。

有一定开发经验的应该知道如果要修改数据时执行其他业务逻辑,通常需要通过拦截器来实现。

Vue2响应式

Vue2响应式原理是通过Object.defineProperty实现,这个方法可以给对象定义属性描述信息,里面有getset的拦截

属性描述

每个JS对象都有一组描述信息,定义JS数据能执行一些什么操作。

const obj1 = {
  name: "gary",
  age: 18
};
const descriptors1 = Object.getOwnPropertyDescriptor(obj1, 'name');
console.log(descriptors1)

输出一些信息,以name为例,如果自定义有getset还有getset信息:

{
  configurable: true, // 是否可以重新定义属性描述符
  enumerable: true, // 是否可以迭代,如果配置成false,遍历时没有该属性
  value: "gary", // 数据值
  writable: true // 是否可以写
}

尝试修改属性描述信息Object.defineProperty

const obj1 = {
  name: "gary",
  age: 18,
};
Object.defineProperty(obj1, "name", {
  configurable: false,
  enumerable: false,
  value: "gary",
  writable: false,
});
for (const key in obj1) { // 遍历的时候没有name了,实际可以读取到name
  console.log(key);
}

有了基础之后,可以尝试解读Vue2响应式原理了

响应式原理

Vue2data函数返回数据中,修改对象定义,插入依赖搜集与触发依赖的代码

const obj = {name:'gary', age: 19}
const objCache = {...obj}
for(const key in obj) {
    Object.defineProperty(obj, key, {
        get(){
            console.log(`拦截get ${key},搜集依赖`)
            return objCache[key]
        },
        set(value) {
            console.log(`拦截set ${key} = ${value},触发依赖`)
            objCache[key] = value
        }
    })
}
obj.name
obj.age
obj.name = 'gary1'
obj.age = 18

缺点

  1. 由于是遍历对象,因此性能相对要差一点
  2. 对象新增的属性由于没有重新定义过属性,因此不能响应

Vue3响应式

Vue2存在一些缺点,因此Vue3诞生了,性能更优秀,但是因为使用Proxy,部分很老的浏览器不支持,目前老浏览器基本都退出市场了。

Proxy介绍

Proxy 对象包装另一个对象并拦截诸如读取和写入属性和其他操作,和前面的defineProperty似乎有点类似,不过Proxy代理整个对象的操作,不用针对每个属性去做包装,但是如果属性是对象,在访问到该属性的时候还是要再次用Proxy代理子对象的访问。

注意原始对象和Proxy对象实际是两个对象,无论修改原始对象还是Proxy对象属性,都会影响原始对象的值,但是如果Proxy中定义有响应代码,这些响应代码将不会执行,这点和Object.defineProperty是不一样的。

const obj = {}
const handler = {} // handler不做任何处理
const proxy = new Proxy(obj, handler)
proxy.name = 'gary'
proxy.age = 19
console.log(obj) // 修改proxy会自动反应到原始对象上,直接穿透过去

ProxyHandler支持的方法以及触发时机如下:

参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#object_internal_methods

内部方法 Handler 方法 触发时机
[[Get]] get 读取属性:obj.name
[[Set]] set 写入属性: obj.name = ''
[[HasProperty]] has in 操作符
[[Delete]] deleteProperty delete 操作符
[[Call]] apply 函数调用(仅函数)
[[Construct]] construct new 操作符(函数)
[[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf
[[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf
[[IsExtensible]] isExtensible Object.isExtensible
[[PreventExtensions]] preventExtensions Object.preventExtensions
[[DefineOwnProperty]] defineProperty Object.defineProperty, Object.defineProperties
[[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries

响应式原理

我们用Proxy来模拟对象访问过程,Vue3中叫Effect(副作用)

const obj = {name:'gary', age: 19}
const handler = {
    get(target, key){
        console.log(`拦截get ${key},搜集依赖`)
        return target[key]
    },
    set(target, key, value) {
        console.log(`拦截set ${key} = ${value},触发副作用`)
        target[key] = value
    }
}
const proxy = new Proxy(obj, handler)
obj.name
obj.name = 'gary1' // 修改生效不过不会触发副作用
proxy.name
proxy.name = 'gary2' // 修改生效触发副作用

至此理解了Vue响应式原理,方便在使用Vue的时候大体了解其运行机制。

关于Reflect

上面的写法在普通情况下似乎没有什么问题,不过如果有继承关系且自定义了getter的时候运行就有点问题了。

const obj = {
  name: "gary",
  age: 19,
  get info() { 
    return `${this.name}: ${this.age}`
  }
};
const handler = {
  get(target, key) {
    console.log(`拦截get ${key},搜集依赖`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`拦截set ${key} = ${value},触发副作用`);
    target[key] = value;
  },
};
const proxy = new Proxy(obj, handler);
const child = {
    __proto__: proxy, // 原型继承
    name: 'child'
}
child.info // 'gary: 19' 输出不正确

如果用普通函数没有问题,因为obj使用了代理,代理了getter方法引起的问题,需要修改:

const obj = {
  name: "gary",
  age: 19,
  get info() {
    return `${this.name}: ${this.age}`
  }
};
const handler = {
  get(target, key, receiver) {
    console.log(`拦截get ${key},搜集依赖`);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) { // target:原始对象, key:对象属性, value:对象值, receiver:实际对象,this指向
    console.log(`拦截set ${key} = ${value},触发副作用`);
    return Reflect.set(target, key, value, receiver)
  },
};
const proxy = new Proxy(obj, handler);
const child = {
    __proto__: proxy,
    name: 'child'
}
child.info // 'child: 19' 输出正确

这就是比较完善的写法,用Reflect来写入和读取原始对象数据。

给TA打赏
共{{data.count}}人
人已打赏
运维

群晖NAS安装配置小雅Emby

2024-11-19 10:38:54

运维

新Vue3常见问题与技巧整理

2024-11-19 10:38:57

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索