Skip to content

AHABHGK

Vue Reactivity in Depth

SourceCode, Front End Framework4 min read

Table of Contents

reactivity

首先一起实现一个简易的 reactivity 吧,穿插着会提到源码中的一些细节,建议先跟着写写,细节可以 clone 下来 vue-next 打开源码跟着看

⭐️ reactive

首先是 reactive,它的作用是把一个对象变成响应式对象,更准确的说是 Object, Array, Map, WeakMap, Set, weakSet 这几种对象变为响应式

类似于 reactive 也有 readonly,将一个对象变为不可变对象

reactivity/reactive.js
1export function reactive(target) {
2 return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
3}
4
5export function readonly(target) {
6 return createReactiveObject(target, true, readonlyHandlers, readonlyCollectionHandlers)
7}

实际上是可以 readonly(reactive(obj)) 将一个 reactive 对象转换为 readonly 对象的,而 reactive(readonly(obj)) 仍然返回 readonly 对象,实现是在 reactive 入口判断 target 是不是 readonly,是就直接返回,而 readonly 入口不做判断,可以将 reactive 对象在做一层代理,转换为 readonly

1const original = reactive({ count: 0 })
2const copy = readonly(original)
3
4effect(() => {
5 // works for reactivity tracking
6 console.log(copy.count)
7})
8
9// mutating original will trigger watchers relying on the copy
10original.count++
11// mutating the copy will fail and result in a warning
12copy.count++ // warning!

对于这种情况是在 reactive 代理上在加了一层 readonly 代理,当 readonly 对象的 get 触发时会调用 reactive 对象的 get 以触发 track,之后原 reactive 对象修改后对于 readonly 对象的 effect 就也会触发

这两种对象实现是相似的,我们通过 createReactiveObject 创建这两种对象,但在这之前我们先定义一些常量和工具函数

shared/index.js
1export const isObject = (value) => typeof value === 'object' && value !== null
reactivity/reactive.js
1// proxy 实例上的标识
2export const ReactiveFlags = {
3 IS_REACTIVE: '__v_isReactive',
4 IS_READONLY: '__v_isReadonly',
5 RAW: '__v_raw', // 原对象
6}
7
8const TargetType = {
9 COMMON: 'COMMON', // 表示 Object 和 Array
10 COLLECTION: 'COLLECTION', // 表示 Map、Set、WeakMap、WeakSet
11 INVALID: 'INVALID', // 其他不处理的
12}
13
14// 判断类型
15const getTargetType = (target) => {
16 const getTypeString = (target) => {
17 return Object.prototype.toString.call(target).slice(8, -1)
18 }
19 const typeString = getTypeString(target)
20 switch (typeString) {
21 case 'Object':
22 case 'Array':
23 return TargetType.COMMON
24 case 'Map':
25 case 'Set':
26 case 'WeakMap':
27 case 'WeakSet':
28 return TargetType.COLLECTION
29 default:
30 return TargetType.INVALID
31 }
32}
33
34export function toRaw(ob) {
35 return (ob && toRaw(ob[ReactiveFlags.RAW])) || ob
36}

ReactiveFlags 是 proxy 上的一些常量的定义。对于 target 类型的判断,在 createReactiveObject 中对于 COMMON 和 COLLECTION 有不同的 handlers 来处理。toRaw 用来取一个 reactive 对象的原对象,通过递归实现,非常巧妙

下面来看 createReactiveObject 的实现

reactivity/reactive.js
1export const reactiveMap = new WeakMap()
2export const readonlyMap = new WeakMap()
3
4function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
5 if (!isObject(target)) {
6 throw new Error(`value cannot be made reactive: ${String(target)}`)
7 }
8 if (
9 target[ReactiveFlags.RAW] && // 已经是 reactive 或 readonly 对象
10 !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) // 排除 readonly(reactiveObj) 这种情况
11 ) {
12 return target
13 }
14 // 已经有了对应的 proxy
15 const proxyMap = isReadonly ? readonlyMap : reactiveMap
16 const existingProxy = proxyMap.get(target)
17 if (existingProxy) {
18 return existingProxy
19 }
20 // 获取 typeString 判断是不是 collectionType(Map、Set、WeakMap、WeakSet)
21 const targetType = getTargetType(target)
22 if (targetType === TargetType.INVALID) {
23 return target
24 }
25
26 const observed = new Proxy(
27 target,
28 // Map、Set、WeakMap、WeakSet 通过 collectionHandlers 代理,Object、Array 通过 baseHandlers 代理
29 targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
30 )
31 proxyMap.set(target, observed) // 存对应 proxy
32 return observed
33}

