更多 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) })
完整的 reactivity 模块代码链接。
reactive.spec.ts
通过后的代码链接effect.spec.ts
通过后的代码链接ownKeys
代理收集的依赖不能被触发。__tests__/
测试代码目录src/
主要代码目录src
目录下的文件:
baseHandler.ts
传入给代理的对象,代理 Object/Array
时使用的 Handlers。collectionHandlers.ts
传入给代理的对象,代理 [Week]Set/Map
类型时使用的 Handlers。computed.ts
计算属性代码effect.ts
operations.ts
操作类型枚举reactive.ts
主要代码ref.ts
将 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
是被代理之后的对象。
undefined
get(target, prop, 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
表示失败。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
返回代理对象。
真正执行代理的是这个函数里面。
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)]
google 之后这里有篇文章里给出了问题原因和解决方案
解决方法,在 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
这个文件模块出现了几个 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()
参数:
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 } }
上面我们省略了暂时不关心的是哪个部分:
['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){}
有三个参数:
get
, has
,iterate
,分别是取值,检测属性存在性,以及迭代时。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…
源码简化版:
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 } }
这里主要有几个操作:
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) }
主要有三个步骤:
这里面有两个重要的属性(effects
,computedRunners
)和两个函数(add
,run
)
add: 过滤,run: 执行。
很明显,到这里,我们还是没有解决,依赖对应的 update
是如何收集的问题,因为 set
也只是将已经收集好 dep
执行而已。
该文件中主要包含三个重要函数:
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 中实现)。
那么接下来…
前面都是简化之后的,现在看看完整的这三个函数实现:
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 }) } } }
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) }
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 触发原理。
到此一个比较完整的响应式代码也算告一段落,这里贴一下简化后可运行的完整代码(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,一直递增。 |
在这之前都是在对象基础上做的测试,并没有增加数组的支持,比如: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
收集依赖