Vue3.0源码系列响应式原理 - Reactivity
Posted 若叶岂知秋vip
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue3.0源码系列响应式原理 - Reactivity相关的知识,希望对你有一定的参考价值。
更多 vue3 源码分析尽在:www.cheng92.com/vue
该系列文章,均以测试用例通过为基准一步步实现一个 vue3 源码副本(学习)。
文字比较长,如果不想看文字可直接转到这里看脑图
简介
reactivity
是 vue next 里面通过 proxy
+ reflect
实现的响应式模块。
源码路径: packages/reactivity
入口文件:packages/reactivity/src/index.ts
疑问点解答:
-
shallowReactive
相当于浅复制,只针对对象的一级 reactive,嵌套的对象不会 reactive参考:测试代码 reactive.spec.ts
test('should keep reactive properties reactive', () => const props: any = shallowReactive( n: reactive( foo: 1 ) ) props.n = reactive( foo: 2 ) expect(isReactive(props.n)).toBe(true) )
阶段代码链接
- 测试用例
reactive.spec.ts
通过后的代码链接 - 测试用例
effect.spec.ts
通过后的代码链接 - 05-21号 git pull 后的更新合 并之后的 reactive.js
- 将 reactive.js 拆分成 effect.js + baseHandlers.js
- 完成 collection handlers(set + get)
- 完成 collection Map, Set 支持
- 支持 Ref 类型
- 支持 computed 属性
文中重点链接
- vue 中是如何防止在 effect(fn) 的 fn 中防止 ob.prop++ 导致栈溢出的?
- vue 中为何能对 JSON.parse(JSON.stringify()) 起作用的?
- 集合 handlers 的 get 函数实现 this 问题
- Key 和 rawKey 的问题(get 中),为什么要两次 track:get?
- 为什么 key1 和 toReactive(key1) 后的 key11 前后 set 会改变 key1 对应的值???
- 如果 Ref 类型放在一个对象中 reactive 化会有什么结果???
- 计算属性的链式嵌套使用输出结果详细分析过程(想要透彻computed请看这里!!!)
遗留问题
- DONE
ownKeys
代理收集的依赖不能被触发。 - TODO Ref:a 类型在对象中执行 obj.a++ 之后依旧是 Ref 类型的 a ???
更新
2020-05-21 21:19:07 git pull
模块结构
__tests__/
测试代码目录src/
主要代码目录
src
目录下的文件:
baseHandler.ts
传入给代理的对象,代理Object/Array
时使用的 Handlers。collectionHandlers.ts
传入给代理的对象,代理[Week]Set/Map
类型时使用的 Handlers。computed.ts
计算属性代码effect.ts
operations.ts
操作类型枚举reactive.ts
主要代码ref.ts
Proxy 和 Reflect 回顾
将 reactive -> createReactiveObject 简化合并:
function reactive(target, toProxy, toRaw, baseHandlers, collectionHandlers)
// ... 必须是对象 return
// ... 已经设置过代理了
let observed = null
// ... 本身就是代理
// ... 白名单检测
// ... handlers
// new 代理
let handlers = baseHandlers || collectionHandlers || // ...
observed = new Proxy(target, handlers)
// 缓存代理设置结果到 toProxy, toRaw
return observed
增加一个 reactive 对象:
const target =
name: 'vuejs'
const observed = reactive(target, null, null,
get: function (target, prop, receiver)
console.log(target, prop, receiver === observed, 'get')
)
console.log(target, observed)
输出结果:
name: “vuejs” Proxy name: “vuejs”
=> original.name
“vuejs”
=> observed.name
index.js:28 true “name” true “get”
undefined
=> observed === original
false
访问 target, observed 的属性 name 结果如上,observed
是被代理之后的对象。
- Observed.name 输出结果是 handler.get 执行之后的结果,因为没任何返回所以是
undefined
get(target, prop, receiver)
有三个参数,分别代表- target: 被代理的对象,即原始的那个 target 对象
- prop: 要获取对象的属性值的 key
- receiver: 代理之后的对象,即
observed
其他主要几个代理方法:
set
赋值的时候触发,对应Reflect.set(target, prop, value)
get
取值的时候触发,对应Reflect.get(target, prop, reciver)
ownKeys
使用for...in
时触发,对应Reflect.ownKeys(target)
has
使用prop in obj
时触发,对应语法 :... in ...
deleteProperty
使用delete obj.name
触发,对应delete obj.name
apply
被代理对象是函数的时候,通过fn.apply()
时触发,handler 里对应fn()
construct
构造器,new target()
时触发getPrototypeOf
调用Object.getPrototypeOf(target)
触发,返回对象 或 nullsetPrototypeOf
设置对象原型时触发,如:obj.prototype = xxx
let original =
name: 'vuejs',
foo: 1
original = test
const observed = reactive(original, null, null,
get: function (target, prop, receiver)
console.log(target === original, prop, receiver === observed, 'get')
return Reflect.get(...arguments)
,
set: function (target, prop, value)
console.log(prop, value, 'set')
Reflect.set(target, prop, value)
,
ownKeys: function (target)
console.log('get own keys...')
return Reflect.ownKeys(target)
,
has: function (target, key)
console.log('has proxy handler...')
return key in target
,
deleteProperty: function (target, key)
console.log(key + 'deleted from ', target)
delete target[key]
,
// 适用于被代理对象是函数类型的
apply: function (target, thisArg, argList)
console.log('apply...', argList)
target(...argList)
,
construct(target, args)
console.log('proxy construct ... ', args)
return new target(...args)
,
// 必须返回一个对象或者 null,代理 Object.getPrototypeOf 取对象原型
getPrototypeOf(target)
console.log('proxy getPrototypeOf...')
return null
,
setPrototypeOf(target, proto)
console.log('proxy setPrototypeOf...', proto)
)
console.log(observed.name) // -> true "name" true "get"
observed.name = 'xxx' // -> name xxx set
for (let prop in observed)
// -> get own keys...
'name' in observed // -> has proxy handler
delete observed.foo // foo deleted from name: 'xxx', foo: 1
function test()
console.log(this.name, 'test apply')
observed.apply(null, [1, 2, 3]) // apply... (3) [1, 2, 3]
// 注意点:proxy-construct 的第二个参数是传入构造函数时的参数列表
// 就算是以下面方式一个个传递的
new observed(1, 2, 3) // proxy construct ... (3) [1, 2, 3]
Object.getPrototypeOf(observed) // proxy getPrototypeOf...
observed.prototype =
bar: 2
// prototype bar: 2 set
// index.js:31 true "prototype" true "get"
// index.js:90 bar: 2
console.log(observed.prototype)
需要注意的点:
construct
的代理handler
中的第二个参数是一个参数列表数组。getPrototypeOf
代理里面返回一个正常的对象 或null
表示失败。
reactive 函数
export function reactive(target: object)
// if trying to observe a readonly proxy, return the readonly version.
// 这里对只读的对象进行判断,因为只读的对象不允许修改值
// 只要曾经被代理过的就会被存到 readonlyToRaw 这个 WeakMap 里面
// 直接返回只读版本
if (readonlyToRaw.has(target))
return target
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
传入一个 target
返回代理对象。
createReactiveObject
真正执行代理的是这个函数里面。
参数列表
target
被代理的对象toProxy
一个WeakMap
里面存储了target -> observed
toRaw
和toProxy
刚好相反的一个WeakMap
存储了observed -> target
baseHandlers
代理时传递给Proxy
的第二个参数collectionHandlers
代理时传递给Proxy
的第二个参数(一个包含四种集合类型的Set
)
函数体
下面是将 reactive
和 createReactiveObject
进行合并的代码。
事先声明的变量列表:
// 集合类型的构造函数,用来检测 target 是使用 baseHandlers
// 还是 collectionHandlers
const collectionTypes = new Set([Set, Map, WeakMap, WeakSet])
// 只读对象的 map,只读对象代理时候直接返回原始对象
const readonlyToRaw = new WeakMap()
// 存储一些只读或无法代理的值
const rawValues = new WeakSet()
合并后的 reactive(target, toProxy, toRaw, basehandlers, collectionHandlers)
函数
function reactive(target, toProxy, toRaw, baseHandlers, collectionHandlers)
// 只读的对象
if (readonlyToRaw.has(target))
return target
// ... 必须是对象 return
if (target && typeof target !== 'object')
console.warn('不是对象,不能被代理。。。')
return target
// toProxy 是一个 WeakMap ,存储了 observed -> target
// 因此这里检测是不是已经代理过了避免重复代理情况
let observed = toProxy.get(target)
if (observed !== void 0)
console.log('target 已经设置过代理了')
return observed
// ... 本身就是代理
// toRaw 也是一个 WeakMap 存储了 target -> observed
// 这里判断这个,可能是为了防止,将曾经被代理之后的 observed 传进来再代理的情况
if (toRaw.has(target))
console.log('target 本身已经是代理')
return target
// ...... 这里省略非法对象的判断,放在后面展示 ......
// 根据 target 类型决定使用哪个 handlers
// `Set, Map, WeakSet, SeakMap` 四种类型使用 collectionHandlers 集合类型的 handlers
// `Object, Array` 使用 basehandlers
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// new 代理
observed = new Proxy(target, handlers)
// 缓存代理设置结果到 toProxy, toRaw
toProxy.set(observed, target)
toRaw.set(target, observed)
return observed
-
readonlyToRaw.has(target)
检测是否是只读对象,直接返回该对象 -
检测
target
是引用类型还是普通类型,只有引用类型才能被代理 -
toProxy
中存储了target->observed
内容,检测target
是不是已经有代理了 -
toRaw
中存储了observed->target
检测是否已经是代理了 -
五种不合法的对象类型,不能作为代理源
// ... 白名单检测,源码中调用的是 `canObserve` 这里一个个拆分来检测 // 1. Vue 实例本身不能被代理 if (target._isVue) console.log('target 是 vue 实例,不能被代理') return target // 2. Vue 的虚拟节点,其实就是一堆包含模板字符串的对象解构 // 这个是用来生成 render 构建 DOM 的,不能用来被代理 if (target._isVNode) console.log('target 是虚拟节点,不能被代理') return targtet // 限定了只能被代理的一些对象: 'Object, Array, Map, Set, WeakMap, WeakSet` // Object.prototype.toString.call(target) => [object Object] 取 (-1, 8) // 其实 `Object` 构造函数字符串 const toRawType = (target) => Object.prototype.toString.call(target).slice(8, -1) if ( !['Object', 'Array', 'Map', 'Set', 'WeakMap', 'WeakSet'].includes( toRawType(target) ) ) console.log( `target 不是可代理范围对象('Object', 'Array', 'Map', 'Set', 'WeakMap', 'WeakSet')` ) return target // 那些被标记为只读或者非响应式的WeakSets的值 if (rawValues.has(target)) return target // 被冻结的对象,是不允许任何修改操作的,不可用作响应式对象 if (Object.isFrozen(target)) return target
-
根据 target 的类型检测采用哪种类型的
handlers
,集合类型使用collectionhandlers
,对象类型采用baseHandlers
-
创建代理
new Proxy(target, handlers)
-
缓存代理源及代理结果到
toProxy, toRaw
避免出现重复代理的情况 -
返回代理对象
observed
。
使用 reactive
为了区分两种代理类型(集合类型,普通对象(对象和数组)),这里使用两个对象(setTarget
, objTarget
),创建两个代理(setObserved
, objObserved
),分别传入不同的代理 handlers
,代码如下:
const toProxy = new WeakMap()
const toRaw = new WeakMap()
const setTarget = new Set([1, 2, 3])
const objTarget =
foo: 1,
bar: 2
const setObserved = reactive(setTarget, toProxy, toRaw, null,
get(target, prop, receiver)
console.log(prop, 'set get...')
// return Reflect.get(target, prop, receiver)
,
// set/map 集合类型
has(target, prop)
const ret = Reflect.has(target, prop)
console.log(ret, target, prop, 'set has...')
return ret
)
const objObserved = reactive(
objTarget,
toProxy,
toRaw,
// object/arary, 普通类型
get(target, prop, receiver)
console.log(prop, 'object/array get...')
return Reflect.get(target, prop, receiver)
,
)
输出代理的结果对象如下:console.log(setObserved, objObserved)
结果:Proxy 1, 2, 3 Proxy foo: 1, bar: 2
然后出现了错误,当我试图调用 setObserved.has(1)
的时候报错了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LhGLcgm6-1626260142852)(http://qiniu.ii6g.com/1589614203.png?imageMogr2/thumbnail/!100p)]
获取 setObserved.size
属性报错,不同的是 set proxy handler
有被调用,这里应该是调用 Reflect.get()
时候报错了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vco8Zuhw-1626260142857)(http://qiniu.ii6g.com/1589614685.png?imageMogr2/thumbnail/!100p)]
解决方法,在 get proxy handler
里面加上判断,如果是函数就使用 target
去调用:
const setObserved = reactive(setTarget, toProxy, toRaw, null,
get(target, prop, receiver)
switch (prop)
default:
// 如果是函数,经过代理之后会丢失作用域问题,所以要
// 重新给他绑定下作用域
console.log(prop, 'get...')
return typeof target[prop] === 'function'
? target[prop].bind(target)
: target[prop]
,
结果:
Proxy 1, 2, 3 Proxy foo: 1, bar: 2
-> setObserved.has(1)
has get…
true
baseHandlers.ts
这个文件模块出现了几个 handlers 是需要弄清楚的,比如:
baseHandlers.ts
里面和 Array, Object 有关的四个:
mutableHandlers
readonlyHandlers
shallowReactiveHandlers
,shallowReadonlyHandlers
collectionHandlers.ts
里和集合相关的两个:
mutableCollectionHandlers
readonlyCollectionHandlers
在上一节讲过 createReactiveObject
需要给出两个 handlers 作为参数,一个是针对数组和普通对象的,另一个是针对集合类型的。
下面分别来看看两个文件中分别都干了什么???
列出文件中相关的函数和属性:
属性:
// 符号集合
const builtInSymbols = new Set(/* ... */);
// 四个通过 createGetter 生成的 get 函数
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
// 三个数组函数 'includes', 'indexOf', 'lastIndexOf'
const arrayInstrumentations: Record<string, Function> =
// setter
const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)
函数:
// 创建 getter 函数的函数
function createGetter(isReadonly = false, shallow = false) /* ... */
// 创建 setter 函数的函数
function createSetter(shallow = false) /* ... */
// delete obj.name 原子操作
function deleteProperty(target: object, key: string | symbol): boolean /*...*/
// 原子操作 key in obj
function has(target: object, key: string | symbol): boolean /* ... */
// Object.keys(target) 操作,取对象 key
function ownKeys(target: object): (string | number | symbol)[] /*...*/
四个要被导出的 handlers
:
export const mutableHandlers: ProxyHandler<object> = /*...*/
export const readonlyHandlers: ProxyHandler<object> = /*...*/
export const shallowReactiveHandlers: ProxyHandler<object> = /*...*/
export const shallowReadonlyHandlers: ProxyHandler<object> = /*...*/
接下来一个个来分析分析,看看每个都有什么作用???
先从 createGetter
说起吧 ->
为了下面方便调试,对上面的 reactive()
进行了简化,只保留了与 handlers 有关的部分:
const collectionTypes = new Set([Set, Map, WeakMap, WeakSet])
function reactive(target, toProxy, toRaw, baseHandlers, collectionHandlers)
// 简化
if (typeof target !== 'object') return target
//... isVue, VNode...
let observed = null
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
const toProxy = new WeakMap(),
toRaw = new WeakMap()
createGetter(isReadonly = false, shallow = false)
参数:
isReadonly = false
shallow = false
简化之后的 createGetter
,先用它来创建一个 get
然后创建一个 baseHandler: mutableHandlers
可变的 handlers
。
以上是关于Vue3.0源码系列响应式原理 - Reactivity的主要内容,如果未能解决你的问题,请参考以下文章 敲黑板,划重点!!!Vue3.0响应式实现原理 —— proxy()