可以看到通过两个 WeakMap 来存对象对应的 reactive 实例和 readonly 实例,再次调用时就可以直接返回

然后通过 Proxy 进行代理,我们先看对于 Object 和 Array 的代理 mutableHandlers 和 readonlyHandlers

🌥 baseHandlers

reactivity/baseHandlers.js
1const get = createGetter()
2const readonlyGet = createGetter(true)
3
4const set = createSetter()
5
6export const mutableHandlers = {
7 get,
8 set,
9 deleteProperty,
10 has,
11 ownKeys,
12}
13
14export const readonlyHandlers = {
15 get: readonlyGet,
16 set(target, key) {
17 console.warn(
18 `Set operation on key "${String(key)}" failed: target is readonly.`,
19 target
20 )
21 return true
22 },
23 deleteProperty(target, key) {
24 console.warn(
25 `Delete operation on key "${String(key)}" failed: target is readonly.`,
26 target
27 )
28 return true
29 },
30}

可以看到 readonly 与 reactive 的不同就在于 readonly 代理的修改操作,修改时不会真正去修改对象,并在开发模式下报警告;readonly 代理的 get、has、ownKeys 操作不会去 track 收集依赖,get 比较特殊,has、ownKeys 可以直接用原对象操作就不会 track,直接不加即可

呜呜呜~ 我的 PR 没被 merge:PR: readonly object should not track on 'has' and 'ownKeys'

对于 get、readonlyGet 和 set 则是通过 createGetter 和 createSetter 创建

reactivity/baseHandlers.js
1function createGetter(isReadonly = false) {
2 return function get(target, key, receiver) {
3 if (key === ReactiveFlags.IS_REACTIVE) return !isReadonly
4 if (key === ReactiveFlags.IS_READONLY) return isReadonly
5 if (
6 key === ReactiveFlags.RAW &&
7 receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
8 ) {
9 return target
10 }
11
12 const res = Reflect.get(target, key, receiver)
13 if (!isReadonly) track(target, TrackOpTypes.GET, key)
14 if (isObject(res)) {
15 return isReadonly ? readonly(res) : reactive(res)
16 }
17 return res
18 }
19}
20
21function createSetter() {
22 return function set(target, key, value, receiver) {
23 const oldValue = target[key]
24 const res = Reflect.set(target, key, value, receiver)
25 if (target === receiver[ReactiveFlags.RAW] && hasChanged(value, oldValue)) {
26 trigger(target, TriggerOpTypes.SET, key)
27 }
28 return res
29 }
30}

这两个函数通过 isReadonly 的不同创建不同的 getter 和 setter,形成闭包存 isReadonly 的值,返回的 getter 代理 reactive 对象和 readonly 对象的 get 操作,当 key 是 __v_isReactive__v_raw__v_isReadonly 这几个 ReactiveFlags 常量时,就直接通过参数返回对应的结果,所以这几个常量并没有挂载在 proxy 实例上,而是通过代理 get 操作实现,保证了 proxy 实例上没有多余的属性

setter 并没有传 isReadonly,这里实际上并不需要 createSetter 实现,但在源码中还有 shallowReactive 的情况,需要判断 isShallow,这里为了精简省略了 shallow 对应的实现

之后就是代理后主要的操作了,首先通过 Reflect.get 取得 value,然后进行 track,也就是依赖收集,track 其实是实现响应式的前半部分,后半部分就是 trigger 触发依赖

最后判断 value 是否是对象,如果是就进行对应的 reactive 或 readonly,这样在结果处进行响应式,lazy 的进行深度递归,实现深度响应式的同时,也防止了循环引用导致的无限递归

Proxy 的代理只能代理一层,是浅的,reactive 实现的是深度响应式

源码中 reactive Object get 取出来如果是 ref 会自动返回 value,reactive Array 则不会,详细可以看看 Issues: Stable mutation of reactive arrays containing refs

源码中 arrayInstrumentations 的原因可以看这个 commit 的 test case

