更新日志&Todos

  1. DONE [2021-01-20 15:16:45] ref

诗号:六道同坠,魔劫万千,引渡如来。

/img/bdx/yiyeshu-001.jpg

stb-vue-next 完全拷贝于 vue-next ,主要目的学习及尝试应用于机顶盒环境。

本文依据 commit 进程进行记录

所有用例请按 <f12> 打开控制台查看 >

add: createReactiveObject

feat: reactive-fn · gcclll/stb-vue-next@cb3470d

/img/vue3/reactivity/reactivity-reactive.svg

仅增加函数声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export function reactive(target: object) {
  // 如果试图 observe 一个只读 proxy,返回只读版本
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }

  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    {}
    // mutableCollectionHandlers
  )
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {}

mutableHandlers: 普通对象类型的 proxy handlers

mutableCollectionHandlers: 集合类型的 proxy handlers,因为 Reflect 并没有对集 合类型做底层映射,所以需要特殊处理。

feat: reactive(target)

feat: createReactiveObject · gcclll/stb-vue-next@443a0b5

/img/vue3/reactivity/reactivity-create-reactive-object.svg

  1. 重点: new Proxy(target, collection)

  2. 被代理类型必须是对象(引用类型)

  3. target 本身已经是 proxy 了

  4. target 代理有缓存不用重复创建

  5. 必须是合法的类型(Object|Array|[Weak]Map|[Weak]Set)才能被代理

  6. 记得缓存新创建的代理关系(proxyMap 全局变量)

    用例一:普通对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const { effect, reactive, targetMap, isReactive } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const original = { foo: 1 }
const observed = reactive(original)

console.log('observed is not orignial,' + original !== observed)
console.log('observed is reactive, ' + isReactive(observed))
console.log('original is reactive, ' + isReactive(original))
console.log('observed.foo === 1, ' + observed.foo === 1)
console.log('`foo` in observed, ' + (`foo` in observed))
console.log(`Object.keys(observed) == ['foo'], ` + (Object.keys(observed).toString() === 'foo'))

+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

用例二:原型

 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 obj = {}
const reactiveObj = reactive(obj)
console.log('reactiveObj is reactive, ' + isReactive(reactiveObj))
const prototype = reactiveObj['__proto__']
const otherObj = { data: ['a'] }
console.log('otherObj is reactive, ' + isReactive(otherObj))
const reactiveOther = reactive(otherObj)
console.log('reactiveOther is reactive, ' + isReactive(reactiveOther))
console.log('reactiveOther.data[0] is `a`, ' + ( reactiveOther.data[0] === 'a' ))
console.log(`__proto__, ` + prototype)

+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

用例三:嵌套对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const {isReactive, effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')
const original = {
  nested: {
    foo: 1
  },
  array: [{ bar: 2 }]
}

const observed = reactive(original)
console.log(`observed.nested is reactive ${isReactive(observed.nested)}`)
console.log(`observed.array is reactive ${isReactive(observed.array)}`)
console.log(`observed.array[0] is reactive ${isReactive(observed.array[0])}`)

+RESULTS:

observed.nested is reactive true
observed.array is reactive true
observed.array[0] is reactive true

用例四:代理后的对象操作也会体现在原对象上

1
2
3
4
5
6
7
8
9
const { isReactive, effect, reactive, targetMap } =
      require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const or = { foo: 1 }
const ob = reactive(or)
ob.bar = 1
console.log(`ob.bar = ${ob.bar}, or.bar = ${or.bar}`)
delete ob.foo
console.log(`'foo' in ob: ${'foo' in ob}, 'foo' in or: ${'foo' in or}`)

+RESULTS:

ob.bar = 1, or.bar = 1
'foo' in ob: false, 'foo' in or: false

结果删除后,依旧在,需要实现 delete proxy handler。

用例五:原始对象上的操作也要能在代理后对象有所体现

1
2
3
4
5
6
7
8
9
const { isReactive, effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const original = { foo: 1 }
const observed = reactive(original)

original.bar = 1
console.log(`observed.bar = ${observed.bar}, original.bar = ${original.bar}`)
delete original.foo
console.log(`'foo' in original: ${'foo' in original}, 'foo' in observed: ${'foo' in observed}`)

+RESULTS:

observed.bar = 1, original.bar = 1
'foo' in original: false, 'foo' in observed: false

用例六:被设置的值如果是对象,该对象也会被 Reactive

1
2
3
4
5
6
7
const { isReactive, effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const observed = reactive({})
const raw = {}
observed.foo = raw // #0
console.log(`observed.foo === faw, ${observed.foo === raw}`) // #1
console.log(`observed.foo is reactive, ${isReactive(observed.foo)}`)

+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 结果

1
2
3
4
5
6
const { isReactive, effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const original = { foo: 1 } // #1
const observed1 = reactive(original) // #2
const observed2 = reactive(observed1) // #3
console.log(`observed2 === observed1, ${observed2 === observed1}`)
observed2 === observed1, true
undefined

因为 reactive() 实现中组了检测,如果自身是个 proxy 就直接返回,所以 #3 中实 际直接将 observed1 返回了。

TODO 用例八:不应该用 proxies 污染原始对象?

1
2
3
4
5
6
7
8
9
const { isReactive, effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const original = { foo: 1 }
const original2 = { bar: 2 }
const observed = reactive(original)
const observed2 = reactive(original2)
observed.bar = observed2
console.log(`observed.bar === observed2, ${observed.bar === observed2}`)
console.log(`original.bar === original2, ${original.bar === original2}`)

+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 脑图: /img/vue3/reactivity/reactivity-basehd-get-01.svg

  1. 调用 Reflect.get(target, key, receiver) 执行原子操作

  2. 返回执行结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function createGetter(isReadonly = false, shallow = false) {
  // target: 被取值的对象,key: 取值的属性,receiver: this 的值
  return function get(target: Target, key: string | symbol, receiver: object) {
    const res = Reflect.get(target, key, receiver)

    // 是否只需要 reactive 一级属性(不递归 reactive)
    if (shallow) {
      return res
    }

    return res
  }
}
export const mutableHandlers: ProxyHandler<object> = {
  get
}

测试:

1
2
3
4
5
6
7
const { effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const target = { count: 0 }
const ob = reactive(target)
effect(() => ob.count) // ob.count 属性 收集 effect fn

console.log(targetMap.get(target))

+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) 监听取值收集依赖:

/img/vue3/reactivity/reactivity-basehd-get-02-track.svg

 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
30
31
32
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return;
  }

  // Map< obj -> Map<key, Set[...deps]> >
  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()));
  }

  // 正在请求收集的 effect ,是初次出现
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    // 自身保存一份被依赖者名单
    activeEffect.deps.push(dep);
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key,
      });
    }
  }
}

effect(fn, options)

/img/vue3/reactivity/reactivity-effect.svg

  • 参数列表

    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 // ???
    }
    
  • 解决问题 :

    1. fn 封装之后,执行 fn 过程中使用 try…finally ,防止 fn 执行异常导致 effect 进程中断

    2. 结合 shouldTrack, activeEffect 和 track() 函数,有效的避免了在 fn 中执行 obj.value++ 导致 effect 死循环问题,因为 try…finally 确保了只有 fn 函数 完成之后才会进入 finally 恢复 effect 状态(shouldTrack = true, activeEffect = last || null)。

