Vue3 优点
在深入阅读理解 Vue3 响应式系统前,我们首先要知道的是 Vue3 是通过 Proxy
进行数据双向绑定,而 Vue2 采用的是 Object.defineProperty
。那 Proxy
相对于 Object.defineProperty
特性存在的优劣性对比如下:
Proxy
是对整个对象的代理,而 Object.defineProperty
只能代理某个属性。
对象上新增属性,Proxy
可以监听到,Object.defineProperty
不能。
数组新增修改,Proxy
可以监听到,Object.defineProperty
不能。
若对象内部属性要全部递归代理,Proxy
可以只在调用的时候递归,而 Object.definePropery
需要一次完成所有递归,性能比 Proxy
差。
Proxy
不兼容 IE,Object.defineProperty
不兼容 IE8 及以下
Proxy
使用上比 Object.defineProperty
方便多。
如果你对上面两个用法不了解,可以先阅读下这篇文章:Proxy与Object.defineProperty的用法与区别
接下来,正式阅读 Vue3 的响应式相关实现的代码。
PS:近期正在阅读 Vue3 源码,将陆续推出序列文章,感兴趣,点个关注,一起学习~
源码目录说明
这是从 GitHub 上下载下来的 Vue3 源码目录结构,画红色边框的 reactivity 目录是实现响应式的代码包,它可以被单独构建作为独立的包使用。
下面对 reactivity 目录主要文件进行说明:
__tests__:描述的是代码包相关的测试用例。
index.ts:用于导出包相关实现方法。
reactive.ts:描述的是采用 Proxy 去代理对象,并进行劫持操作。
原理是:用 Proxy 创建代理对象,并在 Proxy 代理对象执行 get 陷阱函数读值的时候进行 track
操作,执 行 set 陷阱函数写值的时候进行 trigger
操作。注意这里是只针对对象的代理,不能对基本数据类型。
refs.ts:主要描述的是如何解决基本数据类型代理的问题。
原理是:利用对象本身的 get
| set
函数,并在 get
函数进行 track
操作,set
函数进行trigger
操作。
computed.ts:描述的是计算属性的实现。实际是带有 lazy
属性的 effect
。
effect.ts:描述如何跟踪属性的变化并执行回调函数。
baseHandlers.ts:对 Object、Array 数据类型进行代理指定的自定义拦截行为。
collectionHandlers.ts:对 Set, Map, WeakMap, WeakSet 数据类型进行代理指定的自定义拦截行为。
使用案例
这里来看两种场景,一种直接使用 vue3 库,另一种使用 reactivity 目录单独构建的包。
场景一:Vue3 库
// 直接使用vue3库
import { reactive, effect } from 'vue3.js'
const obj = reactive({ x: 1 })
effect(() => {
patch()
})
setTimeout(() => {
obj.x = 2
}, 1000)
function patch() {
document.body.innerText = obj.x
}
复制代码
很显然,1s 后,页面显示内容变为 2。说明 reactive 对 obj 对象的 setter 方法进行了劫持,当赋新值的时候触发了 effect 函数。
场景二:reactivity 包
构建 reactivity 库,可以下载 vue-next 的 vue3 源码,然后在根目录执行如下命令,这里我采用的是 yarn。
// 安装依赖
yarn install
// 执行此命令开启构建,构建完成,会在reactivity包下生成dist目录,里面是reactivity.glob.js文件
// 该文件暴露了全局的VueReactivity对象,里面集成了包暴露的所有方法以及配置对象
// ps:为了方便查看targetMap是什么,我自个新增了该对象的暴露
yarn dev reactivity
复制代码
下面看看案例代码,实现效果与场景一是一样的。
// VueReactivity 是 reactivity目录单独构建暴露的全局变量
const { effect, track, trigger, targetMap } = VueReactivity
var obj = {
x: 1
}
effect(() => {
patch();
// 这里 track 只能放在effect里面,track需要使用effect内的activeEffect
track(obj, 'get', 'x');
console.log(targetMap, 'targetMap')
})
setTimeout(() => {
obj.x = 2;
trigger(obj, 'set', 'x')
}, 1000)
function patch() {
document.body.innerText = obj.x
}
复制代码
reactive 源码分析
reactive 用于创建响应式对象,原理是,通过 Proxy 对目标对象 target 进行代理,进行数据操作劫持。下面看下源码,具体实现:
// 枚举了一些响应式对象的类型标识,不同类型采用不同的劫持方式
export const enum ReactiveFlags {
// 标志SKIP,则此对像永远不会被转为代理
SKIP = '__v_skip',
// 是reactive处理的代理对象
IS_REACTIVE = '__v_isReactive',
// 只读代理
IS_READONLY = '__v_isReadonly',
// 原始对象
RAW = '__v_raw',
REACTIVE = '__v_reactive',
READONLY = '__v_readonly'
}
// 对外暴露的创建响应式对象的API
export function reactive(target: object) {
// 如果是只读代理,则直接返回
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
// 创建响应式对象
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
// 只读代理,这里不过多解读
export function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
true,
readonlyHandlers,
readonlyCollectionHandlers
)
}
复制代码
/*
* 返回对象是否可以观察:
* 1. 标志了SKIP状态的对象,用于不会被转换为代理,对应API是,markRaw
* 2. 对象类型属于其中一种:Object,Array,Map,Set,WeakMap,WeakSet
* 3. 对象非冻结,冻结的对象无法被修改的
*/
const canObserve = (value: Target): boolean => {
return (
!value[ReactiveFlags.SKIP] &&
isObservableType(toRawType(value)) &&
!Object.isFrozen(value)
)
}
// 这里会根据不同的参数创建不同类型的代理
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 非对象不能返回,不能进行代理劫持,直接返回
if (!isObject(target)) {
return target
}
// 已经是代理,直接返回,不能重复劫持
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 已经具有相应的代理[reactive/readonly],则返回
const reactiveFlag = isReadonly
? ReactiveFlags.READONLY
: ReactiveFlags.REACTIVE
if (hasOwn(target, reactiveFlag)) {
return target[reactiveFlag]
}
// 判断目标是否可进行代理劫持
if (!canObserve(target)) {
return target
}
// 目标对象设置代理
const observed = new Proxy(
target,
// 如果是Set, Map, WeakMap, WeakSet集合数据类型,走collectionHandlers
// 基础数据类型Object/Array 走 baseHandlers
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
)
def(target, reactiveFlag, observed)
// 返回代理对象
return observed
}
复制代码
到此处的代码,主要讲述的是通过 reactive 创建响应式对象,对入参的必要满足条件,总结下有如下几点:
非对象不能进行代理劫持。
目标已经被劫持,则返回,不能重复劫持。
只读劫持。
对象__v_skip 属性非真,且数据类型属于Object,Array,Map,Set,WeakMap,WeakSet
其中一种,并且对象非冻结状态,因为对象被冻结后不能被修改。
根据不同数据类型选择不同的劫持方式。
集合类型Set, Map, WeakMap, WeakSet,
走collectionHandlers
;
基础数据类型Object,Array
走 baseHandlers
。
能够传递一个正确的对象,下面看看如何通过代理对对象进行数据劫持操作,才能达到响应式的效果。这里以 baseHandlers
为例。
首先 baseHandlers
是对接口 ProxyHandler
的实现,先看看接口都定义了什么。
interface ProxyHandler<T extends object> {
getPrototypeOf? (target: T): object | null;
setPrototypeOf? (target: T, v: any): boolean;
isExtensible? (target: T): boolean;
preventExtensions? (target: T): boolean;
getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined;
has? (target: T, p: PropertyKey): boolean;
get? (target: T, p: PropertyKey, receiver: any): any;
set? (target: T, p: PropertyKey, value: any, receiver: any): boolean;
deleteProperty? (target: T, p: PropertyKey): boolean;
defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean;
ownKeys? (target: T): PropertyKey[];
apply? (target: T, thisArg: any, argArray?: any): any;
construct? (target: T, argArray: any, newTarget?: any): object;
}
复制代码
其实这里包含的是 Proxy 基本所有的陷阱函数。 Proxy 可以拦截 JavaScript 引擎内部目标的底层对象操作,这些操作被拦截后会触发响应特定操作的陷阱函数。
看看 mutableHandlers 具体实现,挑选基本的 get/和 set 理解
const set = /*#__PURE__*/ createSetter()
const get = /*#__PURE__*/ createGetter()
// 劫持的处理程序
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
// 主要用于追踪收集数据变化,并放入到targetMap
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
// 标志状态相关处理,并没有进行收集
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? (target as any)[ReactiveFlags.READONLY]
: (target as any)[ReactiveFlags.REACTIVE])
) {
return target
}
// 数组相关处理
const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 通过反射获取对象的默认行为(属于语言内部的方法),不管Proxy怎么改都没用
const res = Reflect.get(target, key, receiver)
// key是Symbol类型,原型链,或者是Ref引用类型标志,则返回,不追踪
if (
isSymbol(key)
? builtInSymbols.has(key)
: key === `__proto__` || key === `__v_isRef`
) {
return res
}
// 追踪属性变化,这里会放入到targetMap,在effect文件里面声明的存储追踪数据的weakMap变化
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 浅层代理(如,shallowReactive),不做递归,并且ref引用没有做拆包,因为直接return。
if (shallow) {
return res
}
// 引用对象,如果是数组,则返回数组,如果是对象,则返回值。
// 非浅层代理(reactive),则做了ref的拆包
if (isRef(res)) {
return targetIsArray ? res : res.value
}
// 如果是对象,则递归进行响应式处理
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
// 用于触发依赖,从targetMap获取存在数据,执行对应的effect
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
if (!shallow) {
value = toRaw(value)
// 引用类型Ref的处理
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// 在浅层模式下,不管是否响应式,对象都按原样设置
}
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 如果目标是原型链中的某个东西,就不要触发
if (target === toRaw(receiver)) {
// 根据key是否存在,去触发对应的操作
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
复制代码
看了上面 get|set 两个代码块实现,我们可以总结出,在实现响应式过程中的结论:
在 get 陷阱函数中,对不同类型属性做对应处理,并进行数据的 track 操作(追踪收集缓存)
在 set 陷阱函数中,根据 key 类型以及是否存在做对应处理,并执行 trigger 操作,会触发 effect 方法
因为这里的 track
和trigger
方法是在 effect.ts 文件中,那这里就不做解决,放到下一篇再讲。
其他源码说明
1. 响应式对象
我们知道 Vue3 能创建多种类型的响应式对象,这些响应式对象是如何定义的呢。如下可以看出其他类型的响应式对象的定义也是使用采用方法 createReactiveObject
,只是采用了不一样的处理程序。
export function shallowReactive<T extends object>(target: T): T {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers
)
}
// 第二个参数,表示的是是否采用浅层shallow模式,
// shallow=true,任务属性ref都不会自动解包,具体看代码
const shallowGet = /*#__PURE__*/ createGetter(false, true)
// 第一个参数,表示的是是否采用浅层shallow模式,
const shallowSet = /*#__PURE__*/ createSetter(true)
复制代码
shallowReactive
创建一个响应式对象,只能跟踪自身属性的变化,不对嵌套对象进行响应式处理。与 reactive
不同,使用的任何属性 ref
都不会被代理自动解包。因为采用的是shallow = true
的模式。
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// mutating state's own properties is reactive
state.foo++
// ...but does not convert nested objects
isReactive(state.nested) // false
state.nested.bar++ // non-reactive
复制代码
shallowReadonly
创建一个代理,使其自身的属性为只读,但不执行嵌套对象的深度只读转换。与 readonly
不同,使用的任何属性 ref
都不会被代理自动解包
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
// mutating state's own properties will fail
state.foo++
// ...but works on nested objects
isReadonly(state.nested) // false
state.nested.bar++ // works
复制代码
2. 其他 API
直接看下 API 源码实现。
// 检测对象是否是由reactive创建的
export function isReactive(value: unknown): boolean {
if (isReadonly(value)) {
return isReactive((value as Target)[ReactiveFlags.RAW])
}
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}
// 检测对象是否是只读代理
export function isReadonly(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}
// 检测对象是否代理
export function isProxy(value: unknown): boolean {
return isReactive(value) || isReadonly(value)
}
// 返回reactive或readonly代理的原始对象
export function toRaw<T>(observed: T): T {
return (
(observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed
)
}
// 标记一个对象,使其永远不会转换为代理
export function markRaw<T extends object>(value: T): T {
def(value, ReactiveFlags.SKIP, true)
return value
}
复制代码
总结
至此大概阅读了下 reactive 主要核心的相关源码,这里以简易代码描述下其核心实现过程:
const reactive = (target){
// 代理数据
return new Proxy(target, {
get(target, prop) {
// 执行追踪,数据放入targetMap
track(target, prop);
return Reflect.get(target, prop);
},
set(target, prop, newVal) {
Reflect.set(target, prop, newVal);
// 触发effect
trigger(target, prop);
return true;
}
})
}
复制代码
解读如有不正确的地方,欢迎指正。下一篇预告:解读 track、trigger 和 effect。
评论