setter 代理操作也类似,先通过 Reflect.set 得到 set 的结果,然后 trigger 触发依赖,最后返回结果,这里一定要先执行 set 后再 trigger,effect 中可能有操作依赖于 set 后的对象,先 set 能保证 effect 中的函数执行出正确的结果

其他的 deleteProperty、has、ownKeys 也类似,得到结果然后触发 track 或 trigger,最后返回结果。deleteProperty 类似 set 会触发依赖比较好理解,那 has、ownKeys 为什么会收集依赖呢?因为有时需要判断 proxy 是否有 key 属性,或者依赖于 proxy 的 keys 等情况,在新增和删除 key 时就需要触发对应的 effect

1effect(() => console.log(Object.keys(proxy)))
2effect(() => console.log(key in proxy))

代理 ownKeys 时只有一个 target 参数,ownKeys 的操作并不需要 key,但是 track 和 trigger 至少需要 target 和对应的 key 以找到依赖,这时可以定义一个 ITERATE_KEY 常量,专门处理这种遍历而不需要 key 的情况

reactivity/baseHandlers.js
1export const ITERATE_KEY = Symbol('iterate')
2
3function ownKeys(target) {
4 track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
5 return Reflect.ownKeys(target)
6}

所以什么是响应式对象呢?个人理解响应式对象与普通对象的区别就在于响应式对象的操作可以通过 proxy 代理以调用 track 收集依赖或调用 trigger 触发依赖

🚀 effect

上面提到很多次依赖收集和触发依赖了,那到底什么是依赖呢?依赖其实就是 effect 函数,我们先看 track 的实现

reactivity/effect.js
1const targetMap = new WeakMap()
2
3let activeEffect
4
5export function track(target, type, key) {
6 if (activeEffect == null) return
7
8 let depsMap = targetMap.get(target)
9 if (!depsMap) {
10 targetMap.set(target, (depsMap = new Map()))
11 }
12
13 let dep = depsMap.get(key)
14 if (!dep) {
15 depsMap.set(key, (dep = new Set()))
16 }
17
18 if (!dep.has(activeEffect)) {
19 dep.add(activeEffect)
20 activeEffect.deps.push(dep)
21 }
22}

首先判断当前的 activeEffect 有没有,没有就直接返回,之后通过 targetMap 拿到 target 对应的 depsMap,再通过 depsMap 拿到 key 对应的 dep,dep 是一个 Set,存储 target.key 需要的 effect 依赖,而 effect 又通过 deps 数组存储依赖于 effect 的所有 dep,建立一个双向的收集,dep 到 effect 是为了 trigger 使用,而 effect 到 dep 是为了 effect 调用时找到依赖于这个 effect 所有 dep,从 dep 中删除这个调用过的 effect,用来清除上一轮的依赖,防止本轮触发多余的依赖

reactivity/effect.js
1export function effect(fn, options = {}) {
2 const effect = createReactiveEffect(fn, options)
3 if (!options.lazy) {
4 effect()
5 }
6 return effect
7}
8
9// 停止监听
10export function stop(effect) {
11 if (effect.active) {
12 cleanup(effect)
13 effect.active = false
14 }
15}
16
17function createReactiveEffect(fn, options) {
18 const effect = function reactiveEffect() {
19 if (!effect.active) return effect.options.scheduler ? undefined : fn()
20 if (!effectStack.includes(effect)) {
21 cleanup(effect) // effect 调用时会清除上一轮的依赖,防止本轮触发多余的依赖
22 try {
23 effectStack.push(effect) // 可能有 effect 中调用另一个 effect 的情况,模拟一个栈来处理
24 activeEffect = effect
25 return fn() // let value = effect() 将函数的结果返回,可以从外面去到结果
26 } finally {
27 effectStack.pop()
28 activeEffect = effectStack[effectStack.length - 1]
29 }
30 }
31 }
32 effect.active = true // active 判断 effect 是否还活着,stop(effect) 后 active 就是 false
33 effect.deps = [] // 收集对应的 dep,cleanup 时以找到 dep,从 dep 中清除 effect
34 effect.options = options // 存放 onTrack、onTrigger、onStop 等钩子函数,为了精简我们只实现 scheduler
35 return effect
36}
37
38function cleanup(effect) {
39 const { deps } = effect
40 deps.forEach(dep => dep.delete(effect)) // deps 中的 dep 清 effect
41 deps.length = 0 // 清空 effect 的 deps
42}

