Vue3改用proxy替代object.defineProperty。因为proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法.
响应式原理
在说响应式原理之前,需要理解一些重要的api
proxy
1.什么是proxy
用于修改某些操作的默认行为,在目标对象之前架设一层“拦截”,可以对外界的访问进行过滤和改写。
2.proxy的一些方法
通过构造函数生成实例,var proxy = new Proxy(target,handler),target表示所要拦截的目标对象,handler参数也是一个对象,用于定制拦截行为.
get
代码语言:javascript复制var obj = {
a: "123"
}
const handler = {
// 当使用代理对象访问其属性时会触发get函数,get方法的两个参数分别是目标对象和所要访问的属性。
get: function(obj, prop) {
console.log('prop: ', prop);
return prop in obj ? obj[prop] : 37
},
}
const p = new Proxy(obj, handler)
p.a //123
p.b //37
如果访问目标对象不存在的属性,会返回37.如果没有这个拦截函数,访问不存在的属性,只会返回undefined
。
get方法可以继承
这里需要说一下Object.create()
语法:Object.create(proto[,propertiesObject])
参数:proto,创建对象的原型,表示要继承的参数.propertiesObject(可选):也是一个对象,用于对新创建的对象进行初始化.
看一段代码
var obj = {
a: "123"
}
var myObj = Object.create(obj,{
c:{value:1},
d:{
get:function(){
console.log("333333")
return "3333"
}
}
})
输出结果obj在myObj的原型上,第一个参数,在返回对象的原型上,而不是直接属性
代码语言:javascript复制var obj = {
a: "123"
}
const handler = {
//继承
get:function(obj, propertyKey, receiver){
console.log('obj: ', obj);
return obj[propertyKey];
},
}
const p = new Proxy(obj, handler)
// p.b = "456"
// console.log('p: ', p.a);
let extendValue = Object.create(p);
console.log(extendValue.a) //123
使用get拦截,实现数组读取复数的索引
var arr = ["a","b","c"]
const handler = {
get:function(arr,key,value){
console.log('arr: ', arr); //["a","b","c"]
console.log('key: ', key); //-1
console.log('value: ', value); //proxy{0:"a",1:"b",2:"c"}
let index = Number(key)
console.log('index: ', index); //-1
if(index < 0){
key = String(arr.length index);
}
return Reflect.get(arr, key, value);
}
}
const p = new Proxy(arr,handler)
console.log('p: ', p[-1]); //c
get方法的第三个参数,总是指向原始的读操作所在的那个对象,一般情况下就是Proxy
实例。
const proxy = new Proxy({}, {
get: function(target, key, receiver) {
return receiver;
}
});
proxy.getReceiver === proxy // true
如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。
代码语言:javascript复制const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,
configurable: false
},
});
const handler = {
get(target, propKey) {
return 'abc';
}
};
const proxy = new Proxy(target, handler);
proxy.foo
// TypeError: Invariant check failed
set
set
方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
var obj = {
age:100
}
const handler = {
//添加或者修改属性值的时候执行
set :function(obj,prop,value){
if(prop === "age"){
if(!Number.isInteger(value)){
console.log("age不是整数")
}
if(value > 200){
console.log("age不大于200")
}
}
}
}
const p = new Proxy(obj,handler)
p.age = "300"
console.log("p",p.age )
set
设置了数据验证的方法,
var obj = {
a:1
}
const handler = {
set:function(obj,prop,value,receiver){
obj[prop] = receiver;
return true;
}
}
const p = new Proxy(obj,handler)
console.log("p" ,p.a === 1)
p并没有a属性.因此会去到p的原型链去找a属性,
注意
set
代理返回一个布尔值,严格模式下,set
代理如果没有返回true
,就会报错。
'use strict';
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = receiver;
// 无论有没有下面这一行,都会报错
return false;
}
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
// TypeError: 'set' on proxy: trap returned falsish for property 'foo'
apply
apply
方法拦截函数的调用、call
和apply
操作。
var a = function(){
return "我是target"
}
const handler = {
apply:function(){
return "我是proxy"
}
}
const p = new Proxy(a,handler)
console.log('p: ', p());
p
作为proxy的实例,当它作为函数调用时(p()
),就会被apply
方法拦截,返回一个字符串。
has
has()
方法用来拦截HasProperty
操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in
运算符。
has()
方法可以接受两个参数,分别是目标对象、需查询的属性名。
var obj ={ _prop: 'foo', prop: 'foo' };
const handler = {
has:function(target, key) {
console.log('target: ', target);
console.log('key: ', key);
if(key[0] === '_'){
return false
}
return key in target
}
}
const p = new Proxy(obj,handler)
console.log('p: ', p);
console.log('p: ', '_prop' in p); //false
has
隐藏某些属性,不被in
运算符发现.上面代码中,如果原对象的属性名的第一个字符是下划线,proxy.has()
就会返回false
,从而不会被in
运算符发现。has()
拦截对for...in
循环不生效。
construct()
拦截new
命令
接受三个参数,target:目标对象,args:构造函数的参数数组, newTarget:创造实例对象时,new
命令作用的构造函数
const handler = {
construct:function(obj,args){
console.log('obj: ', obj);
console.log('args: ', args);
return {args:args[0]}
}
}
const p = new Proxy(function(){},handler)
console.log('p: ', p);
console.log(new p(1))
construct()
方法返回的必须是一个对象,否则会报错。
另外,由于construct()
拦截的是构造函数,所以它的目标对象必须是函数,否则就会报错。
construct()
方法中的this
指向的是handler
,而不是实例对象。
deleteProperty
deleteProperty
方法用于拦截delete
操作,如果这个方法抛出错误或者返回false
,当前属性就无法被delete
命令删除。
var obj = {
_prop:1
}
console.log(obj[0])
const handler = {
deleteProperty:function(obj,key){
invariant(key, 'delete');
delete target[key];
return true
}
}
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const p = new Proxy(obj,handler)
console.log('p: ', p);
delete p._prop //```
// Error: Invalid attempt to delete private "_prop" property
defineProperty
拦截了Object.defineProperty()
操作。
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不会生效
上面代码中,defineProperty()
方法内部没有任何操作,只返回false
,导致添加新属性总是无效。注意,这里的false
只是用来提示操作失败,本身并不能阻止添加新属性。
getOwnPropertyDescriptor
拦截Object.getOwnPropertyDescriptor()
,返回一个属性描述对象或者undefined
。
var handler = {
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }
getPrototypeOf
用来拦截获取对象的原型 主要用来拦截以下操作
代码语言:javascript复制- `Object.prototype.__proto__`
- `Object.prototype.isPrototypeOf()`
- `Object.getPrototypeOf()`
- `Reflect.getPrototypeOf()`
- `instanceof`
getPrototypeOf()
方法拦截Object.getPrototypeOf()
,返回proto
对象。getPrototypeOf()
方法的返回值必须是对象或者null
,否则报错
isExtensible
拦截Object.isExtensible()
(判断一个对象是否可扩展)
var p = new Proxy({}, {
isExtensible: function(target) {
console.log("called");
return true;
}
});
Object.isExtensible(p)
// "called"
// true
调用Object.isExtensible()
输出called
只能返回布尔值
该方法有个强限制,返回值必须与目标对象的.isExtensible属性保持一致
ownKeys
ownKeys()
方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for...in
循环
let target = {
a: 1,
b: 2,
c: 3
};
let handler = {
ownKeys(target) {
return ['a'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// [ 'a' ]
拦截了对于target
对象的Object.keys()
操作,只返回a
、b
、c
三个属性之中的a
属性。
使用Object.keys()
方法时,有三类属性会被ownKeys()
方法自动过滤,不会返回。
- 目标对象上不存在的属性
- 属性名为 Symbol 值
- 不可遍历(
enumerable
)的属性
let target = {
a: 1,
b: 2,
c: 3,
[Symbol.for('secret')]: '4',
};
Object.defineProperty(target, 'key', {
enumerable: false,
configurable: true,
writable: true,
value: 'static'
});
let handler = {
ownKeys(target) {
return ['a', 'd', Symbol.for('secret'), 'key'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
console.log('Object.keys(proxy): ', Object.keys(proxy));
// ['a']
preventExtensions
preventExtensions()
方法拦截Object.preventExtensions()
(让一个对象变的不可扩展,也就是永远不能再添加新的属性。)。该方法必须返回一个布尔值,否则会被自动转为布尔值。
setPrototypeOf
用来拦截Object.setPrototypeOf()
(为现有对象设置原型,返回一个新对象)方法。
var handler = {
setPrototypeOf (target, proto) {
throw new Error('Changing the prototype is forbidden');
}
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
只要修改target
的原型对象,就会报错。
set和map数据结构
set
基础概念
类似数组,没有重复的值,成员的值是唯一的.
代码语言:javascript复制const s = new Set()
console.log('s: ', s);
[1,1,3,4,4,4,4,46].forEach(x => s.add(x));
console.log('s: ', s);
for(let i of s){
console.log('i: ', i); //1,3,4,46
}
代码语言:javascript复制const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
set
可用于数组去重和字符串里面的字符去重
set实例的属性和方法
Set 结构的实例有以下属性。
Set.prototype.constructor
:构造函数,默认就是Set
函数。Set.prototype.size
:返回Set
实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
Set.prototype.add(value)
:添加某个值,返回 Set 结构本身。Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set
的成员。Set.prototype.clear()
:清除所有成员,没有返回值。Array.from
方法可以将 Set 结构转为数组。
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
遍历操作
Set 结构的实例有四个遍历方法,可以用于遍历成员。
Set.prototype.keys()
:返回键名的遍历器Set.prototype.values()
:返回键值的遍历器Set.prototype.entries()
:返回键值对的遍历器Set.prototype.forEach()
:使用回调函数遍历每个成员
WeakSet
不重复值的集合,与set区别,
成员只能是对象;
对象都是弱引用.如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,
weakset
接收的参数只能是对象,注意
:[[1, 2], [3, 4]]是对象类型,如果不是对象会报错
WeakSet 结构有以下三个方法。
- WeakSet.prototype.add(value) :向 WeakSet 实例添加一个新成员。
- WeakSet.prototype.delete(value) :清除 WeakSet 实例的指定成员。
- WeakSet.prototype.has(value) :返回一个布尔值,表示某个值是否在 WeakSet 实例之中。 WeakSet没有size属性,且不可遍历
Map
类似对象,是键值对集合,但各种类型的值都可以当做键。只有对同一个对象的引用,map结构才将其视为同一个键。简单类型的话如果严格相等则被视为同一个键.
map实例的属性和方法
**size 属性:**返回 Map 结构的成员总数。
Map.prototype.set(key, value):set
方法设置键名key
对应的键值为value
Map.prototype.get(key):get
方法读取key
对应的键值,如果找不到key
,返回undefined
。
Map.prototype.has(key):has
方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
Map.prototype.delete(key):delete
方法删除某个键,返回true
。如果删除失败,返回false
。
Map.prototype.clear():clear
方法清除所有成员,没有返回值。
遍历方法
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
Map.prototype.keys()
:返回键名的遍历器。Map.prototype.values()
:返回键值的遍历器。Map.prototype.entries()
:返回所有成员的遍历器。Map.prototype.forEach()
:遍历 Map 的所有成员。
WeakMap
键值对的集合,WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。不计入垃圾回收机制
不计入垃圾回收机制,没有遍历操作,也没有size属性。没有Get() set() has() delete()
Reflect
从reflect对象上可以拿到语言内部的方法 方法和proxy一一对应。
vue3响应式原理的实现
通过proxy
代理我们所需要的对象,reactive()
创建一个响应式对象或数组,查看源码关于reactive()方法
//判断是否为object
function isObject(val) {
return val !== null && typeof val === 'object'
}
function reactive(target) {
console.log('target: ', target);
return createReactiveObject(target)
}
创建响应式对象,此方法是响应式的核心方法,该方法判断是否可读,后返回createReactiveObject()
方法
为什么要用Reflect
在proxy代理情况下,如果目标对象下的属性有函数类型且内部使用this,在使用代理对象访问属性(函数类型)的时候,此时this指向proxy代理对象。 解决办法 通过 Reflect.get获取值后,判断值的类型,如果是函数,通过bind改变函数this指向,否则直接返回。
代码语言:javascript复制user = new Proxy(user, { get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value; }
});
代码语言:javascript复制function createReactiveObject(target) {
if(!isObject(target)){
return target
}
const handler = {
get:function(target,key,receiver){
//proxy reflect 反射 此种写法等同于return target[key]
console.log("获取")
let result = Reflect.get(target,key,receiver)
return result
},
set:function(target,key,value,receiver){
// target[key] = value //如果设置没成功,可能报错,所有采用和此种写法等价的方法
let res = Reflect.set(target,key,value,receiver)
console.log("修改")
return res
},
deleteProperty:function(target,key){
console.log("删除")
let res = Reflect.deleteProperty(target,key)
return res
}
}
const p = new Proxy(target,handler)
console.log('p: ', p);
return p
}
const proxy = reactive({
a:1
})
proxy.a
proxy.a = 123
console.log('proxy.a: ', proxy.a);
// delete proxy.a
此方法创建响应式对象,返回proxy代理.实现对象的读取和修改(Reflect优点:不会报错,会有返回值,会替代object上的方法)
深层代理
以上的方法可实现第一层代理,但是对象难免是多层,这时我们需要修改get方法,在获取代理时利用递归修改返回值。这样的好处是只有访问内部属性才会进行深层响应式,减少性能消耗。
代码语言:javascript复制const handler = {
get:function(target,key,receiver){
//proxy reflect 反射 此种写法等同于return target[key]
console.log("获取")
let result = Reflect.get(target,key,receiver)
return isObject(result) ? reactive(result) : result //是个递归
},
}
proxy.a.b = 456
console.log("proxy",proxy.a.b) // 456
此方法在get获取目标对象时,对目标对象进行判断,如果目标对象是对象,则通过proxy对其进行再一次代理.
解决重复代理的问题
在我们进行代理时,如果这个对象代理过了,就不要再new了,通过WeakMap
(弱引用对象,一旦弱引用对象未被使用,会被垃圾回收机制回收)来解决,toRaw
方法返回原对象
let toProxy = new WeakMap() //弱引用映射表,放置的是原对象和代理过的对象
let toRaw = new WeakMap() //被代理过的对象,原对象
const p = new Proxy(target,handler)
toProxy.set(target,p)
let proxy = toProxy.get(target)
console.log('proxy: ', proxy);
if(proxy){
return proxy //如果取到值,说明被代理过了
}
if(toRaw.has(target)){ //防止一个对象被多次代理
console.log('toRaw.has(target): ', toRaw.has(target));
return target
}
const p = new Proxy(target,handler)
toProxy.set(target,p)
toRaw.set(p,target)
return p
let obj = {
a:{b:1}
}
const proxy = reactive(obj)
reactive(obj)
reactive(obj)
reactive(obj)
关于数组的问题
代码语言:javascript复制let arr = [1, 2, 3]
const proxy = reactive(arr)
proxy.push(4)
此时会修改数组的值和长度
此时我们需要判断代理对象是修改属性还是添加长度
代码语言:javascript复制//判断当前对象是否有此属性
function hasOwn(target, key) {
return target.hasOwnProperty(key);
}
set: function (target, key, value, receiver) {
// target[key] = value //如果设置没成功,可能报错,所有采用和此种写法等价的方法
let hadKey = hasOwn(target,key)
let oldValue = target[key]
let res = Reflect.set(target, key, value, receiver)
if (!hadKey) {
console.log("新增属性") //需要数组的长度
} else if (oldValue !== value) { //表示属性更改过了
console.log("修改属性") //为了屏蔽无意义的修改
}
console.log("修改")
return res
},
解构丢失响应式
将响应式对象属性赋值或解构(注意:
解构出来是基本数据类型时,失去响应式,解构出来是引用数据类型时,不会失去响应式)到本地,或将该属性传入一个函数时,会失去响应式。
完整代码
代码语言:javascript复制let toProxy = new WeakMap() //弱引用映射表,放置的是原对象和代理过的对象
let toRaw = new WeakMap() //被代理过的对象,原对象
//判断是否为object
function isObject(val) {
return val !== null && typeof val === 'object'
}
//判断当前对象是否由此属性
function hasOwn(target, key) {
return target.hasOwnProperty(key);
}
function reactive(target) {
return createReactiveObject(target)
}
function createReactiveObject(target) {
if (!isObject(target)) {
return target
}
let proxy = toProxy.get(target)
if (proxy) {
return proxy //如果取到值,说明被代理过了
}
if (toRaw.has(target)) { //防止一个对象被多次代理
console.log('toRaw.has(target): ', toRaw.has(target));
return target
}
const handler = {
get: function (target, key, receiver) {
//proxy reflect 反射 此种写法等同于return target[key]
console.log("获取")
let result = Reflect.get(target, key, receiver)
return isObject(result) ? reactive(result) : result
},
set: function (target, key, value, receiver) {
// target[key] = value //如果设置没成功,可能报错,所有采用和此种写法等价的方法
let hadKey = hasOwn(target,key)
let oldValue = target[key]
console.log('oldValue: ', oldValue);
let res = Reflect.set(target, key, value, receiver)
if (!hadKey) {
console.log("新增属性")
} else if (oldValue !== value) { //表示属性更改过了
console.log("修改属性") //为了屏蔽无意义的修改
}
console.log("修改")
return res
},
deleteProperty: function (target, key) {
console.log("删除")
let res = Reflect.deleteProperty(target, key)
return res
}
}
const p = new Proxy(target, handler)
toProxy.set(target, p)
toRaw.set(p, target)
return p
}
// let obj = {
// a:{b:1}
// }
// const proxy = reactive(obj)
// reactive(obj)
// reactive(obj)
// reactive(obj)
// proxy.a
// proxy.a = 123
// proxy.a.b = 456
// console.log("proxy",proxy.a.b)
// delete proxy.a
let arr = [1, 2, 3]
const proxy = reactive(arr)
proxy.push(4)
// proxy.length = 100
但不得不提,reactive具有局限性,仅对对象类型有用(对象,数组,set,map这样的集合类型),而对string,number,boolean这样的原始类型无效.
用ref定义响应式变量
reactive的响应式不能作用于所有值类型,因此,vue提供了ref来允许定义所有值类型的响应式.