相关函数及变量列表

nametypedesc
activeEffectReactiveEffect当前正在处理的 Effect,fn 还未执行完成,finally 还没结束
effectStackArray, []缓存所有状态还没完成的 Effect
shouldTrackboolean, truetrack() 中用来检测当前 effect 是否结束,从而判定是否可以继续执行 track() 收集依赖
trackStackArray, []保存着所有 Effect 的 shouldTrack 值
effect()function封装 fn成 ReactiveEffect 结构
track(target, type, key)function收集依赖,并且响应式递归
trigger(...)function当值更新时触发所有依赖更新
createReactiveEffect(fn, options)functioneffect() 函数主题功能分离出来
cleanup(effect: ReactiveEffect)function清空所有 fn 的依赖 effect.deps[]
enableTracking()function使能 Effect ,shouldTrack = true, 并将其加入 trackStack
resetTracking()function重置 Effect, shouldTrack = 上一个 Effect 的 shouldTrack 值或 true
 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw // 取出原始的函数,封装之前的
  }

  // 封装成 ReactiveEffect
  const effect = createReactiveEffect(fn, options)

  if (!options.lazy) {
    // 如果并没指定 lazy: true 选项,则立即执行 effect 收集依赖
    // 因为 effect 一般都会有取值操作,此时会触发 proxy get handler
    // 然后执行 track() 结合当前的 activeEffect 即 effect() 执行时候的这个
    // effect,这样取值操作就和当前取值作用域下的依赖函数建立的依赖关系
    effect()
  }
  return effect
}

let uid = 0

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  // 将 fn 执行封装成  ReactiveEffect 类型的函数
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      // 非激活状态,可能是手动调用了 stop
      // 那么执行的时候就需要考虑调用 stop 者是否提供了手动调度该 effect
      // 的函数 scheduler ? 也就是说你停止你可以重新启动
      return options.scheduler ? undefined : fn()
    }

    if (!effectStack.includes(effect)) {
      // 1. cleanup, 保持纯净
      cleanup(effect)
      try {
        // 2. 使其 tracking 状态有效,track() 中有用
        enableTracking() // track() 可以执行收集操作
        effectStack.push(effect) // effect 入栈
        // 3. 保存为当前的 activeEffect, track() 中有用
        activeEffect = effect // 记录当前的 effect -> track/trigger
        // 4. 执行 fn 并返回结果
        return fn() // 返回执行结果
      } finally {
        // 始终都会执行,避免出现异常将 effect 进程卡死
        // 5. 如果执行异常,丢弃当前的 effect ,并将状态重置为上一个 effect
        //   由一个 effect 栈来维护。

        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect

  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn // 这里保存原始函数引用
  effect.deps = []
  effect.options = options

  return effect
}

依赖和属性变更发生联系的桥梁模块。

  1. effect(fn, options) 封装执行 fn,触发取值操作 ->

  2. track(target, type, key) 收集对象及属性所有依赖 ->

  3. fn 中设值操作触发 trigger(...) 执行所有 deps,更新 DOM。

add trigger() proxy set handler

feat: proxy set and trigger operation · gcclll/stb-vue-next@20afde9

proxy set handler(createSetter)

 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

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    // TODO shallow or not, or ref ?
    //

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)

    const result = Reflect.set(target, key, value, receiver)

    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // TODO ADD
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }

    return result
}

trigger()

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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) {
    return
  }

  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  if (type === TriggerOpTypes.CLEAR) {
    // TODO collection clear operation
  } else if (key === 'length' && isArray(target)) {
    // TODO array change operation
  } else {
    // SET | ADD | DELETE operation
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    // TODO 迭代器 key,for...of, 使用迭代器是对数据的监听变化
  }

  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()
    }
  }

  effects.forEach(run)
}

observe object recursively

feat: observe object recursively · gcclll/stb-vue-next@b2143f9

针对嵌套对象进行递归 Reactive 。

/img/vue3/reactivity/reactivity-basehd-get-03-track-recursively.svg

effect -> track -> trigger 关系图

到此 effect + track + trigger 完成了最简单的响应式代码。

/img/vue3/reactivity/reactivity-effect-track-trigger.svg

  1. effect 封装注册函数

  2. track 取值触发收集依赖函数

  3. trigger 设值触发所有依赖函数执行

add delete(deleteProperty) proxy handler

feat: delete proxy handler · gcclll/stb-vue-next@05b98c5

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    // 删除成功,触发 DELETE
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

export const mutableHandlers: ProxyHandler<object> = {
  get,	  get,
  set	  set,
  deleteProperty
}

删除成功调用 trigger() 触发 DELETE

add has, ownKeys proxy handlers

feat: has + ownKeys proxy handler · gcclll/stb-vue-next@ab69fe9

增加 has, ownKeys proxy handlers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}

function ownKeys(target: object): (string | num | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

测试:

1
2
3
4
5
6
7
8
9
const { isReactive, effect, reactive, targetMap } = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const obj = reactive({ n: 0 })
let dummy = false
const runner = effect(() => (dummy = 'n' in obj), { lazy: true })

console.log(`before run effect, dummy = ${dummy}`)
runner()
console.log(`after run effect, dummy = ${dummy}`)

+RESULTS:

before run effect, dummy = false
after run effect, dummy = true

TODO add array support

feat: array support · gcclll/stb-vue-next@9aeb678

修改点:

 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
30
31
32
33
34
35
36
37
38
// 数组内置方法处理
const arrayInstrumentations: Record<string, Function> = {}
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
  const method = Array.prototype[key] as any
  arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
    const arr = toRaw(this)
    for (let i = 0, l = this.length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + '')
    }

    const res = method.apply(arr, args)
    if (res === -1 || res === false) {
      return method.apply(arr, args.map(toRaw))
    } else {
      return res
    }
  }
})
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
  const method = Array.prototype[key] as any
  arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
    pauseTracking()
    const res = method.apply(this, args)
    resetTracking()
    return res
  }
})

// createGetter
function createGetter(isReadonly = false, shallow = false) {
  // ...
  // 4. target is array
  const targetIsArray = isArray(target)
  if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
    return Reflect.get(arrayInstrumentations, key, receiver)
  }
  // ...
}
  1. 索引操作(includes, lastIndexOf, indexOf)处理

    确保索引取值的时候,能使用 track() 正确收集对应索引的依赖列表。

  2. 可改变原数组长度操作(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_isReactivecreateGetter() 里面会直接返回 !isReadonly

    • #03 同上

    • #04 取值的时候返回结果之前会检测当前是不是对象如果是会执行递归 reactive

    • #05 slice 实现过程并非深拷贝

    • #06observed[0] !== original[0] 一个原因

    • #07 浅拷贝问题

    • #08observed[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 length

     1
     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

增加添加数组元素支持。

1

  1. createGetter -> get proxy handler 中增加属性添加 trigger 操作

    trigger(target, TriggerOpTypes.ADD, key, value)

  2. effect.ts -> trigger() 中增加数组长度变更依赖收集和 ADD 操作依赖收集

    http://qiniu.ii6g.com/img/20201118105046.png

add shallow reactive

feat(add): shallowReactive api · gcclll/stb-vue-next@e85dfc6

正常 track 过程中会检测嵌套内的是不是对象,如果是对象会进行递归 reactive 让内部嵌套的对象也 reactive 化。

shallow reactive 意思是当对象存在嵌套的时候,不进行递归 reactive 。

这个通过在 track() 函数中做一次拦截处理。

测试:

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const props = shallowReactive({ n: { foo: 1} })

console.log(`props.n is reactive, ${isReactive(props.n)}`)

const props2 = shallowReactive({ n: reactive({ foo: 1 }) })
props2.n = reactive({ foo: 2 })
console.log(`props2.n is reactive, ${isReactive(props2.n)}`)

// array test
const shallowArray = shallowReactive([])
const a = {}
let size
effect(() => {
  size = shallowArray.length
})

console.log(`>> array`)
console.log(`before push a, size = ${size}`)
shallowArray.push(a)
console.log(`after push a, size = ${size}`)
shallowArray.pop()
console.log(`after pop, size = ${size}`)

console.log(`>> 迭代时不应观察`)
shallowArray.push(a)
const spreadA = [...shallowArray][0]
// 迭代也有取值过程,shallow = true 不会递归 reactive
console.log(`spreadA is reactive, ${isReactive(spreadA)}`)

console.log(`>> onTrack`)
const onTrackFn = () => console.log('on tracking...')
let b
effect(() => {
  b = Array.from(shallowArray)
}, {
  onTrack: onTrackFn
})

+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):

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive,
  readonly,
  isProxy,
  isReadonly
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

