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 } = VueReactivityvar 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'}// 对外暴露的创建响应式对象的APIexport 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}
// 主要用于追踪收集数据变化,并放入到targetMapfunction 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获取存在数据,执行对应的effectfunction 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 reactivestate.foo++// ...but does not convert nested objectsisReactive(state.nested) // falsestate.nested.bar++ // non-reactive
复制代码
shallowReadonly
创建一个代理,使其自身的属性为只读,但不执行嵌套对象的深度只读转换。与 readonly 不同,使用的任何属性 ref 都不会被代理自动解包
const state = shallowReadonly({ foo: 1, nested: { bar: 2 }})
// mutating state's own properties will failstate.foo++// ...but works on nested objectsisReadonly(state.nested) // falsestate.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。
评论