更多 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)
触发,返回对象 或 null -
setPrototypeOf
设置对象原型时触发,如: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
。
{
// 很明显这个 proxy handler get, 简化之后...
return function get(target, key, receiver) {
const res = Reflect.get(...arguments)
// ... 省略1,如果是数组,且是 includes, indexOf, lastIndexOf 操作
// 直接返回它对应的 res
// ... 省略2,如果是符号属性,直接返回 res
// ... 省略3, 浅 reactive,不支持嵌套
// ... 省略4,isRef 类型,判断是数组还是对象,数组执行 track(...), 对象返回 res.value
// 非只读属性,执行 track(),收集依赖
!isReadonly && track(target, 'get', key)
console.log(res, key, 'get...')
// return res
// 非对象直接返回原结果,如果是对象区分只读与否
return typeof res === 'object' && res !== null
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
res // ... readonly(res)
: reactive(res, toProxy, toRaw, mutableHandlers)
: res
}
}
上面我们省略了暂时不关心的是哪个部分:
- 数组类型且 key 是
['includes', 'indexOf', 'lastIndexOf']
其中任一一个 - 符号属性处理
-
ref
类型处理
目前我们只关心如何创建 get
和一个最简单的 basehandler: mutableHandler
使用 createGetter: get
// 示例 1
const objTarget = {
foo: 1,
bar: {
name: 'bar'
}
}
// 将 createGetter 生成的 get -> mutableHandlers 传入 reactive
const objObserved = reactive(objTarget, toProxy, toRaw, mutableHandlers)
这里 get
我认为只有两个目的:
递归 reactive
,就在最后返回的时候检测 res
结果时候
这里我们首先来验证下递归 reactive
问题,即当我们访问对象中嵌套对象里面的属性时候,实际上是不会触发 get
的,我们在 createGetter
的 return
前面加上一句 return res
。
也就是说不检测结果是不是对象,而直接返回当前取值的结果:
=> objObserved.foo
“foo” “get…”
1
=> objObserved.bar
{name: “bar”} “bar” “get…”
{name: “bar”}
{name: “bar”} “bar” “get…”
=> objObserved.bar.name
{name: “bar”} “bar” “get…”
“bar”
=> const bar = objObserved.bar
{name: “bar”} “bar” “get…”
undefined
=> bar.name
“bar”
分析上面的测试结果:
-
objObserved.foo
直接取对象的成员值,触发了proxy get
-
objObserved.bar
取对象的对象成员,触发了proxy get
-
objObserved.bar.name
取嵌套对象的成员,触发了proxy get
但请注意实际上触发get
的是objObserved.bar
得取值过程,因为输出的res
是{name: "bar"}
,也就是说取bar.name
的name
时候实际并没有触发proxy get
,这说明proxy get
只能代理一级。
- 为了证明代理只能代理一级,下面通过
bar = objObserved.bar
再去取bar.name
就很明显并没有触发proxy get
通过上面的分析,这也就是为什么要在 return
的时候去检测是不是对象,如果是对象需要进行递归 reactive
的动作。
那么,我们将 return res
注释掉再来看看结果如何:
=> objObserved.foo
1 “foo” “get…”
1
=> objObserved.bar
{name: “bar”} “bar” “get…”
Proxy {name: “bar”}
=> objObserved.bar.name
{name: “bar”} “bar” “get…”
bar name get…
“bar”
=> const bar = objObserved.bar
{name: “bar”} “bar” “get…”
bar.name
=> bar name get…
“bar”
看到差异没,首先从 objObserved.bar.name
就可看出差异了,这里首先触发的实际是 objObserved.bar
的 proxy get
,此时 return
的时候发现结果是个对象,因此将 bar
传入 reactive(bar)
进一步代理,完成之后取 bar.name
的时候 bar
已经是 reactive 对象了,因此就在 {name: “bar”} “bar” “get…” 后面紧跟着出现了bar name get… 输出。
此时,无论后面是赋值到变量 bar
再取 bar.name
结果一样会触发对应的 proxy get
,毕竟对象是引用类型,类似指针一样,新增了一个变量指向它,它依旧在哪里。
到此,最基本的 proxy get
响应式也完成了,并且能做到嵌套对象的 reactive 化,感觉相比 vue3 之前的通过 defineProperty
实现更加清晰容易理解。
收集依赖(track
)
既然有了响应式数据,那么接下来的重点就是如果利用其特性为我们做点事情,但是它又如何知道为我们做什么的,这个时候就有了所谓的“收集依赖”。
“收集依赖”就是在 get
取值期间发生的,也就是 createGetter
中的 track()
调用时触发了依赖收集动作。
track()
相关的代码在 effect.ts
中:
函数定义:
export function track(target: object, type: TrackOpTypes, key: unknown){}
有三个参数:
- target:proxy get 时候传递给 proxy 的那个对象
- type: 要 track 的类型,有三种:
get
,has
,iterate
,分别是取值,检测属性存在性,以及迭代时。 - Key: 针对 target 对象里面的属性,收集依赖到
targetMap -> depsMap -> dep:Set
中
简化 track(target, type)
代码:
// trackType -> get, has, iterate
function track(target, type, key) {
// ...省略1 检测 shouldTrack 和 activeEffect 标记
// 取 target 自己的依赖 map ,如果没有说明是首次,需要给它创建一个
// 空的集合,这里使用 Map 而不是 WeakMap,为的是强引用,它涉及到
// 数据的更新触发 UI 渲染,因此不该使用 WeakMap,否则可能会导致依赖丢失问题
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 接下来对 key 取其依赖
// 如果属性的依赖不存在,说明该对象是首次使用,需要创建其依赖库
// 且这里使用了 `Set` 是为了避免重复注册依赖情况,避免数据的更新导致重复触发
// 同一个 update 情况
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 注册实际的 update: activeEffect 操作
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
代码实现主要有三个过程:
- 检测全局的
targetMap
中是不是有target
自己的依赖仓库(Map
) - 检测
depsMap = targetMap.get(target)
中是不是有取值key
对应的依赖集合dep
- 注册
activeEffect
对象,然后将当前 target-key-dep 注册到 activeEffect,然后发现每个activeEffect
会有自己的deps
保存了所有对象key
的依赖。
收集依赖的过程如图:,执行取值 activeEffect.deps
中就会新增一个 Set
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZERAwjuj-1626260142859)(http://qiniu.ii6g.com/1589694976.png?imageMogr2/thumbnail/!100p)]
到这里,依赖收集算是完成,但并不是很明白 activeEffect
具体是做什么的???
既然依赖收集,要搞明白 activeEffect
是做什么的,估计的从 set
入手了,下面来实现 set
从而完成一个完整的 get -> dep -> set -> update
的过程。
go on…
createSetter(shallow = false)
源码简化版:
function createSetter(shallow = false) {
// 标准的 proxy set
return function set(target, key, value, receiver) {
// 取旧值
const oldValue = target[key]
// 先不管 shallow mode
// 还记得 reactive 里面的 toRaw啊,对象这里就是取出
// value 的原始对象 target,前提是它有 reactive() 过
// 才会被存入到 toRaw: observed -> target 中
// 暂时简化成: toRaw.get(value)
value = toRaw.get(value)
// ... 省略,ref 检测
const hadKey = hasOwn(target, key)
// 先执行设置原子操作
const result = Reflect.set(target, key, value, receiver)
// 只有对象是它自身的时候,才触发 dep-update(排除原型链)
if (target === toRaw(receiver)) {
if (!hadKey) {
// 新增属性操作
trigger(target, 'add', key, value)
} else if (hasChanged(value, oldValue)) {
// 值改变操作,排除 NaN !== NaN 情况
trigger(target, 'set', key, value, oldValue)
}
}
return result
}
}
这里主要有几个操作:
- shallow mode 检测,已省略。
-
value = toRaw(value)
如果 value 是 observed,那么可以通过 toRaw 取出被代理之前的对象 target,还记得reactive()
里面的那个 toRaw, toProxy 缓存操作吧。 - 调用
Reflect.set()
先将值设置下去,然后再考虑是否触发依赖 - 检测对象原型链,只有当对象是自身的时候才触发依赖
- 触发的行为只有两种要么是新增属性(
add
),要么是更改值(set
, 值不变的情况不触发)
这里有个与 createGetter
里面收集依赖 (track()
)对应的触发依赖函数: trigger
。
接下来就是要看看 trigger()
里面都做了啥。
function trigger(target, type, key, newValue, oldValue, oldTarget) {
// step1: 检测是否被 track 过,没有根本就没有依赖
const depsMap = targetMap.get(target)
if (!depsMap) return
// step2: 将 dep 加入到 effects
// 创建两个 effects, 一个普通的,一个计算属性
const effects = new Set()
const computedRunners = new Set()
// 根据 effect 的选项 computed 决定是添加到那个 Set 中
const add = (effectsToAdd) =>
effectsToAdd.forEach(
(effect) =>
(effect !== activeEffect || !shouldTrack) &&
(effect.options.computed
? computedRunners.push(effect)
: effects.push(effect))
)
// if ... clear
if (false) {
// TODO 清空动作,触发所有依赖
}
// 数组长度变化
else if (false) {
// TODO 触发更长度变化有关的所有依赖
} else {
// 例如: SET | ADD | DELETE 操作
if (key !== void 0) {
add(depsMap.get(key))
}
const isAddOrDelete =
type === 'add' || (type === 'delete' && !Array.isArray(target))
if (isAddOrDelete || (type === 'set' && target instanceof Map)) {
// 删除或添加操作,或者 map 的设置操作
add(depsMap.get(Array.isArray(target) ? 'length' : ITERATE_KEY))
}
// Map 的添加或删除操作
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
// step3: 执行 effects 中所有的 dep
const run = (effect) => {
// 选项提供了自己的调度器,执行自己的
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
// 触发应该触发的依赖
computedRunners.forEach(run)
effects.forEach(run)
}
主要有三个步骤:
- step1: 检测是否收集过依赖,如果没有说明可能没有被用过,没什么可触发的
- step2: 主要是过滤收集到依赖,针对当前更改操作的所有依赖触发(add)
- step2: 经过第二步的依赖过滤之后,触发所有的依赖(run)
这里面有两个重要的属性(effects
,computedRunners
)和两个函数(add
,run
)
add: 过滤,run: 执行。
很明显,到这里,我们还是没有解决,依赖对应的 update
是如何收集的问题,因为 set
也只是将已经收集好 dep
执行而已。
effect.ts
该文件中主要包含三个重要函数:
-
trigger(target, type, key?, newValue?, oldValue?, oldTarget?)
触发依赖函数 -
effect->createReactiveEffect(fn, options)
转换依赖函数成ReactiveEffect类型,并且立即执行它。 track(target, type, key)
以及一些辅助函数:
-
isEffect()
检测是不是ReactiveEffect
类型isEffect = fn => fn?._isEffect === true
-
stop(effect: ReactiveEffect)
停止 effect ,如果选项中提供了 onStop 监听该动作,执行它,重置 effect.active。export function stop(effect: ReactiveEffect) { if (effect.active) { cleanup(effect) if (effect.options.onStop) { effect.options.onStop() } effect.active = false } }
-
cleanup(effect: ReactiveEffect)
// 在 track 的时候,加入 effect 时,对其做一次清理工作 // 保证 effect.deps 干净 function cleanup(effect: ReactiveEffect) { const { deps } = effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } }
-
pauseTracking()
// 暂停 track 动作 export function pauseTracking() { trackStack.push(shouldTrack) shouldTrack = false }
-
enableTracking()
// 恢复 track 动作 export function enableTracking() { trackStack.push(shouldTrack) shouldTrack = true }
-
resetTracking()
// 重置 track,可能 fn 执行失败了,try ... finally ... 丢弃 fn:effect 时候调用 export function resetTracking() { const last = trackStack.pop() shouldTrack = last === undefined ? true : last }
包含的属性变量:
// 保存着 target 对象的所有依赖的 Map <target, dep<Set>>
// target -> Map<key, dep[]>
const targetMap = new WeakMap<any, KeyToDepMap>()
// effect 栈,保存所有的 fn->effect
const effectStack: ReactiveEffect[] = []
// 当前激活状态的 effect
let activeEffect: ReactiveEffect | undefined
export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
// 执行 effect 时,uid++,即每个 effect 都会有自己的唯一的 uid
let uid = 0
// 记录当前 effect 的状态,
let shouldTrack = true
// 当前 effect -> shouldTack
// 每增加一个 effect 记录 shouldTrack = true, push 到 trackStack
// 如果 effect.raw<fn> 执行异常会 pop 掉,还原 shouldTrack -> last,
// pop trackStack
const trackStack: boolean[] = []
一直到这里我们基本完成了 reactive->get->set->track->trigger->effect
一系列动作,
也该我们测试的时候了,按正常应该会有我们想要的结果,响应式->注册fn:update->取值收集依赖-> 设置触发 fn:udpate 调用
=>>>>>>>>>
比如:
const r = (target) => reactive(target, toProxy, toRaw, mutableHandlers)
const fn = () => console.log('effect fn')
let res = effect(fn, {})
console.log(Object.keys(res), 'after effect')
let dummy
const counter = r({ num: 0 })
effect(() => (dummy = counter.num))
console.log(dummy, 'before')
counter.num = 7
console.log(dummy, 'after')
上面的例子运行之后,并没有得到我们想要的结果!!!
effect fn
[“id”, “_isEffect”, “active”, “raw”, “deps”, “options”] “after effect”
0 “num” “get…”
0 “before”
0 “after”
按照我们的实现,理论上 after 的结果应该是 7 才对,但结果显示依然是 0,这说明了我们调用 effect(fn)
并没有与上面的 r({ num: 0 })
发生任何联系,即 fn 并没有被收集到 counter.num
的依赖 deps 中去,那这是为什么呢???
我们来回顾分析下之前所作工作的整个过程(reactive->get->set->track->trigger->effect
):
-
reactive
将数据通过proxy
转成响应式 -
get->track
收集依赖,相关属性:targetMap, depsMap, dep, activeEffect, activeEffect.deps。 -
set->trigger
触发依赖 update 函数,涉及到的 targetMap, depsMap, add, run -
effect
将 update 函数,转换成 ReactiveEffect 类型
纵观这整个过程,尤其是 get->track
, set->trigger -> effect
收集,触发和 effect 三个过程,唯一有可能让他们发生联系的应该就是这个 activeEffect
模块域里的变量,标识着当前处于激活状态的 effect,它的使用几乎贯穿了整个过程(track->trigger->effect,这三个函数也都在 effect.ts 中实现)。
那么接下来…
前面都是简化之后的,现在看看完整的这三个函数实现:
track(target, type, key)
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
trigger(…)
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
} else {
// the effect mutated its own dependency during its execution.
// this can be caused by operations like foo.value++
// do not trigger or we end in an infinite loop
}
})
}
}
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
const isAddOrDelete =
type === TriggerOpTypes.ADD ||
(type === TriggerOpTypes.DELETE && !isArray(target))
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
effects.forEach(run)
}
effect(fn, options)
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn(...args)
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
对比三个函数
过程 | shouldTrack/activeEffect | |
---|---|---|
track |
if (!shouldTrack || activeEffect === undefined) return | |
trigger |
add 里面有个判断:if (!shouldTrack || effect !== activeEffect)`才会继续往下执行添加操作 | |
effect |
effectStack.push(effect) activeEffect = effect // enable tracking trackStack.push(shouldTrack) shouldTrack = true
|
对下面测试代码逐行分析:
let dummy
const counter = r({ num: 0 })
effect(() => (dummy = counter.num))
console.log(dummy, counter, 'before')
counter.num = 7
console.log(dummy, 'after')
-
const counter = r({sum: 0})
这里将 { sum: 0 } reactive 代理之后赋值给了counter
也就是说这个counter
是个Proxy
:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-48A4P2ZR-1626260142863)(http://qiniu.ii6g.com/1589705626.png?imageMogr2/thumbnail/!100p)] -
effect(() => (dummy = counter.num))
在这里调用effect(fn)
注册了一个 updater,里面用到了counter.num
那么就会触发counter.num
的proxy get
,然后会触发track()
收集依赖:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bhhc9QmV-1626260142866)(http://qiniu.ii6g.com/1589705890.png?imageMogr2/thumbnail/!100p)]
并且我们从图中结果可知, fn 实际被立即执行了一次,这是effect
函数里面的操作。
按预期,这里的 fn 应该会被收集到 counter.num 的 deps 中。
我们在track()
最后加上打印if (!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect?.deps?.push(dep) console.log(dep, activeEffect.deps) }
结果:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bnvmTwXC-1626260142867)(http://qiniu.ii6g.com/1589706174.png?imageMogr2/thumbnail/!100p)]
即,activeEffect.deps 以及收集到了
counter.num
的依赖:Map(1) {"num" => Set(1)}
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-67nQkoXx-1626260142869)(http://qiniu.ii6g.com/1589706408.png?imageMogr2/thumbnail/!100p)] -
console.log(dummy, counter, 'before')
经过上面的结果分析,在第2步的时候,确实已经收集到了 counter.num 的 fn:updater,且存放到了targetMap -> despMap -> num:Set(1)
中。
因此这里的输出内容是: 0 “num” “get…” 没什么毛病,那继续往下,问题或许处在设置的时候??? -
counter.num = 7
最后发现问题所在,原始是个超级低级的问题(捂脸~~,没脸见人~~~)。
没有创建set handler
并添加到 mutableHandlers 里面。
只要添加两句:const set = createSetter()
然后:const mutableHandlers = { get, set }
就能得到我们想要的结果。 -
console.log(dummy, 'after')
最后看下最终输出:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlUJIiCr-1626260142870)(http://qiniu.ii6g.com/1589707939.png?imageMogr2/thumbnail/!100p)]1
effect(() => (dummy = counter.num))
取值时 proxy get 里面的输出2: 设置值为 7 之前的输出
3: 设置值当中的输出
4: 最后一个log取值 proxy get 的输出
5: 最后 log 的输出内容
虽然犯了个非常低级的错误,但也正因为这个低级错误,促使自己一步步的去跟踪 get->track
, set->trigger
, effect
整个过程,从而了解了依赖收集,updater 触发原理。
小结 1
到此一个比较完整的响应式代码也算告一段落,这里贴一下简化后可运行的完整代码(reactive.js)如下:
const hasChanged = (value, oldValue) =>
value !== oldValue && (value === value || oldValue === oldValue)
const __DEV__ = false
let shouldTrack = true
const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
const effectStack = []
const trackStack = []
let uid = 0
const reactiveToRaw = new WeakMap()
const rawToReactive = new WeakMap()
// baseHandlers.ts start
const get = createGetter()
const set = createSetter()
// 存放目标依赖的 map: target -> depsMap
// 一个目标,有自己的一个 map 存放依赖
const targetMap = new WeakMap()
let activeEffect = {
_isEffect: true,
id: 0,
active: false,
raw: null,
deps: [],
options: {}
}
function toRaw(observed) {
return reactiveToRaw.get(observed) || observed
}
function effect(fn, options = {}) {
// 如果是个 activeEffect 类型,那么其执行函数应该是 fn.raw
if (fn?._isEffect === true) {
fn = fn.raw
}
// 接下来要创建一个 effect
const _effect = function reactiveEffect(...args) {
if (!_effect.active) {
// 非激活状态
return options.scheduler ? undefined : fn(...args)
}
if (!effectStack.includes(_effect)) {
// 如果栈中不包含当前的 effect,即没有注册过该 effect
// 注册过就不需要重复注册了
// 添加前先执行清理工作 cleanup -> effect.deps[i].delete(effect)
try {
shouldTrack = true
effectStack.push(_effect)
activeEffect = _effect
return fn(...args)
} finally {
// fn 执行异常了,移除对应的 effect
effectStack.pop()
const last = trackStack.pop()
// 还原状态值
shouldTrack = last === undefined ? true : last
// 还原当前激活的 effect
activeEffect = effectStack[effectStack.length - 1]
}
}
}
_effect.id = uid++
_effect._isEffect = true
_effect.active = true
_effect.raw = fn
_effect.deps = []
_effect.options = options
if (!options.lazy) {
_effect()
}
return _effect
}
function trigger(target, type, key, newValue, oldValue, oldTarget) {
// step1: 检测是否被 track 过,没有根本就没有依赖
const depsMap = targetMap.get(target)
if (!depsMap) return
// step2: 将 dep 加入到 effects
// 创建两个 effects, 一个普通的,一个计算属性
const effects = new Set()
const computedRunners = new Set()
// 根据 effect 的选项 computed 决定是添加到那个 Set 中
const add = (effectsToAdd) => {
effectsToAdd?.forEach(
(effect) =>
(effect !== activeEffect || !shouldTrack) &&
(effect.options.computed
? computedRunners.add(effect)
: effects.add(effect))
)
}
// if ... clear
if (false) {
// TODO 清空动作,触发所有依赖
}
// 数组长度变化
else if (false) {
// TODO 触发更长度变化有关的所有依赖
} else {
// 例如: SET | ADD | DELETE 操作
if (key !== void 0) {
add(depsMap.get(key))
}
const isAddOrDelete =
type === 'add' || (type === 'delete' && !Array.isArray(target))
if (isAddOrDelete || (type === 'set' && target instanceof Map)) {
// 删除或添加操作,或者 map 的设置操作
add(depsMap.get(Array.isArray(target) ? 'length' : ITERATE_KEY))
}
// Map 的添加或删除操作
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
// step3: 执行 effects 中所有的 dep
const run = (effect) => {
// 选项提供了自己的调度器,执行自己的
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
// 触发应该触发的依赖
computedRunners.forEach(run)
effects.forEach(run)
}
// trackType -> get, has, iterate
function track(target, type, key) {
if (!shouldTrack || activeEffect === undefined) return
// ...省略1 检测 shouldTrack 和 activeEffect 标记
// 取 target 自己的依赖 map ,如果没有说明是首次,需要给它创建一个
// 空的集合,这里使用 Map 而不是 WeakMap,为的是强引用,它涉及到
// 数据的更新触发 UI 渲染,因此不该使用 WeakMap,否则可能会导致依赖丢失问题
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 接下来对 key 取其依赖
// 如果属性的依赖不存在,说明该对象是首次使用,需要创建其依赖库
// 且这里使用了 `Set` 是为了避免重复注册依赖情况,避免数据的更新导致重复触发
// 同一个 update 情况
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 注册实际的 update: activeEffect 操作
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect?.deps?.push(dep)
}
}
function createGetter(isReadonly = false, shallow = false) {
// 很明显这个 proxy handler get, 简化之后...
return function get(target, key, receiver) {
const res = Reflect.get(...arguments)
// ... 省略1,如果是数组,且是 includes, indexOf, lastIndexOf 操作
// 直接返回它对应的 res
// ... 省略2,如果是符号属性,直接返回 res
// ... 省略3, 浅 reactive,不支持嵌套
// ... 省略4,isRef 类型,判断是数组还是对象,数组执行 track(...), 对象返回 res.value
// 非只读属性,执行 track(),收集依赖
!isReadonly && track(target, 'get', key)
console.log(res, key, 'get...')
// return res
// 非对象直接返回原结果,如果是对象区分只读与否
return typeof res === 'object' && res !== null
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
res // ... readonly(res)
: reactive(res, toProxy, toRaw, mutableHandlers)
: res
}
}
function createSetter(shallow = false) {
// 标准的 proxy set
return function set(target, key, value, receiver) {
// 取旧值
const oldValue = target[key]
// 先不管 shallow mode
// 还记得 reactive 里面的 toRaw啊,对象这里就是取出
// value 的原始对象 target,前提是它有 reactive() 过
// 才会被存入到 toRaw: observed -> target 中
// 暂时简化成: toRaw.get(value)
value = toRaw(value)
// ... 省略,ref 检测
console.log(target, key, value, reactiveToRaw, 'set')
const hadKey = Object.hasOwnProperty(target, key)
// 先执行设置原子操作
const result = Reflect.set(target, key, value, receiver)
// 只有对象是它自身的时候,才触发 dep-update(排除原型链)
if (target === toRaw(receiver)) {
if (!hadKey) {
// 新增属性操作
trigger(target, 'add', key, value)
} else if (hasChanged(value, oldValue)) {
// 值改变操作,排除 NaN !== NaN 情况
trigger(target, 'set', key, value, oldValue)
}
}
return result
}
}
const mutableHandlers = {
get,
set
}
// baseHandlers.ts end
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 r = (target) =>
reactive(target, rawToReactive, reactiveToRaw, mutableHandlers)
const fn = () => console.log('effect fn')
let res = effect(fn, {})
console.log(Object.keys(res), 'after effect')
// 使用示例
let dummy
const counter = r({ num: 0 })
effect(() => (dummy = counter.num))
console.log(dummy, counter, 'before')
counter.num = 7
console.log(dummy, counter, 'after')
核心函数:
函数名 | 功能 |
---|---|
createGetter->get |
创建 proxy 的 get handler,里面会调用 track 收集依赖 |
createSetter->set |
创建 proxy 的 set handler,里面会调用 trigger 触发 targetMap>depsMap>dep:Set依赖执行 |
track(target, type, key) |
收集 target 对象或 target[key] 属性的依赖 |
trigger(target, type, key?, newValue?, oldValue?, oldTarget?) |
触发 target 对象的依赖调用 |
effect(fn, options) |
注册reactive属性的updater |
涉及到的核心属性:
ReactiveEffect 类型定义:
export interface ReactiveEffect<T = any> {
(...args: any[]): T
_isEffect: true
id: number
active: boolean
raw: () => T
deps: Array<Dep>
options: ReactiveEffectOptions
}
属性名 | 类型 | 作用 |
---|---|---|
activeEffect |
ReactiveEffect |
记录当前的 effect,在 effect() 注册updater的时候置为当前的 RE,在 get->track 里面添加到 targetMap->depsMap->dep 中,且同时更新自己的 activeEffect.deps.push(dep)
|
effectStack |
Array<ReactiveEffect> |
存放所有的 ReactiveEffect 的数组,也就是说页面中所有的 updater<ReactiveEffect> 都是存在这里面。但是每个 updater 执行完之后就会被移出 effectStack ,因为 efffect() 调用里面有个 try...finally 无论结果如何都会被 pop 掉。 |
shouldTrack |
Boolean |
用来追踪当前 effect->activeEffect 的状态 |
trackStack |
Array<Boolean> |
用来存放当前 effect 的 shouldTrack 状态值 |
targetMap |
WeakMap |
存放被 reactive 对象依赖的 Map,即:每个 target 在 targetMap 里面有自己的一个 depsMap,里面以 target => <key, Set> 形式存在,key 表示 target 上的一个属性键,Set 存放了该 key 的所有依赖 dep。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QqSGePh7-1626260142871)(http://qiniu.ii6g.com/1589709260.png?imageMogr2/thumbnail/!100p)]层级关系:targetMap:WeakMap -> depsMap:Map -> dep:Set |
depsMap |
Map |
target 对象里所有属性和其依赖对应的关系集合,如:counter.num 的依赖: { "num" => Set(1) }
|
reactiveToRaw |
WeakMap |
作为 reactive 的第三个参数 toRaw,保存了 observed->target 关系的 WeakMap。 |
rawToReactive |
WeakMap |
作为 reactive 的第二个参数 toProxy,保存了 target->observed 关系的 WeakMap,和 reactiveToRaw 刚好相反。 |
uid |
Number |
每个 effect 都有一个唯一的 id,一直递增。 |
支持数组 reactive
在这之前都是在对象基础上做的测试,并没有增加数组的支持,比如:jest(所有测试用例都来自官方仓库) ->
test('嵌套的 reactives', () => {
const original = {
nested: {
foo: 1
},
array: [{ bar: 2 }]
}
const observed = reactive(original)
expect(isReactive(observed.nested)).toBe(true)
expect(isReactive(observed.array)).toBe(true)
expect(isReactive(observed.array[0])).toBe(true)
})
测试结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W7SPatqf-1626260142875)(http://qiniu.ii6g.com/1589852337.png?imageMogr2/thumbnail/!100p)]
也就是说做到现在,并不支持数组的 reactive,这也是这节将要完善的点。
-
数组三个方法(
includes, indexOf, lastIndexOf
)的依赖收集:// 数组三个方法的处理 const arrayInstrumentations = {} // 兼容数组三个索引方法,收集他们相关的依赖 ;['includes', 'indexOf', 'lastIndexOf'].forEach((key) => { arrayInstrumentations[key] = function (...args) { const arra = toRaw(this) for (let i = 0, l = this.length; i < l; i++) { track(arr, 'get', i + '') } // 使用原始方法执行一次(有可能是 reactive 的) const res = arr[key](...args) if (res === -1 || res === false) { // 如果结果失败,使用原始方法再执行一次 return arr[key](...args.map(toRaw)) } else { return res } } })
-
createGetter -> get
的时候增加数组支持:function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { const targetIsArray = Array.isArray(target) if (targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } // ...省略 } }
到这里,我们已经可以正常收集到数组的依赖了,测试代码:
<script type="module"> import { reactive, effect, targetMap } from './packages/reactive.js' let n let arr = ['vue', 'reactive'] const observed = reactive(arr) effect(() => (n = observed[0])) // 这里还可以添加多个依赖,比如:effect(() => (m = observed[0])) // 这样,targetMap>depsMap:arr>dep 里面就会有两个了 [f, f] console.log({n, targetMap}) </script>
输出结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NHwB2DPH-1626260142876)(/Users/simon/Library/Application Support/typora-user-images/image-20200519095740412.png)]
-
effect(() => (n = observed[0]))
会执行一次fn
,即取了一次数组的0
下标值,触发了get
- 检测到是数组进入数组依赖收集程序
arrayInstrumentations
,触发track
收集依赖
-