Modern Reactivity
Reactive System
effect.ts:effect,track,trigger.baseHandlers.ts: proxy handler (getandset).reactive.ts:reactiveusing ES6 Proxy.ref.ts:- reactive reference using Object Accessors.
refperformant overreactive.
computed.ts:computedusingeffectand return aref.
Reactive Effects
Data getter/setter -> Notify -> Watcher -> Trigger --> Renderer:
console.log(data.a) // getHook() get called.
data.a = 2 // setHook() get called.
Effects bucket:
targetMap=target: effectsMap.effectsMap=keyName: effectsSet.
type Primitive = string | number | boolean
type Key = string | symbol
type Effect<T> = () => T
const runningEffects = []
const targetMap = new WeakMap()
const IterateKey = Symbol('IterateKey')
const LengthKey = 'length'
// runEffect -> effect -> proxy.get -> track.
function createEffect<T>(effect: Effect<T>) {
runningEffects.push(effect)
effect()
runningEffects.pop()
}
function track<T extends object>(target: T, key: Key) {
for (const effect of runningEffects) {
let effectsMap = targetMap.get(target)
if (!effectsMap)
targetMap.set(target, (effectsMap = new Map()))
let effects = effectsMap.get(key)
if (!effects)
effectsMap.set(key, (effects = new Set()))
effects.add(effect)
}
}
function trigger<T extends object>(target: T, key: Key, type: Type) {
const effectsMap = targetMap.get(target)
if (!effectsMap)
return
const effectsToRun = new Set()
const ordinaryEffects = effectsMap.get(key)
ordinaryEffects?.forEach((effect) => {
// Remove current running effect
// to avoid infinite call stack
// (skip triggering current tracking effect):
// reactive.foo = reactive.foo + 1;
if (effect !== runningEffects.top())
effectsToRun.add(effect)
})
if (type === 'ADD' || type === 'DELETE') {
const iterateEffects = effectsMap.get(IterateKey)
iterateEffects?.forEach((effect) => {
if (effect !== runningEffects.top())
effectsToRun.add(effect)
})
}
if (type === 'LENGTH') {
const lengthEffects = effectsMap.get(LengthKey)
lengthEffects?.forEach((effect) => {
if (effect !== runningEffects.top())
effectsToRun.add(effect)
})
}
effectsToRun.forEach((effect) => {
if (effect.options.scheduler)
effect.options.scheduler(effect)
else effect()
})
}
export function reactive<T extends object>(target: T) {
const handler: ProxyHandler<T> = {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
track(target, key)
if (value !== null && typeof value === 'object')
return reactive(value)
else return value
},
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
ownKeys(target) {
// Proxy `for key in target` operator.
track(target, Array.isArray(target) ? LengthKey : IterateKey)
return Reflect.ownKeys(target)
},
set(target, key, value, receiver) {
const type = Array.isArray(target)
? Number(key) < target.length
? 'SET'
: 'LENGTH'
: Object.hasOwn(target, key)
? 'SET'
: 'ADD'
const oldValue = Reflect.get(target, key, receiver)
const result = Reflect.set(target, key, value, receiver)
if (result && oldValue !== value)
trigger(target, key, type)
return result
},
deleteProperty(target, key) {
const isOwnKey = Object.hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (result && isOwnKey)
trigger(target, key, 'DELETE')
return result
},
}
return new Proxy(target, handler)
}
export function ref<T extends Primitive>(raw?: T) {
const refObject = {
get value() {
track(refObject, 'value')
return raw
},
set value(newValue: T) {
raw = newValue
trigger(refObject, 'value')
},
}
return refObject
}
export function computed<T extends Primitive>(getter: () => T) {
const refObject = ref<T>()
createEffect(() => (refObject.value = getter()))
return refObject
}
interface Product {
price: number
quantity: number
}
const product = reactive<Product>({ price: 5, quantity: 2 })
const salePrice = computed(() => product.price * 0.9)
const total = computed(() => salePrice.value * product.quantity)
console.assert(salePrice.value === 4.5)
console.assert(total.value === 9)
product.quantity = 3
console.assert(total.value === 13.5)
product.quantity = 4
console.assert(total.value === 18)
product.price = 6
console.assert(salePrice.value === 5.4)
console.assert(total.value === 21.6)
Reactive Proxy
- Simple:
Proxy使用上比Object.defineProperty方便.Object.defineProperty只能监听对象, 导致Vue 2data属性必须通过一个返回对象的函数方式初始化.Vue 3更加多元化, 可以监听任意数据.
- Performant:
Proxy代理整个对象,Object.defineProperty只代理对象上的某个属性.Object.defineProperty由于每次只能监听对象一个键的get/set, 导致需要循环监听浪费性能.Proxy可以一次性监听到所有属性.
- Lazy:
Proxy性能优于Object.defineProperty.- 如果对象内部要全部递归代理, 则
Proxy可以只在调用时递归. Object.defineProperty需要在一开始就全部递归.
- 如果对象内部要全部递归代理, 则
- Feature:
- 对象上定义新属性时, 只有
Proxy可以监听到:- Vue2: 提供
Vue.set/Vue.delete等辅助方法. - Vue3:
Proxy监听新属性.
- Vue2: 提供
- 数组新增删除修改时, 只有
Proxy可以监听到:Object.defineProperty无法监听数组,Proxy则可以直接监听数组变化.- Vue2: 重写数组方法监听数组变化.
- Vue3:
Proxy监听数组变化.
- 对象上定义新属性时, 只有
Proxy不兼容 IE,Object.defineProperty不兼容 IE8 及以下.
Vue 2:
Vue.set(app.items, indexOfItem, newValue)
Vue.set(app.product, newField, newValue)
Vue 3:
app.items[indexOfItem] = newValue
app.product[newField] = newValue