Vue3响应式原理彻底掰开揉碎!
前言
笔者在准备面试的时候,被vue3响应式原理困扰许久,参考多篇文章终于弄懂,今日来对此进行一个总结,本篇文章会从proxy reflect map weakmap是什么?怎么用,给大家一步一步推出如何手写出响应式原理
Proxy Reflect
Proxy是什么?
proxy顾名思义,就是代理。代理就是目标对象的抽象,也可以理解为是目标对象的替身,但是又独立于目标对象。跟火影忍者中呐力多(鸣人)的影分身很像,默认情况下,在代理对象上进行的所有操作都会无障碍地传递给目标对象。目标对象既可以被直接操作,也可以通过代理进行操作,只不过直接操作目标对象不能得到代理赋予的拦截行为(后续会介绍)
Proxy主要目的
使用proxy的主要目的就是可以定义捕获器(trap),所谓捕获器就是——对基础操作的拦截器,每个操作都有对应的拦截器。每次在代理对象调用这些基础操作的时候,proxy内部就会触发一个个捕获器,从而拦截以及修改相应的操作。
!注意只有在代理对象上执行这些操作才会触发捕获器,在目标对象上执行则不会触发
基本用法
首先我们需要先new一个proxy对象 ,并且传入对应需要进行侦听的对象,以及一个处理对象,可以称之为handle。
语法:const proxy = new Proxy(target,handle)
参数:
1.target:要创建代理对象的目标对象
2.handle:各对象属性中的函数(比如get set)分别定义了在执行各种操作时代理proxy的行为。
由于篇幅有限,这里只演示部分捕获器,其他的大家感兴趣可以去js高级程序设计了解
get set捕获器
get函数有三个参数:
1.target:目标对象
2.propety:被获取的属性key
3.receiver:调用的代理对象
set函数有四个参数:
1.target:目标对象
2.propety:被获取的属性key
3.value:新属性值
4.receiver:调用的代理对象
const proxy = {
name: "rabbit",
height:1.88
}
const objProxy = new Proxy(obj, {
set: function(target, key, value) {
console.log(`代理对象被进行set操作了!!!!,快点处理!`, target, key, value)
},
get: function(target, key) {
console.log(`代理对象被进行get操作了!!!!,快点处理!`, target, key)
}
})
objProxy.name = "kobe"
objProxy.height = 1.98
console.log(objProxy.name)
console.log(objProxy.height)
其他捕获器
proxy一共是有13个捕获器,例如
- handler.getPrototypeOf()
-
- Object.getPrototypeOf 方法的捕捉器。
- handler.setPrototypeOf()
-
- Object.setPrototypeOf 方法的捕捉器。
- handler.isExtensible()
-
- Object.isExtensible 方法的捕捉器。
- handler.preventExtensions()
-
- Object.preventExtensions 方法的捕捉器。
- handler.getOwnPropertyDescriptor()
-
- Object.getOwnPropertyDescriptor 方法的捕捉器。
- handler.defineProperty()
-
- Object.defineProperty 方法的捕捉器。
- handler.ownKeys()
-
- Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
- handler.has()
-
- in 操作符的捕捉器。
- handler.get()
-
- 属性读取操作的捕捉器。
- handler.set()
-
- 属性设置操作的捕捉器。
- handler.deleteProperty()
-
- delete 操作符的捕捉器。
- handler.apply()
-
- 函数调用操作的捕捉器。
- handler.construct()
-
- new 操作符的捕捉器。
const objProxy = new Proxy(obj, {
has: function(target, key) {
console.log("has捕捉器", key)
// 还原原始操作
return key in target
},
set: function(target, key, value) {
console.log("set捕捉器", key)
// 还原原始操作
target[key] = value
},
get: function(target, key) {
console.log("get捕捉器", key)
// 还原原始操作
return target[key]
},
deleteProperty: function(target, key) {
console.log("delete捕捉器")
// 还原原始操作
delete target[key]
}
})
console.log("name" in objProxy)
objProxy.name = "kobe"
console.log(objProxy.name)
delete objProxy.name
Reflect是什么?
顾名思义就是反射,是一个ES6之后新增API
主要目的
因为早期ECMA规范里面没有考虑到这种对对象本身的操作规范,所以直接一股脑地所有对对象操作的API塞进了Object,又因为Object本身是一个构造函数,把这些API放在上面总归有些不合适,所以ES6之后就增加的Reflect,把对对象的一系列操作全转换到Reflect身上。
与Proxy搭配的作用
为proxy的捕获器重建原始操作
例如get操作重现
const target = {
foo: 'bar'
};
const handler = {
get(trapTarget, property, receiver) {
return trapTarget[property];
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
虽然get操作重现逻辑简单,但并非所有捕获器的行为都像get这样简单,因此想每次都通过手写代码来还原操作的话,是不现实的,而Reflect则是解决了这个问题,Reflect对象上以及封装了和原始操作同名的方法来进行原始操作的重建
const target = {
foo: 'bar'
};
const handler = {
get(trapTarget, property, receiver) {
return Reflect.get(trapTarget, property, receiver);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
常见方法——和Proxy是一一对应的
- Reflect.getPrototypeOf(target)
-
- 类似于 Object.getPrototypeOf()。
- Reflect.setPrototypeOf(target, prototype)
-
- 设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true。
- Reflect.isExtensible(target)
-
- 类似于 Object.isExtensible()
- Reflect.preventExtensions(target)
-
- 类似于 Object.preventExtensions()。返回一个Boolean。
- Reflect.getOwnPropertyDescriptor(target, propertyKey)
-
- 类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined.
- Reflect.defineProperty(target, propertyKey, attributes)
-
- 和 Object.defineProperty() 类似。如果设置成功就会返回 true
- Reflect.ownKeys(target)
-
- 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响).
- Reflect.has(target, propertyKey)
-
- 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
- Reflect.get(target, propertyKey[, receiver])
-
- 获取对象身上某个属性的值,类似于 target[name]。
- Reflect.set(target, propertyKey, value[, receiver])
-
- 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
- Reflect.deleteProperty(target, propertyKey)
-
- 作为函数的delete操作符,相当于执行 delete target[name]。
- Reflect.apply(target, thisArgument, argumentsList)
-
- 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。
- Reflect.construct(target, argumentsList[, newTarget])
-
- 对构造函数进行 new 操作,相当于执行 new target(...args)。
把之前Proxy案例中还原对象的操作都改成用Reflect来操作
const obj = {
name: "rabbit",
height: 1.o8,
set height(newValue) {
}
}
const objProxy = new Proxy(obj, {
has: function(target, key) {
return Reflect.has(target, key)
},
set: function(target, key, value) {
return Reflect.set(target, key, value)
},
get: function(target, key) {
return Reflect.get(target, key)
},
deleteProperty: function(target, key) {
return Reflect.deleteProperty(target, key)
}
})
console.log("name" in objProxy)
objProxy.name = "kobe"
console.log(objProxy.name)
delete objProxy.name
console.log(objProxy)
补充:receiver的作用
如果我们的原对象obj里面有setter getter的访问器属性,那么可以通过receiver来改变里面的this,他们的this原来是指向obj的,而不是objProxy,如果我们在使用Reflect的时候传入了receiver参数,就会把this的指向指回objProxy。——毕竟我们希望所有的操作都是在代理对象中执行
const obj = {
name: "lebi",
age: 18,
_height: 1.88,
set height(newValue) {
this._height = newValue
},
get height() {
return this._height
}
}
接下来补充两个ES6之后新增的数据结构——Map WeakMap
Map
基本介绍:
Map用法——存储映射关系
哎,存储映射关系又不是只有Map可以,对象也可以啊。那么他和对象的区别在哪呢?
最大的区别就是—— 对象只能用字符串(ES6新增了Symbol)作为属性名,不能用对象作为属性名,而Map则可以。
常用方法
set(key, value):在Map中添加key、value,并且返回整个Map对象
get(key):根据key获取Map中的value
has(key):判断是否包括某一个key,返回Boolean类型;
delete(key):根据key删除一个键值对,返回Boolean类型;
clear():清空所有的元素;
forEach(callback, [, thisArg]):通过forEach遍历Map;
WeakMap
基本介绍
和Map相似,有以下两个区别
区别一:WeakMap的key只能使用对象,不接受其他的类型作为key
区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;
常用方法
set(key, value):在Map中添加key、value,并且返回整个Map对象;
get(key):根据key获取Map中的value;
has(key):判断是否包括某一个key,返回Boolean类型;
delete(key):根据key删除一个键值对,返回Boolean类型;
前置知识补充完,现在该进入正题,Vue3响应式原理
响应式
什么是响应式
某个变量,某个对象的属性,发生改变时,与该变量/属性有相关依赖的代码会重新执行一遍
就比如对象obj的name属性发生了改变之后,五六两行代码开始执行
const obj = {
name:'rabbit',
height:1.88
}
obj.name = 'james'
console.log(obj.name,'name变了啊!');
console.log('那我也要执行了');
响应式函数封装
那么我们可以把5,6两行代码放入函数fn中,现在我们把问题转换成了——** name属性发生改变时,函数fn执行
接着我们就可以来封装一个函数watchFn,将那些需要进行响应式处理的函数进行统一收集和保存
const obj = {
name:'rabbit',
height:1.88
}
obj.name = 'james'
function foo() {
console.log('name变了啊!');
console.log('那我也要执行了');
}
const reactiveFns = []
function watchFn(fn) {
reactiveFns.push(fn)
fn()
}
watchFn(foo)
依赖收集类Depend的封装
接着我们发现,要是我们有很多个对象,对象又有很多个属性,我们就需要创建一大堆reactiveFns数组去管理那些响应式函数,想想就可怕!所以我们需要进行优化!所以需要创建一个类,用来管理每一个对象的某一个属性的所有响应式函数
类Depend:
每一个对象属性相关的依赖函数,都可以存在一个depend里面,不污染对象属性的依赖,控制哪一个属性变化就调用哪些相关依赖,封装depend notify函数,使得外部操作简单,直接调用函数即可
class Depend {
constructor() {
this.reactiveFns = []
}
addDepend(reactive) {
this.reactiveFns.push(reactive)
}
notify() {
this.reactiveFns.forEach(fn => fn())
}
}
// 封装一个响应式函数
const depend = new Depend()
function watchFn(fn) {
depend.addDepend(fn)
}
// 对象的响应式
const obj = {
name: 'sph',
age: 18
}
watchFn(function() {
const newName = obj.name
console.log('你好啊!乐比');
console.log('你好啊!兔兔');
})
obj.name = 'lebi'
depend.notify()
于是,这样我们就创建好了一个类来管理对象属性的响应式函数,接着,我们进行下一步——自动监听对象变化
自动监听对象变化
这里就用到了上面提到Proxy和Reflect了,同时设计函数reactive,只要传入某一个对象,就能让它变成被侦听的响应式对象
const objProxy = new Proxy(obj,{
get: function(target,key,receiver) {
return Reflect.get(obj,key,receiver)
},
set: function(target,key,newValue,receiver) {
Reflect.set(target,key,newValue,receiver)
}
})
objProxy改变属性值的时候,在set函数里面执行依赖函数
const objProxy = new Proxy(obj,{
get: function(target,key,receiver) {
return Reflect.get(obj,key,receiver)
},
set: function(target,key,newValue,receiver) {
Reflect.set(target,key,newValue,receiver)
depend.notify()
}
})
接下来我们来试试对obj的两个属性name,height进行侦听,于是我们只能再new一个Depend出来,用于管理height的依赖函数
const depend1 = new Depend()
// 创建一个函数用来添加执行对象属性的依赖函数
function watchFn(depend,fn) {
depend.addDepend(fn)
}
watchFn(depend1,function() {
const newName = objProxy.name
console.log('你好啊!乐比');
console.log('你好啊!兔兔');
})
const depend2 = new Depend()
watchFn(depend2,function() {
const newAge = objProxy.age
console.log('你好啊!乐比');
console.log('你好啊!兔兔');
})
所以意思就是说若是一个对象有一百个属性需要侦听,我就得重复写一百个以上的watchFn函数,这当然是不现实,那我们应该思考,该如何收集属性的依赖函数?
我们之前是在watchFn里面收集依赖函数的,但是这种收集方式不能 自动识别哪一个key需要把哪些函数作为依赖函数进行添加 。
const depend = new Depend()
function watchFn(fn) {
depend.addDepend(fn)
}
watchFn(function() {
const newName = obj.name
console.log('你好啊!乐比');
console.log('你好啊!兔兔');
})
我们可以封装一个getDepend的函数,用来区分对象的不同属性需要添加的fn
// getDepend初级
const map = new Map()
function getDepend(key) {
let depend = map.get(key)
if(!depend) {
depend = new Depend()
map.set(key,depend)
}
return depend
}
** 那么,接下来应该去哪里收集属性的依赖函数呢?去get里面收集!
因为当一个函数中使用了对象的某个key,那么他就是这个key的依赖对象,在key发生变化的时候,这个函数就应该重新执行一遍。
const objProxy = new Proxy(obj,{
get: function(target,key,receiver) {
// 根据key 获取对应的depend
const depend = getDepend(key)
// 给depend对象添加响应函数?
return Reflect.get(obj,key,receiver)
},
set: function(target,key,newValue,receiver) {
Reflect.set(target,key,newValue,receiver)
// 根据key 获取对应的depend
const depend = getDepend(key)
depend.notify()
}
})
在每次传入依赖函数时,都需要调用一下,来告诉objProxy需要收集一下依赖函数了
function watchFn(fn) {
fn()
}
那么这时候已经已经拿到对应属性的depend了,该为depend对象添加响应函数了
可是现在问题在于,如何拿到watchFn里面的依赖函数fn呢? 可以这样操作
let activeReactiveFn = null
function watchFn(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
然后修改get函数的代码
const objProxy = new Proxy(obj,{
get: function(target,key,receiver) {
// 根据key 获取对应的depend
const depend = getDepend(key)
// 给depend对象添加响应函数?
depend.addDepend(activeReactiveFn)
return Reflect.get(obj,key,receiver)
const depend = getDepend(key)
depend.notify()
}
})
那么至此对单个对象的多个进行侦听就完成了,但是我们在开发总又不可能只有一个对象,大概率需要对很多个对象的不同属性进行响应式处理。
所以需要对多个对象进行侦听,可进行以下优化来批量生成代理对象
const reactive = (obj) => {
return new Proxy(obj,{
get: function(target,value,receiver) {
const depend = getDepend(key)
depend.addDepend(activeReactiveFn)
return Reflect.set(target,value,receiver)
},
set: function(target,value,newValue,receiver) {
Reflect.set(target,value,newValue,receiver)
depend.notify()
}
})
}
那么如何判断一个依赖函数是哪一个obj的哪一个属性的依赖?
这时就需要对对象依赖进行管理,重点来了,提起精神往下看
依赖收集管理
整个依赖收集框架图
于是,接着我们就可以对getDepend进行优化
const targetMap = WeakMap()
function getDepend(target,key) {
// 通过target 拿到第一层数据结构
let map = targetMap.get(target)
if(!map) {
map = new Map()
targetMap.set(target,map)
}
// 再拿第二层数据结构
let depend = map.get(key)
if(!depend) {
depend = new Depend()
map.set(key,depend)
}
return depend
}
再对set get函数代码进行调整,重新获取depend
// 封装一个响应式函数————正确收集依赖
let activeReactiveFn = null
function watchFn(fn) {
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
const reactive = function(obj) {
return new Proxy(obj,{
get: function(target,key,receiver) {
const depend = getDepend(target,key)
depend.addDepend(activeReactiveFn)
return Reflect.get(obj,key,receiver)
},
set: function(target,key,newValue,receiver) {
Reflect.set(target,key,newValue,receiver)
const depend = getDepend(target,key)
depend.notify()
}
})
}
最后进行一步优化
问题:如果一个依赖函数里面两次用到一个属性,这个依赖函数就会被重复添加进depend里面
解决办法:把reactiveFns换成一个新的数据结构Set,用Set结构代替数组达到不重复执行依赖函数的目的
代码如下
class Depend {
constructor() {
// 用set代替数组就是为了不多次调用同一个函数
this.reactiveFns = new Set()
}
depend(fn) {
if(activeReactiveFn) {
this.reactiveFns.add(fn)
}
}
}
完结撒花了!
转载自:https://juejin.cn/post/7340678592970571827