我正在参加「掘金·启航计划」
1. Proxy和Reflect的用法
- 要实现vue3的响应式之前,先要知道Proxy和Reflect的基础用法
1.1 Proxy的定义
- Proxy对象对于创建一个对象的代理,也可以理解成为在对象前面设了一层拦截,可以实现基本操作的拦截和一些自定义操作(比如一些赋值,属性查找,函数调用等)
- 用法: let proxy = new Proxy(target,handler)
- target:目标对象(即将进行改造的对象)
- handler:一些自定义操作(比如vue中的getter和setter)
1.2 Reflect
- Reflect是es6为操作对象而提供的新API,设计它的目的有:
- 把Object对象上一些明显属于语言内部的方法放到Reflect对象身上,比如
Object.defineProperty
; - 修改了某些object方法返回的结果;
- 让Object操作都变成函数行为;
- Reflect对象上的方法和Proxy对象上的方法一一对应,这样就可以让Proxy对象方便地调用对应的Reflect方法;
- 把Object对象上一些明显属于语言内部的方法放到Reflect对象身上,比如
Reflect.get(target, propertyKey, receiver)
:等价于target[propertyKey], Reflect.get方法查找并返回target对象的propertyKey属性,如果没有该属性,则返回undefined。Reflect.set(target, propertyKey, value, receiver)
:等价于target[propertyKey] = value,Reflect.set方法设置target对象的propertyKey属性等于value
1.3 Proxy和Reflect的使用
const obj = {
name: 'win'
}
const handler = {
get: function(target, key){
console.log('get--', key)
return Reflect.get(...arguments)
},
set: function(target, key, value){
console.log('set--', key, '=', value)
return Reflect.set(...arguments)
}
}
const data = new Proxy(obj, handler)
data.name = 'ten'
console.log(data.name,'data.name22')
2.为什么要用Proxy来重构
在 Proxy
之前,JavaScript
中就提供过 Object.defineProperty
,允许对对象的 getter/setter
进行拦截
Vue3.0之前的双向绑定是由 defineProperty
实现, 在3.0重构为 Proxy
,那么两者的区别究竟在哪里呢?
首先我们再来回顾一下它的定义
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
上面给两个词划了重点,对象上,属性,我们可以理解为是针对对象上的某一个属性做处理的
语法
- obj 要定义属性的对象
- prop 要定义或修改的属性的名称或 Symbol
- descriptor 要定义或修改的属性描述符
Object.defineProperty(obj, prop, descriptor)
举个例子
const obj = {}
Object.defineProperty(obj, "name", {
value : '张三',
writable : false, // 是否可写
configurable : false, // 是否可配置
enumerable : false // 是否可枚举
})
// 上面给了三个false, 下面的相关操作就很容易理解了
obj.name = '李四' // 无效
delete obj.name // 无效
for(key in obj){
console.log(key) // 无效
}
2.1 Vue中的defineProperty
Vue3之前的双向绑定都是通过 defineProperty 的 getter,setter 来实现的,我们先来体验一下 getter,setter
const obj = {};
Object.defineProperty(obj, 'name', {
set(val) {
console.log(`开始设置新值: ${val}`)
},
get() {
console.log(`开始读取属性`)
return '张三';
},
writable : true
})
obj.name = '李四' // 开始设置新值: 2
obj.name // 开始获取属性
看到这里,我相信有些同学已经想到了实现双向绑定背后的流程了,其实很简单嘛,只要我们观察到对象属性的变更,再去通知更新视图就好了
2.2 对象新增属性为什么不更新
data () {
return {
obj: {
name: '张三'
}
}
}
methods: {
update () {
this.obj.age = 18
}
}
这个其实很好理解,我们在初始化data
的时候,是在created
之前,会对data
绑定的一个观察者Observer
,之后 data
中的字段更新都会通知依赖收集器Dep触发视图更新
然后我们回到 defineProperty
本身,是对对象上的属性做操作,而非对象本身
简单来说:就是我们Observer data
时,新增的属性根本不存在,也就不会有getter
和setter
了,所以就解释了为什么会有$set
方法来新增对象属性了
3. reactive的方法简单实现
3.1 reactive()
返回一个对象的响应式代理。
- 我们新建一个reactive()函数
const reactiveMap = new WeakMap(); // 会自动垃圾回收,不会造成内存泄漏, 存储的key只能是对象
export function reactive(target,baseHandlers){
// 如果目标不是对象 没法拦截了,reactive这个api只能拦截对象类型
if( !isObject(target)){
return target;
}
// 如果某个对象已经被代理过了 就不要再次代理了 可能一个对象 被代理是深度 又被仅读代理了
const proxyMap = reactiveMap
const existProxy = proxyMap.get(target);
if(existProxy){
return existProxy; // 如果已经被代理了 直接返回即可
}
const proxy = new Proxy(target,mutableHandlers);
proxyMap.set(target,proxy); // 将要代理的对象 和对应代理结果缓存起来
return proxy;
}
- 我们将get和set函数的方法抽离到一个对象中,mutableHandlers
export const mutableHandlers = {
get(target, key, receiver) { // let proxy = reactive({obj:{}})
// proxy + reflect
// Reflect 方法具备返回值
const res = Reflect.get(target, key, receiver); // target[key];
// 收集依赖,等会数据变化后更新对应的视图
console.log('执行effect时会取值','收集effect')
track()
if(isObject(res)){ // vue2 是一上来就递归,vue3 是当取值时会进行代理 。 vue3的代理模式是懒代理
return reactive(res)
}
return res;
},
set(target, key, value, receiver) {
const oldValue = target[key]; // 获取老的值
const result = Reflect.set(target, key, value, receiver); // target[key] = value
// 当数据更新时 通知对应属性的effect重新执行
trigger()
return result;
}
}
track()
和trigger()
是依赖收集和触发的方法- 可以看到vue3的代理模式是一个懒代理的方式,是在取值的时候,才进行代理,相比于vue2中data数据初始化就进行了深度递归代理,性能上会好一些
4.拓展思考
- 对reactive我们可以看到是通过proxy来进行带来的,那
shallowReactive
,readonly
,shallowReadonly
等几个API是如何实现的了?我们可以如何在上面的基础上面来改造抽取公共的代码部分?
4.1 shallowReactive
定义:reactive()
的浅层作用形式
和 reactive()
不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了
简单理解就是只会代理对象的第一层级,嵌套的对象就不会再被代理,那我们开始在上面的地方处理我们的逻辑,在get中处理我们的逻辑
get(target, key, receiver) { // let proxy = reactive({obj:{}})
const res = Reflect.get(target, key, receiver); // target[key];
track()
//我们加上这么一个判断,就表示如果是浅层的代理,那我们直接返回第一层代理结果,
// 后面就不在深度递归遍历,那不是不是就实现了这个api了
if(shallow){
return res;
}
if(isObject(res)){
return reactive(res)
}
return res;
},
4.2 readonly
定义:接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理
表示那我们的对象就不是响应式的,那我们只要传入一个参数isReadonly判断是否是只读的就可以实现,那我们还是先改造get
get(target, key, receiver) { // let proxy = reactive({obj:{}})
const res = Reflect.get(target, key, receiver); // target[key];
//表示如果是不是只读的,我们才收集其依赖,如果是只读的那我们就不收集依赖
if(!isReadonly){
track(target,TrackOpTypes.GET,key)
}
//我们加上这么一个判断,就表示如果是浅层的代理,那我们直接返回第一层代理结果,
// 后面就不在深度递归遍历,那不是不是就实现了这个api了
if(shallow){
return res;
}
if(isObject(res)){
return reactive(res)
}
return res;
},
对readonly的set单独提出来
set: (target, key) => {
console.warn(set on key ${key} falied
)
}
set: (target, key) => {
console.warn(`set on key ${key} falied`)
}
4.3 总结
其实说了这么多,发现我们对其API里面的很多逻辑都是通用的,都是都只是进行了传参的处理,那接下来贴出来完整代理,利用了函数柯里化的思想,来进行对传参的处理
reactive.ts
let mutableHandlers = {}
let shallowReactiveHandlers ={}
let readonlyHandlers = {}
let shallowReadonlyHandlers = {}
export function reactive(target){
return createReactiveObject(target,false,mutableHandlers)
}
export function shallowReactive(target){
return createReactiveObject(target,false,shallowReactiveHandlers)
}
export function readonly(target){
return createReactiveObject(target,true,readonlyHandlers)
}
export function shallowReadonly(target){
return createReactiveObject(target,true,shallowReadonlyHandlers)
}
// 1.三个参数 第一个是代理的数据 第二个判断是否只读,第三个判断不同的代理
const reactiveMap = new WeakMap() //会自动回收,不会造成内存泄露,存储的key只能是对象
const readonlyMap = new WeakMap()
export function createReactiveObject(target,isReadonly,baseHandlers){
//如果目标不是对象 没法拦截,reactive这个api只能拦截对象
if(!isObject(target)){
return target
}
//优化点 如果某个对象已经被代理过了,就不要再次代理了 所以需要缓存下来
//思考点 可能一个对象 被代理时深度 又被仅读代理了
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existProxy = proxyMap.get(target)
//如果已经被代理过了 直接返回即可
if(existProxy){
return existProxy
}
const proxy = new Proxy(target,baseHandlers)
proxyMap.set(target,proxy)
return proxy
}
baseHandler.ts
- 新建baseHanlders.ts文件,编写代理方法
export mutableHandlers ={
get:()=>{},
set:()=>{}
}
export shallowReactiveHandlers = {
get:()=>{},
set:()=>{}
}
export readonlyHandlers = {
get:()=>{},
set:()=>{}
}
export shallowReadonlyHandlers = {
get:()=>{},
set:()=>{}
}
- 想到都是get和set方法,想到创建一个公共createGetter函数,用传参的方式的来区分
//是不是仅读的,仅读的属性set时会包异常
//是不是深度的
function createGetter(isReadonly=false,shallow=false){
return function get(target,key,recevier){
// 使用 proxy 和 reflect
const res = Reflect.get(target,key,recevier) // 等同于 target[key]
if(!isReadonly){
//如果不是只读的,那么就会收集依赖,等数据变化后更新视图
console.log("执行effect时会取值","收集effect")
}
if(shallow){
return res
}
if(isObject){ //vue2是上来就递归,vue3是当取值的会进行代理。vue3是懒代理
return isReadonly ? readonly(res):reactive(res)
}
return res
}
}
// 暂不处理set逻辑
function createSetter(shallow = false){
return function set(target,key,value,receiver){
const result = Reflect.set(target, key, value, receiver); // target[key] = value
return result
}
}
- 再来定义不同handlers中的不同的set方法
const get = createGetter()
const shallowGet = createGetter(false,true)
const readonlyGet = createGetter(true,false)
const shallowReadonlyGetter = createGetter(true,true)
const set = createSetter();
const shallowSet = createSetter(true);
export const mutableHandlers = {
get,
set
}
export const shallowReactiveHandlers = {
get: shallowGet,
set: shallowSet
}
//如果是只读的,那么set会报警告
export const readonlyHandlers = {
get:readonlyGet,
set: (target, key) => {
console.warn(`set on key ${key} falied`)
}
}
export const shallowReadonlyHandlers = {
get:showllowReadonlyGet,
set: (target, key) => {
console.warn(`set on key ${key} falied`)
}
}
结语
看到这里我们大概清楚 reactive
是做为整个响应式的入口,负责处理目标对象是否可观察以及是否已被观察的逻辑,最后使用 Proxy
进行目标对象的代理,对 es6
Proxy
概念清楚的同学应该 Proxy
重点的逻辑处理在 Handlers