关于vue3的Proxy
关于vue3的Proxy
引入
数据响应式:从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom。因此实现数据响应式有两个重点问题:
- 如何知道数据发生了变化?
- 如何知道数据变化后哪里需要修改?
对于第一个问题,Vue3 之前使用了 ES5 的一个 APIObject.defineProperty
, Vue3 中使用了 ES6 的 Proxy
都是对需要侦测的数据进行 变化侦测,添加 getter 和 setter ,这样就可以知道数据何时被读取和修改。
第二个问题,Vue 对于每个数据都收集了与之相关的依赖,这里的依赖其实就是一个对象,保存有该数据的旧值及数据变化后需要执行的函数。每个响应式的数据变化时会遍历通知其对应的每个依赖,依赖收到通知后会判断一下新旧值有没有发生变化,如果变化则执行回调函数响应数据变化(比如修改 dom)。依赖收集与 Vue3与Vue2 类似,在 getter 中收集依赖,setter 中触发依赖。
什么是Proxy对象
在MDN上对于Proxy
的解释是:
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
首先Proxy的语法是:let p = new Proxy(target, handler);
:
target
是你要代理的对象.它可以是JavaScript中的任何合法对象.如: (数组, 对象, 函数等等);handler
是你要自定义操作方法的一个集合;p
是一个被代理后的新对象,它拥有target的一切属性和方法.只不过其行为和结果是在handler中自定义的。
let obj = {
a: 1,
b: 2,
}
const p = new Proxy(obj, {
get(target, key, value) {
if (key === 'c') {
return '我是自定义的一个结果';
} else {
return target[key];
}
},
set(target, key, value) {
if (value === 4) {
target[key] = '我是自定义的一个结果';
} else {
target[key] = value;
}
}
})
console.log(obj.a) // 1
console.log(obj.c) // undefined
console.log(p.a) // 1
console.log(p.c) // 我是自定义的一个结果
obj.name = '李白';
console.log(obj.name); // 李白
obj.age = 4;
console.log(obj.age); // 4
p.name = '李白';
console.log(p.name); // 李白
p.age = 4;
console.log(p.age); // 我是自定义的一个结果
- 总结:
Proxy
对象就是可以让你去对JavaScript中的一切合法对象的基本操作进行自定义.然后用你自定义的操作去覆盖其对象的基本操作.也就是当一个对象去执行一个基本操作时,其执行的过程和结果是你自定义的,而不是对象的. * 类似浅拷贝
Proxy的作用:
- 拦截和监视外部对对象的访问;
- 降低函数或类的复杂度
- 复杂操作前对操作进行校验或对所需资源进行管理
Reflect
在MDN上对于Reflect
的解释是:
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。(类似Math)
Reflect所定义的静态方法包含了proxy handlers所处理的代理操作,举例:Reflect.get(target, name);
效果等同于target[name];
为什么使用Reflect与Proxy搭配?
- 用函数处理相关操作,例如删除属性:比起
delete target[name]
,Reflect.deleteProperty(target, name)
更能保持一致性;
const loggedObj = new Proxy(obj, {
deleteProperty: function(target, name) {
// instead of `delete target[name]...
return Reflect.deleteProperty(target, name);
}
});
- 解决this指向:在 Proxy 代理的情况下,目标对象内部的
this
关键字会指向 Proxy 代理。
const target = {
get foo() {
return this.bar;
},
bar: 3
};
const handler = {
get(target, propertyKey, receiver) {
if (propertyKey === 'bar') return 2;
console.log('Reflect.get ', Reflect.get(target, propertyKey, receiver));
console.log('target[propertyKey] ', target[propertyKey]);
}
};
const obj = new Proxy(target, handler);
console.log(obj.bar);
// 2
console.log(obj.foo);
// Reflect.get 2
// target[propertyKey] 3
兼容性:
Vue2和Vue3数据响应式对比
温习:Vue2.x数据响应式原理:定义依赖收集对象dep,在get部分调用dep.depend收集依赖,在set部分调用dep.notify更新依赖。virtualDom在vue中表现为抽象语法树ast,包含子节点,本质就是一个json对象,外面包裹了一层观察者模式,dep.notify只需要触发observe,由observe触发ast进行更新。
VUE3.0 的响应式原理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="./vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const {createApp, reactive} = Vue
const App = {
template: `<h1 @click="updateTitle">{{title.name}}</h1>`,
setup() {
const title = reactive({name: 'vue2'});
const updateTitle = () => {
title.name = 'vue3';
}
return {title, updateTitle};
}
}
createApp(App).mount('#app')
</script>
</body>
</html>
从上面的代码可以看出,当reactive({name: 'vue2'});
是应该触发一次更新视图的,而当变更值proxy.name = 'vue3';
的时候需要再触发一次更新视图。vue3.0这里的主要逻辑是通过Vue.effect()
这个方法去实现的,effect这个方法会先执行一次,当数据变化的时候会再执行。通过示例可以看到实现 Vue3 这个数据响应式需要有 reactive、effect 这两个函数,下面通过从变化侦测及依赖收集两个方面介绍,简单实现这几个函数。
变化侦测
let sourceProxy = new WeakMap(); // 存放原对象和代理过的对象
let toRaw = new WeakMap(); // 存放被代理过的对象和原对象
function reactive(target) {
return createReactive(target);
}
function createReactive(target) {
// 创建响应式对象
if(!(typeof target === 'object' && target !== null)) {
return target;
}
// 判断target是否被代理过
if(sourceProxy.get(target)) {
return sourceProxy.get(target);
}
// 判断代理过的里面是否有target, 防止一个对象被多次代理
if (toRaw.has(target)) {
return target;
}
// 创建观察者
let baseHandle = {
get(target, key, receiver) {
// 获取
let datas = Reflect.get(target, key, receiver);
// 订阅
subscribeTo(target, key); // 当key变化的时候重新执行effect
// 结果代理(递归)
return typeof target === 'object' && target !== null ? reactive(datas) : datas;
},
set(target, key, value, receiver) {
// 设置
let oldValue = target[key];
let res = Reflect.set(target, key, value, receiver);
// 此处的判断是屏蔽无意义的修改
if (!target.hasOwnProperty(key)) {
// 新增属性
console.log('新增');
trigger(target, 'add', key);
} else if (oldValue !== value) {
// 修改属性
console.log('修改');
trigger(target, 'set', key);
}
console.log(res);
return res;
}
}
let observed = new Proxy(target, baseHandle);
// 设置weakmap
sourceProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
reactive 中使用 Proxy 对目标进行代理,代理的行为是 baseHander ,然后对目标对象及代理后的对象进行缓存,防止多次代理。track 函数收集依赖,trigger 函数触发依赖更新。
依赖收集(发布订阅)
let stacks = []; // 保存当前待收集的依赖对象
let targetMap = new WeakMap(); // 记录所有数据及其对应依赖的表
// 响应式
function effect(fn) {
let effect = createReactiveFn(fn); // 创建响应式的函数
effect(); // 默认执行一次
}
function createReactiveFn(fn) {
let eFn = function() {
// 执行fn并且将fn存入栈中
return carryOut(eFn, fn)
}
return eFn;
}
function carryOut(effect, fn) {
// 执行fn并且将fn存入栈中
try {
stacks.push(effect);
fn();
} finally {
stacks.pop(effect);
}
}
function subscribeTo(target, key) { // 收集依赖
let effect = stacks[stacks.length - 1];
if (effect) { //如果有,则创建关联
let maps = targetMap.get(target);
if (!maps) {
targetMap.set(target, maps = new Map);
}
let deps = maps.get(key);
if (!deps) {
maps.set(key, deps = new Set());
}
if(!deps.has(effect)) {
deps.add(effect);
}
}
}
function trigger(target, type, key) { // 触发依赖更新
let tMap = targetMap.get(target);
if (tMap) {
let keyMaps = tMap.get(key);
// 将key对应的effect执行
if (keyMaps) {
keyMaps.forEach((eff) => {
eff();
})
}
}
}
作者:Luin
转载自:https://juejin.cn/post/7134250178676654110