Vue3 源码解析系列 - 响应式原理(reactive 篇) reactive 核心入口 话不多说,我们先来看下核心的 reactive
的源码,先看下有哪些依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { isObject, toTypeString } from "@vue/shared" ;import { mutableHandlers, readonlyHandlers } from "./baseHandlers" ;import { mutableCollectionHandlers, readonlyCollectionHandlers, } from "./collectionHandlers" ; import { ReactiveEffect } from "./effect" ;import { UnwrapRef , Ref } from "./ref" ;import { makeMap } from "@vue/shared" ;
可以很清楚的看到,重点依赖项就是那一堆 handler
了,其他都是一些工具方法和泛型类型
接下来的源码里是一堆变量的定义,不过我们先跳过,先来看下 reactive
的方法和类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 export type Dep = Set <ReactiveEffect >export type KeyToDepMap = Map <string | symbol, Dep >const canObserve = (value : any): booleantype UnwrapNestedRefs <T> = T extends Ref ? T : UnwrapRef <T> export function reactive<T extends object>(target : T): UnwrapNestedRefs <T>export function readonly<T extends object>( target : T ): Readonly <UnwrapNestedRefs <T>> function createReactiveObject ( target: any, toProxy: WeakMap <any, any>, toRaw: WeakMap <any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ): anyexport function isReactive (value: any ): booleanexport function isReadonly (value: any ): booleanexport function toRaw<T>(observed : T): Texport function markReadonly<T>(value : T): Texport function markNonReactive<T>(value : T): T
看完方法和类型,大致有以下几个问题:
Dep
依赖是如何追踪的?
UnwrapRef
是如何展开嵌套的响应式数据类型的(俗称解套),比如 reactive({ name: reactive(ref('Jooger')) })
如何判断是否是(只读的)响应式数据?往响应式数据上面挂载一个标记位来标记?还是用一个对象来存储响应式数据?
如何将响应式数据转化为原始数据?proxy 数据如何转化成 object?
问题 1,后面的 effect
会讲,这里先不讨论
问题 2,后面的 ref
会讲,这里先不讨论
问题 3,4 就需要看下我刚才跳过的一堆变量的定义了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 export const targetMap = new WeakMap <any, KeyToDepMap >()const rawToReactive = new WeakMap <any, any>()const reactiveToRaw = new WeakMap <any, any>()const rawToReadonly = new WeakMap <any, any>()const readonlyToRaw = new WeakMap <any, any>()const readonlyValues = new WeakSet <any>()const nonReactiveValues = new WeakSet <any>()const collectionTypes = new Set <Function >([Set , Map , WeakMap , WeakSet ])const isObservableType = makeMap ( ['Object' , 'Array' , 'Map' , 'Set' , 'WeakMap' , 'WeakSet' ] .map (t => `[object ${t} ]` ) .join (',' ) )
看完上面这些变量定义,我们来解答一下问题 3,4:
问题 3:如何判断是否是(只读的)响应式数据?往响应式数据上面挂载一个标记位来标记?还是用一个对象来存储响应式数据?
用 readonlyToRaw
来存储只读响应式数据的,参见下面代码:
1 2 3 export function isReadonly (value: any ): boolean { return readonlyToRaw.has (value); }
问题 4:如何将响应式数据转化为原始数据?proxy 数据如何转化成 object?
用 reactiveToRaw
和 readonlyToRaw
来存储响应式数据 -> 原始数据
的映射关系,然后:
1 2 3 export function toRaw<T>(observed : T): T { return reactiveToRaw.get (observed) || readonlyToRaw.get (observed) || observed; }
总的来讲,就是利用各种集合来存储原始数据和响应式数据的映射关系,以便快速根据这种映射关系拿到对应的数据。
再回头看下 canObserve
方法,来看看到底有哪些数据是可以观察的:
1 2 3 4 5 6 7 8 9 10 11 12 const canObserve = (value : any): boolean => { return ( !value._isVue && !value._isVNode && isObservableType (toTypeString (value)) && !nonReactiveValues.has (value) ); };
相比于 Vue2
的是否可观察判断,则少了很多条件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (hasOwn (value, "__ob__" ) && value.__ob__ instanceof Observer ) { ob = value.__ob__ ; } else if ( shouldObserve && !isServerRendering () && (Array .isArray (value) || isPlainObject (value)) && Object .isExtensible (value) && !value._isVue ) { ob = new Observer (value); }
接下来就讲一下重点的 reactive
和 readonly
这两个核心方法的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 export function reactive (target: object ) { if (readonlyToRaw.has (target)) { return target } if (readonlyValues.has (target)) { return readonly (target) } return createReactiveObject ( target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers ) } export function readonly<T extends object>( target : T ): Readonly <UnwrapNestedRefs <T>> { if (reactiveToRaw.has (target)) { target = reactiveToRaw.get (target) } return createReactiveObject ( target, rawToReadonly, readonlyToRaw, readonlyHandlers, readonlyCollectionHandlers ) }
其实我们看 Vue3
的源码会发现,很多入口方法都变得短小精简,不像 Vue2
里的一些 exposed function 那样写的很长,这两个核心方法也一样,逻辑很简单,主要是进行一些原始数据检查和转换,核心实现逻辑都是放在 createReactiveObject
里的
下面继续看下核心实现方法 createReactiveObject
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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; } let observed = toProxy.get (target); if (observed !== void 0 ) { return observed; } if (toRaw.has (target)) { return target; } if (!canObserve (target)) { return target; } const handlers = collectionTypes.has (target.constructor ) ? collectionHandlers : baseHandlers; observed = new Proxy (target, handlers); toProxy.set (target, observed); toRaw.set (observed, target); if (!targetMap.has (target)) { targetMap.set (target, new Map ()); } return observed; }
我们可以看到:
目前来看 reactive
和 readonly
的区别仅有两点:映射关系存储集合不同 and proxy handler
不同
object``array
和集合类型 Set``Map``WeakSet``WeakMap
的 proxy handler
是不同的
所以下面再来依次看下响应式核心中的核心 - 各种 proxy handler
baseHandler 方法跟上面一样,先看依赖:
1 2 3 4 5 6 7 8 9 10 11 12 import { reactive, readonly, toRaw } from "./reactive" ;import { OperationTypes } from "./operations" ;import { track, trigger } from "./effect" ;import { LOCKED } from "./lock" ;import { isObject, hasOwn, isSymbol } from "@vue/shared" ;import { isRef } from "./ref" ;
这里有两个疑问:
track
和 trigger
的实现
LOCKED
的作用?为什么会有这个全局锁?
问题 1 在后面的 effect
部分会讲到,现在只需要知道是用来追踪依赖和触发依赖回调方法就行
问题 2 现在我也不是特别了解,只知道是在组件 mount
和 update
的时候会对组件的 props
的代理进行修改,因为我们都知道单向数据流中,子组件内部是不能更改 props
的,但是子组件更新,进行 vnode patch 后需要更新子组件的 props
,包括一些动态 props
再来看下变量和方法概览:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 const builtInSymbols = new Set ( Object .getOwnPropertyNames (Symbol ) .map (key => (Symbol as any)[key]) .filter (isSymbol) ) function createGetter (isReadonly: boolean ) {}function set ( target: any, key: string | symbol, value: any, receiver: any ): booleanfunction deleteProperty (target: any, key: string | symbol ): booleanfunction has (target: any, key: string | symbol ): booleanfunction ownKeys (target: any ): (string | number | symbol)[]export const mutableHandlers : ProxyHandler <any> = { get : createGetter (false ), set, deleteProperty, has, ownKeys } export const readonlyHandlers : ProxyHandler <any> = { get : createGetter (true ), set (target : any, key : string | symbol, value : any, receiver : any): boolean { if (LOCKED ) { if (__DEV__) { console .warn ( `Set operation on key "${String (key)} " failed: target is readonly.` , target ) } return true } else { return set (target, key, value, receiver) } }, deleteProperty (target : any, key : string | symbol): boolean { if (LOCKED ) { if (__DEV__) { console .warn ( `Delete operation on key "${String ( key )} " failed: target is readonly.` , target ) } return true } else { return deleteProperty (target, key) } }, has, ownKeys }
可以看到,mutableHandlers
和 readonlyHandlers
都是定义了 5 个 trap 方法:get
、set
、deleteProperty
、has
、ownKeys
,前 3 个不用多家介绍,has
trap 针对与 in
操作符,而 ownKeys
针对于 for in
和 Object.keys
这些遍历操作的
而 readonlyHandlers
相比于 mutableHandlers
其实只是在 get
、set
和 deleteProperty
这三个 trap 方法里有区别,而对于可能改变数据的 set
和 deleteProperty
方法,则是利用 LOCKED
来锁定,不让修改数据,这个变量我在上面也提了一下
下面来一个一个的看下各个 trap 方法
get 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 function createGetter (isReadonly: boolean ) { return function get (target: any, key: string | symbol, receiver: any ) { const res = Reflect .get (target, key, receiver); if (isSymbol (key) && builtInSymbols.has (key)) { return res; } if (isRef (res)) { return res.value ; } track (target, OperationTypes .GET , key); return isObject (res) ? isReadonly ? readonly (res) : reactive (res) : res; }; }
看完 get trap
其实很简单,但是也会有写疑问:
1. 为什么用 Reflect.get
,而不是直接 target[key]
返回呢? 我们可以看它的第三个参数:
receiver:如果 target 对象中指定了 getter,receiver 则为 getter 调用时的 this 值
举个例子:
1 2 3 4 5 6 7 8 const target = { foo : 24 , get bar () { return this .foo ; }, }; const observed = reactive (target);
此时,如果不用 Reflect.get
,而是 target[key]
,那么 this.foo
中的 this
就指向的是 target
,而不是 observed
,此时 this.foo
就不能收集到 foo
的依赖了,如果 observed.foo = 20
改变了 foo 的值,那么是无法触发依赖回调的,所以需要利用 Reflect.get
将 getter 里的 this
指向代理对象
2. 为什么在结尾 return 的时候还要调用 reactive
或者 readoonly
呢? 原注释是这样写的:
need to lazy access readonly and reactive here to avoid circular dependency 翻译过来是:需要延迟地使用 readonly 和 readtive 来避免循环引用
为什么这样说呢?这里不得不说一下 Proxy
的特性:只能代理一层,对于嵌套的深层对象,如果不按源码中的方法,那就需要一层层递归来代理劫持对象,即每次递归都判断是否是对象,如果是对象,那么再调用 reactive
来响应式化
但是问题又来了,JS 里是有循环引用这个概念的,就像下面这样:
1 2 3 4 5 const a = { b : {}, }; a.b .c = a;
这样的话,如果每次递归调用 reactive
的话,会造成调用栈溢出 Maximum call stack size exceeded
,但是我们只需要加上一个判断条件即可解决,在上面解析的 createReactiveObject
方法里我们知道如果原始数据已经被观察过,则直接返回对应的响应式数据,那么我们可以在递归调用 reactive
的时候判断 toProxy.get(target)
是否存在,如果存在就不往下递归了,我写了一个例子代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const target = { b : { c : 1 } };target.b .d = target; const rawToReactive = new WeakMap ();function reactive (data ) { const observed = new Proxy (data, { get (target, key, receiver ) { const res = Reflect .get (target, key, receiver); const observed = rawToReactive.get (res); return observed || res; }, }); rawToReactive.set (data, observed); for (let key in data) { const child = data[key]; if (typeof child === "object" && !rawToReactive.get (child)) { reactive (child); } } return observed; } const p = reactive (target);console .log (p.b .c ); console .log (p.b .d .b );
可以去看下我在 vue3 响应式源码解析-Reactive 篇 这篇文章下的评论部分
而源码中的 lazy access
方式很取巧,只代理一层,当用到某个属性值对象时,再进行响应式观察这一层
所以相比于初始化时递归劫持,延迟访问劫持的方式更能提升初始化性能 ,也有利于对数据劫持做更细的控制,特别是针对于数据对象比较大时(比如接口返回数据嵌套过深),有些数据并非需要劫持,所以按需劫持代理我们用到的数据这种方式更好
set 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 function set ( target: any, key: string | symbol, value: any, receiver: any ): boolean { value = toRaw (value); const oldValue = target[key]; if (isRef (oldValue) && !isRef (value)) { oldValue.value = value; return true ; } const hadKey = hasOwn (target, key); const result = Reflect .set (target, key, value, receiver); if (target === toRaw (receiver)) { if (__DEV__) { const extraInfo = { oldValue, newValue : value }; if (!hadKey) { trigger (target, OperationTypes .ADD , key, extraInfo); } else if (value !== oldValue) { trigger (target, OperationTypes .SET , key, extraInfo); } } else { if (!hadKey) { trigger (target, OperationTypes .ADD , key); } else if (value !== oldValue) { trigger (target, OperationTypes .SET , key); } } } return result; }
同样,set trap
看起来也很简单,但是同时也会有一些问题:
1. target === toRaw(receiver)
是什么鬼逻辑? 首先看下
handler.set()developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set#%E5%8F%82%E6%95%B0
的关于第三个参数的说明:
最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是 proxy 本身)。 比如,假设有一段代码执行 obj.name = “jen”,obj 不是一个 proxy 且自身不含 name 属性,但它的原型链上有一个 proxy,那么那个 proxy 的 set 拦截函数会被调用,此时 obj 会作为 receiver 参数传进来
上面已经给出例子了,这里我再写一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const child = { name : "child" };const parent = new Proxy ( { name : "parent" }, { set (target, key, value, receiver ) { console .log (target, receiver); return Reflect .set (target, key, value, receiver); }, } ); Object .setPrototypeOf (child, parent);child.foo = 1 ;
这里有两个先决条件:
child 的原型链是一个 Proxy
child 在设置值的时候,本身不包含 key 的
可以看到,当满足上面两个条件的时候,设置 child 的值,会触发原型链上的 set trap
方法,并且 target
是原型链数据,而 receiver
则是真实数据
所以,源码中的那个条件逻辑也就不难看懂了,当满足上述两个条件时,我们当然不希望触发 parent 的 set trap
了
2. 像数组的 unshift
,splice
这些操作是如何触发 set trap
方法的呢? 1 2 3 4 5 6 7 8 console .log ( !hadKey ? "add" : value !== oldValue ? "set" : "unknow" , target, key, value, oldValue );
然后
1 2 3 4 5 6 7 8 9 a = reactive ([1 , 2 , 3 ]); a.unshift (0 );
一共打印了 5 次,根据打印内容我们可以看到 unshift
的实际操作过程,即把数组的每一项依次都往后移动一位,然后再把首位设置成 0
,至于为什么这么操作,
ECMA-262 Array.property.unshiftwww.ecma-international.org/ecma-262/6.0/#sec-array.prototype.unshift
标准中有原理介绍,我就不赘述了,还有像 shift
和 splice
也是一样的操作步骤
可以看到 unshift
或者 splice
是会带来多次的 trigger
的,当然这些会有批量跟新优化的,有时间我再展开讲一下
细心的同学可能会发现,还触发了 length
属性的 set,而且 value
和 oldValue
是一样的,那么根据源码所示,就不会触发 set 类型的回调了呀,那我们如果在 template 里用到了 a.length
那也不会更新了么?
肯定是会更新的,解决办法就在 trigger
这个方法里,后续 effect
部分会讲到,先简单说一下,对于会导致数组 length
改变的操作,比如 add 和 delete,在 effect
的 trigger
方法里会单独处理,来触发 length
属性的依赖回调的
其他 trap 方法 还有 deleteProperty
、has
和 ownKeys
这几个 trap,代码不多,都很简单,直接看下面的源码就能明白,我就不在赘述了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function deleteProperty (target: any, key: string | symbol ): boolean { const hadKey = hasOwn (target, key); const oldValue = target[key]; const result = Reflect .deleteProperty (target, key); if (result && hadKey) { if (__DEV__) { trigger (target, OperationTypes .DELETE , key, { oldValue }); } else { trigger (target, OperationTypes .DELETE , key); } } return result; } function has (target: any, key: string | symbol ): boolean { const result = Reflect .has (target, key); track (target, OperationTypes .HAS , key); return result; } function ownKeys (target: any ): (string | number | symbol)[] { track (target, OperationTypes .ITERATE ); return Reflect .ownKeys (target); }
总结
baseHandler
是针对于数组和对象类型的数据的 proxy handler
每个 trap
方法都是用 Reflect
来反射到原始数据上的
对于 get
、has
和 ownKeys
这一类读操作,会进行 track
来收集依赖,而对于 set
和 deleteProperty
这类写操作,则是会进行 trigger
来触发依赖回调
响应式数据的读取是 lazy
的,即初始化的时候不会对嵌套对象全盘观察,而是只有用到了每个值才会生成对应的响应式数据
collectionHandler 还记得我们在看 reactive
方法那里有个 collectionTypes
的判断对吧,collectionHandler
就是专门来处理 Set|Map|WeakSet|WeakMap
这类集合类型数据的
这里可以参考相学长的
vue3 响应式源码解析-Reactive 篇 - 掘金 juejin.im/post/5da9d7ebf265da5bbb1e52b7#heading-12
这篇文章,写的很详细,我这里也不再赘述了
总结 最开始阅读 reactive
源码时,总体的逻辑是比较清晰的,但是仍然有几个地方当时有疑惑:
rawToReactive|rawToReadonly
等这几个变量是干嘛的?
targetMap
的是干什么的?为什么是 WeakMap<any, KeyToDepMap>
类型
LOCKED
是用来干嘛的?
baseHandler
的 get trap
为什么又返回了一个 reactive(res)
?
collectionHandler
里的 trap 方法为什么只有 get
?为什么跟 baseHandler
不一样?
在读完源码后,除了 LOCKED
那个疑惑,其他几个问题我都已经找到答案,并且也在上面解惑了,我相信大家看完这篇文章后也应该都有自己的答案了
最后再来个源码里的知识点总结吧:
reactive
是利用 Proxy
来进行数据观察,Reflect
相关操作来反射到原始数据的,并且数据的访问是一个 lazy reactive
方式,即按需观察
普通对象、数组和集合类型数据的代理 handler 是不同的,这是因为 Proxy
的一些限制,参考
Proxy and Reflectjavascript.info/proxy#proxy-limitations
利用几个 WeakMap
来存储原始数据 <-> 响应式数据的双向映射关系,以便在响应式入口方法里判断是否原始数据已经被观察过,这个相比于 Vue2 的直接在原始数据上挂载 __ob__
要少一些冗余数据,并且由于 WeakMap
的 GC 特性,在运行时会有一定的内存优化
响应式数据的读操作会 track
来收集依赖,写操作则是会 trigger
来触发依赖回调
整个 reactive|readonly
的流程如下: