Vue3 源码头脑风暴之 1 ☞reactivity
文章目录
诗号:六道同坠,魔劫万千,引渡如来。更新日志&Todos :
DONE [2021-01-20 15:16:45] ref
stb-vue-next 完全拷贝于 vue-next ,主要目的学习及尝试应用于机顶盒环境。
本文依据 commit 进程进行记录
所有用例请按
<f12>
打开控制台查看 >
关键知识点
init project
add: createReactiveObject
feat: reactive-fn · gcclll/stb-vue-next@cb3470d
仅增加函数声明:
|
|
mutableHandlers: 普通对象类型的 proxy handlers
mutableCollectionHandlers: 集合类型的 proxy handlers,因为 Reflect
并没有对集
合类型做底层映射,所以需要特殊处理。
feat: reactive(target)
feat: createReactiveObject · gcclll/stb-vue-next@443a0b5
重点:
new Proxy(target, collection)
被代理类型必须是对象(引用类型)
target 本身已经是 proxy 了
target 代理有缓存不用重复创建
必须是合法的类型(
Object|Array|[Weak]Map|[Weak]Set
)才能被代理记得缓存新创建的代理关系(
proxyMap
全局变量)用例一:普通对象
|
|
+RESULTS:
true observed is reactive, true original is reactive, false false `foo` in observed, true Object.keys(observed) == ['foo'], true undefined
b2143f9 FIX: isReactive(observed): false
fix: get object's __v_isReactive prop · gcclll/stb-vue-next@1005ef3
在 createGetter
中增加判断,如果来取的属性为 __v_isReactive
则直接返回
!isReadonly
。
用例二:原型
|
|
+RESULTS:
reactiveObj is reactive, true otherObj is reactive, false reactiveOther is reactive, true reactiveOther.data[0] is `a`, true __proto__, [object Object] undefined
FIX: 1005ef3 当取值时属性名为 __proto__
时:直接返回取值结果。
feat: get key is symbol or proto or __v_isRef · gcclll/stb-vue-next@1e2a3fe
用例三:嵌套对象
|
|
+RESULTS:
observed.nested is reactive true observed.array is reactive true observed.array[0] is reactive true
用例四:代理后的对象操作也会体现在原对象上
|
|
+RESULTS:
ob.bar = 1, or.bar = 1 'foo' in ob: false, 'foo' in or: false
结果删除后,依旧在,需要实现 delete proxy handler。
用例五:原始对象上的操作也要能在代理后对象有所体现
|
|
+RESULTS:
observed.bar = 1, original.bar = 1 'foo' in original: false, 'foo' in observed: false
用例六:被设置的值如果是对象,该对象也会被 Reactive
|
|
+RESULTS:
observed.foo === faw, false observed.foo is reactive, true
访问 raw 之前(#1 之前)它还不是 reactive,因为递归 reactive 发生在 track() 中,即取值阶段。
如:控制台测试输出
var ob = reactive({}) var raw = {} ob.foo = raw ob Proxy {foo: {…}} [[Handler]]: Object deleteProperty: ƒ deleteProperty(target, key) get: ƒ (target, key, receiver) set: ƒ (target, key, value, receiver) [[Target]]: Object foo: {} // 注意这里 [[IsRevoked]]: false
进行一次取值:
ob.foo Proxy {} [[Handler]]: Object [[Target]]: Object [[IsRevoked]]: false
用例七:不该重复 proxy,返回第一个 proxy 结果
|
|
observed2 === observed1, true undefined
因为 reactive()
实现中组了检测,如果自身是个 proxy 就直接返回,所以 #3 中实
际直接将 observed1
返回了。
TODO 用例八:不应该用 proxies 污染原始对象?
|
|
+RESULTS:
observed.bar === observed2, true original.bar === original2, false
basic proxy get handler(createGetter)
feat: reactive proxy get handler · gcclll/stb-vue-next@598e047
commit: 只实现对象的 get proxy handler
,对象属性被访问的时候会触发代理,比如下面
实例中,当访问 observed.count
时候会触发 console.log({ res }, "get")
执行。
最简单 proxy get handler 脑图:
调用
Reflect.get(target, key, receiver)
执行原子操作返回执行结果
|
|
测试:
|
|
+RESULTS: effect 会立即执行 fn, ob.count
取值触发 get proxy 收集 fn -> count => deps<Set>
Map(1) { 'count' => Set(1) { [Function: reactiveEffect] { id: 0, allowRecurse: false, _isEffect: true, active: true, raw: [Function (anonymous)], deps: [Array], options: {} } } }
add track() and effect()
feat: track+effect · gcclll/stb-vue-next@3fc9634
为了完成观察属性,通过属性的取值操作来收集依赖过程,这里同时实现了 track()
和
effect()
函数。
track(target, type, key) 监听取值收集依赖:
|
|
effect(fn, options)
参数列表 :
fn - 被封装的函数,里面可对对象执行 get/set 操作。
主要功能 :将 fn 封装成
ReactiveEffect
函数1 2 3 4 5 6 7 8 9 10
export interface ReactiveEffect<T = any> { (): T // effect函数主题 _isEffect: true // 标记自身是不是一个 ReactiveEffect 类型 id: number // uid++ 而来,全局的一个相对唯一的 id active: boolean // 记录当前的 effect 是不是激活状态 raw: () => T // 封装之前的那个 fn deps: Array<Dep> // fn 的被依赖者列表 options: ReactiveEffectOptions // 额外选项,如:lazy allowRecurse: boolean // ??? }
解决问题 :
fn 封装之后,执行 fn 过程中使用 try…finally ,防止 fn 执行异常导致 effect 进程中断
结合 shouldTrack, activeEffect 和 track() 函数,有效的避免了在 fn 中执行 obj.value++ 导致 effect 死循环问题,因为 try…finally 确保了只有 fn 函数 完成之后才会进入 finally 恢复 effect 状态(
shouldTrack = true, activeEffect = last || null
)。
相关函数及变量列表
name | type | desc |
---|---|---|
activeEffect | ReactiveEffect | 当前正在处理的 Effect,fn 还未执行完成,finally 还没结束 |
effectStack | Array, [] | 缓存所有状态还没完成的 Effect |
shouldTrack | boolean, true | track() 中用来检测当前 effect 是否结束,从而判定是否可以继续执行 track() 收集依赖 |
trackStack | Array, [] | 保存着所有 Effect 的 shouldTrack 值 |
effect() | function | 封装 fn成 ReactiveEffect 结构 |
track(target, type, key) | function | 收集依赖,并且响应式递归 |
trigger(...) | function | 当值更新时触发所有依赖更新 |
createReactiveEffect(fn, options) | function | effect() 函数主题功能分离出来 |
cleanup(effect: ReactiveEffect) | function | 清空所有 fn 的依赖 effect.deps[] |
enableTracking() | function | 使能 Effect ,shouldTrack = true, 并将其加入 trackStack |
resetTracking() | function | 重置 Effect, shouldTrack = 上一个 Effect 的 shouldTrack 值或 true |
|
|
依赖和属性变更发生联系的桥梁模块。
effect(fn, options)
封装执行 fn,触发取值操作 ->track(target, type, key)
收集对象及属性所有依赖 ->fn 中设值操作触发
trigger(...)
执行所有 deps,更新 DOM。
add trigger() proxy set handler
feat: proxy set and trigger operation · gcclll/stb-vue-next@20afde9
proxy set handler(createSetter)
|
|
trigger()
|
|
observe object recursively
effect -> track -> trigger 关系图
到此 effect + track + trigger 完成了最简单的响应式代码。
effect 封装注册函数
track 取值触发收集依赖函数
trigger 设值触发所有依赖函数执行
add delete(deleteProperty) proxy handler
feat: delete proxy handler · gcclll/stb-vue-next@05b98c5
|
|
删除成功调用 trigger()
触发 DELETE 。
add has, ownKeys proxy handlers
feat: has + ownKeys proxy handler · gcclll/stb-vue-next@ab69fe9
增加 has, ownKeys proxy handlers.
|
|
测试:
|
|
+RESULTS:
before run effect, dummy = false after run effect, dummy = true
TODO add array support
feat: array support · gcclll/stb-vue-next@9aeb678
修改点:
|
|
索引操作(
includes, lastIndexOf, indexOf
)处理确保索引取值的时候,能使用 track() 正确收集对应索引的依赖列表。
可改变原数组长度操作(
push, pop, shift, unshift, splice
)因为这些函数内部实现都需要访问及改变原数组的长度,因此这里需要做一层保护,它 们执行之前
shouldTrack = false
,执行完成之后shouldTrack = true
,避免track()
死循环。
下面均为 vue-next 源码中用例分析。
T1: 读写操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
const { isReactive, effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') const original = [{ foo: 1 }, { bar: 2 }] const observed = reactive(original) console.log(`#01 original !== observed, ${original !== observed}`) console.log(`#02 original is reactive, ${isReactive(original)}`) console.log(`#03 observed is reactive, ${isReactive(observed)}`) console.log(`#04 observed[0] is reactive, ${isReactive(observed[0])}`) const clone = observed.slice() console.log(`#05 clone[0] is reactive, ${isReactive(clone[0])}`) console.log(`#06 clone[0] !== original[0], ${clone[0] !== original[0]}`) console.log(`#07 clone[0] === observed[0], ${clone[0] === observed[0]}`) const value = { baz: 3 } const reactiveValue = reactive(value) observed[0] = value console.log(`#08 observed[0] === reactiveValue, ${observed[0] === reactiveValue}`) console.log(`#09 original[0] === value, ${original[0] === value}`) delete observed[0] console.log(`#10 observed[0] === undefined, ${observed[0] === undefined}`) console.log(`#11 original[0] === undefined, ${original[0] === undefined}`) observed.push(value) console.log(`#12 observed[2] === reactiveValue, ${observed[2] === reactiveValue}`) console.log(`#13 original[2] === value, ${original[2] === value}`)
+RESULTS:
#01 original !== observed, true #02 original is reactive, false #03 observed is reactive, true #04 observed[0] is reactive, true #05 clone[0] is reactive, true #06 clone[0] !== original[0], true #07 clone[0] === observed[0], true #08 observed[0] === reactiveValue, true #09 original[0] === value, true #10 observed[0] === undefined, true #11 original[0] === undefined, true #12 observed[2] === reactiveValue, true #13 original[2] === value, true
分析:
#01 因为 Proxy 内部实现实际会创建新对象
#02 读取
__v_isReactive
在createGetter()
里面会直接返回!isReadonly
#03 同上
#04 取值的时候返回结果之前会检测当前是不是对象如果是会执行递归 reactive
#05 slice 实现过程并非深拷贝
#06 和
observed[0] !== original[0]
一个原因#07 浅拷贝问题
#08 先
observed[0]
对 value 取值操作,此时 Reactive value 对象时,发现该对
象已经有映射了(proxyMap 中已存在 value -> reactiveValue 关系。)
#09 proxy 的改变也会体现在 original 对象上。
1 2 3 4
const target = { } const ob = new Proxy(target, {}) ob.value = { test: 1 } console.log(target)
+RESULTS:
{ value: { test: 1 } }
#10 同上
#11 同上
#12 同 #08
proxyMap
中有缓存了#13 同上
T2:索引方法(includes, lastIndexOf, indexOf)
1 2 3 4 5 6 7 8 9 10 11
const { isReactive, effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') const raw = {} const arr = reactive([{}, {}]) arr.push(raw) console.log(`arr.indexOf(raw), ${arr.indexOf(raw)}`) console.log(`arr.indexOf(raw, 3), ${arr.indexOf(raw, 3)}`) console.log(`arr.includes(raw), ${arr.includes(raw)}`) console.log(`arr.includes(raw, 3), ${arr.includes(raw, 3)}`) console.log(`arr.lastIndexOf(raw), ${arr.lastIndexOf(raw)}`) console.log(`arr.lastIndexOf(raw, 1), ${arr.lastIndexOf(raw, 1)}`)
+RESULTS:
arr.indexOf(raw), 2 arr.indexOf(raw, 3), -1 arr.includes(raw), true arr.includes(raw, 3), false arr.lastIndexOf(raw), 2 arr.lastIndexOf(raw, 1), -1
T3:数组元素本身已经是 Proxy
1 2 3 4 5 6
const { isReactive, effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') const raw = [] const obj = reactive({}) raw.push(obj) const arr = reactive(raw) console.log(`arr.includes(obj), ${arr.includes(obj)}`)
+RESULTS: 这个应该很好理解,对象已经是 proxy 之后不会再继续代理,而是返回 proxyMap 中缓存过的代理结果。
arr.includes(obj), true
T4: reverse 方法也应该是 reactive 的
TODO: reverse 之后找不到(
indexOf
)原始对象了?根据 reverse() 的实现原理,本质上是元素之间的替换操作,因此并不会改变数组或元 素本身是 proxy 性质,且属于索引赋值操作,因此会触发索引的 reactive 相关操作。
1 2 3 4 5 6 7 8 9 10 11
const { isReactive, effect, reactive, targetMap, toRaw } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') const obj = { a: 1 } const arr = reactive([obj, { b: 2 }]) let index = -1 console.log(`#1 obj === arr[0], ${obj === toRaw(arr[0])}`) effect(() => (index = arr.indexOf(obj))) // index = 0 console.log(`#2 before reverse, index = ${index}`) arr.reverse() // #3 console.log(`#4 after reverse, index = ${index}`) console.log(`#5 obj === arr[1], ${obj === toRaw(arr[1])}`)
#1 obj === arr[0], true #2 before reverse, index = 0 #4 after reverse, index = -1 #5 obj === arr[1], true undefined
+RESULTS: 失败
before reverse, index = 0 after reverse, index = -1 [ { b: 2 }, { a: 1 } ]
T5: 使用 delete 删除数组元素时不应该触发
length
依赖1 2 3 4 5 6 7 8 9 10
const { isReactive, effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') const arr = reactive([1,2,3]) let dummy = 0 effect(() => { dummy = arr.length + 1 }) console.log(`before delete, dummy = ${dummy}, arr = ${arr}, len = ${arr.length}`) delete arr[1] console.log(`after delete, dummy = ${dummy}, arr = ${arr}, len = ${arr.length}`)
+RESULTS: 删除操作并不会改变数组长度
before delete, dummy = 4, arr = 1,2,3, len = 3 after delete, dummy = 4, arr = 1,,3, len = 3 undefined
PS: 赋值已有的下标元素值、添加非正整数类型的属性到数组上都不会触发
length
依 赖,本质上并没有改变数组长度。T6: 在 effect fn 中使用
for ... in
迭代语句应该 track length1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
const { isReactive, effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') const nums = [1] const array = reactive(nums) let len = '' effect(() => { len = '' for (const key in array) { len += key } }) console.log(`before push, len = ${len}`) array.push(1) console.log(`after push, len = ${len}`)
before push, len = 0 after push, len = 01 undefined
+RESULTS: 输出显示,length 依赖已经 track 到了,只是 Length 变化并没有触发
Map(1) { 'length' => Set(1) { [Function: reactiveEffect] { id: 0, allowRecurse: false, _isEffect: true, active: true, raw: [Function (anonymous)], deps: [Array], options: {} } } } before push, len = 0 after push, len = 0
FIX: feat(add): array add element support · gcclll/stb-vue-next@21b4881
array add element support
feat(add): array add element support · gcclll/stb-vue-next@21b4881
增加添加数组元素支持。
|
|
createGetter -> get
proxy handler 中增加属性添加 trigger 操作trigger(target, TriggerOpTypes.ADD, key, value)
effect.ts ->
trigger()
中增加数组长度变更依赖收集和ADD
操作依赖收集
add shallow reactive
feat(add): shallowReactive api · gcclll/stb-vue-next@e85dfc6
正常 track 过程中会检测嵌套内的是不是对象,如果是对象会进行递归 reactive 让内部嵌套的对象也 reactive 化。
shallow reactive 意思是当对象存在嵌套的时候,不进行递归 reactive 。
这个通过在 track() 函数中做一次拦截处理。
测试:
|
|
+RESULTS: Array.from 本质是迭代器操作,所以会触发迭代器 tracking 。
props.n is reactive, false props2.n is reactive, true >> array before push a, size = 0 after push a, size = 1 after pop, size = 0 >> 迭代时不应观察 spreadA is reactive, false >> onTrack on tracking... on tracking... undefined
add readonly reactive
feat(add): readonly reactive · gcclll/stb-vue-next@66e7903
测试(for Object
):
|
|
+RESULTS: readonly 会递归嵌套对象,所以它内部的对象都会是 readonly。
>>> should make nested values readonly wrapped !== original, true wrapped is proxy, true wrapped is reactive, false wrapped is readonly, true original is reactive, false original is readonly, false wrapped.bar is reactive, false wrapped.bar is readonly, true original.bar is reactive, false original.bar is readonly, false >> get wrapped.foo = 1 >> has 'foo' in wrapped, true >> ownKeys Object.keys(wrapped), [foo,bar] >> set or delete, should fail after 'wrapped2.foo = 2', wrapped2.foo = 1 after 'wrapped2.bar.baz = 3', wrapped2.bar.baz = 2 after 'wrapped2[qux] = 4', wrapped2[qux] = 3 after 'delete wrapped2.foo', wrapped2.foo = 1 after 'delete wrapped2.bar.baz', wrapped2.bar.baz = 2 after 'delete wrapped2[qux]', wrapped2[qux] = 3
测试(for Array
):
|
|
+RESULTS:
>>> should make nested values readonly wrapped !== original wrapped is proxy, true wrapped is reactive, false wrapped is readonly, true original is reactive, false original is readonly, false wrapped[0] is reactive, false wrapped[0] is readonly, true original[0] is reactive, false original[0] is readonly, false > get wrapped[0].foo = 1 > has 0 in wrapped, true > ownKeys Object.keys(wrapped) = [0] after 'wrapped2[0] = 1', wrapped2[0] = [object Object] after 'wrapped2[0].foo = 2', wrapped2[0].foo = 1 after 'wrapped2.length = 0', wrapped2.length = 1 after 'wrapped2.length = 0', wrapped2[0].foo = 1 after 'wrapped2.push(2)', wrapped2.length = 1 undefined
测试(reactive, readonly 互撩)
|
|
+RESULTS:
*#1* isReadonly(b), true *#2* toRaw(a) === toRaw(b), true *#3* a === b, true undefined
#1 b is readonly:
createReactive
中的处理1 2 3
if (target[ReactiveFlags.Raw] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) { return target }
上面的处理针对
b = reactive(a)
有:a 满足 target[ReactiveFlags.Raw] 因为它是 readonly 的.
isReadonly = false
target[ReactiveFlags.IS_REACTIVE] 不满足
因此上面的判断满足
target[ReactiveFlags.RAW] && !target[ReactiveFlags.IS_REACTIVE]
直接返回 target 。#2
toRaw(a) === toRaw(b)
这个结果为 true,因为 #1 中的原因,直接返回了 target, 所以 b 实际上就是 a(如结果 #3)
add shallow readonly reactive
feat(add): shallow readonly reactive · gcclll/stb-vue-next@aaaf911
测试:
|
|
+RESULTS:
>>> should not make non-reactive properties reactive isReactive(props.n), false >>> should make root level properties readonly after 'props.n = 2', props.n = 1 >>> should NOT make nested properties readonly after 'props.n.foo = 2', props.n.foo = 2 undefined
这里的结果不难理解
shallow 不会递归 reactive
readonly 让属性只读,但是由于是 shallow 所以只有对象根属性才是只读
add effect stop
feat(add): effect stop · gcclll/stb-vue-next@f1e5b3a
stop() 函数操作:
清空所有 effect 上的 deps,同时将当前的 effect 从所有依赖它的 dep 中删除
effect.deps[i].delete(effect)
, 这一步是将targetMap > depsMap > deps
中 的 effect 删除。effect.deps.length = 0
将 effect.active 置为 false
执行 stop()
之后,只能手动调用 runner()
来触发 effect fn(前提是没有提供
options.scheduler
,否则永远不会被执行) 。
被 stopped 的 effect 可以当做另一个正常的 effect 的 fn。
集合类型代理(proxy handlers)脑图
add collection handlers
feat(add): mutable collection handlers · gcclll/stb-vue-next@521f755
collection proxy handlers 脑图链接
因为 Reflect 没有集合操作的对应接口,所以针对集合类型需要通过 get proxy
来中转
做特殊处理。
|
|
添加集合类型的 handlers。
add collection get proxy handler
feat(add): collection get proxy · gcclll/stb-vue-next@a5e8e06
针对集合的所有操作代理都是通过 get proxy 变相完成的,所以搞懂这里是至关重要的。
collection proxy handler:
|
|
简单吧,别被假🐘给迷惑了!!!
这里的原理如果想通了也简单。
试想下,我们调用集合类型的方法是怎么调用的???
map.get()
, map.set()
, map.delete()
, ...
都是通过点语法使用的,点语法前提也必须是先取出值来进行操作,即要调用方法之前,先 将方法取出来,因此这里就是取值操作。
从这一个层级上去理解去实现,就可以通过集合的 proxy get
来变相实现所有集合的方
法和属性代理。
注意 Reflect.get(target, key, receiver)
第一个传的是什么?
boolean ? instrumentations : target
即封装后的 instrumentations
啊 !
如: map.get()
-> target: map, key: get
-> target: instumentations, key:
get
-> get(target, key, isReadonly, isShallow)
集合的操作最终 —–> 转变成 instrumentations 对象上的操作。
去掉暂时不需要的代码(65ea709):
实现顺序(原理)
|
|
理解过程:
首先要理解执行这一句
map.get('foo')
发生了什么
首先是
map.get
取值操作,即createInstrumentationGetter()
最后 return 的 那一句其实是针对
map.get
操作的代理,将 "get" 方法从 map 对象中取出来的代理。所以
Reflect.get(target, key, receiver)
这里的key = "foo"
经过 #1 之后,需要立即执行 "get" 方法即
()
操作此时执行的是
mutableInstrumentations.get(this, key)
方法所以这里的
key = 'foo'
,this
就是调用get()
方法的对象 map 。最后 get 操作会被模块全局函数
get(target, key, isReadonly, isShallow)
代替, 做了许多特殊处理,收集依赖。
12bc4da add get handler
feat(add): get function for collection proxy · gcclll/stb-vue-next@12bc4da
FIX: edc1d3f 死循环问题(直接放回 target.get(key) 又会触发 get -> …) fix: infinite loop · gcclll/stb-vue-next@edc1d3f
|
|
+RESULTS:
{ key: 'get', target: Map(1) { 'foo' => 1 }, x: 'in createInstrumentationsGetter' } { key: 'foo', target: Map(1) { 'foo' => 1 }, x: 'in get' } { res: 100 }
结果如上(参见.原理详细分析)
reactive(map) -> 将 map 代理给
instrumentations{ get }
observed.get -> 得到 instrumentations 里面的 "get" 方法
('foo') -> 执行
instrumentations.get(this, key)
, key = 'foo'返回结果
至此,完成 collection get proxy handler 的完整流程。
0b3fd71 add get handler track
feat(add): collection proxy get -> global get · gcclll/stb-vue-next@0b3fd71
新增get 操作,track 添加依赖。
|
|
+RESULTS:
{ key: 'get', target: Map(1) { 'foo' => 1 }, x: 'in createInstrumentationGetter' } { key: 'foo', type: 'get', dep: Set(1) { [Function: reactiveEffect] { id: 0, allowRecurse: false, _isEffect: true, active: true, raw: [Function (anonymous)], deps: [Array], options: {} } }, x: 'in track' } { key: 'foo', target: Map(1) { 'foo' => 1 }, x: 'in global get' } dummy = 100
分为三个阶段
collection proxy handler 取 map.get 方法,
key = 'get'
('prop')
执行期触发instrumentations.get(this, key), key = 'foo'
执行 global get 触发
track
收集依赖,返回结果值
假设 map.get(key)
的 key 也是个 proxy :
|
|
+RESULTS:
{ #1 key: 'get', target: Map(1) { { k: 1 } => { v: 2 } }, x: 'in createInstrumentationGetter' } #2 { key: { k: 1 }, rawKey: { k: 1 }, eq: false } { #3 key: { k: 1 }, type: 'get', dep: Set(1) { [Function: reactiveEffect] { id: 0, allowRecurse: false, _isEffect: true, active: true, raw: [Function (anonymous)], deps: [Array], options: {} } }, x: 'in track' } { #4 key: { k: 1 }, type: 'get', dep: Set(1) { [Function: reactiveEffect] { id: 0, allowRecurse: false, _isEffect: true, active: true, raw: [Function (anonymous)], deps: [Array], options: {} } }, x: 'in track' } { #5 key: { k: 1 }, target: Map(1) { { k: 1 } => { v: 2 } }, x: 'in global get' } dummy = 100
#1 proxy collection get handler
#2 global get 函数里调用 track 之前输出,显示
key
和rawKey
是不同的 (eq = false
),因为前者是个 proxy 后者是 key proxy 的 rawValue 。#3 track() 调用时的输出,显示的是需要收集依赖的是
proxy key{k: 1}
#4 track() 调用时的输出,显示的是需要收集依赖的是
raw key{k: 1}
从 #3, #4 可知如果 key 本身已经是 proxy 那么它及其对应的 rawKey 同时也会收集 当前的 effect 。
77b14ef add get handler return value
feat(add): collection proxy get with value return · gcclll/stb-vue-next@77b14ef
这里处理分为两部分:
取出
has
方法检测存在性根据
isReadonly
和isShallow
决定对返回值做什么处理,如:递归 reactive/readonly使用 target.get(key) 取出结果值返回
add collection set proxy handler
feat(add): collection set proxy handler · gcclll/stb-vue-next@7b680df
set proxy handler 处理
设值的时候可能有两种情况 a) set, b) add
需要考虑 proxy key 和 raw key 问题
最后 trigger 触发依赖
|
|
测试
|
|
+RESULTS:
> before get, deps undefined > after get, deps <ref *1> Set(1) { [Function: reactiveEffect] { id: 0, allowRecurse: false, _isEffect: true, active: true, raw: [Function (anonymous)], deps: [ [Circular *1] ], options: {} } } #1 before set, dummy = undefined #2 after set, dummy = 1
add collection size,has,add proxy handler
feat(add): size, has, add collection proxy handlers · gcclll/stb-vue-next@73fa5eb
has: proxy key, raw key 都需要 track has 操作依赖
|
|
size: 取size 内部实现过程中是需要对 collection 进行迭代操作的,所以 track 用的是 ITERATE_KEY
|
|
add: set.add 操作,根据 set 特性,key,value 都是同一个且元素是不重复的,所以只需 要检测是不是新增,新增就需要 trigger ADD 。
|
|
trigger 处理:838b402
feat(add): collection trigger cases · gcclll/stb-vue-next@838b402
测试:
|
|
+RESULTS:
before set, get map size -> dummy = 0 after set, get map size -> dummy = 1 observed has 'foo' -> dummy = true before add, get set size -> dummy = 0 after add, get set size -> dummy = 1
add collection delete,clear proxy handler
feat(add): collection delete and clear · gcclll/stb-vue-next@b3c5087
delete:
|
|
clear:
|
|
测试:
|
|
+RESULTS:
>>> map before delete, dummy = 1 after delete, dummy = 0 before clear, dummy = 2 after clear, dummy = 0 >>> set before delete, dummy = 1 after delete, dummy = 0 before clear, dummy = 3 after clear, dummy = 0
add collection forEach proxy handler
feat(add): collection forEach proxy handler · gcclll/stb-vue-next@77a0222
|
|
将 forEach 封装了一层,对传递给回调的值 reactive 化,使用 ITERATE_KEY
收集调用
该方法的依赖。
测试:
|
|
+RESULTS:
#1 before set 1, dummy = 0 #2 before set 2, dummy = 1 #3 after set, dummy = 4
#1 effect 会立即执行一次,但是此时 map 没数据
#1 添加
foo => 1
之后执行 effect fn forEach 迭代器进行累加操作的结果#2 添加
bar => 2
结果是 4,原因是到这一步的时候dummy = 1
的,所以再累加之
后就是 4
add collection iterators methods proxy handler
feat(add): collection iterable methods · gcclll/stb-vue-next@e5497be
add code:
|
|
test:
|
|
+RESULTS:
>>> #1 set before set, dummy = foo,1,bar,2,dax,[object Object] after set, dummy = foo,1,bar,2,dax,[object Object],foo,1,bar,2,dax,[object Object],baz,3 obj in map is reactive true >>> #2 clear before clear, dummy = 4 after clear, dummy = 0 >>> #3 should not observe custom property before set cumstom prop, dummy = undefined after set cumstom prop, dummy = undefined >>> #4 不应该使 Proxies 污染原来的 Map 对象 map2.get('key') !== value, true map2.get('key') === toRaw(value), true
#1 在遍历过程中 get -> track -> 递归 reactive,所以 obj 是
obsreved.get('dax')
结果是 reactive 。#2 clear 内部实现会取迭代器进行迭代删除,并且改变最终 size 值。
#3 collectionHandlers.ts 中的方法都是针对集合本身元素进行操作的,对于自定义 属性是不在响应式 Map/Set 之列的。
#4 set proxy handler 里面的实现会先取
toRaw(value)
再进行设置操作。
add collection readonly proxy handlers
feat(add): readonly collection handlers · gcclll/stb-vue-next@fa2636d
创建几个设置型的方法(add,set,delete,clear
)
create readonly method for settable handlers(add,set,delete,clear
)
|
|
readonly instrumentations:
|
|
测试:
add collection shallow proxy handlers
feat(add): shallow collection handlers · gcclll/stb-vue-next@676bc70
不会递归 reactive 版本。
computed
types definitions
feat(add): computed type definitions · gcclll/stb-vue-next@e9e53a1
computed 计算属性的一些类型定义。
|
|
computed 函数重载(315e0d9): feat(add): computed function reloads · gcclll/stb-vue-next@315e0d9
|
|
implementation
feat(add): computed tpl and computed function · gcclll/stb-vue-next@64d380d
计算属性实现全在 ComputedRefImpl<T>
类的实现中,实现关键点
使用 effect 封装 getter 函数,收集所有依赖,在特定时候执行 effect
_dirty 标记,一旦
_dirty = true
表示数据有更新,下次取值的时候就要立即执行 effect 取最新值
class ComputedRefImpl
|
|
computed 函数:
|
|
computed 函数的 options 可以是函数或一个对象,可以用外部自定义 setter 函数,比如 在更新之前记录当前状态,就可以在 options.set 中去实现。
测试请移步“计算属性测试用例”
脑图请直接查看“完整脑图 computed 部分”
add ref
这部分因为之前没有单独拎出来一步步实现,而是直接拷贝过来了为了先测试 computed 属 性。
所以这里直接根据源码以及使用方式来进行逐步分析。
APIs:
api | function |
---|---|
isRef(r:any) | 检测函数 |
ref(value) | 将 value 转成 Ref 类型 |
shallowRef(value) | 不进行递归 reactive |
createRef(rawValue, shallow = false) | create ref, for ref/shallowRef |
triggerRef(ref: Ref) | 触发 ref 变量上的所有依赖 |
unref(ref) | 取消 ref 化,返回 ref.value 原始值 |
proxyRefs(objectWithRefs) | refs 代理 |
RefImpl | Ref 变量模板 |
CustomRefImpl | 自定义 Ref 变量模板 |
ref & shallow ref
ref()
和 shallowRef()
函数都是调用的同一个函数 createRef(val, shallow)
来
创建ref 变量,而 createRef
本身也很简单,就是 new
了一个 RefImpl
实例出来。
另外针对已经是 ref 的值不需要重复 new
操作,直接返回原 ref。
|
|
参数:
val 需要进行 ref 化的变量
shallow 如果 val 是对象的时候决定是否需要对该对象进行递归 reactive 化
看下 ref 结构模板类: RefImpl<T>
|
|
代码是相当简单的,四个属性(_value/__v_isRef/_rawValue/_shallow
)+两个访问器方法
(value getter/setter
)。
根据 es6 class 语法,构造参数如果使用了权限修饰符会自动转成成员属性,所以
_rawValue
和_shallow
也是 RefImpl 成员属性和_value
是一样的,区别在于这 两个值可以通过外部控制。
开始测试吧:
|
|
1. base usage >>> before, a.value = 1 after, a.value = 2 2. should be reactive >>> ge/set value 里面使用的是 track/trigger before set a.value, dummy=2, calls=1 after set a.value, dummy=3, calls=2 3. 默认情况下对嵌套对象属性进行 reactive >>> before set, dummy = ${dummy} after set, dummy = ${dummy} 4. ref() 不传值的时候也应该可以工作 before set, dummy = undefined after set, dummy = 100 5. ref 作为多级对象的值时 before set, dummy1=1, dummy2=1 d.value++, dummy1=2, dummy2=2 obj.d++, dummy1=3, dummy2=3 obj.b.c++, dummy1=4, dummy2=4 undefined
测试分析:
基本使用
ref 实现基于 effect 的 track 和 trigger
get value 实现
1 2 3 4 5 6 7 8
class RefImpl { // ... get value() { track(toRaw(this), TrackOpTypes.GET, "value"); return this._value; } // ... }
set value 实现
1 2 3 4 5 6 7 8 9 10 11 12 13
class RefImpl { set value(newVal) { if (hasChanged(toRaw(newVal), this._rawValue)) { this._rawValue = newVal; // convert() 检测 newVal 是对象就调用 reactive // 注意这里只是简单的赋值操作,所以 ref 可以接受任意类型的值 this._value = this._shallow ? newVal : convert(newVal); // trigger 触发 value 属性上的依赖进行 // 因为 track 也是在这个属性上进行收集的 trigger(toRaw(this), TriggerOpTypes.SET, "value", newVal); } } }
should be reactive
这个在 1 列出了 get value 源码,结合 track/trigger 达到 reactive 目的。
这一点直接看源码
set value(newVal)
this._value = this._shallow ? newVal : convert(newVal)
convert() 针对对象 reactive
因为
new RefImpl(rawValue, shallow)
是简单的赋值操作,给啥值都行因为
new RefImpl(rawValue, shallow)
返回的是个 RefImpl 实例对象,属于引用类 型,不管对象层级多深,最终引用的都是这个对象。
object ref
对对象的所有属性进行 ref 化,在进行 proxy ref 之前需要先完成这部分,因为它依赖 object ref。
function | 描述 |
---|---|
toRefs(object) | ref对象的所有属性 |
ObjectRefImpl | 对象 ref 模板 |
toRef(object, key) | 针对 object[key] 进行 ref |
相关源码:
|
|
源码分析 : ObjectRefImpl
中针对每个 object[key]
持有了一份 object 引
用 _object, 且在最开始 new
实例的时候将属性名保存到了 _key,后续取值设值用的都
是这个值,然后重写了 getter/setter 函数,在取值设值的时候操作 _object + _key
。
打个比方:
|
|
before set, rawObj.count = 0 after set, rawObj.count = 100 before set on obj, rawObj.count = 100 after set on obj, rawObj.count = 200 undefined
看到没,实际都是改变了 rawObj.count
所以, object ref 等于是创建了一个全新的对象里面包含的属性和 raw object 属性名一 致,但是值是
ObjectRefImpl
创建的实例,用来间接操作 raw object 。
测试:
|
|
ref proxy
作用:代理 ref 遍历的 get/set 操作,让 get 在返回之前先 unref(result)
得到原始
的值返回,让 set 在 set 之前确保设值的值是在 ref.value 上。
和 ref proxy 有关的函数和属性:
function | 描述 |
---|---|
shallowUnwrapHandlers { get, set } | 代理的 handler |
proxyRefs(objectWithRefs) | 代理 ref 对象 |
源码:
|
|
ref proxy 作用:针对被 ref 的 object 对象,进行 get 和 set 操作代理。
get 操作, Reflect.get(...)
拿到的是个 unref,通过 proxy get handler 去 ref 化,
返回其原始值。
set 操作, Reflect.set(...)
新值是 ref 直接覆盖之前的 target[key]
值,如果不是
ref,将其设置到 oldValue.value
上。
测试:
|
|
>>> before proxy 1. dummy = 0 2. dummy = 1 undefined
custom ref
相关函数和类:
function | 描述 |
---|---|
CustomRefImpl | 自定义的 ref 模板 |
customRef(factory) | 自定义 ref,即由外部决定如何实现 getter & settter value |
让使用者自定义以什么方式在 get 的时候 track 或 set 的时候 trigger
|
|
就是我只负责给你构造一个 Ref 结构的对象,至于什么 get 和 set 怎么实现全权交给使 用者,get/set 默认 track/trigger value 属性。
测试:
|
|
custom is ref, true dummy = 1 dummy = 2 undefined
辅助函数
function | desc |
---|---|
isRef(value) | 检测 value.__v_isRef 值 |
triggerRef(ref) | 手动触发 ref value 上的依赖 |
unref(ref) | 返回 ref.value 值 |
jest 🏃跑🏃用例
除了 runtime-dom 没实现的部分,都通过了测试。
@vue/runtime-dom (guessing 'runtimeDom') created packages/vue/dist/vue.global.js in 1.4s FAIL packages/reactivity/__tests__/effect.spec.ts ● Test suite failed to run packages/reactivity/__tests__/ref.spec.ts:11:26 - error TS2307: Cannot find module '@vue/runtime-dom' or its corresponding type declarations. 11 import { computed } from '@vue/runtime-dom' ~~~~~~~~~~~~~~~~~~ PASS packages/reactivity/__tests__/reactive.spec.ts PASS packages/reactivity/__tests__/collections/Set.spec.ts PASS packages/reactivity/__tests__/collections/Map.spec.ts PASS packages/reactivity/__tests__/reactiveArray.spec.ts PASS packages/reactivity/__tests__/collections/WeakMap.spec.ts PASS packages/reactivity/__tests__/collections/WeakSet.spec.ts PASS packages/reactivity/__tests__/shallowReactive.spec.ts PASS packages/reactivity/__tests__/readonly.spec.ts FAIL packages/reactivity/__tests__/ref.spec.ts ● Test suite failed to run Configuration error: Could not locate module @vue/runtime-dom mapped as: /Users/simon/github/vue/stb-vue-next/packages/$1/src. Please check your configuration for these entries: { "moduleNameMapper": { "/^@vue\/(.*?)$/": "/Users/simon/github/vue/stb-vue-next/packages/$1/src" }, "resolver": undefined } 2 | ref, 3 | effect, > 4 | reactive, | ^ 5 | isRef, 6 | toRef, 7 | toRefs, at createNoMappedModuleFoundError (node_modules/jest-resolve/build/index.js:551:17) at Object.<anonymous> (packages/reactivity/__tests__/ref.spec.ts:4:23) PASS packages/reactivity/__tests__/computed.spec.ts Test Suites: 2 failed, 9 passed, 11 total Tests: 169 passed, 169 total Snapshots: 0 total Time: 15.568 s
用例分析
Map.spec.ts
instanceof
1 2 3 4 5 6 7
test('instanceof', () => { const original = new Map() const observed = reactive(original) expect(isReactive(observed)).toBe(true) expect(original instanceof Map).toBe(true) expect(observed instanceof Map).toBe(true) })
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
const { isReactive, effect, reactive, targetMap, shallowReactive } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') const map = new Map() const ob = reactive(map) console.log(`#1 ob is reactive, ${isReactive(ob)}`) console.log(`#2 ${map instanceof Map}`) console.log(`#3 ${ob instanceof Map}`) console.log(map, ob)
+RESULTS:
#1 ob is reactive, true #2 true #3 true Map(0) {} Map(0) {}
should observe mutations(应该观察变化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
it('should observe mutations', () => { let dummy const map = reactive(new Map()) effect(() => { dummy = map.get('key') }) expect(dummy).toBe(undefined) map.set('key', 'value') expect(dummy).toBe('value') map.set('key', 'value2') expect(dummy).toBe('value2') map.delete('key') expect(dummy).toBe(undefined) })
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
const { isReactive, effect, reactive, targetMap, shallowReactive } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') let dummy const map = reactive(new Map()) effect(() => { dummy = map.get('key') }) console.log(`#1 dummy = ${dummy}`) map.set('key', 'value') console.log(`#2 dummy = ${dummy}`) map.set('key', 'value2') console.log(`#3 dummy = ${dummy}`) map.delete('key') console.log(`#4 dummy = ${dummy}`)
+RESULTS:
#1 dummy = undefined #2 dummy = value // set 触发 trigger effect fn #3 dummy = value2 // 同上 #4 dummy = undefined // 删除触发 DELETE trigger 与该
#4 属性的 ADD | DELETE | SET 操作首先会将所有与该 key 有关的依赖添加到将执行序列。
1 2 3 4
// SET | ADD | DELETE operation if (key !== void 0) { add(depsMap.get(key)) }
should observe mutations with observed value as key
将 reactive 类型的值作为 key 的时候也应该能被观察变化。
computed.spec.ts
下面所有的用例都可以参考 用例 01 的脑图(原理图)
should return updated value
|
|
测试:
|
|
+RESULTS:
#1 before set value.foo, cValue.value = undefined #2 after set value.foo, cValue.value = 1
分析结果
#1 : 因为 computed 默认是 effect lazy 的,所以不会立即执行 effect,所以 cValue 也就不会有值
#2 : 此时值为 1,并不是因为 value.foo = 1
触发的 effect 执行,
而是因为 _dirty
默认是 true
的,所以在 #1 处取值的时候触发了
effect()
执行, _dirty = false
了,值结果已出。
|
|
也正是由于 effect
的执行,让 value.foo
收集到了这个依赖。
随后设置 value.foo
也会将 computed effect 执行,由于 options 中提供了
scheduler,所以会执行置 _dirty = true
,但是此时计算属性值并不会立即计算得出
结果(因为它的计算操作始终是在 get value()
里面发生的),所有当下次取值操作(即
#2 行执行时),检测到 _dirty = true
了才会重新计算返回最新结果。
用例脑图:
should compute lazily
|
|
测试:
|
|
+RESULTS: 分析如代码注释
#1 before get value, getter called 0 times. #2 get value(), cValue.value = undefined #3 after get value, getter called 1 times #4 after get value 2, getter called 1 times #5 after set 'value.foo = 1', getter called 1 times #6 should re-compute value, getter called 1 times, cValue.value = 1 #7 should not re-compute value, getter called 2 times, cValue.value = 1
Tips: 总是记着计算属性重新计算的关键点:“_dirty = true 且随后的取值操作”。
should no longer update when stopped
|
|
测试:
|
|
+RESULTS:
before set value, dummy = undefined after set value, dummy = 1 after stop and set value, dummy = 1
TODO should support setter
|
|
should be readonly
只读版本,是有 computed 使用的时候传入的参数(option
)决定的。
如果 option 是函数(
getter
) 那就是只读的如果 option 是对象且提供了
option.set
那么是非只读如果 option 是对象但是没有提供
option.set
那么也是只读的
|
|
测试:
|
|
+RESULTS:
#1 x is readonly, true #2 x.value is readonly, false #3 x.value.a is readonly, false #4 z is readonly, false #5 z.value.a is readonly, false
effect.ts
effect + track + trigger
commit: feat: effect-trigger · gcclll/stb-vue-next@b5f97b4
lazy: true 标识 effect fn 不会立即执行
点击 set 操作,此时并没有依赖,所以只会触发 count++
当点击 get 操作,触发
track()
收集依赖 fn -> deps再点击 set 操作,此时已经有依赖,所以会
trigger()
所有依赖更新options.scheduler 选项作用
如果 options 有 scheduler 选项,
trigger()
的时候不会立即执行 effects 而是 调用 scheduler 并将当前需要被执行的 effect 当做参数给 scheduler,由使用者决定 何时去执行 effect,比如需要在 dummy 更新之前做点什么。
effect 测试
测试1(base, prototype)
测试内容:
effect 基本使用
effect 作用域原型链
|
|
结果分析:
#1 obj1.num 依赖是在 effect 第一次执行的时候收集的
#2 obj2.num 在执行
delete counter.num
之前是没有任何依赖因为此时并没有任何
parentCounter
上的操作#3 obj2.num 有了自己的依赖
此时,执行了
delete counter.num
逻辑如下:对 counter.num 执行删除会触发
num
上的所有依赖 deps,即执行 effect fn,在 effect fn 里面有
counter.num
的取值操作,但是发现属性被删除,根据取值查找 原理,会在对象的原型链上逐级往上查找(parentCounter
),找到parentCounter.num
随机进行取值操作,所以删除操作之后的dummy = 2
,且取值 操作触发 tracking 因此此时parentCounter.num
就有了自己的依赖 effect fn。#4 给 parentCounter 设值触发 effect fn,查找原型链 , 所以 dummy = 4
#5 给 counter 设值触发 effect fn,不查找原型链(自身属性),所以 dummy = 3
+RESULTS:
before set, dummy = 0, dummy1 = 0 after set, dummy = 8, dummy1 = 8 after delete, dummy = undefined, dummy1 = undefined >>> 原型链 dummy = 0 > #1 obj1.num 的依赖 <ref *1> Set(1) { [Function: reactiveEffect] { id: 2, allowRecurse: false, _isEffect: true, active: true, raw: [Function (anonymous)], deps: [ [Circular *1] ], options: {} } } > #2 obj2.num 的依赖, delete 之前 undefined after delete, dummy = 2 > #3 obj2.num 的依赖, delete 之后 <ref *1> Set(1) { [Function: reactiveEffect] { id: 2, allowRecurse: false, _isEffect: true, active: true, raw: [Function (anonymous)], deps: [ [Circular *1], [Set] ], options: {} } } #4 after 'parentCounter.num = 4', dummy = 4 #5 after counter.num = 3', dummy = 3 undefined
测试2(stop, …)
stop :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
const { isReactive, effect, reactive, targetMap, shallowReactive, stop } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') console.log(`>>> stop effect`) let dummy const obj = reactive({ prop: 1 }) const runner = effect(() => { dummy = obj.prop }) obj.prop = 2 console.log(`#1, after 'obj.prop = 2', dummy = ${dummy}`) console.log(`> prop deps, before stop`) console.log(targetMap.get(obj.__v_raw).get('prop')) // 清空了所有依赖 stop(runner) // stop the effect, set effect.active = false console.log(`> prop deps, after stop`) console.log(targetMap.get(obj.__v_raw).get('prop')) obj.prop = 3 console.log(`#2, after stop, 'obj.prop = 3', dummy = ${dummy}`) obj.prop = 4 console.log(`#3, after stop, 'obj.prop = 4', dummy = ${dummy}`) runner() console.log(`#4, after run runner, dummy = ${dummy}, runner.active = ${runner.active}`)
+RESULTS:
>>> stop effect #1, after 'obj.prop = 2', dummy = 2 > prop deps, before stop <ref *1> Set(1) { [Function: reactiveEffect] { id: 0, allowRecurse: false, _isEffect: true, active: true, raw: [Function (anonymous)], deps: [ [Circular *1] ], options: {} } } > prop deps, after stop Set(0) {} #2, after stop, 'obj.prop = 3', dummy = 2 #3, after stop, 'obj.prop = 4', dummy = 2 #4, after run runner, dummy = 4, runner.active = false undefined
stop 干了两件事(a. 清空所有 effect.deps, b. 将 effect.active 置为 false)
stop 之后 trigger 时没有 deps 可执行,所以无论如何 effect fn 不会被执行
手动执行 runner() 之后执行effect fn 重新收集依赖(此时 active 依旧为
false
)
stop + scheduler :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
const { isReactive, effect, reactive, targetMap, stop, shallowReactive } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') let dummy const obj = reactive({ prop : 1 }) const queue = [] const runner = effect(() => (dummy = obj.prop), { scheduler: e => queue.push(e) }) obj.prop = 2 console.log(`#1 after 'obj.prop = 2', dummy = ${dummy}`) console.log(`#2 after 'obj.prop = 2', queue.length = ${queue.length}`) stop(runner) queue.forEach(e => e()) console.log(`#3 after stop, queue forEach, dummy = ${dummy}`)
+RESULTS:
#1 after 'obj.prop = 2', dummy = 1 #2 after 'obj.prop = 2', queue.length = 1 #3 after stop, queue forEach, dummy = 1
提供了 scheduler 选项的 effect 永远不会被执行,源码:
1 2 3
if (!effect.active) { return options.scheduler ? undefined : fn() }
onStop :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
const { isReactive, effect, reactive, targetMap, stop, shallowReactive } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') let n = 0 const runner = effect(() => {}, { onStop() { console.log(`stopped ${++n} times`) } }) stop(runner) stop(runner) stop(runner)
+RESULTS:
stopped 1 times
只会被执行一次,因为
effect.active = true
时才可以被 stop 。stop: 一个 stopped 的 effect 在一个正常的 effect 中调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
const { isReactive, effect, reactive, targetMap, stop, shallowReactive } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js') let dummy const obj = reactive({ prop: 1 }) const runner = effect(() => { dummy = obj.prop }) stop(runner) obj.prop = 2 console.log(`#1 after stop runner, dummy = ${dummy}`) // 这里等于是手动执行了 runner effect `dummy = obj.prop` // 所以下面的 effect 被 obj.prop 收集进 deps<Set> effect(() => { runner() }) obj.prop = 3 console.log(`#2 after runner in effect, dummy = ${dummy}`)
+RESULTS:
#1 after stop runner, dummy = 1 #2 after runner in effect, dummy = 3
#1 值依旧是 1 ,是因为 stop 了
#2 值为 3,是因为 effect 执行 runner() 使得
obj.prop
收集到第二个 effect fn 。