console.log(`>>> should make nested values readonly`)
const original = { foo: 1, bar: { baz: 2 } }
const wrapped = readonly(original)
console.log(`wrapped !== original, ${wrapped !== original}`)
console.log(`wrapped is proxy, ${isProxy(wrapped)}`)
console.log(`wrapped is reactive, ${isReactive(wrapped)}`)
console.log(`wrapped is readonly, ${isReadonly(wrapped)}`)
console.log(`original is reactive, ${isReactive(original)}`)
console.log(`original is readonly, ${isReadonly(original)}`)
console.log(`wrapped.bar is reactive, ${isReactive(wrapped.bar)}`)
console.log(`wrapped.bar is readonly, ${isReadonly(wrapped.bar)}`)
console.log(`original.bar is reactive, ${isReactive(original.bar)}`)
console.log(`original.bar is readonly, ${isReadonly(original.bar)}`)
console.log(`>> get`)
console.log(`wrapped.foo = ${wrapped.foo}`)
console.log(`>> has`)
console.log(`'foo' in wrapped, ${'foo' in wrapped}`)
console.log(`>> ownKeys`)
console.log(`Object.keys(wrapped), [${Object.keys(wrapped)}]`)

console.log(`>> set or delete, should fail`)
const qux = Symbol('qux')
const original2 = {
  foo: 1,
  bar: {
    baz: 2
  },
  [qux]: 3
}

const wrapped2 = readonly(original2)
wrapped2.foo = 2 // fail
console.log(`after 'wrapped2.foo = 2',  wrapped2.foo = ${wrapped2.foo}`)
wrapped2.bar.baz = 3
console.log(`after 'wrapped2.bar.baz = 3', wrapped2.bar.baz = ${wrapped2.bar.baz}`)
wrapped2[qux] = 4
console.log(`after 'wrapped2[qux] = 4',  wrapped2[qux] = ${wrapped2[qux]}`)

delete wrapped2.foo
console.log(`after 'delete wrapped2.foo', wrapped2.foo = ${wrapped2.foo}`)
delete wrapped2.bar.baz
console.log(`after 'delete wrapped2.bar.baz', wrapped2.bar.baz = ${wrapped2.bar.baz}`)
delete wrapped2[qux]
console.log(`after 'delete wrapped2[qux]', wrapped2[qux] = ${wrapped2[qux]}`)

+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):

 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
30
31
32
33
34
35
36
37
38
39
40
41
const {
  isReactive,
  effect,
  readonly,
  isReadonly,
  reactive,
  targetMap,
  isProxy,
  shallowReactive
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

console.log(`>>> should make nested values readonly`)
const original = [{ foo: 1 }]
const wrapped = readonly(original)
console.log(`wrapped !== original`)
console.log(`wrapped is proxy, ${isProxy(wrapped)}`)
console.log(`wrapped is reactive, ${isReactive(wrapped)}`)
console.log(`wrapped is readonly, ${isReadonly(wrapped)}`)
console.log(`original is reactive, ${isReactive(original)}`)
console.log(`original is readonly, ${isReadonly(original)}`)
console.log(`wrapped[0] is reactive, ${isReactive(wrapped[0])}`)
console.log(`wrapped[0] is readonly, ${isReadonly(wrapped[0])}`)
console.log(`original[0] is reactive, ${isReactive(original[0])}`)
console.log(`original[0] is readonly, ${isReadonly(original[0])}`)
console.log(`> get`)
console.log(`wrapped[0].foo = ${wrapped[0].foo}`)
console.log(`> has`)
console.log(`0 in wrapped, ${0 in wrapped}`)
console.log(`> ownKeys`)
console.log(`Object.keys(wrapped) = [${Object.keys(wrapped)}]`)

const wrapped2 = readonly([{ foo: 1 }])
wrapped2[0] = 1
console.log(`after 'wrapped2[0] = 1', wrapped2[0] = ${wrapped2[0]}`)
wrapped2[0].foo = 2
console.log(`after 'wrapped2[0].foo = 2', wrapped2[0].foo = ${wrapped2[0].foo}`)
wrapped2.length = 0
console.log(`after 'wrapped2.length = 0', wrapped2.length = ${wrapped.length}`)
console.log(`after 'wrapped2.length = 0', wrapped2[0].foo = ${wrapped2[0].foo}`)
wrapped2.push(2)
console.log(`after 'wrapped2.push(2)', wrapped2.length = ${wrapped2.length}`)

+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 互撩)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const {
  isReactive,
  effect,
  reactive,
  readonly,
  isReadonly,
  targetMap,
  toRaw,
  shallowReactive
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const a = readonly({})
const b = reactive(a)
console.log(`*#1* isReadonly(b), ${isReadonly(b)}`)
console.log(`*#2* toRaw(a) === toRaw(b), ${toRaw(a) === toRaw(b)}`)
console.log(`*#3* a === b, ${ a === b }`)

+RESULTS:

*#1* isReadonly(b), true
*#2* toRaw(a) === toRaw(b), true
*#3* a === b, true
undefined
  1. #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. #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

http://qiniu.ii6g.com/img/20201119153149.png

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive,
  shallowReadonly
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

// 嵌套对象不应该 reactive
console.log(`>>> should not make non-reactive properties reactive`)
let props = shallowReadonly({ n: {foo: 1} })
console.log(`isReactive(props.n), ${isReactive(props.n)}`)

// 根属性应该是 readonly
console.log(`>>> should make root level properties readonly`)
props = shallowReadonly({n : 1})
props.n = 2
console.log(`after 'props.n = 2', props.n = ${props.n}`)
// 嵌套的属性不应该是 readonly ,因为是 shallow
console.log(`>>> should NOT make nested properties readonly`)
props = shallowReadonly({ n: { foo: 1 } })
props.n.foo = 2
console.log(`after 'props.n.foo = 2', props.n.foo = ${props.n.foo}`)

+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

这里的结果不难理解

  1. shallow 不会递归 reactive

  2. readonly 让属性只读,但是由于是 shallow 所以只有对象根属性才是只读

add effect stop

feat(add): effect stop · gcclll/stb-vue-next@f1e5b3a

http://qiniu.ii6g.com/img/20201119162119.png

stop() 函数操作:

  1. 清空所有 effect 上的 deps,同时将当前的 effect 从所有依赖它的 dep 中删除

    effect.deps[i].delete(effect) , 这一步是将 targetMap > depsMap > deps 中 的 effect 删除。

    effect.deps.length = 0

  2. 将 effect.active 置为 false

执行 stop() 之后,只能手动调用 runner() 来触发 effect fn(前提是没有提供 options.scheduler ,否则永远不会被执行) 。

被 stopped 的 effect 可以当做另一个正常的 effect 的 fn。

集合类型代理(proxy handlers)脑图

/img/vue3/reactivity/reactivity-collection-proxy.svg

add collection handlers

feat(add): mutable collection handlers · gcclll/stb-vue-next@521f755

collection proxy handlers 脑图链接

因为 Reflect 没有集合操作的对应接口,所以针对集合类型需要通过 get proxy 来中转 做特殊处理。

1
2
3
4
5
6
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  // TODO
}
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  // get: createInstrumentationGetter(false, false)
}

