仓库地址:vue-next
Vue2.x 中,实现数据的可响应,需要对 Object 和 Array 两种类型采用不同的处理方式。 Object 类型通过 Object.defineProperty 将属性转换成 getter/setter ,这个过程需要递归侦测所有的对象 key,来实现深度的侦测。
为了感知 Array 的变化,对 Array 原型上几个改变数组自身的内容的方法做了拦截,虽然实现了对数组的可响应,但同样存在一些问题,或者说不够方便的情况。 同时,defineProperty 通过递归实现 getter/setter 也存在一定的性能问题。
vue3.x实现响应式通过 ES6 提供的 Proxy API。
proxy
Proxy API 具有更加强大的功能, 相比旧的 defineProperty API ,Proxy 可以代理数组,并且 API 提供了多方法 ,可以实现诸多功能。
这里主要说两属性: get 、 set , 以及其中的一些比较容易被忽略的细节。
1.get,set
- proxy基本使用
var data = {
name: '就是玩儿',
age: 666
}
var proxyData = new Proxy(data, {
get(target, key, receiver) {
// receiver --> proxyData
const result = Reflect.get(target, key, receiver)
console.log('get', key)
return result // 返回结果
},
set(target, key, val, receiver) {
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
return result // 是否设置成功
},
deleteProperty(target, key){
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
return result // 是否删除成功
}
})
// vue2.x
/* 响应式监听属性的函数,一旦有赋新值就发生变化 , 监听每个数据的改变*/
function defineReactive(obj, key, val){
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function(){
if(Dep.target){
console.log('获取')
dep.addSub(Dep.target);
}
return val;
},
set: function(newVal){
if(newVal === val){
return;
}
val = newVal;
// console.log(`${val}改变了`)
// 一旦更新立马通知
dep.notify();
}
})
}
// 相比于vue2.x设置属性值简洁不少
// 输出
proxyData.name = '不靠谱'
set name 不靠谱
"不靠谱"
// 也可以直接直接监测数组的操作
var data = ['a', 'b', 'c']
// 输出
proxyData.push('d')
get push
get length
set 3 d
set length 4
通过输出结果可以看到,进行push操作,除了操作当前数据,还触发了数组本身length属性的更改。push 操作除了给数组的第 3 位下标设置值 d ,还给数组的 length 值更改为 4。 同时这个操作还触发了 get 去获取 push 和 length 两个属性。
2.多次触发
var proxyData = new Proxy(data, {
get(target, key, receiver) {
// receiver --> proxyData
const result = Reflect.get(target, key, receiver)
console.log('get', key)
return result // 返回结果
},
set(target, key, val, receiver) {
// 重复数据不做处理, 上面push d 之后,length已经为4,无需再次设置
if(val === target[key]){
return true
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
return result // 是否设置成功
},
deleteProperty(target, key){
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
return result // 是否删除成功
}
})
// 输出
proxyData.push('d')
get push
get length
set 3 d
3.深度监听
const data = {
name: '就是玩儿',
age: 666,
info: {
title: '不靠谱'
},
a: {
b: {
c: {
d: 1
}
}
}
}
function reactive(target = {}) {
if(typeof target !== 'object' || target == null){
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
// receiver --> proxyData
const result = Reflect.get(target, key, receiver)
console.log('get', key)
return result
// 深度监听
// return reactive(result)
},
set(target, key, val, receiver) {
// 重复数据不做处理, 上面push d 之后,length已经为4,无需再次设置
// if(val === target[key]){
// return true
// }
// const ownKeys = Reflect.ownKeys(target)
// if(ownKeys.includes(key)) {
// // 已存在,修改
// }else {
// // 新增
// }
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
return result // 是否设置成功
},
deleteProperty(target, key){
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
return result // 是否删除成功
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
const proxyData = reactive(data)
从输出结可以看到,proxy代理了最外层data,当获取
proxyData.info时,info依然是一个普通的对象,并没有代理属性。那如何实现内部对象的代理呢,可以看下vue3.x的做法,vue3将内部对象代理放在了get中
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
下面是加入深度监听后获取到的数据,可以看到info已经被监听了
优缺点
vue2用Object.defineProperty来作为响应式原理的实现,会有局限性,比如:
- 深度监听需要一次性递归
- 无法监听data内新增属性,需要用Vue.set
- 无法监听删除属性,要用Vue.delete才能解决删除但视图不更新的限制
- 无法监听原生数组
vue3.x:
- 对数组不用单独做拦截处理
- 惰性递归,什么时候用到什么时候递归
Reflect
- 将
Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。 - 修改某些
Object方法的返回结果,让其变得更合理 - 让
Object操作都变成函数行为。
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true