Vue3中的响应式原理
响应式原理
Vue2中使用的是Object.defineProperty,而Vue3使用的是ES6新增的Proxy
先复习下它俩的相关知识。
Object.defineProperty
Object.defineProperty() 是直接在对象上定义新属性,或修改对象上的现有属性,然后返回该对象。
语法如下:
descript有两种形式:数据描述符和存取描述符。一个描述符只能是这两者中的一个,不能同时是两者,意味着使用了数据描述符就不能同时使用存取描述符。
数据描述符:决定属性的值以及该值是否可写,可选键值有:
value:默认值为 undefined ,决定该属性所对应的值,可以是任何有效值。
writable:默认值为 false ,只有当键值为 true 时,value 才能被赋值运算符改变。
具体使用:
存取描述符:由 getter 和 setter 函数所描述的属性,可选键值有:
get:默认值为 undefined ,值应为一个 getter() 函数,当访问该属性时会自动调用该函数,返回的返回值会被用作属性的值。
set:默认值为 undefined ,值应为一个 setter() 函数,当该属性被修改时,会调用此函数。
具体使用:

由此可见使用 Object.defineProperty() 代理监听不到 对象新增属性 与 数组的新增修改 。
差点忘了说,两种描述符都是对象形式。他们还共享以下属性:
configurable(默认false) 是否可配置,为 true 时,属性描述符才能够被改变,同时属性可以从对应对象上被删除。
enumerable(默认false) 是否可枚举,为 true 时,属性才会出现在对象的枚举属性中。
Proxy
Proxy 主要用于改变对象的默认访问行为,实际上是在访问对象之前增加一层拦截 感觉与servlet里的拦截器差不多 在任何对对象的访问行为都会通过这层拦截,如对对象属性的读、写、删除等。在这层拦截中我们可以增加自定义的行为。
基本语法:
有一句话叫打狗还要看主人,这里狗就是target,其主人就是对应的Proxy实例。打狗这个行为,可以看作是对狗的一个交互行为。你要打狗之前,得先看看狗主人的看法。狗主人能够决定你能不能打狗,决定的过程就是handler。
一个简单实例:
我们通过 Proxy 构造器创建了 target 的代理 yushen ,此时对 yushen 属性的访问都会转发到 target 上,通过自定义的 handler 对象进行相应的拦截操作。
代理可以拦截JavaScript引擎内部的底层对象操作,这些操作被拦截后会触发相应特定操作的陷阱函数,如例子中的get与set就是陷阱函数。
总结一下二者区别:
Proxy是对整个对象的代理,而Object.defineProperty只能代理某个属性。
对象上新增属性,数组新增修改。Proxy可以监听到,Object.defineProperty不能。
若对象内部属性要全部递归代理,Proxy可以只在调用的时候递归,而Object.definePropery需要一次完成所有递归,性能比Proxy差。
Proxy不兼容IE,Object.defineProperty不兼容IE8及以下。
Proxy使用上比Object.defineProperty方便多。
Vue3中的响应式:
让我们来看看这个场景:
可以发现两次输出的总价都是10,因为改变 price 并不会让total再跑一次。
我们想要做的,就是让 price 改变时,让与 price 相关的语句都重新再跑一次。该怎么做呢?
由于是运行同样的语句,我们很容易想到将这段语句提取出来封装成一个函数,并将其保存起来以供之后的调用。
我们要把 effect 保存起来,需要用到 track() 函数,并在保存后运行一次 effect() 。在之后的某个时刻,调用 trigger() 来运行所有以及存储了的代码。
那么如何保存 effect 呢?
我们可以使用 Set 来保存,Set 不允许重复值,如果我们尝试添加相同的 effect,它不会变成两个。
运行下试试:
这样我们就完成了 price 这个属性的依赖关系。但一个对象并不止有一个属性,要实现一个对象的响应式,其每个属性都应该有自己的 dep(依赖关系),或者说 effect 的 Set 集合。那么这又该如何存储呢?
我们可以使用一个 map 集合。每个属性的属性名作为 key ,属性对应的 effect 的 Set 集合作为 map 的 value 。
现在我们对不同的属性有了跟踪依赖关系的方法。但如果我们有多个响应式对象呢?
我们可以使用 WeakMap ,将每一个对象名(target)作为 key,depsMap 作为 value。
WeakMap:key必须是对象,对于对象是弱引用。没用的对象就不会影响垃圾回收机制,优化了性能。
整个数据结构如下:

但我们发现,track() 和 trigger() 现在还需要我们自己手动去调用,这太麻烦了。能不能让其自动调用呢?
让我们想想,一般我们在读取属性值的时候需要其去调用track(),在改变属性值后需要调用 trigger() 。对应的行为即 get 和 set 。是不是很熟悉,没错,这时候就要使用我们的 Proxy 了。
这里模仿了Vue3的 reactive 函数,其返回一个 Proxy 实例。
可以发现,这个对象已经变为响应式对象了。改变 price 或是 quantity 的值时,total 值都会实时更新。
但是,我们还应该把 effect 进行集中管理,不然当一个属性对应的 effect 变多时,代码会变得冗余。并且在添加 effect 时,effect 应该自动运行一次。
改造后的代码如下:
这样我们就大概实现了一个基本的响应式功能。让我们去测试一下。
测试 html 代码如下:
运行结果:



最后
感谢你能看到这里 虽然我感觉我写成这b样也没人看 。如果有什么疑惑或是发现了什么错误,欢迎评论区交流噢。