前言
随着vue3的发布和完善,许多开发同学正在尝试去将vue2的项目迁移升级vue3,其中跟vue3针对vue2有了很大改进有关系,比如:
- Vue 3.0 在编译方面优化:静态提升(hoistStatic)、Block Tree、patch flag;
- vue3.0中Composition Api 比 Vue 2.x 使用的 Options Api(高内聚,低耦合);
- vue3.0更模块化的代码结构;
- 基于Proxy 实现的响应式系统;
- ......
那么我们就基于vue3中的响应式原理系列的学习下其中的实现。
Object.defineProperty到底有什么问题?
1、Object.defineProperty在vue2.x中存在的问题
-
object.defineProperty只监听属性,如果要监听对象的所有属性需要通过object.keys等遍历实现
-
object.defineProperty索引访问或者修改数组中已经存在的元素,是可以出发get和set的,但是对于通过push、unshift增加的元素,会增加一个索引,这种情况需要手动初始化,新增加的元素才能被监听到
-
监听对象的某个属性又基本数据类型变为对象时,需要递归对象的属性对象;
Proxy
1、Proxy基本使用
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
语法
const p = new Proxy(target, handler)
参数
target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理的行为。
复制代码
返回值
proxy代理对象
简单例子:
//定义一个需要代理的对象
let applicant = {
age: 22,
name: '张三'
}
//定义handler对象
let hander = {
get(obj, key) {
// 对象里有这个属性,就返回属性值,如果没有,就返回默认值66
return key in obj ? obj[key] : '无身份证'
},
set(obj, key, val) {
obj[key] = val
return true
}
}
//把handler对象传入Proxy
let proxyObj = new Proxy(applicant, hander)
// 测试get能否拦截成功
console.log(proxyObj.age) //输出22
console.log(proxyObj.name) //输出张三
console.log(proxyObj.cerNo) //输出默认值无身份证
// 测试set能否拦截成功
proxyObj.age = 25
console.log(proxyObj.age) //输出25 修改成功
复制代码
可以看出,Proxy代理的是整个对象,而不是对象的某个特定属性,不需要我们通过遍历来逐个进行数据绑定。
值得注意的是:之前我们在使用Object.defineProperty()给对象添加一个属性之后,我们对对象属性的读写操作仍然在对象本身。
但是一旦使用Proxy,如果想要读写操作生效,我们就要对Proxy的实例对象proxyObj
进行操作。
另外,MDN上明确指出set()方法应该返回一个布尔值,否则会报错
TypeError
。
vue3中的reactive
1、reactive基础实现
基于上面proxy的基本语法及实例,我们来一步一步实现一个rective
/**
* @description 判断是否是一个对象
* @param {val} 判断的目标val
* @return {Boolean} true | false
*/
function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false;
}
/**
* @description 实现一个reactive方法
* @param {target} 响应式目标对象
* @return {Proxy} 创建的代理对象实例
*/
function reactive(target) {
//判断是否是一个对象,不是的话直接返回
if (!isObject(target)) {
return {}
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
//去代理上取值,执行get
console.log(`获取属性:${key}, 值为:${target[key]}`, )
return target[key]
},
set(target, key, value, receiver) {
//去代理上设置值,执行set
console.log(`设置属性:${key}, 值为:${value}`, )
return target[key] = value
}
})
return proxy
}
// reactive验证demo
const applicant = {
name: 'didi',
age: 22,
}
const state = reactive(applicant)
console.log(state.name) //打印属性 didi
state.name = 'newDidi' //设置属性:name, 值为:newDidi
复制代码
上面我们实现了一个最基础的 reactive 响应式函数,目前已经能够满足基础的属性拦截和设置,但是是否存在什么问题呢?我们接着继续探索.....
2、reactive完善-- Reflect
我们把上面的使用demo稍微进一步的验证:
// 验证case2 -- this 指向问题
const applicant = {
_name: 'didi',
age: 22,
get name() {
return this._name;
}
}
const applicantProxy = reactive(applicant)
let admin = {
__proto__: applicantProxy,
_name: "Admin"
};
// Expected: Admin
console.log(admin.name); // 输出:didi
复制代码
有没有发出疑问为什么是这样?为什么没有输出我们期望的内容呢?
Proxy 的一个难点就是this
绑定。我们希望任何方法都绑定到这个 Proxy,而不是target
对象,为什么要尽量把this放在代理对象receiver
上,而不建议放原对象target
上呢?因为原对象target
有可能本来也是是另一个代理的代理对象,所以如果this一直放target
上的话,出bug的概率会大大提高。针对什么的问题怎么解决呢?
答案就是: Reflect
**Reflect**
的两个方法
get(target, key, receiver)
:个人理解就是,访问target
的key
属性,但是this
是指向receiver
,所以实际是访问的值是receiver的key
的值,但是这可不是直接访问receiver[key]
属性,Proxy的get
对应Reflect.get
set(target, key, value, receiver)
:个人理解就是,设置target
的key
属性为value
,但是this
是指向receiver
,所以实际是是设置receiver的key
的值为value
,但这可不是直接receiver[key] = value,``Proxy的get
对应Reflect.set
3、reactive改造--对象重复代理问题
我们验证下如下的情景:
// 验证case3 -- 重复代理同一个对象
const applicant = {
name: 'didi',
age: 22,
}
const applicantProxy1 = reactive(applicant)
const applicantProxy2 = reactive(applicant)
// 代理对象 applicantProxy1 和 代理对象 applicantProxy2 相等吗?
console.log(applicantProxy1 === applicantProxy2) // 输出 false
复制代码
在真实的使用场景中,这两个代理对象应该指向同一个引用,在创建第一个后如果意外重新调用了 reactive 进行二次代理我们应该是返回上一个引用的,针对这个应该如何解决呢?
如果你对es6使用比较多的话应该很容易想到了一个中处理方式: weakMap
WeakMap
对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。如果想了解更多,请移步:developer.mozilla.org/zh-CN/docs/… 学习
现在在我们自己实现的 reactive 代码中使用 weakMap 来解决上面的问题吧
/**
* @description 判断是否是一个对象
* @param {val} 判断的目标val
* @return {Boolean} true | false
*/
function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false;
}
// 维护代理过的目标对象 map 映射
const reactiveWeakMap = new WeakMap()
/**
* @description 实现一个reactive方法
* @param {target} 响应式目标对象
* @return {Proxy} 创建的代理对象实例
*/
function reactive(target) {
//
if (!isObject(target)) {
return
}
// 创建对象前验证当前对象是否已经被代理过
const exisitProxy = reactiveWeakMap.get(target)
if (exisitProxy) {
return exisitProxy
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
//去代理上取值,执行get
console.log(`获取属性:${key}, 值为:${target[key]}`, )
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
//去代理上设置值,执行set
console.log(`设置属性:${key}, 值为:${value}`, )
//return target[key] = value
return Reflect.set(target, key, value, receiver)
}
})
// 首次代理的对象放入map中
reactiveWeakMap.set(target, proxy) return proxy
}
复制代码
重新验证我们的 case3, 符合预期,成功通过。
3、reactive改造--代理对象的二次代理
如果把一个已经代理的对象,再次调用 reactive 方法又会摩擦出怎么的火花呢?
// 验证case4-- 代理对象的二次代理
const applicant = {
name: 'didi',
age: 22,
}
const applicantProxy = reactive(applicant)
const againProxy = reactive(applicantProxy)
// 第一次代理对象 applicantProxy
// applicantProxy对象再次调用 reactive 的 againProxy 是怎么的关系呢
console.log(applicantProxy === againProxy) // 输出 false
复制代码
经过验证 返回了 false, 针对这种情况又应该如何处理呢?
如何标记一个对象已经被操作过呢?其实也很简单,我们只需要在被操作的对象上增加 flag 标识是不是就完美解决了我们的问题。
继续改造我们的 reactive 代码
/**
* @description 判断是否是一个对象
* @param {val} 判断的目标val
* @return {Boolean} true | false
*/
function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false;
}
// 维护代理过的目标对象 map 映射
const reactiveWeakMap = new WeakMap()
// 创建一个常量对象 维护各种 flag 标识
const REACTIVE_FLAGS = {
IS_REAXTIVE: 'Has_Reactive'
}
/**
* @description 实现一个reactive方法
* @param {target} 响应式目标对象
* @return {Proxy} 创建的代理对象实例
*/
function reactive(target) {
// 判断是否是对象
if (!isObject(target)) {
return
}
// 判断对象是否有代理标识
if (target[REACTIVE_FLAGS.IS_REAXTIVE]) {
return target
}
// 创建对象前验证当前对象是否已经被代理过
const exisitProxy = reactiveWeakMap.get(target)
if (exisitProxy) {
return exisitProxy
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 第一次普通对象代理,通过 new Proxy代理返回代理对象,
// 第一次传入的是代理对象,调用get方式获取 Flag 标识时命中改逻辑
// 返回已经代理过的对象
if (key === REACTIVE_FLAGS.IS_REAXTIVE) {
return true;
}
//去代理上取值,执行get
console.log(`获取属性:${key}, 值为:${target[key]}`, )
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
//去代理上设置值,执行set
console.log(`设置属性:${key}, 值为:${value}`, )
//return target[key] = value
return Reflect.set(target, key, value, receiver)
}
})
// 首次代理的对象放入map中
reactiveWeakMap.set(target, proxy)
return proxy
}
复制代码
再次验证我们的 case 4, 发现完美验证通过。
小结
至此,一个简单的vue3中 reactive 精简版的源码实现完毕,当然正式版本中的逻辑比这个要复杂,我们只是实现了当中的核心源码来学习其中的思想与技巧。
至于 vue3的其他源码,我们会一步一步接着去学习。