添加集合类型的 handlers。

add collection get proxy handler

feat(add): collection get proxy · gcclll/stb-vue-next@a5e8e06

针对集合的所有操作代理都是通过 get proxy 变相完成的,所以搞懂这里是至关重要的。

collection proxy handler:

1
2
3
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(false, false)
}

简单吧,别被假🐘给迷惑了!!!

这里的原理如果想通了也简单。

试想下,我们调用集合类型的方法是怎么调用的???

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):

feat: add get proxy handler · gcclll/stb-vue-next@65ea709

实现顺序(原理)

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 1. 对外的 handlers
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(false, false)
}


// 2. 封装 get proxy 所有 collection 操作的入口
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }

    // 将集合操作代理到 instrumentations 对象上
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

// 3. map -> instrumentations -> proxy 中间对象
const mutableInstrumentations: Record<string, Function> = {
  // get proxy handler, this -> target
  get(this: MapTypes, key: unknown) {
    return get(this, key)
  }
}


// 4. 最终执行操作得到结果的函数
function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  // TODO

  console.log({ target, key })
  return target.get(key)
}

理解过程:

首先要理解执行这一句 map.get('foo') 发生了什么

  1. 首先是 map.get 取值操作,即 createInstrumentationGetter() 最后 return 的 那一句

    其实是针对 map.get 操作的代理,将 "get" 方法从 map 对象中取出来的代理。

    所以 Reflect.get(target, key, receiver) 这里的 key = "foo"

  2. 经过 #1 之后,需要立即执行 "get" 方法即 () 操作

    此时执行的是 mutableInstrumentations.get(this, key) 方法

    所以这里的 key = 'foo' , this 就是调用 get() 方法的对象 map

  3. 最后 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const map = new Map([['foo', 1]])
const observed = reactive(map)

const res = observed.get('foo')
console.log({ res })

+RESULTS:

{
  key: 'get',
  target: Map(1) { 'foo' => 1 },
  x: 'in createInstrumentationsGetter'
}
{ key: 'foo', target: Map(1) { 'foo' => 1 }, x: 'in get' }
{ res: 100 }

结果如上(参见.原理详细分析)

  1. reactive(map) -> 将 map 代理给 instrumentations{ get }

  2. observed.get -> 得到 instrumentations 里面的 "get" 方法

  3. ('foo') -> 执行 instrumentations.get(this, key), key = 'foo'

  4. 返回结果

至此,完成 collection get proxy handler 的完整流程。

0b3fd71 add get handler track

feat(add): collection proxy get -> global get · gcclll/stb-vue-next@0b3fd71

新增get 操作,track 添加依赖。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const map = new Map([['foo', 1]])
const observed = reactive(map)

let dummy
effect(() => {
  dummy = observed.get('foo')
})

console.log(`dummy = ${dummy}`)

+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

分为三个阶段

  1. collection proxy handler 取 map.get 方法, key = 'get'

  2. ('prop') 执行期触发 instrumentations.get(this, key), key = 'foo'

  3. 执行 global get 触发 track 收集依赖,返回结果值

假设 map.get(key) 的 key 也是个 proxy :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

let dummy
const key = reactive({ k: 1 })
const value = reactive({ v: 2 })
const map = reactive(new Map([[key, value]]))

effect( () => {
  dummy = map.get(key)
} )

console.log(`dummy = ${dummy}`)

+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. #1 proxy collection get handler

  2. #2 global get 函数里调用 track 之前输出,显示 keyrawKey 是不同的 (eq = false),因为前者是个 proxy 后者是 key proxy 的 rawValue 。

  3. #3 track() 调用时的输出,显示的是需要收集依赖的是 proxy key{k: 1}

  4. #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

http://qiniu.ii6g.com/img/20201121095654.png

这里处理分为两部分:

  1. 取出 has 方法检测存在性

  2. 根据 isReadonlyisShallow 决定对返回值做什么处理,如:递归 reactive/readonly

  3. 使用 target.get(key) 取出结果值返回

add collection set proxy handler

feat(add): collection set proxy handler · gcclll/stb-vue-next@7b680df

set proxy handler 处理

  1. 设值的时候可能有两种情况 a) set, b) add

  2. 需要考虑 proxy key 和 raw key 问题

  3. 最后 trigger 触发依赖

 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

function set(this: MapTypes, key: unknown, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const { has, get } = getProto(target)

  let hadKey = has.call(target, key)
  // 考虑 key 可能是 proxy
  if (!hadKey) {
    // to add
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    checkIdentityKeys(target, has, key)
  }

  const oldValue = get.call(target, key)
  // 设值结果
  const result = target.set(key, value)
  if (!hadKey) {
    // 添加操作
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else {
    // 设值操作
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }

  return result
}

测试

 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,
  shallowReactive
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')


const map = new Map()
const observed = reactive(map)

console.log(`> before get, deps`)
console.log(targetMap.get(map))
let dummy
effect(() => {
  dummy = observed.get('foo')
})

console.log(`> after get, deps`)
console.log(targetMap.get(map).get('foo'))

console.log(`#1 before set, dummy = ${dummy}`)
observed.set('foo', 1)
console.log(`#2 after set, dummy = ${dummy}`)

+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 操作依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
  const target = (this as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)

  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)
}

size: 取size 内部实现过程中是需要对 collection 进行迭代操作的,所以 track 用的是 ITERATE_KEY

1
2
3
4
5
function size(target: IterableCollections, isReadonly = false) {
  target = (target as any)[ReactiveFlags.RAW]
  !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.get(target, 'size', target)
}

add: set.add 操作,根据 set 特性,key,value 都是同一个且元素是不重复的,所以只需 要检测是不是新增,新增就需要 trigger ADD 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function add(this: SetTypes, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  const result = target.add(value)
  // 因为 set 是不会存在重复元素的,所以只会在没有当前 key 的情况下才会执行
  // 添加操作
  if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  return result
}

trigger 处理:838b402

feat(add): collection trigger cases · gcclll/stb-vue-next@838b402

测试:

 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
30
31
32
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const map = new Map()
const observed = reactive(map)
let dummy
effect(() => {
  dummy = observed.size
})

