Vue2 和 Vue3 响应式的区别
Vue2
的响应式,利用了 ES5 的一个 API :Object.defineProperty
。它的基本用法是这样的:
const obj = {name: 'kw'}
Object.defineProperty(obj, key, {
get() {
return obj[key]
},
set(val) {
obj[key] = val
}
})
这样,就能拦截到对象属性的基本操作,比如访问属性和给属性设置新值。当拦截到访问属性时,可以做依赖收集;当监听到属性更改时,可以做派发更新,从而实现响应式。
它存在几个缺点:
1、重写了对象的属性,性能较差;
2、只能拦截到对象属性的操作,不能处理数组。所以 Vue2
需要单独对数组数据进行处理。
3、对于属性的新增和删除,无法拦截到。所以额外提供了 $set
和 $delete
方法,整体不和谐。
Vue3
采用了 ES6 的API Proxy
来实现响应式。由于该 API 不兼容 IE 浏览器,所以在使用 Vue3
开发时要考虑项目是否需要兼容 IE系列。
Proxy 和 Reflect
先来看下 Proxy
的基本语法:
const proxy = new Proxy(target, handler);
target
:用 Proxy
包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler
:是一个对象,其声明了代理 target
的一些操作,其属性是当执行一个操作时定义代理的行为的函数。
简单示例:
const target = {
name: "kw",
age: 18
};
const handler = {
get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
target[key] = value
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // "kw"
proxy.name = 'zk'
console.log(proxy.name); // "zk"
可以看到,Proxy
的使用其实和 Object.defineProperty
是差不多的,也是能拦截到对象属性的一些操作。但它的特点是:
1、不仅可以代理普通的对象,还可以代理数组,函数
2、不仅能拦截到 get
和 set
操作,还支持 apply
、delete
等一共13种操作
3、不需要重写 target
,性能更高
再来看一下 Reflect
对象。
Proxy
和 Reflect
是一对好兄弟,形影不离。
按照 MDN 文档的说明:
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handlers 的方法相同。
Reflect
不是一个函数对象,因此它是不可构造的。
也就是说,我们在使用 Proxy
时传入的 handler
参数,它所有的属性,在 Reflect
中都有一一对应的。比如上面我们说了 Proxy
对象的 handler
可以支持 get
,get
,apply
,delete
操作,那么 Reflect 对象就提供了对应的静态方法:
const person = {
name: 'kw',
age: 18,
sayHello: function() {
console.log(`Hello! 我是${this.name}`);
}
}
// 返回 name 属性的值
Reflect.get(person, 'name'); // 'kw'
// 执行 sayHello 方法
// param1:要执行的函数
// param2:指定 this
// param3:函数执行需要的参数
Reflect.apply(person.sayHello, person, []); // Hello! 我是kw
// 更新 age 属性。属性设置成功,返回 true
Reflect.set(person, 'age', 20); // true
Reflect.get(person, 'age'); // 20
初看起来,Reflect
的使用很繁琐,远不如传统的点语法来的方便简洁。确实如此,但是在这些 API 的设计上,它和 Proxy
拥有一致的属性和方法,所以搭配起来更加合适。再者,有些场景下,比如需要用到 receiver
参数时,此时就只有 Reflect
能堪大任了。
reactive 的基本使用
官网文档,点击访问
import { reactive } from 'vue'
const obj = {name: 'kw', age: 18, skill: ['JS', 'Vue']}
// 返回对象的响应式代理对象,并且是深层次的代理,所有嵌套的属性包括数组,都能被代理到
const state = reactive(obj)
// 修改响应式对象,组件可以自动更新
state.name = 'zk'
state.age = 20
state.skill.push('Node')
使用 reactive
时需要注意的地方:
- 只能实现对象数据的响应式
- 同一个对象,只会被代理一次
- 被代理过的对象,不会被再次代理
- 支持嵌套属性的响应式
实现 reactive
这是 Vue3
中一个最基础的响应式 API,它内部采用了 Proxy
来实现对象属性的拦截操作。
如下:
// reactivity/src/reactive.ts
import { isObject } from '@my-vue/shared'
export function reactive (target) {
// 只能代理对象
if(!isObject(target)) {
return target
}
const handler = {
// 监听属性访问操作
get(target, key, receiver) {
console.log(`${key}属性被访问,依赖收集`)
return Reflect.get(target, key)
},
// 监听设置属性操作
set(target, key, value, receiver) {
console.log(`${key}属性变化了,派发更新`)
// 当属性的新值和旧值不同时,再进行设置
if(target[key] !== value) {
const result = Reflect.set(target, key, value, receiver);;
return result
}
}
}
// 实例化代理对象
const proxy = new Proxy(target, handler)
return proxy
}
无需多次代理
前面我们提到,如果一个对象被代理过了,就无需再被代理。实现的思路就是利用缓存,将代理过的对象进行缓存,每当调用 reactive
方法时,先判断缓存中是否存在 target
;每次 target
被代理后,都将 target
和 proxy
放到缓存中:
// 缓存响应式对象
const reactiveMap = new WeakMap
export function reactive (target) {
// ...
// 判断 target 是否被代理过。如果被代理过,则直接返回代理对象
const existing = reactiveMap.get(target)
if(existing) {
return existing
}
// ...
// 将代理对象进行缓存
reactiveMap.set(target, proxy)
return proxy
}
仅能够被代理一次
除此之外,对于一个已经被代理的对象 proxy
,再次调用响应式 API 时,应该直接返回该 proxy
对象,而不会对 proxy
再做一次代理。
它的实现思路也很简单,是通过标识符的方式进行判断。
Vue2
中通过给 Observer
实例 增加一个__ob__
属性作为标识,表示它已经被观测过了,无需再被观测。Vue3
是通过给 proxy
对象增加一个__v_isReactive
属性,表示该 proxy
对象已经是响应式数据了,从而无需再被代理:
const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
}
export function reactive (target) {
// 判断 target 是否是响应式对象
// 每当一个 target 需要响应式时,先判断其有没有该属性。此时产生属性访问操作,如果 target 被代理过,则会进入下面的 get 方法中,做进一步的判断。
if(target[ReactiveFlags.IS_REACTIVE]) {
return target
}
// ...
const handler = {
// 监听属性访问操作
get(target, key, receiver) {
// 访问到 __v_isReactive 属性时,说明此时的 target 其实是一个 proxy 对象,无需再被代理
if(key === ReactiveFlags.IS_REACTIVE) {
return true
}
console.log(`${key}属性被访问,依赖收集`)
return Reflect.get(target, key)
},
}
// ...
}
嵌套代理
Vue3
实现响应式采用的原则是懒代理,并不像 Vue2
那样在初始化时,就递归所有的属性进行属性重写。
只有在访问到某个属性,且该属性是对象类型时,才会再进行一层响应式包装:
到此,我们实现的 reactive
方法,就能监听到对象属性的访问和设置操作,从而在此时机做一些处理,从而实现响应式系统。同时也做了一些优化处理。
将 reactive
方法通过响应式模块的入口文件对外暴露出去:
// reactivity/src/index.ts
export { reactive } from './reactive'
测试
执行打包命令:
pnpm dev
编写测试文件:
// test/1.reactive.html
<script src="../dist/reactivity.global.js"></script>
<script>
// VueReactivity:在响应式模块的 package.json 中通过 buildOptions.name 起的名字
const { reactive } = VueReactivity
const obj = { name: 'kw', age: 18, grade: { math: 88 } }
const state = reactive(obj)
// 访问响应式对象的属性
console.log(state.name)
console.log(state.grade.math)
// 修改响应式对象的属性
state.age = 20
</script>
打开浏览器,查看控制台:
和我们预想的一样,reactive
方法能监听到对象属性的取值和设置值的操作。
小结
这只是实现响应式系统的第一步。我们还需要在访问属性时,做依赖收集,也就是记录下此刻谁用到了这个属性;当以后属性发生变化时,就可以告诉它,你用到的属性更新了,你也可以更新一下啦。这样,就能实现一个完整的响应式系统了。下一篇文章,我们就来实现这个功能。
仓库地址,点击访问。