可以看到 effect API 传入一个函数,effect API 通过 createReactiveEffect 创建一个 effect 函数,并返回这个函数,这个函数的返回值就是传入 effect API 函数的结果,只不过在调用 effect 函数时会把 activeEffect 赋值为当前这个调用中的 effect,并在调用结束后把 activeEffect 改回去

effect 函数创建后如果不是 lazy 的会首先执行一次,这次执行是为了调用 fn,触发 get 等代理,以先收集一遍依赖,先 track 了之后再 trigger 才能有依赖来触发,如果 options 传入了 lazy 为 true,就需要保证先手动执行一遍 effect 函数来收集依赖

effect API 其实是一个比较底层的函数,我们平时使用都是用 watchEffect 和 watch,这两个都是基于 effect 实现的,比如调用这两个函数返回的是一个 stop 函数用来停止监听,其实就是对上面的一个封装,但为什么 effect API 不直接返回一个 stop 函数,而是返回一个 effect 函数?因为 effect 函数可以取传入函数的结果,其他一些 API 的实现需要这个结果,所以 effect API 和 stop 函数设计成了分开的

下面我们看 trigger 的实现

reactivity/effect.js
1export function trigger(target, type, key) {
2 const depsMap = targetMap.get(target)
3 if (!depsMap) return
4 // 需要新建一个 set,如果直接 const effect = depsMap.get(key)
5 // effect 函数执行时 track 的依赖就也会在这一轮 trigger 执行,导致无限循环
6 const effects = new Set()
7 const add = (effectsToAdd) => {
8 if (effectsToAdd) {
9 effectsToAdd.forEach(effect => {
10 // 不要添加自己当前的 effect,否则之后 run(mutate)的时候
11 // 遇到 effect(() => foo.value++) 会导致无限循环
12 if (effect !== activeEffect) effects.add(effect)
13 })
14 }
15 }
16 // SET | ADD | DELETE
17 if (key !== undefined) {
18 add(depsMap.get(key))
19 }
20 const shouldTriggerIteration =
21 (type === TriggerOpTypes.ADD) ||
22 (type === TriggerOpTypes.DELETE)
23 // iteration key on ADD | DELETE
24 if (shouldTriggerIteration) {
25 add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
26 }
27
28 effects.forEach((effect) => {
29 if (effect.options.scheduler) {
30 effect.options.scheduler(effect)
31 } else {
32 effect()
33 }
34 })
35}

也很好理解,就是新建一个 Set,存通过 targetMap 和 depsMap 拿到的依赖(effect 函数),要注意不能将当前的 activeEffect 添加进去,否则可能会无限循环,同时针对触发 trigger 的不同方式(type)也有不同的添加方式,比如在新增或删除 key 导致 trigger 时需要把 length 或 ITERATE_KEY 的依赖也添加进去,对应上面 track ITERATE_KEY,最后依次执行即可

为什么需要新建一个 Set,而不直接用 targetMap.get(target).get(key).forEach(run) 呢?因为 effect 函数在执行的过程中会继续 track 向 depsMap 的 dep 中添加依赖,导致这里一直 trigger effect,effect 中又一直 track,无限循环

为什么不能解构?由于是通过 Proxy 代理对象的 get 操作,相当于 proxy.key 每次都这样访问数据才能成功收集到依赖,解构的话是 let key = proxy.key 获取了 key 的值,之后通过 key 访问数据没有进入代理的 get 操作,所以不会收集到依赖

最后我们来看一个例子,走一遍整体的流程,以便更好的理解

1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <title>Counter</title>
5</head>
6<body>
7 <div class="count"></div>
8 <button class="inc">+</button>
9 <button class="dec">-</button>
10<script type="module">
11 import { reactive, effect } from '../../packages/reactivity/index.js'
12
13 const $inc = document.querySelector('.inc')
14 const $dec = document.querySelector('.dec')
15 const $count = document.querySelector('.count')
16
17 const obj = { count: 0 }
18 const state = reactive(obj)
19
20 effect(() => {
21 $count.innerHTML = state.count
22 })
23
24 $inc.addEventListener('click', () => {
25 state.count++
26 })
27
28 $dec.addEventListener('click', () => {
29 state.count--
30 })
31</script>
32</body>
33</html>

