Proxy 简介
Proxy 可以理解为,在操作对象时要先经过一层“拦截器”。访问对象时,都必须先经过这一层拦截。这就意味着你可以在拦截里做各种骚操作。 比如,整一个 Proxy 的对象用来对后端返回的数据类型进行类型校验,不通过直接 throw new Error('大兄弟,说好的对象,你给我返回一个数组?!'),记得搭配 try catch 食用,风味更佳!
让我们先看看怎么使用 Proxy.
const p = new Proxy(target, handler);
target: 要代理的原始对象
handler: 定义哪些操作将被拦截以及如何重新定义被拦截的操作的对象
-
const p =
new
Proxy({}, {
-
get(target, propKey) {
-
Ha ha, you have been intercepted by me;
-
}
-
});
-
-
console.log(p.name);
-
//Ha ha, you're intercepted by me
以上例子,只是展示了 proxy 怎么操作对象属性,而他的核心是为了扩展对象的能力。
让我看看另外一个例子 我们可以使用 set 方法,让对象的 name 属性无法被修改
-
const p =
new
Proxy({}, {
-
set(target, propKey, value) {
-
if (propKey ===
'name') {
-
Throw
new typeerror (
'name attribute is not allowed to be modified ');
-
}
-
//Not name attribute, save directly
-
target[propKey] = value;
-
}
-
});
-
p.name =
'proxy';
-
//Typeerror: the name property is not allowed to be modified
-
p.a =
111;
-
console.log(p.a);
// 111
Babel is used to translate syntax, such as new APIs (such as Array.from , Array.prototype.includes )We need to install additional packages for support, such as [core js / stable] () and [regenerator Runtime / runtime] () (PS: Babel 7. X @ Babel / Polyfill is not recommended), and then there are some APIs (string normalize, proxy, fetch, etc.)core-jsFor details, please refer to the official document core JS ? missing polyfills.
vue2.X 是如何实现响应式系统的?
当你把一个普通的 JavaScript 对象传入 vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty() 把这些 property 全部转为 getter/setter。在 getter 方法中收集数据依赖,在 setter 中监听数据变化。一旦数据发生变化,再通知订阅者。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
图片来源:Vue 官网
让我们先看看部分源码 Src / core / observer/ index.js#L156 -L193, version 2.6.11
-
let childOb = !shallow && observe(val)
-
//The data in the data is deeply traversed, and a response formula is added to each attribute of the object
-
Object.defineProperty(obj, key, {
-
enumerable:
true,
-
configurable:
true,
-
get:
function reactiveGetter () {
-
const value = getter ? getter.call(obj) : val
-
if (Dep.target) {
-
//Do dependency collection
-
dep.depend()
-
if (childOb) {
-
childOb.dep.depend()
-
if (
Array.isArray(value)) {
-
//If it is an array, you need to collect the dependency of each member. If the member of the array is still an array, it will be recursive.
-
dependArray(value)
-
}
-
}
-
}
-
return value
-
},
-
set:
function reactiveSetter (newVal) {
-
const value = getter ? getter.call(obj) : val
-
/* eslint-disable no-self-compare */
-
if (newVal === value || (newVal !== newVal && value !== value)) {
-
return
-
}
-
/* eslint-enable no-self-compare */
-
if (process.env.NODE_ENV !==
'production' && customSetter) {
-
customSetter()
-
}
-
if (getter && !setter)
return
-
if (setter) {
-
setter.call(obj, newVal)
-
}
else {
-
val = newVal
-
}
-
//New values need to be observed again to ensure data response
-
childOb = !shallow && observe(newVal)
-
//Inform all observers of data changes
-
dep.notify()
-
}
-
})
能看出 defineProperty 的痛点吗?
他无法发现对象中新增和被删除的属性:当你给一个对象添加一个新的属性时,这个新增的属性没有被添加到 Vue 的数据更新侦查机制里。vue.set 可以让 Vue 知道你新增了一个属性,其实 Vue.set可以让Vue知道你新增了一个属性,其实Vue.set内部也是通过调用 Object.defineProperty() 来实现的
当你利用索引直接设置一个数组项或修改数组长度时,Vue 不能检测到数组的变动
当对象嵌套层数特别深时,递归遍历带来的性能开销就会比较大
Vue.set: 向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi')
Proxy 在 Vue3.0 中上位
为什么 Proxy 可以解决以上的痛点呢? 本质的原因在于 Proxy 是一个内置了拦截器的对象,所有的外部访问都得先经过这一层拦截。不管是先前就定义好的,还是新添加属性,访问时都会被拦截。
Take a simple one
先用 Object.defineProperty() 来实现:
-
class Observer {
-
constructor(data) {
-
//Traverse the property of parameter data and add it to this
-
for(
let key
of
Object.keys(data)) {
-
if(
typeof data[key] ===
'object') {
-
data[key] =
new Observer(data[key]);
-
}
-
Object.defineProperty(
this, key, {
-
enumerable:
true,
-
configurable:
true,
-
get() {
-
console.log(
'You visited '+ key);
-
return data[key];
// the bracket method can use a variable as the attribute name, but the dot method cannot;
-
},
-
set(newVal) {
-
console.log (
'You set '+ key);
-
console.log (
'New '+ key +
' = '+ newVal);
-
if(newVal === data[key]) {
-
return;
-
}
-
data[key] = newVal;
-
}
-
})
-
}
-
}
-
}
-
-
const obj = {
-
name:
'app',
-
age:
'18',
-
a: {
-
b:
1,
-
c:
2,
-
},
-
}
-
const app =
new Observer(obj);
-
app.age =
20;
-
console.log(app.age);
-
app.newPropKey =
'new attribute';
-
console.log(app.newPropKey);
上述代码的输出结果是
-
//Modify the output of obj's original attribute age
-
You set age
-
New age =
20
-
You visited age
-
20
-
//Set the output of the new property
-
new property
正如你看到的,新添加的属性没有被监测到。只有手动调用 Object.defineProperty() 才能被 Vue 添加到侦查机制里。 这就是为什么在 $set 添加或删除对象属性失败后,会调用 Object.defineProperty() 来解决的原因.
让我们再用 Proxy 来实现:
-
const obj = {
-
name:
'app',
-
age:
'18',
-
a: {
-
b:
1,
-
c:
2,
-
},
-
}
-
const p =
new
Proxy(obj, {
-
get(target, propKey, receiver) {
-
console.log (
'You visited '+ propKey);
-
return
Reflect.get(target, propKey, receiver);
-
},
-
set(target, propKey, value, receiver) {
-
console.log (
'you set ' + propKey);
-
console.log (
'New '+ propKey +
' = '+ value);
-
Reflect.set(target, propKey, value, receiver);
-
}
-
});
-
p.age =
'20';
-
console.log(p.age);
-
p.newPropKey =
'new attribute';
-
console.log(p.newPropKey);
输出如下:
-
//Modify the age property of the original object
-
You set age
-
New age =
20
-
You visited age
-
20
-
-
//Set new properties
-
You set up newpropkey
-
New newpropkey =
new property
-
You visited newpropkey
-
new property
正如你看到的,新增的属性不需要再重新执行响应式操作就能被拦截。待到天荒地老,海枯石烂,Proxy 对对象属性的拦截永不变。
Reflect (introduced in ES6) is a built-in object that provides a way to intercept JavaScript operations. Some object objects are obviously internal methods of the language (such as Object.defineProperty())PutReflectObject. Modify the returned results of some object methods to make them more reasonable. Make object operations function behavior. See MDN for details
总结
Proxy 是用来操作对象并且扩展对象能力的。而 Object.defineProperty() 只是单纯地操作对象的属性
Vue2.x 是用 Object.defineProperty() 实现数据响应的。但是受限于 Object.defineProperty() 的实现,必须递归遍历至对象的最底层
Vue3.0 用 Proxy 来拦截对象。不管对对象执行任何操作,都会先通过 Proxy 的处理逻辑
除了 Vue3.0,还有其他的库也在使用 Proxy。所以要跟上大佬的步伐,赶紧上手 Proxy 吧!