你了解 Vue 3.0 响应式数据怎么实现吗?
Posted JavaScript
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你了解 Vue 3.0 响应式数据怎么实现吗?相关的知识,希望对你有一定的参考价值。
https://juejin.im/post/5cf8b51ae51d45590a445b0d
从 Proxy 说起
什么是Proxy
const target = {}; // 要被代理的原对象
// 用于描述代理过程的handler
const handler = {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}}
// obj就是一个被新的代理对象
const obj = new Proxy(target, handler);
obj.a = 1 // setting a!
console.log(obj.a)// getting a!
obj.a = 1; // setting a!
console.log(target.a) // 1 不会打印 "getting a!"
const target = {};
const handler = {};
const obj = new Proxy(target, handler);
obj.a = 1;
console.log(target.a) // 1
const target = {};
const obj = {};
const handler = {
get: function(target, key){
console.log(`get ${key} from ${JSON.stringify(target)}`);
return Reflect.get(target, key);
}}
const proxy = new Proxy(target, handler);
Object.setPrototypeOf(obj, proxy);
proxy.a = 1;
obj.b = 1
console.log(obj.a) // get a from {"a": 1} 1
console.log(obj.b) // 1
ES6的Proxy实现了对哪些属性的拦截?
get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo'];
set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值;
has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值;
ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、
Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性;getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象;
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值;
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值;
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象;
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值;
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截;
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…);
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args);
实际场景中 Proxy 可以做什么?
实现私有变量
const obj = {
_name: 'nanjin',
age: 19,
getName: () => {
return this._name;
},
setName: (newName) => {
this._name = newName;
}}
const proxyObj = obj => new Proxy(obj, {
get: (target, key) => {
if(key.startsWith('_')){
throw new Error(`${key} is private key, please use get${key}`)
}
return Reflect.get(target, key);
},
set: (target, key, newVal) => {
if(key.startsWith('_')){
throw new Error(`${key} is private key, please use set${key}`)
}
return Reflect.set(target, key, newVal);
}})
const newObj = proxyObj(obj);
console.log(newObj._name) // Uncaught Error: _name is private key, please use get_name
newObj._name = 'newname'; // Uncaught Error: _name is private key, please use set_name
console.log(newObj.age) // 19
console.log(newObj.getName()) // nanjin
vue响应式数据实现
2.x版本
const defineReactiveData = data => {
Object.keys(data).forEach(key => {
let value = data[key];
Object.defineProperty(data, key, {
get : function(){
console.log(`getting ${key}`)
return value;
},
set : function(newValue){
console.log(`setting ${key}`)
notify() // 通知相关的模板进行编译
value = newValue;
},
enumerable : true,
configurable : true
})
})}
const data = {
name: 'nanjing',
age: 19
}
defineReactiveData(data)
data.name // getting name 'nanjing'
data.name = 'beijing'; // setting name
const data = {
userIds: ['01','02','03','04','05']
}
defineReactiveData(data);
data.userIds // getting userIds ["01", "02", "03", "04", "05"]
// get 过程是没有问题的,现在我们尝试给数组中push一个数据
data.userIds.push('06') // getting userIds
不仅如此,很多数组的方法都不会触发setting,比如:push,pop,shift,unshift,splice,sort,reverse这些方法都会改变数组,但是不会触发set,所以Vue为了解决这个问题,重新包装了这些函数,同时当这些方法被调用的时候,手动去触发notify();看下源码:
// 获得数组原型const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 重写以下函数const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function(method) {
// 缓存原生函数
const original = arrayProto[method]
// 重写函数
def(arrayMethods, method, function mutator(...args) {
// 先调用原生函数获得结果
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
// 调用以下几个函数时,监听新数据
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 手动派发更新
ob.dep.notify()
return result
})
})
const push = Array.prototype.push;
Array.prototype.push = function(...args){
console.log('push is happenning');
return push.apply(this, args);
}
data.userIds.push('123') // push is happenning
当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLength
这个最根本的原因是因为这2种情况下,受制于js本身无法实现监听,所以官方建议用他们自己提供的内置api来实现,我们也可以理解到这里既不是defineProperty可以处理的,也不是包一层函数就能解决的,这就是2.x版本现在的一个问。
3.x版本
const defineReactiveProxyData = data => new Proxy(data,{
get: function(data, key){
console.log(`getting ${key}`)
return Reflect.get(data, key);
},
set: function(data, key, newVal){
console.log(`setting ${key}`);
if(typeof newVal === 'object'){ // 如果是object,递归设置代理
return Reflect.set(data, key, defineReactiveProxyData(newVal));
}
return Reflect.set(data, key, newVal);
}
})
const data = {
name: 'nanjing',
age: 19
};
const vm = defineReactiveProxyData(data);
vm.name // getting name nanjing
vm.age = 20; // setting age 20
vm.userIds = [1,2,3] // setting userIds
vm.userIds.push(1);
// getting userIds 因为我们会先访问一次userids
// getting push 调用了push方法,所以会访问一次push属性
// getting length 数组push的时候 length会变,所以需要先访问原来的length
// setting 3 通过下标设置的,所以set当前的index是3
// setting length 改变了数组的长度,所以会set length
// 4 返回新的数组的长度
vm.userIds.length = 2
// getting userIds 先访问
// setting length 在设置
vm.userIds[1] = '123'
// getting userIds 先访问
// setting 1 设置index=1的item
// "123"
总结1
总结2
扩展:
1.Proxy.revocable()
const obj = {};
const handler = {};
const {proxy, revoke} = Proxy.revocable(obj, handler);
proxy.a = 1
proxy.a // 1
revoke();
proxy.a // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
2.代理对象的this问题
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate(); // Uncaught TypeError: this is not a Date object.
const target = new Date();
const handler = {
get: function(target, key){
if(typeof target[key] === 'function'){
return target[key].bind(target) // 强制绑定
this到原对象
}
return Reflect.get(target, key)
}
};
const proxy = new Proxy(target, handler);
proxy.getDate(); // 6
以上是关于你了解 Vue 3.0 响应式数据怎么实现吗?的主要内容,如果未能解决你的问题,请参考以下文章