这里首先用 reactive 定义 state,传入 effect,然后 effect 会先执行一遍,将 activeEffect 设置为它,然后 cleanup 一遍,因为本来就是空的所以也没什么用,之后执行到 $count.innerHTML = state.count 触发 state 的 get 代理,get 中调用 track(obj, TrackOpTypes.GET, 'count'),将 activeEffect 收集到 dep(targetMap.set(obj, depsMap.set('count', activeEffect)))中,最后赋值 activeEffect 为 undefined,到此 effect 首次执行完毕

然后绑定好事件,之后点击触发事件会触发 state 的 set 代理,调用 trigger(obj, TriggerOpTypes.SET, 'count'),依次执行刚刚收集好的 effects,触发依赖,也就是执行 effect(() => { $count.innerHTML = state.count) },其中 effect 执行时更上面第一次执行一样,设置 activeEffect 然后 cleanup 上一次的依赖,再访问 state.count 触发 get 代理,再把 activeEffect track 起来,完成依赖收集,最后重新设置 activeEffect

所以流程基本上是这样的:(track effects) => (trigger effects, track effects) => (trigger effects, track effects) => ...

现在已经讲完了响应式的核心原理,剩下的来讲一下 collectionHandlers、ref、computed

☁️ collectionHandlers

为什么 Map、Set、WeakMap、WeakSet 需要另一种 handlers 来处理呢?因为这些类型的 key 都是固定的,比如调用 map.set、map.get、map.has、map.delete 都会触发代理的 get 操作,set、get、has、delete 这些就是他们 key,只有 map.get = ... 才能触发代理的 set 操作,所以就需要 collectionHandlers 代理 get,针对不同的 key 进行不同的代理

reactivity/collectionHandlers.js
1export const mutableCollectionHandlers = {
2 get: createInstrumentationGetter(false),
3}
4export const readonlyCollectionHandlers = {
5 get: createInstrumentationGetter(true),
6}

同样的通过 createInstrumentationGetter 传入 isReadonly 创建不同的 handlers

reactivity/collectionHandlers.js
1function createInstrumentationGetter(isReadonly) {
2 const instrumentations = createInstrumentation(isReadonly)
3
4 return (target, key, receiver) => {
5 if (key === ReactiveFlags.IS_REACTIVE) return !isReadonly
6 if (key === ReactiveFlags.IS_READONLY) return isReadonly
7 if (key === ReactiveFlags.RAW) return target
8
9 return Reflect.get(
10 hasOwn(instrumentations, key) && key in target
11 ? instrumentations
12 : target,
13 key,
14 receiver,
15 )
16 }
17}

这里创建真正的 instrumentations 来处理 get 的代理操作,注意 Reflect.get 的第三个参数 receiver 指 proxy 实例,用来指定 instrumentations 中的 this

reactivity/collectionHandlers.js
1const createInstrumentation = (isReadonly) => ({
2 get(key) {
3 return get(this, key, isReadonly) // 通过 Reflect 调用时 this 是 receiver(proxy 实例)
4 },
5 get size() {
6 return size(this, isReadonly)
7 },
8 has(key) {
9 return has(this, key, isReadonly)
10 },
11 add(value) {
12 return add(this, value, isReadonly)
13 },
14 set(key, value) {
15 return set(this, key, value, isReadonly)
16 },
17 delete(key) {
18 return deleteEntry(this, key, isReadonly)
19 },
20 clear() {
21 return clear(this, isReadonly)
22 },
23 forEach(callback, thisArg) {
24 return forEach(this, callback, thisArg, isReadonly)
25 },
26 keys(...args) {
27 return createIterableMethod('keys', isReadonly)(this, ...args)
28 },
29 values(...args) {
30 return createIterableMethod('values', isReadonly)(this, ...args)
31 },
32 entries(...args) {
33 return createIterableMethod('values', isReadonly)(this, ...args)
34 },
35 [Symbol.iterator](...args) {
36 return createIterableMethod(Symbol.iterator, isReadonly)(this, ...args)
37 },
38})

我们只实现 get、set 和 iterator 这几个方法

