在10月5日尤大大Vue3的源代码正式发布了,闲暇之余也简单研究了下源码。
vue3 目前的版本是Pre-Alpha
,源码仓库地址vue-next,有需要的朋友可以自行下载
Vue 的核心之一就是响应式系统,通过监听数据的变化,来驱动更新视图。因此,一拿到源码,就先研究了下它的数据监听机制。
当然,在介绍数据监听知识之前,还需要了解一些其他东西
第一个就是实现数据监听的核心Proxy,第二个就是WeakMap,如果已经了解了这两个知识点,可直接看数据监听部分内容
Proxy API 简介
我们知道在vue 2.x版本中,数据监听的实现核心是defineProperty
,defineProperty在处理数组和对象时需要对应不同的方式,而在处理监听的深度时,需要递归处理对象的每一个key
,这样在一定程度上存在一些性能问题。
而 Proxy API提供了更加强大的功能
- 不仅可以代理
Object
,还能代理Array
- 提供了很多traps,包括get和set
- proxy只能代理一层
基本结构
/**
* target: 目标对象,即要被代理的对象(可以是对象、数组、函数甚至另一个Proxy)
* handler: 一个对象,其属性是当执行一个操作时定义代理的默认的行为的函数,其中包括get和set
**/
let p = new Proxy(target, handler);
get 和 set
在vue3里主要用到了Proxy里的两个traps,get和set
- get : 当读取代理对象的属性时执行
- set : 当为代理对象的属性设置值时执行
let data = { age: 1 }
let p = new Proxy(data, {
// target: 目标对象
// key:属性
// receiver:
get(target, key, receiver) {
// 读取属性时执行
console.log(target, key, receiver)
return target[key]
},
set(target, key, value, receiver) {
// 设置值时执行
console.log(target, key, receiver)
console.log('set value')
target[key] = value
}
})
p.age = 12
// set value
当然,若被代理的对象是数组,则需要修改部分代码
let data = [1,2]
let p = new Proxy(data, {
get(target, key, receiver) {
// 读取属性时执行
console.log('get value:', key)
return target[key]
},
set(target, key, value, receiver) {
// 设置值时执行
console.log('set value')
target[key] = value
return true
}
})
p.push(3)
// get value: push
// get value: length
// set value
// set value
控制台执行以上代码,会看到如上输出。 why?
那是因为当我们执行数组的push
方法时会获取数组的push
属性和length
属性,当我们为数组赋值时,我们会为数组下标2
设置值3
,同时将数组的length
设置为3
。所以我们执行了两次get和两次set。
此外,在Proxy
里我们还可以使用一个叫Reflect的东西,他的作用就是来规范我们trap的默认的行为,不需要你自己写一堆代码,是js提供的一种最"标准"的行为。使用Reflect
来修改我们的代码
let data = [1,2]
let p = new Proxy(data, {
get(target, key, receiver) {
// 读取属性时执行
console.log('get value:', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
// 设置值时执行
console.log('set value')
return Reflect.set(target, key, receiver)
}
})
p.push(3)
// get value: push
// get value: length
// set value
// set value
只能代理一层
let data = { foo: 'foo', bar: { key: 1 }, ary: ['a', 'b'] }
let p = new Proxy(data, {
get(target, key, receiver) {
console.log('get value:', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set value:', key, value)
return Reflect.set(target, key, value, receiver)
}
})
p.bar.key = 2
// get value: bar
运行以上代码,可以发现并没有执行set方法,那是因为Proxy只能代理一层。
那么现在我们就发现了使用Proxy的两个问题:
- 执行一次操作时可能触发多次set或get
- Proxy只能代理一次
关于这两个问题,我们稍后分析vue3数据监听机制时会一一解答,看看vue3是如何解决这两个问题的。
WeakMap
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
由于WeakMap的键是若引用的,所以在没有其他引用存在时垃圾回收能正确进行,这也是vue3中使用它的原因,可以防止内存泄漏。
WeakMap提供了几个方法
get(key)
: 获取某个wm对象的键值set(key,value)
: 为wm上某个键设置值has(key)
: 判断wm上的某个键是否有值,返回一个Boolean
值
var wm = new WeakMap();
wm.set(window, "foo");
wm.get(window); // 返回 "foo".
wm.get("baz"); // 返回 undefined.
wm.has(window) // true
vue3 中的数据监听
vue3 中的源码采用了 TS 的形式。而我们的数据监听主要在vue-next\packages\reactivity\src\reactive.ts
文件。
reactive.ts 文件提供了 reactive 函数,该函数是实现响应式的核心
// 这里对源码进行了一定程度的简化
//
const rawToReactive: WeakMap<any, any> = new WeakMap() // 用来存放代理数据的对象
const reactiveToRaw: WeakMap<any, any> = new WeakMap() // 用来存放原始数据的对象
// reactive函数
export function reactive(target: object) {
// 这里有一些处理只读属性的逻辑,这里省略
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
// 创建Reactive的方法,在这里执行new Proxy()
/**
*
* @param target 目标对象
* @param toProxy 保存proxy对象的weakMap
* @param toRaw 保存原始数据的WeakMap
* @param baseHandlers Proxy的handler
* @param collectionHandlers Proxy的handler
*/
function createReactiveObject(
target: any,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 目标对象已经被代理
// target already has corresponding Proxy
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// 目标对象已经是一个被代理的对象
// target is already a Proxy
if (toRaw.has(target)) {
return target
}
// 是否可被observe
// only a whitelist of value types can be observed.
if (!canObserve(target)) {
return target
}
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// 执行Proxy
observed = new Proxy(target, handlers)
// 保存proxy对象和原始对象
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}
我们先来分析以上代码,首先,rawToReactive
和reactiveToRaw
是两个weakMap类型的结构,分别保存了被代理的数据和原始的数据,这两个值会作为参数传给createReactiveObject
函数
我们可以通过这两个结构确定传入的target
是否被代理或者是否是一个proxy对象,之后执行new Proxy()
方法,这里的重点就是传入Proxy的baseHandlers
对象。
// 这里简化部分源码
// 这里的mutableHandlers就是传入Proxy的baseHandlers对象
export const mutableHandlers: ProxyHandler<any> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
// createGetter方法
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
const res = Reflect.get(target, key, receiver)
return isObject(res)
reactive(res)
: res
}
}
// 判断key是否是val的属性
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
val: object,
key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
// 返回被监听对象的原始数据
export function toRaw<T>(observed: T): T {
return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
}
// set方法
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
value = toRaw(value)
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 如果目标在原型链上,不要触发,
// 如果receiver存在于taRaw里,即receiver是proxy,即不再原型链上
if (target === toRaw(receiver)) {
/* istanbul ignore else */
// 如果没有属性值,则执行add方法
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) { // 否则如果新旧值不同,则执行SET方法
trigger(target, OperationTypes.SET, key)
}
// 通过以上判断可以解决数组重复执行set问题
}
return result
}
回到之前我们遇到过的问题
- proxy只能代理一层的问题
- 重复执行set的问题
先看第一个问题
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
const res = Reflect.get(target, key, receiver)
return isObject(res)
reactive(res)
: res
}
}
可以看到这里判断了 Reflect 返回的数据是否还是对象,如果是对象,则再走一次 proxy ,从而获得了对对象内部的侦测,这样就解决了只能代理一层的问题。
注意proxy在这里的处理并不是递归。而是在使用这个数据的时候会返回多个res,这时候执行多次reactive,在vue 2.x中先递归整个data,并为每一个属性设置set和get。vue3中使用proxy则在使用数据时才设置set和get.
并且,每一次的 proxy 数据,都会保存在 Map 中,访问时会直接从中查找,从而提高性能。
举个🌰
假设现在传入 reactive 函数的数据是
const origin = {
count: 0,
a: { b: 0}
}
const state = reactive(origin)
当我对这个对象内部属性操作时,例如 state.a.b = 6
,这个时候,get 会被触发两次,而 Reflect.get 会返回两个 res ,分别是data 的内层结构 {b: 0}
和 0
,这两个res,若是对象会重新 new Proxy
来代理,并且存入 map 中。
如果是递归proxy,那么 data 的每一层都会是 proxy 对象。而这里,proxy 对象是两个 {b: 0}
和{count: 0, a: { b: 0}}
,每个代理对象只有外层是 proxy 的。
再来看第二个问题
if (!hadKey) {
// 如果没有属性值,则执行add方法
console.log('trigger add')
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) { // 否则如果新旧值不同,则执行SET方法
console.log('trigger set')
trigger(target, OperationTypes.SET, key)
}
例如执行
let data = ['a', 'b']
let r = reactive(data)
r.push('c')
// 打印一次 trigger add
执行r.push('c')
,会触发两次set
,第一次是设置新值'c'
,第二次修改数组的length
。
当第一次触发时,这时侯key
是2
,hadKey
为false
,所以打印trigger add
当第二次触发时,这时候key
是length
,hadKey
为true
,oldValue
为3
,value
为3,所以只执行一次trigger.