前言
距离国庆假期尤大发布vue3
前瞻版本发布已经有一个月的时间,大家都知道在vue2x
版本中的响应式数据更新是用的defineProperty
这个API。
在vue2
中,针对Object
和Array
两种数据类型采用了两种不同的处理方式。
对于Object
类型,通过Object.defineProperty
通过getter/setter
递归侦测所有对象的key
,实现深度侦测
对于Array
类型,通过拦截Array
原型上的几个操作实现了对数组的响应式,但是存在一些问题。
总之,通过defineProperty
这种方式存在一定的性能问题
为了解决这个问题,从很早之前vue3
就计划将采用ES6 Proxy
代理的方式实现数据的响应式。(IE不支持这个API,所以vue3也不支持IE11了,垃圾IE)
关于Proxy
可以先查看MDN Proxy详细用法。
这里主要讲一下基本语法
const obj = new Proxy(target,{
// 获取对象属性会走这里
get(target, key, receiver){},
// 修改对象属性会走这里
set(target, key, value, receiver){},
// 删除对象上的方法会走这里
deleteProperty(target,key){}
})
尝试使用一下Proxy
这个API,尝试几种用法,发现一些问题
- 代理普通对象
const obj = {
name: 'ahwgs',
age: 22,
}
const res = new Proxy(obj, {
// 获取对象属性会走这里
get(target, key, receiver) {
console.log('get', target, key)
return target[key]
},
// 修改对象属性会走这里
set(target, key, value, receiver) {
console.log('set', target[key])
target[key] = value
return true
},
// 删除对象上的方法会走这里
deleteProperty(target, key) {
console.log('deleteProperty', target[key])
},
})
const n = res.name
res.age = 23
console.log(obj)
// get { name: 'ahwgs', age: 22 } name
// set 22
// { name: 'ahwgs', age: 23 }
- 代理数组
// const obj = {
// name: 'ahwgs',
// age: 22,
// }
const obj = [1, 2, 3]
const res = new Proxy(obj, {
// 获取对象属性会走这里
get(target, key, receiver) {
console.log('get', target, key)
return target[key]
},
// 修改对象属性会走这里
set(target, key, value, receiver) {
console.log('set', target[key])
target[key] = value
return true
},
// 删除对象上的方法会走这里
deleteProperty(target, key) {
console.log('deleteProperty', target[key])
},
})
res.push(4)
console.log(obj)
// get [ 1, 2, 3 ] push
// get [ 1, 2, 3 ] length
// set undefined
// set 4
// [ 1, 2, 3, 4 ]
代理数组的时候发现了一个问题,get
调用的两次,一次是push
一次是length
这两个都是数组自身的属性
那么vue3中是如何解决这个问题的呢?
- 代理深层次对象
const obj = {
name: 'ahwgs',
age: 22,
arr: [1, 2, 3],
}
const res = new Proxy(obj, {
// 获取对象属性会走这里
get(target, key, receiver) {
console.log('get', target, key)
return target[key]
},
// 修改对象属性会走这里
set(target, key, value, receiver) {
console.log('set', target, key)
target[key] = value
return true
},
// 删除对象上的方法会走这里
deleteProperty(target, key) {
console.log('deleteProperty', target[key])
},
})
res.arr.push(4)
console.log(obj)
// get { name: 'ahwgs', age: 22, arr: [ 1, 2, 3 ] } arr
// { name: 'ahwgs', age: 22, arr: [ 1, 2, 3, 4 ] }
发现并没有执行set逻辑,并没有代理到第二层级的对象,那么vue中是如何做到深层次的代理的呢?
解决问题
上面的代码我们遇到了两个问题:
- 多次触发了
get/set
- 无法代理深层级的对象
我们手写一个简单的vue3
尝试解决上面这些问题,具体看下述代码:
const toProxy = new WeakMap() // 存放的是代理后的对象
const toRaw = new WeakMap() // 存放的是代理前的对象
function isObject(target) {
// 这里为什么!==null 因为typeof null =object 这是js的一个bug
return typeof target === 'object' && target !== null;
}
// 模拟UI更新
function trigger() {
console.log('UI更新了!!');
}
// 判断key是否是val的私有属性
function hasOwn(val, key) {
const { hasOwnProperty } = Object.prototype
return hasOwnProperty.call(val, key)
}
// 数据代理
// target是要代理的对象,vue中data()return的那个对象
function reactive(target) {
// 先判断如果不是对象 不需要做代理 直接返回
if (!isObject(target)) return target;
// 如果代理表中已经存在 就不需要再次代理 直接返回已存在的代理对象
const proxy = toProxy.get(target)
if (proxy) return proxy
// 如果传入的对象被代理过
if (toRaw.has(target)) return target
const handler = {
set(tar, key, value, receiver) {
// 触发更新
// 如果触发的是私有属性的话才去更新视图 用以解决类似于数组操作中多次set的问题
if (hasOwn(target, key)) {
trigger()
}
// 这里使用ES6 Reflect 为Proxy设置一些属性
// 用于简化自定义的一些方法
return Reflect.set(tar, key, value, receiver)
},
get(tar, key, receiver) {
const res = Reflect.get(tar, key, receiver)
// 判断当前修改的值是否是否是对象 如果是对象的话 递归再次代理 解决深层级代理的问题
if (isObject(tar[key])) {
return reactive(res)
}
return res
},
deleteProperty(tar, key) {
return Reflect.deleteProperty(tar, key)
},
}
// 被代理的对象
const observed = new Proxy(target, handler)
// 将代理过的对象 放入缓存中
// 防止代理过的对象再次被代理
// WeekMap因为的key是弱引用关系,涉及到垃圾回收机制,要比Map的效率高
toProxy.set(target, observed) // 源对象 : 代理后的结果
toRaw.set(observed, target) //
return observed
}
const data = {
name: 'ahwgs',
age: 22,
list: [1, 2, 3],
}
let user = reactive(data)
user = reactive(data)
user = reactive(data)
user.list.push(4)
针对上面的几个问题做以下解释:
- 多次触发了
get/set
通过hasOwn
这个方法,判断当前修改的属性是否是私有属性,如果是的话才去更新视图。
对于这一点,源码中是这样做的:
// 判断是否有
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
/* istanbul ignore else */
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
}
判断要set
的key
是否是存在的,如果是存在的就去更新视图(trigger
方法),如果不是的话往视图中新增
- 无法代理深层级的对象
通过在get
方法中判断当前的值是否是对象,如果是对象的话再去代理一次,做一个递归的操作
对于源码中是这样的:
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
if (isRef(res)) {
return res.value
}
track(target, OperationTypes.GET, key)
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
总结
- 整体是通过
ES6 Proxy
这个新特性去实现的响应式,并且还通过WeakWap
去缓存的整个代理数据的保存,提高响应式数据的性能 - 简单版是这么简单处理的,但是源码中对每一个细节处理的都很细致,并且结构分明,具体可以查看https://github.com/vuejs/vue-next/tree/master/packages/reactivity/src
关于
- 本文首发于:实现一个简化版的Vue3数据侦测
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。