前言
Vue3.0 的响应式底层是使用了 new Proxy()
对数据的 getter
和 setter
进行了拦截,过程中进行了依赖的收集,如果数据发生了变化,就会通知相应的依赖去做变化。如果了解过 Vue2.0 响应式原理应该就知道,Vue2.0的响应式底层是用 Object.defineProperty
进行实现的。为什么会有如此大的变化?这篇文章将通过学习 Vue2.0 和 Vue3.0 的源码比较两者在响应式实现上的差异。
响应式源码
Vue2.0
- 源码位置
\src\core\observer\index.js
- 响应式函数
defineReactive
,初始化时需要循环频繁调用,因为Object.defineProperty
每次只能对对象中的一个属性进行拦截操作,并且如果当前属性的值val
也是一个对象就会调用一个观察者函数observe(val)
,为对象创建观察者
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建依赖
const dep = new Dep()
...
// 如果属性还是对象,递归observe
let childOb = !shallow && observe(val)
// 数据拦截操作
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 收集依赖
dep.depend()
if (childOb) {
//递归收集依赖
childOb.dep.depend()
...
}
}
return value
},
set: function reactiveSetter (newVal) {
...
// 通知依赖更新
dep.notify()
}
})
}
复制代码
- 观察者函数
observe
中会创建并返回一个ob
是Observer
类的实例
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
...
// 观察者对象
ob = new Observer(value)
...
return ob
}
复制代码
Observer
类会执行其中的walk
方法,walk
会对当前对象进行遍历并继续调用defineReactive
这样就形成了一个递归调用
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 数组
...
} else {
// 如果是对象
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
// 循环当前对象进行响应式拦截
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
...
}
复制代码
- 到这里就看出了使用
Object.defineProperty
方式进行响应式操作的一些缺点- 每次只能监控一个key
- 初始化时需要循环递归遍历obj中的所有key,速度慢,并且会产生闭包,资源占用比较大
- 不能对 Collection 类型(set map)的数据进行响应式的监听
Vue2对于
Object.defineProperty
一些先天性的缺陷做了一些兼容操作
- 无法检测动态属性新增和删除,新增了
set
和del
方法
export function set (target: Array<any> | Object, key: any, val: any): any {
...
// 对新增属性重新定义响应式
defineReactive(ob.value, key, val)
// 通知依赖更新
ob.dep.notify()
return val
}
export function del (target: Array<any> | Object, key: any) {
...
// 手动删除属性
delete target[key]
if (!ob) {
return
}
// 通知依赖更
ob.dep.notify()
}
复制代码
- 无法检测调用 Array 上的方法对数组进行的改变,比如
push pop
等,Vue2通过对Array.prototype
数组原型上的方法进行重写实现响应式拦截(但是对 数组下标 的操作还是无法监听到,还是需要使用set
)
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 拦截原型上的方法并派发事件
methodsToPatch.forEach(function (method) {
// cache original 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
})
})
...
// Observer类
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 包装之前重写的数组原型
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 给数组元素添加响应式
this.observeArray(value)
} else {
this.walk(value)
}
}
...
// 给数组元素添加响应式
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
复制代码
Vue3.0
由于 Object.defineProperty
存在的这些缺点,Vue3.0使用了 Proxy
去做响应式,自然就解决了这些问题,但是 Proxy
不支持IE11及以下版本
- 在之前的文章 Vue3.0源码学习——响应式原理(一) 中学习了,如果在响应式API
reactive
中传入的是普通对象,最终使用的是一个Proxy
,并在对应的 get、set、deleteProperty、has、ownKeys 中进行响应式的拦截操作
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
...
// 创建代理对象,将传入的原始对象作为代理目标
const proxy = new Proxy(
target,
// 如果代理的是普通对象,handler使用的是baseHandlers,就是之前传入的mutableHandlers
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
...
// handler
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
复制代码
小结
- 实现方式
- Vue2:Object.defineProperty(obj, key, {})
- Vue3:new Proxy(obj, {})
Object.defineProperty
与Proxy
的优缺点- defineProperty方式每次只能监控一个key,初始化时需要循环递归遍历obj中的所有key,速度慢,资源占用大,闭包
- defineProperty无法检测动态属性新增和删除
- defineProperty无法很好的支持数组,需要额外的数组响应式实现
- Vue2无法支持Collection类型:set、map
- Proxy不支持IE11及以下版本
- https://juejin.cn/post/7067912794808516638