console.log(`before set, get map size -> dummy = ${dummy}`)
observed.set('foo', 1)
console.log(`after set, get map size -> dummy = ${dummy}`)

effect(() => {
  dummy = observed.has('foo')
})
console.log(`observed has 'foo' -> dummy = ${dummy}`)

const set = new Set()
const observedSet = reactive(set)
effect(() => {
  dummy = observedSet.size
})
console.log(`before add, get set size -> dummy = ${dummy}`)
observedSet.add(1)
console.log(`after add, get set size -> dummy = ${dummy}`)

+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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function deleteEntry(this: CollectionTypes, key: unknown) {
  const target = toRaw(this)
  const { has, get } = getProto(target)
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    checkIdentityKeys(target, has, key)
  }

  const oldValue = get ? get.call(target, key) : undefined
  const result = target.delete(key)
  if (hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

clear:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function clear(this: IterableCollections) {
  const target = toRaw(this)
  const hadItems = target.size !== 0
  const oldTarget = __DEV__
    ? isMap(target)
      ? new Map(target)
      : new Set(target)
    : undefined

  const result = target.clear()
  if (hadItems) {
    trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  }
  return result
}

测试:

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const map = new Map()
const observedMap = reactive(map)
let dummy
effect(() => {
  dummy = observedMap.size
})

console.log(`>>> map`)
observedMap.set('foo', 1)
console.log(`before delete, dummy = ${dummy}`)
observedMap.delete('foo')
console.log(`after delete, dummy = ${dummy}`)
observedMap.set('foo', 1)
observedMap.set('bar', 1)
console.log(`before clear, dummy = ${dummy}`)
observedMap.clear()
console.log(`after clear, dummy = ${dummy}`)
console.log(`>>> set`)

const set = new Set()
const observedSet = reactive(set)
effect(() => {
  dummy = observedSet.size
})
observedSet.add(1)
console.log(`before delete, dummy = ${dummy}`)
observedSet.delete(1)
console.log(`after delete, dummy = ${dummy}`)
observedSet.add(1)
observedSet.add(2)
observedSet.add(3)
console.log(`before clear, dummy = ${dummy}`)
observedSet.clear()
console.log(`after clear, dummy = ${dummy}`)

+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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function createForEach(isReadonly: boolean, isShallow: boolean) {
  return function forEach(
    this: IterableCollections,
    callback: Function,
    thisArg?: unknown
  ) {
    const observed = this as any
    const target = observed[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
    !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
    return target.forEach((value: unknown, key: unknown) => {
      // 重要:确保回调
      // 1. 在 reactive map 作用域下被执行(this, 和第三个参数)
      // 2. 接受的 value 值应该是个 reactive/readonly 类型
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}

将 forEach 封装了一层,对传递给回调的值 reactive 化,使用 ITERATE_KEY 收集调用 该方法的依赖。

测试:

 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,
  shallowReactive
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const map = new Map()
const ob = reactive(map)
let dummy = 0
effect(() => {
  ob.forEach((value) => (dummy += value || 0))
})

console.log(`#1 before set 1, dummy = ${dummy}`)
ob.set('foo', 1)
console.log(`#2 before set 2, dummy = ${dummy}`)
ob.set('bar', 2)
console.log(`#3 after set, dummy = ${dummy}`)

+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:

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
interface Iterable {
  [Symbol.iterator](): Iterator
}

interface Iterator {
  next(value?: any): IterationResult
}

interface IterationResult {
  value: any
  done: boolean
}

function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  isShallow: boolean
) {
  return function(
    this: IterableCollections,
    ...args: unknown[]
  ): Iterable & Iterator {
    const target = (this as any)[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    const targetIsMap = isMap(rawTarget)
    const isPair =
      method === 'entries' || (method === Symbol.iterator && targetIsMap)
    const isKeyOnly = method === 'keys' && targetIsMap
    const innerIterator = target[method](...args)
    const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
    !isReadonly &&
      track(
        rawTarget,
        TrackOpTypes.ITERATE,
        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
      )

    // 重写迭代器,让其返回的对象也是 reactive/readonly 类型
    return {
      next() {
        const { value, done } = innerIterator.next()
        return done
          ? { value, done }
          : {
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

test:

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive,
  toRaw
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const map = new Map()

const obj = { name: 'dax' }
map.set("foo", 1)
map.set("bar", 2)
map.set('dax', obj)
const observed = reactive(map)
let dummy = []
effect(() => {
  for (let key of observed.entries()) {
    dummy.push(key)
  }
})

console.log(`>>> #1 set`)
console.log(`before set, dummy = ${dummy}`)
observed.set('baz', 3)
console.log(`after set, dummy = ${dummy}`)
console.log(`obj in map is reactive ${isReactive(observed.get("dax"))}`)
effect(() => {
  dummy = observed.size
})
console.log(`>>> #2 clear`)
console.log(`before clear, dummy = ${dummy}`)
observed.clear()
console.log(`after clear, dummy = ${dummy}`)
console.log(`>>> #3 should not observe custom property`)
effect(() => (dummy = observed.customProp))
console.log(`before set cumstom prop, dummy = ${dummy}`)
observed.customProp = 'Hello World'
console.log(`after set cumstom prop, dummy = ${dummy}`)
console.log(`>>> #4 不应该使 Proxies 污染原来的 Map 对象`)
const map2 = new Map()
const observed2 = reactive(map2)
const value = reactive({})
observed2.set('key', value)
console.log(`map2.get('key') !== value, ${map2.get('key') !== value}`)
console.log(`map2.get('key') === toRaw(value), ${map2.get('key') === toRaw(value)}`)

+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)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function createReadonlyMethod(type: TriggerOpTypes): Function {
  return function(this: CollectionTypes, ...args: unknown[]) {
    if (__DEV__) {
      const key = args[0] ? `on key "${args[0]}"` : ``
      console.warn(
        `${capitalize(type)} operation ${key} failed: target is readonly.`,
        toRaw(this)
      )
    }
    return type === TriggerOpTypes.DELETE ? false : this
  }
}

readonly instrumentations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const readonlyInstrumentations: Recor<string, Function> = {
  get(this: MapTypes, key: unknown) {
    return get(this, key, true)
  },
  get size() {
    return size((this as unknown) as IterableCollections, true)
  },
  has(this: MapTypes, key: unknown) {
    return has.call(this, key, true)
  },
  add: createReadonlyMethod(TriggerOpTypes.ADD),
  set: createReadonlyMethod(TriggerOpTypes.SET),
  delete: createReadonlyMethod(TriggerOpTypes.DELETE),
  clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
  forEach: createForEach(true, false)
}

测试:

add collection shallow proxy handlers

computed

types definitions

feat(add): computed type definitions · gcclll/stb-vue-next@e9e53a1

computed 计算属性的一些类型定义。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { Ref } from './ref'

export interface ComputedRef<T = any> extends WritableComputedRef<T> {}

export interface WritableComputedRef<T> extends Ref<T> {}

export type ComputedGetter<T> = (ctx?: any) => T

export type ComputedSetter<T> = (v: T) => void

export interface WritableComputedOptions<T> {}

computed 函数重载(315e0d9): feat(add): computed function reloads · gcclll/stb-vue-next@315e0d9

1
2
3
4
5
6
7
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {}

implementation

feat(add): computed tpl and computed function · gcclll/stb-vue-next@64d380d

计算属性实现全在 ComputedRefImpl<T> 类的实现中,实现关键点

  1. 使用 effect 封装 getter 函数,收集所有依赖,在特定时候执行 effect

  2. _dirty 标记,一旦 _dirty = true 表示数据有更新,下次取值的时候就要立即执行 effect 取最新值

class ComputedRefImpl

 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
30
31
32
33
34
35
36
37
38
39
40
41
// 计算属性模板
class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    if (this._dirty) {
      this._value = this.effect()
      this._dirty = false
    }
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

computed 函数:

 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

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}

computed 函数的 options 可以是函数或一个对象,可以用外部自定义 setter 函数,比如 在更新之前记录当前状态,就可以在 options.set 中去实现。

测试请移步“计算属性测试用例

脑图请直接查看“完整脑图 computed 部分

add ref

/img/vue3/reactivity/reactivity-ref.svg

这部分因为之前没有单独拎出来一步步实现,而是直接拷贝过来了为了先测试 computed 属 性。

所以这里直接根据源码以及使用方式来进行逐步分析。

APIs:

apifunction
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 代理
RefImplRef 变量模板
CustomRefImpl自定义 Ref 变量模板

ref & shallow ref

ref()shallowRef() 函数都是调用的同一个函数 createRef(val, shallow) 来 创建ref 变量,而 createRef 本身也很简单,就是 new 了一个 RefImpl 实例出来。

另外针对已经是 ref 的值不需要重复 new 操作,直接返回原 ref。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 // ref
 export function ref(value?: unknown) {
   return createRef(value);
 }

// shallow ref
export function shallowRef(value?: unknown) {
  return createRef(value, true)
}

// createRef(rawValue, shallow = false)
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

参数:

  1. val 需要进行 ref 化的变量

  2. shallow 如果 val 是对象的时候决定是否需要对该对象进行递归 reactive 化

看下 ref 结构模板类: RefImpl<T>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

代码是相当简单的,四个属性(_value/__v_isRef/_rawValue/_shallow)+两个访问器方法 (value getter/setter)。

根据 es6 class 语法,构造参数如果使用了权限修饰符会自动转成成员属性,所以 _rawValue_shallow 也是 RefImpl 成员属性和 _value 是一样的,区别在于这 两个值可以通过外部控制。

开始测试吧:

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 源文件:/js/vue/lib.js
const {
  rt: { ref, shallowRef, effect, reactive },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

const a = ref(1);
log("1. base usage >>>");
log(["before, a.value = ", a.value]);
a.value = 2;
log(["after, a.value = ", a.value]);

log("\n2. should be reactive >>>");
log("ge/set value 里面使用的是 track/trigger");
let dummy,
  calls = 0;
effect(() => {
  calls++;
  dummy = a.value;
});
log(`before set a.value, dummy=${dummy}, calls=${calls}`);
a.value = 3;
log(`after set a.value, dummy=${dummy}, calls=${calls}`);

log("\n3. 默认情况下对嵌套对象属性进行 reactive >>>");
const b = ref({ count: 1 });
effect(() => (dummy = b.value.count));
log("before set, dummy = ${dummy}");
log("after set, dummy = ${dummy}");

log("\n4. ref() 不传值的时候也应该可以工作");
const c = ref();
// 简单的赋值操作,给什么值都可以
effect(() => (dummy = c.value));
log(`before set, dummy = ${dummy}`);
c.value = 100;
log(`after set, dummy = ${dummy}`);

// 当嵌套在一个多层级的对象里的时候
// 因为 ref() 返回的是个对象,所以放在对象里面本质上操作的还是
// ref 本身
log("\n5. ref 作为多级对象的值时");
const d = ref(1);
const obj = reactive({
  d,
  b: { c: d },
});
let dummy1, dummy2;
effect(() => {
  dummy1 = obj.d;
  dummy2 = obj.b.c;
});
log(`before set, dummy1=${dummy1}, dummy2=${dummy2}`);
d.value++;
log(`d.value++, dummy1=${dummy1}, dummy2=${dummy2}`);
// 注意这里直接针对 ref 进行赋值操作而不是obj.d.value++
// 这样也是可以的,因为 set proxy 里面有检测该属性是不是 ref
// 如果是 Ref 会转变成对 obj.d.value++ 也就是说 vue 内部帮
// 我们这么做了,下面对 obj.b.c++ 同
obj.d++;
log(`obj.d++, dummy1=${dummy1}, dummy2=${dummy2}`);
obj.b.c++;
log(`obj.b.c++, dummy1=${dummy1}, dummy2=${dummy2}`);
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

测试分析:

  1. 基本使用

    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);
        }
      }
    }
    
  2. should be reactive

    这个在 1 列出了 get value 源码,结合 track/trigger 达到 reactive 目的。

  3. 这一点直接看源码 set value(newVal)

    this._value = this._shallow ? newVal : convert(newVal)

    convert() 针对对象 reactive

  4. 因为 new RefImpl(rawValue, shallow) 是简单的赋值操作,给啥值都行

  5. 因为 new RefImpl(rawValue, shallow) 返回的是个 RefImpl 实例对象,属于引用类 型,不管对象层级多深,最终引用的都是这个对象。

object ref

对对象的所有属性进行 ref 化,在进行 proxy ref 之前需要先完成这部分,因为它依赖 object ref。

function描述
toRefs(object)ref对象的所有属性
ObjectRefImpl对象 ref 模板
toRef(object, key)针对 object[key] 进行 ref

相关源码:

 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
30
31
32
33
34
35
class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true

  constructor(private readonly _object: T, private readonly _key: K) {}

  get value() {
    return this._object[this._key]
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }
}

// ref object 属性的
export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): ToRef<T[K]> {
  return isRef(object[key])
    ? object[key]
    : (new ObjectRefImpl(object, key) as any)
}

// ref object 所有属性
export function toRefs<T extends object>(object: T): ToRefs<T> {
  if (__DEV__ && !isProxy(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  const ret: any = isArray(object) ? new Array(object.length) : {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}

源码分析ObjectRefImpl 中针对每个 object[key] 持有了一份 object 引 用 _object, 且在最开始 new 实例的时候将属性名保存到了 _key,后续取值设值用的都 是这个值,然后重写了 getter/setter 函数,在取值设值的时候操作 _object + _key

打个比方:

 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
30
31
// 原始对象
const rawObj = { count: 0 };

// toRef(rawObj, 'count') 之后
const reffed = {
  _key: "count",
  _object: rawObj,
  get value() {
    return this._object[this._key];
  },
  set value(newVal) {
    this._object[this._key] = newVal;
  },
};

// 然后当你改变 reffed.value 值时实际上是在改变 rawObj.count 的值
console.log(`before set, rawObj.count = ${rawObj.count}`)
reffed.value = 100
console.log(`after set, rawObj.count = ${rawObj.count}`)

// toRefs(rawObj) 就是对所有属性进行 toRef(),然后将返回的值用
// 同样的 key 组成新的对象返回
// 如: 沿用上面的测试
// toRefs(rawObj) 等于是返回了一个全新的对象这个对象内容为:
const reffedObj = {
 count: reffed
}
// 随后我们直接通过修改 reffedObj.count 来间接操作 rawObj 对象里属性的值
console.log('before set on obj, rawObj.count = ' + rawObj.count)
reffedObj.count.value = 200
console.log('after set on obj, rawObj.count = ' + rawObj.count)
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 。

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 源文件:/js/vue/lib.js
const { rc: { effect, reactive, ref, toRef, proxyRefs, toRefs }, f, log } = require(process.env.BLOG_JS + '/vue/lib.js')

const objRef = ref({ count: 0 })
let dummy

effect(() => {
  dummy = objRef.value.count
})
// proxyRefs(objRef)

log('>>> ref 基本用法')
log(`1. dummy = ${dummy}`)
objRef.value.count++
log(`2. dummy = ${dummy}`)

log('>>> toRef 用法,ref 对象属性')
const a = reactive({ x: 1 })
const x = toRef(a, 'x')

ref proxy

作用:代理 ref 遍历的 get/set 操作,让 get 在返回之前先 unref(result) 得到原始 的值返回,让 set 在 set 之前确保设值的值是在 ref.value 上。

和 ref proxy 有关的函数和属性:

function描述
shallowUnwrapHandlers { get, set }代理的 handler
proxyRefs(objectWithRefs)代理 ref 对象

源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// proxy handler
const shallowUnwrapHandlers: ProxyHandler<any> = {
  get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
  set: (target, key, value, receiver) => {
    const oldValue = target[key];
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value;
      return true;
    } else {
      return Reflect.set(target, key, value, receiver);
    }
  },
};

export function proxyRefs<T extends object>(
  objectWithRefs: T
): ShallowUnwrapRef<T> {
  return isReactive(objectWithRefs)
    ? objectWithRefs
    : new Proxy(objectWithRefs, shallowUnwrapHandlers);
}

ref proxy 作用:针对被 ref 的 object 对象,进行 get 和 set 操作代理。

get 操作, Reflect.get(...) 拿到的是个 unref,通过 proxy get handler 去 ref 化, 返回其原始值。

set 操作, Reflect.set(...) 新值是 ref 直接覆盖之前的 target[key] 值,如果不是 ref,将其设置到 oldValue.value 上。

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 源文件:/js/vue/lib.js
const { rc: { effect, reactive, ref, toRef, proxyRefs, toRefs }, f, log } = require(process.env.BLOG_JS + '/vue/lib.js')

const objRef = ref({ count: 0 })
let dummy

effect(() => {
  dummy = objRef.value.count
})
// proxyRefs(objRef)

log('>>> ref 基本用法')
log(`1. dummy = ${dummy}`)
objRef.value.count++
log(`2. dummy = ${dummy}`)

log('>>> toRef 用法,ref 对象属性')
const a = reactive({ x: 1 })
const x = toRef(a, 'x')
>>> before proxy
1. dummy = 0
2. dummy = 1
undefined

custom ref

相关函数和类:

function描述
CustomRefImpl自定义的 ref 模板
customRef(factory)自定义 ref,即由外部决定如何实现 getter & settter value

让使用者自定义以什么方式在 get 的时候 track 或 set 的时候 trigger

 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
30
31
export type CustomRefFactory<T> = (
  track: () => void,
  trigger: () => void
) => {
  get: () => T;
  set: (value: T) => void;
};

class CustomRefImpl<T> {
  private readonly _get: ReturnType<CustomRefFactory<T>>["get"];
  private readonly _set: ReturnType<CustomRefFactory<T>>["set"];

  public readonly __v_isRef = true;

  constructor(factory: CustomRefFactory<T>) {
    const { get, set } = factory(
      () => track(this, TrackOpTypes.GET, "value"),
      () => trigger(this, TriggerOpTypes.SET, "value")
    );
    this._get = get;
    this._set = set;
  }

  get value() {
    return this._get();
  }

  set value(newVal) {
    this._set(newVal);
  }
}

就是我只负责给你构造一个 Ref 结构的对象,至于什么 get 和 set 怎么实现全权交给使 用者,get/set 默认 track/trigger value 属性。

测试:

 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
30
31
32
// 源文件:/js/vue/lib.js
const {
  rc: { reactive, toRefs, customRef, effect, isRef },
  f,
  log,
} = require(process.env.BLOG_JS + "/vue/lib.js");

let value = 1;
let _trigger;

const custom = customRef((track, trigger) => ({
  get() {
    track();
    return value;
  },
  set(newValue) {
    value = newValue;
    _trigger = trigger;
  },
}));

log('custom is ref, ' + isRef(custom))
let dummy
effect(() => {
  dummy = custom.value
})
log('dummy = ' + dummy)
custom.value = 2
// 这个时候还不会触发依赖更新,因为 trigger 并没有执行
// 只是缓存到了 _trigger 上
_trigger() // 手动触发 effects
log('dummy = ' + dummy)
custom is ref, true
dummy = 1
dummy = 2
undefined

辅助函数

functiondesc
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

1
2
3
4
5
6
7
it('should return updated value', () => {
    const value = reactive<{ foo?: number }>({})
    const cValue = computed(() => value.foo)
    expect(cValue.value).toBe(undefined)
    value.foo = 1
    expect(cValue.value).toBe(1)
})

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const {
    isReactive,
    effect,
    reactive,
    targetMap,
    shallowReactive,
    computed
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const value = reactive({})
const cValue = computed(() => value.foo)
console.log(`#1 before set value.foo, cValue.value = ${cValue.value}`)
value.foo = 1
console.log(`#2 after set value.foo, cValue.value = ${cValue.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 了,值结果已出。

1
2
3
4
5
6
7
8
get value() {
    if (this._dirty) {
        this._value = this.effect()
        this._dirty = false
    }
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
}

也正是由于 effect 的执行,让 value.foo 收集到了这个依赖。

随后设置 value.foo 也会将 computed effect 执行,由于 options 中提供了 scheduler,所以会执行置 _dirty = true ,但是此时计算属性值并不会立即计算得出 结果(因为它的计算操作始终是在 get value() 里面发生的),所有当下次取值操作(即 #2 行执行时),检测到 _dirty = true 了才会重新计算返回最新结果。

用例脑图:

/img/vue3/reactivity/reactivity-computed-test-01.svg

should compute lazily

 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

  it('should compute lazily', () => {
    const value = reactive<{ foo?: number }>({})
    const getter = jest.fn(() => value.foo)
    const cValue = computed(getter)

    // lazy
    expect(getter).not.toHaveBeenCalled()

    expect(cValue.value).toBe(undefined)
    expect(getter).toHaveBeenCalledTimes(1)

    // should not compute again
    cValue.value
    expect(getter).toHaveBeenCalledTimes(1)

    // should not compute until needed
    value.foo = 1
    expect(getter).toHaveBeenCalledTimes(1)

    // now it should compute
    expect(cValue.value).toBe(1)
    expect(getter).toHaveBeenCalledTimes(2)

    // should not compute again
    cValue.value
    expect(getter).toHaveBeenCalledTimes(2)
  })

测试:

 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
30
31
32
33
34
35
36
37
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive,
  computed
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const value = reactive({})
let n = 0
const getter = () => {
  ++n
  return value.foo
}
const cValue = computed(getter)
// lazy
console.log(`#1 before get value, getter called ${n} times.`)
console.log(`#2 get value(), cValue.value = ${cValue.value}`)
console.log(`#3 after get value, getter called ${n} times`)

// 不该重复计算,因为 _dirty 在 #3 之后值为 false
cValue.value
console.log(`#4 after get value 2, getter called ${n} times`)

// 触发 cValue 的 effect 执行 scheduler,使得  _dirty = true
value.foo = 1 // trigger effect
// 因为上面赋值操作只是让 _dirty = true 了,并没有立即重新计算
// 因为计算属性的结果总是在 get value() 操作中完成的
console.log(`#5 after set 'value.foo = 1', getter called ${n} times`)

// 这里进行取值操作,正式发起重新计算
console.log(`#6 should re-compute value, getter called ${n} times, cValue.value = ${cValue.value}`)

// #6 重新计算后 _dirty 又变成了 false,所以现在取值不会再重新计算
cValue.value
console.log(`#7 should not re-compute value, getter called ${n} times, cValue.value = ${cValue.value}`)

+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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

  it('should no longer update when stopped', () => {
    const value = reactive<{ foo?: number }>({})
    const cValue = computed(() => value.foo)
    let dummy
    effect(() => {
      dummy = cValue.value
    })
    expect(dummy).toBe(undefined)
    value.foo = 1
    expect(dummy).toBe(1)
    stop(cValue.effect)
    value.foo = 2
    expect(dummy).toBe(1)
  })

测试:

 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
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive,
  computed,
  stop
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const value = reactive({})
const cValue = computed(() => value.foo)
let dummy
effect(() => {
  dummy = cValue.value
})

console.log(`before set value, dummy = ${dummy}`)
value.foo = 1
console.log(`after set value, dummy = ${dummy}`)
// 停止了 effect.active = false,在执行 effect fn 的时候
// 会检测 active 如果是 false 会直接执行:
// options.scheduler ? undefined : fn()
// 而由于计算属性是默认提供了 scheduler 的,所以在 stop 之后
// effect 就不会被执行
stop(cValue.effect)
value.foo = 2
console.log(`after stop and set value, dummy = ${dummy}`)

+RESULTS:

before set value, dummy = undefined
after set value, dummy = 1
after stop and set value, dummy = 1

TODO should support setter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

  it('should support setter', () => {
    const n = ref(1)
    const plusOne = computed({
      get: () => n.value + 1,
      set: val => {
        n.value = val - 1
      }
    })

    expect(plusOne.value).toBe(2)
    n.value++
    expect(plusOne.value).toBe(3)

    plusOne.value = 0
    expect(n.value).toBe(-1)
  })

should be readonly

只读版本,是有 computed 使用的时候传入的参数(option)决定的。

  1. 如果 option 是函数(getter) 那就是只读的

  2. 如果 option 是对象且提供了 option.set 那么是非只读

  3. 如果 option 是对象但是没有提供 option.set 那么也是只读的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

  it('should be readonly', () => {
    let a = { a: 1 }
    const x = computed(() => a)
    expect(isReadonly(x)).toBe(true)
    expect(isReadonly(x.value)).toBe(false)
    expect(isReadonly(x.value.a)).toBe(false)
    const z = computed<typeof a>({
      get() {
        return a
      },
      set(v) {
        a = v
      }
    })
    expect(isReadonly(z)).toBe(false)
    expect(isReadonly(z.value.a)).toBe(false)
  })

测试:

 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,
  isReadonly,
  effect,
  reactive,
  targetMap,
  shallowReactive,
  computed
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

const a = { a: 1 }
const x = computed(() => a)
console.log(`#1 x is readonly, ${isReadonly(x)}`) // true
console.log(`#2 x.value is readonly, ${isReadonly(x.value)}`)
console.log(`#3 x.value.a is readonly, ${isReadonly(x.value.a)}`)
const z = computed({
  get() {
    return a
  },
  set(v) {
    a = v
  }
})

console.log(`#4 z is readonly, ${isReadonly(z)}`)
console.log(`#5 z.value.a is readonly, ${isReadonly(z.value.a)}`)

+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

  1. lazy: true 标识 effect fn 不会立即执行

  2. 点击 set 操作,此时并没有依赖,所以只会触发 count++

  3. 当点击 get 操作,触发 track() 收集依赖 fn -> deps

  4. 再点击 set 操作,此时已经有依赖,所以会 trigger() 所有依赖更新

  5. options.scheduler 选项作用

    如果 options 有 scheduler 选项, trigger() 的时候不会立即执行 effects 而是 调用 scheduler 并将当前需要被执行的 effect 当做参数给 scheduler,由使用者决定 何时去执行 effect,比如需要在 dummy 更新之前做点什么。



effect 测试

测试1(base, prototype)

测试内容:

  1. effect 基本使用

  2. 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 只执行一次 effect fn
const {
  isReactive,
  effect,
  reactive,
  targetMap,
  shallowReactive
} = require(process.env.PWD + '/../../static/js/vue/reactivity.global.js')

// 基本的测试用例就不列出来了,这里只列出有疑问的
// 1. effect fn 只执行一次
// 2. observe 基本属性
// 3. observe 多个属性(n1,n2...) -> effect(() => (dummy = obj.n1 + obj.n2))
// 4. 同一个属性多个 effect,会将这多个 effects 收集到 prop 的 deps 中
// 5. observe 属性删除
let dummy, dummy1, dummy2
const ob = reactive({ foo: { bar: 0 } })
effect(() => (dummy = ob.foo.bar)) // effect -> ob, ob.foo, ob.foo.bar deps
effect(() => (dummy1 = ob.foo.bar))
console.log(`before set, dummy = ${dummy}, dummy1 = ${dummy1}`)
ob.foo.bar = 8
console.log(`after set, dummy = ${dummy}, dummy1 = ${dummy1}`)
delete ob.foo.bar
console.log(`after delete, dummy = ${dummy}, dummy1 = ${dummy1}`)
console.log(`>>> 原型链`)

const obj1 = { num: 0 }, obj2 = { num: 2 }
const counter = reactive(obj1)
const parentCounter = reactive(obj2)
// 取值原理: 先自身再往上找原型链,所有只要
Object.setPrototypeOf(counter, parentCounter)
effect(() => (dummy = counter.num))
console.log(`dummy = ${dummy}`)
console.log(`> #1 obj1.num 的依赖`)
console.log(targetMap.get(obj1).get('num'))
console.log(`> #2 obj2.num 的依赖, delete 之前`)
console.log(targetMap.get(obj2))
delete counter.num // 这里删除了属性,触发 effect fn 里面取值操作发现没有属性了
// 往原型链找,找到 parentCounter.num ,此时 parentCounter.num 收集 effect fn 进自己的 deps
// 所以后面的 parentCounter.num 上的操作同样会触发 effect fn
console.log(`after delete, dummy = ${dummy}`)
console.log(`> #3 obj2.num 的依赖, delete 之后`)
console.log(targetMap.get(obj2).get('num'))
parentCounter.num = 4
console.log(`#4 after 'parentCounter.num = 4', dummy = ${dummy}`)
counter.num = 3
console.log(`#5 after counter.num = 3', dummy = ${dummy}`)

结果分析:

  • #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, …)

  1. 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)

  2. 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()
    }
    
  3. 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 。

  4. 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 值依旧是 1 ,是因为 stop 了

    2. #2 值为 3,是因为 effect 执行 runner() 使得 obj.prop 收集到第二个 effect fn 。

完整脑图

/img/vue3/reactivity/reactivity.svg

collection proxy handlers 脑图

/img/vue3/reactivity/reactivity-collection-proxy.svg

effect 脑图

/img/vue3/reactivity/reactivity-effect.svg