更多 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
。
{
// 很明显这个 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, runeffect
将 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) =