reactivity/collectionHandlers.js
1// Set, WeakSet, Map, WeakMap
2const get = (target, key, isReadonly) => {
3 const rawTarget = toRaw(target)
4 const rawKey = toRaw(key)
5 !isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
6 const res = rawTarget.get(rawKey)
7 if (isObject(res)) {
8 return isReadonly ? readonly(res) : reactive(res)
9 }
10 return res
11}

首先看 get,target 参数就是传入的 this,就是 Reflect.get 传入的 receiver,也就是 proxy 实例,先通过 toRaw 得到原对象和原 key,由于对象和数组的 key 都是基本类型,不会是响应式对象,所以可以直接通过 key 来 track,而 collectionType 的 key 可能是引用类型,可能是响应式对象,所以为了保证 track 和 trigger 的 key 是同一个,就要都用原对象或响应式对象,如果使用响应式对象又有的对象并不是响应式的,在转成响应式消耗了内存,所以都通过 rawTarget 和 rawKey 来 track 和 trigger

同样结果如果是对象需要返回结果的响应式版本

reactivity/collectionHandlers.js
1// Map, WeakMap
2const set = (target, key, value, isReadonly) => {
3 if (isReadonly) throw new Error(`operation SET failed: target is readonly.`)
4 const rawTarget = toRaw(target)
5 const rawKey = toRaw(key)
6 const rawValue = toRaw(value)
7 const hadKey = rawTarget.has(rawKey)
8 const res = rawTarget.set(rawKey, rawValue)
9 if (hadKey) {
10 trigger(rawTarget, TriggerOpTypes.ADD, rawKey)
11 } else {
12 trigger(rawTarget, TriggerOpTypes.SET, rawKey)
13 }
14 return res
15}

set 是只有 Map 和 WeakMap 有的,Set、WeakSet 对应的是 add,这两个实现类似,也是先找到 rawTarget 和 rawKey,然后在 trigger 之前求出结果,并判断原来是否有 rawKey,进行对应的 trigger,如果原来没有就是新增了 key,对依赖于 iterator 操作的 effect 会有影响,所以需要区分来处理

  1. 源码中会判断 key !== rawKey,如果是 true 就再 track 一遍 key,具体可以看 Issues: Effect don't work when use reactive to proxy a Map which has reactive object as member's key.

  2. 源码中也对 readonly(reactive(obj)) 进行了处理,先通过 target = (target as any)[ReactiveFlags.RAW] 拿到 reactive 对象,然后通过 toRaw 以保证拿到 rawTarget,后面再通过 target.get 取值,进行相应的 track

reactivity/collectionHandlers.js
1const createIterableMethod = (method, isReadonly) => {
2 return (target, ...args) => {
3 const rawTarget = toRaw(target)
4 const rawIterator = rawTarget[method](...args)
5 const wrap = (value) => {
6 if (isObject(value)) return isReadonly ? readonly(value) : reactive(value)
7 return value
8 }
9 !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
10 return {
11 // iterator protocol
12 next() {
13 const { value, done } = rawIterator.next()
14 return done
15 ? { value, done }
16 : {
17 value: method === 'entries' ? [wrap(value[0]), wrap(value[1])] : wrap(value),
18 done,
19 }
20 },
21 // iterable protocol
22 [Symbol.iterator]() {
23 return this // 返回拦截的 Iterator:rawIterator -> proxyIterator(this)
24 },
25 }
26 }
27}

也比较好理解,就是类似上面的自己实现了 iterator 接口,需要注意 [Symbol.iterator] 方法返回的 this 就是这个对象,指我们实现的 proxyIterator 而不是 rawIterator

源码中对于 MAP_KEY_ITERATE_KEY 的处理可以看看这个 commit,map 在 set 一个已有的 key 时不能触发 key 的 iterator 相关 effect

💫 ref

1export const ref = (value) => reactive({ value }) // 简易 ref

在现在的基础上实现没有优化的 ref 和 computed 就很简单了,但是 Vue 对他们的优化还是很值得学习的

reactivity/ref.js
1export function customRef(factory) {
2 const { get, set } = factory(
3 () => track(ref, TrackOpTypes.GET, 'value'),
4 () => trigger(ref, TriggerOpTypes.SET, 'value'),
5 )
6 const ref = {
7 __v_isRef: true,
8 get value() {
9 return get()
10 },
11 set value(v) {
12 set(v)
13 },
14 }
15 return ref
16}

