之前写了两篇vue2.0的响应式原理,链接在此,对响应式原理不清楚的请先看一部掘金看下两篇
和尤雨溪一起进阶vue
和尤雨溪一起进阶vue(二)
现在来写一个简单的3.0的版本吧
大家都知道,2.0的响应式用的是Object.defineProperty
,结合发布订阅模式实现的,3.0已经用Proxy
改写了
Proxy是es6提供的新语法,Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
语法:
const p = new Proxy(target, handler)
target
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
handler的方法有很多, 感兴趣的可以移步到MDN,这里重点介绍下面几个
handler.has()
in 操作符的捕捉器。
handler.get()
属性读取操作的捕捉器。
handler.set()
属性设置操作的捕捉器。
handler.deleteProperty()
delete 操作符的捕捉器。
handler.ownKeys()
Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
基于上面的知识,我们来拦截一个对象属性的取值,赋值和删除
// version1
const handler = {
get(target, key, receiver) {
console.log('get', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set', key, value)
let res = Reflect.set(target, key, value, receiver)
return res
},
deleteProperty(target, key) {
console.log('deleteProperty', key)
Reflect.deleteProperty(target, key)
}
}
// 测试部分
let obj = {
name: 'hello',
info: {
age: 20
}
}
const proxy = new Proxy(obj, handler)
// get name hello
// hello
console.log(proxy.name)
// set name world
proxy.name = 'world'
// deleteProperty name
delete proxy.name
上面已经可以拦截到对象属性的取值,赋值和删除了,我们来看看新增一个属性可否拦截
proxy.height = 20
// 打印 set height 20
成功拦截!!
我们知道vue2.0新增data上不存在的属性是不可以响应的,需要手动调用$set
的,这就是Proxy
的优点之一
现在来看看嵌套对象的拦截,我们修改info属性的age属性
proxy.info.age = 30
// 打印 get info
只可以拦截到info,不可以拦截到info的age属性,所以我们要递归了,问题是在哪里递归呢?
因为调用proxy.info.age会先触发proxy.info的拦截,所以我们可以在get中拦截,如果proxy.info是对象的话,对象需要再被代理一次,我们把代码封装一下,写成递归的形式
function reactive(target) {
return createReactiveObject(target)
}
function createReactiveObject(target) {
// 递归结束条件
if(!isObject(target)) return target
const handler = {
get(target, key, receiver) {
console.log('get', key)
let res = Reflect.get(target, key, receiver)
// res如果是对象,那么需要继续代理
return isObject(res) ? createReactiveObject(res): res
},
set(target, key, value, receiver) {
console.log('set', key, value)
let res = Reflect.set(target, key, value, receiver)
return res
},
deleteProperty(target, key) {
console.log('deleteProperty', key)
Reflect.deleteProperty(target, key)
}
}
return new Proxy(target, handler)
}
function isObject(obj) {
return obj != null && typeof obj === 'object'
}
// 测试部分
let obj = {
name: 'hello',
info: {
age: 20
}
}
const proxy = reactive(obj)
proxy.info.age = 30
运行上面的代码,打印结果
get info
set age 30
Bingo! 嵌套对象拦截到了
vue2.0用的是Object.defineProperty拦截对象的getter和setter,一次将对象递归到底,
3.0用Proxy,是惰性递归的,只有访问到某个属性,确定了值是对象,我们才继续代理下去这个属性值,因此性能更好
现在我们来测试数组的方法,看看能否拦截到,以push方法为例, 测试部分代码如下
let arr = [1, 2, 3]
const proxy = reactive(arr)
proxy.push(4)
打印结果
get push
get length
set 3 4
set length 4
和预期有点不太一样,调用数组的push方法,不仅拦截到了push, 还拦截到了length属性,set被调用了两次,在set中我们是要更新视图的,我们做了一次push操作,却触发了两次更新,显然是不合理的,所以我们这里需要修改我们的handler的set函数,区分一下是新增属性还是修改属性,只有这两种情况才需要更新视图
set函数修改如下
set(target, key, value, receiver) {
console.log('set', key, value)
let oldValue = target[key]
let res = Reflect.set(target, key, value, receiver)
let hadKey = target.hasOwnProperty(key)
if(!hadKey) {
// console.log('新增属性', key)
// 更新视图
}else if(oldValue !== value) {
// console.log('修改属性', key)
// 更新视图
}
return res
}
至此,我们对象操作的拦截我们基本已经完成了,但是还有一个小问题, 我们来看看下面的操作
let obj = {
some: 'hell'
}
let proxy = reactive(obj)
let proxy1 = reactive(obj)
let proxy2 = reactive(obj)
let proxy3 = reactive(obj)
let p1 = reactive(proxy)
let p2 = reactive(proxy)
let p3 = reactive(proxy)
我们这样写,就会一直调用reactive代理对象,所以我们需要构造两个hash表来存储代理结果,避免重复代理
function reactive(target) {
return createReactiveObject(target)
}
let toProxyMap = new WeakMap()
let toRawMap = new WeakMap()
function createReactiveObject(target) {
let dep = new Dep()
if(!isObject(target)) return target
// reactive(obj)
// reactive(obj)
// reactive(obj)
// target已经代理过了,直接返回,不需要再代理了
if(toProxyMap.has(target)) return toProxyMap.get(target)
// 防止代理对象再被代理
// reactive(proxy)
// reactive(proxy)
// reactive(proxy)
if(toRawMap.has(target)) return target
const handler = {
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver)
// 递归代理
return isObject(res) ? reactive(res) : res
},
// 必须要有返回值,否则数组的push等方法报错
set(target, key, val, receiver) {
let hadKey = hasOwn(target, key)
let oldVal = target[key]
let res = Reflect.set(target, key, val,receiver)
if(!hadKey) {
// console.log('新增属性', key)
} else if(oldVal !== val) {
// console.log('修改属性', key)
}
return res
},
deleteProperty(target, key) {
Reflect.deleteProperty(target, key)
}
}
let observed = new Proxy(target, handler)
toProxyMap.set(target, observed)
toRawMap.set(observed, target)
return observed
}
function isObject(obj) {
return obj != null && typeof obj === 'object'
}
function hasOwn(obj, key) {
return obj.hasOwnProperty(key)
}
接下来就是修改数据,触发视图更新,也就是实现发布订阅,这一部分和2.0的实现部分一样,也是在get中收集依赖,在set中触发依赖
完整代码如下
class Dep {
constructor() {
this.subscribers = new Set(); // 保证依赖不重复添加
}
// 追加订阅者
depend() {
if(activeUpdate) { // activeUpdate注册为订阅者
this.subscribers.add(activeUpdate)
}
}
// 运行所有的订阅者更新方法
notify() {
this.subscribers.forEach(sub => {
sub();
})
}
}
let activeUpdate
function reactive(target) {
return createReactiveObject(target)
}
let toProxyMap = new WeakMap()
let toRawMap = new WeakMap()
function createReactiveObject(target) {
let dep = new Dep()
if(!isObject(target)) return target
// reactive(obj)
// reactive(obj)
// reactive(obj)
// target已经代理过了,直接返回,不需要再代理了
if(toProxyMap.has(target)) return toProxyMap.get(target)
// 防止代理对象再被代理
// reactive(proxy)
// reactive(proxy)
// reactive(proxy)
if(toRawMap.has(target)) return target
const handler = {
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver)
// 收集依赖
if(activeUpdate) {
dep.depend()
}
// 递归代理
return isObject(res) ? reactive(res) : res
},
// 必须要有返回值,否则数组的push等方法报错
set(target, key, val, receiver) {
let hadKey = hasOwn(target, key)
let oldVal = target[key]
let res = Reflect.set(target, key, val,receiver)
if(!hadKey) {
// console.log('新增属性', key)
dep.notify()
} else if(oldVal !== val) {
// console.log('修改属性', key)
dep.notify()
}
return res
},
deleteProperty(target, key) {
Reflect.deleteProperty(target, key)
}
}
let observed = new Proxy(target, handler)
toProxyMap.set(target, observed)
toRawMap.set(observed, target)
return observed
}
function isObject(obj) {
return obj != null && typeof obj === 'object'
}
function hasOwn(obj, key) {
return obj.hasOwnProperty(key)
}
function autoRun(update) {
function wrapperUpdate() {
activeUpdate = wrapperUpdate
update() // wrapperUpdate, 闭包
activeUpdate = null;
}
wrapperUpdate();
}
let obj = {name: 'hello', arr: [1, 2,3]}
let proxy = reactive(obj)
// 响应式
autoRun(() => {
console.log(proxy.name)
})
proxy.name = 'xxx' // 修改proxy.name, 自动执行autoRun的回调函数,打印新值
最后总结下vue2.0和3.0响应式的实现的优缺点:
Object.defineProperty
拦截对象的属性的修改,在getter中收集依赖,在setter中触发依赖更新,一次将对象递归到底拦截,性能较差, 3.0用Proxy
拦截对象,惰性递归,性能好Proxy
可以拦截数组的方法,Object.defineProperty
无法拦截数组的push
, unshift
,shift
, pop
,slice
,splice
等方法(2.0内部重写了这些方法,实现了拦截), proxy可以拦截拦截对象的新增属性,Object.defineProperty
不可以(开发者需要手动调用$set
)Object.defineProperty
支持ie8+,Proxy
的兼容性差,ie浏览器不支持