我们先实现 customRef,他的参数是一个接收封装好的 track 和 trigger 函数的函数,用来生成 ref 的 get value 方法和 set value 方法

reactivity/ref.js
1export function ref(value) {
2 return customRef((track, trigger) => ({
3 get: () => {
4 track()
5 return value
6 },
7 set: (newValue) => {
8 value = newValue
9 trigger()
10 },
11 }))
12}

接着 ref 实现就简单了,就是在 get 时 track,set 时 trigger,而 value 的值通过创建的闭包保存

☄️ computed

最后看一下 computed 的实现,其实 computed 也是一个 customRef

reactivity/computed.js
1export function computed(options) {
2 let getter
3 let setter
4 if (isFunction(options)) {
5 getter = options
6 setter = () => {
7 console.warn('Write operation failed: computed value is readonly')
8 }
9 } else {
10 getter = options.get
11 setter = options.set
12 }
13
14 return createComputedRef(getter, setter)
15}

首先在入口判断是否传入了 setter,如果没有就相当于触发 set 的时候只报一个警告,通过 createComputedRef 创建

reactivity/computed.js
1function createComputedRef(getter, setter) {
2 let dirty = true
3 let value // 通过闭包存
4 const computedRef = customRef((track, trigger) => {
5 const computeEffect = effect(getter, {
6 lazy: true,
7 scheduler() {
8 if (!dirty) {
9 dirty = true
10 trigger()
11 }
12 },
13 })
14 return {
15 get: () => {
16 if (dirty) {
17 value = computeEffect()
18 dirty = false
19 }
20 track()
21 return value
22 },
23 set: (newValue) => {
24 setter(newValue)
25 },
26 }
27 })
28 return computedRef
29}

首先 dirty 一开始是 true,创建 computedRef 时先创建 computeEffect,传入的函数就是我们的 getter,Vue 对于 computed 的优化就在于在需要它的值时才去计算它的值,每次都会计算出最新的,省略了一些没用的值的计算,比如说我改变了三次 computed 依赖的值,但渲染时只需要它最新的第三次的值,那么前两次就不会去计算。为了实现这个我们的 computeEffect 就需要设置成 lazy 的,首次不需要执行 computeEffect,同时 dirty 是 true,真正第一次 get computedRef 时才会手动触发 computeEffect,运行 getter 函数,对 getter 函数中的依赖进行 track,然后返回结果赋值给 value,这里 effect 函数不返回 stop 函数而是返回结果的作用就体现出来了,之后回到 get 方法中继续 track computedRef.value 的依赖,这样下次 getter 中依赖的响应式对象触发 set 代理就会 trigger,进入到 scheduler 中,在 scheduler 中再 trigger 依赖于 computedRef 的依赖,以此实现 computed

大致流程就是:

  1. (first get computed, call computeEffect, track getter dep, set new value, track computed value, return value) =>

  2. (second get computed, dirty is false, return value) =>

  3. (set getter dep, trigger getter dep, scheduler, trigger computed value) =>

  4. (third get computed, call computeEffect, track getter dep, set new value, track computed value, return value) => ...

😃 ramble

前几天在知乎上提了一个问题:Vue3 composition-api 有哪些劣势?,因为是很久之前看的 RFC,忘记了 RFC 中有提到劣势的内容,但是看回答基本上也都是 RFC 中提到过的

首先是 ref 的心智负担,在读源码时可以从 reactive 对象 get 取出来如果是 ref 会自动返回 value 可以看出 Vue 是想将 ref 当作一种新的基本类型,对应 reactive 响应式的引用类型

1let count = ref(0)
2count++
3console.log(count) // 1

但是由于 JavaScript 的不足而没有实现,我也尝试写了 babel macro 去弥补这种不足,但由于难以结合生态就没有写完,而且如果使用 TypeScript 通过类型提示基本没有什么负担

然后是不能解构,因为 setup 只会运行一次,通过 proxy.key 才能成功 track 或 trigger,如果将解构赋值(取值)写到 render function 中,每次渲染都会重新取值,就能使用解构,但这样代码反而更乱

最后是更多的灵活性来自更多的自我克制,我很同意这句话:编写有组织的 JavaScript 代码的技能直接转化为了编写有组织的 Vue 代码的技能

simple-vue/reactivity 实现完整代码