reactivity
这个 Q 详细研究了其中 reactivity 部分源码实现:
Vue 3.0 版响应式系统支持 基本对象/readonly/collectionMap(Set/Map) 的响应式处理。
以基本对象为例:
// demolet value1, value2
const target = { num: 0 }
const state = reactive(target)
const cValue = computed(() => state.num + 1)
let reactiveEffect1 = effect(() => {
value1 = state.num // 依赖收集
})
let reactiveEffect2 = effect(() => {
value2 = state.num // 依赖收集
})
console.log(value1, value2, cValue.value) // 0, 0, 1
state.num++ // 触发依赖更新console.log(value1, value2, cValue.value) // 1, 1, 2
我们可以看到其中使用到的几个核心函数 reactive、computed、effect。
- reactive:定义响应式行为。get、has、ownKeys 行为会触发依赖收集;set、delete 行为会触发依赖更新。
- computed: 计算属性,其实是 effect 函数的封装调用。
- effect:依赖收集的关键。
简单介绍一下这几个函数,以下代码经过大量精简。
reactive
const targetMap = new WeakMap() // 依赖收集的核心存储位置
const rawToReactive = new WeakMap() // 标记已 proxy 的对象
const reactiveToRaw = new WeakMap() // 标记已 proxy 的对象
const effectStack = [] // 存储正在运行的 effect 实例
function reactive(target) {
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers
)
}
function createReactiveObject(target, toReactive, toRaw, baseHandlers) {
let observed = new Proxy(target, baseHandlers)
toReactive.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
可以看到,reactive 函数本质返回的是个 Proxy 实例,用以定义 target 对象的响应式行为,同时会标记该对象,避免重复 proxy,具体行为 baseHandlers 会在下面介绍。
baseHandlers
以 get、set 为例。
// baseHandlers: {
// track: get, has, ownKeys
// trigger: set, deleteProperty
// }
function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, 'get', key)
// 嵌套对象的响应式,触发属性访问时才实现,优化了 Vue2.x 递归实现。
return isObject(res) ? reactive(res) : res
}
function set(target, key, value, receiver) {
value = toRaw(value)
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 如果 target 在原始原型链的上游,不触发
// 举例:Object.setPrototypeOf(someObj, target); someObj.foo = 'bar';
if (target === toRaw(receiver)) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, 'add', key, extraInfo)
} else if (hasChanged(value, oldValue)) {
trigger(target, 'set', key, extraInfo)
}
}
return result
}
这里值得注意的是对于多层嵌套对象,Vue 2.x 实现的响应式,会在初始化时便对 target 递归调用 defineProperty,Vue 3.0 只会在属性访问时触发 reactive,节省了性能。此外,属性修改和新增都会触发 set 行为,这里也通过 hasOwnProperty 做了判断区分。
baseHandlers 定义的其它行为方法有:
- get/has/ownKeys 会触发 track,即依赖收集,后面会介绍。
- set/deleteProperty 会触发 trigger,即依赖更新,后面会介绍。
effect
effect 函数是触发依赖收集的关键,所以还是得先于 track 函数介绍。
function effect(fn, options = EMPTY_OBJ) {
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect() // !!!
}
return effect
}
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect(...args) {
return run(effect, fn, args) // !!!
} // !!!
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
function run(effect, fn, args) {
if (!effect.active) {
return fn(...args)
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
effectStack.push(effect)
// 回调函数执行前 reactiveEffect 入栈了
return fn(...args) // !!!
} finally {
effectStack.pop()
}
}
}
// 回顾之前的 effect 调用
let activeEffect1 = effect(() => {
value1 = state.num // 依赖收集
})
简单概括下:effect 函数执行返回了一个 effect 实例 reactiveEffect,在非 lazy 状态下,会同步调用 run 函数,当前 reactiveEffect 实例会入栈 effectStack,同时调用传入 effect 的回调函数,回调函数内部 state.num 触发了之前 baseHandlers.get 行为,至此,依赖收集正式开始了!!
track
function track(target, type, key) {
if (!shouldTrack || effectStack.length === 0) {
return
}
const effect = effectStack[effectStack.length - 1]
// 触发前置条件:effect 回调内触发 baseHandlers 中 track 行为的方法
if (effect) {
if (type === 'iterate' /* ITERATE */) {
key = ITERATE_KEY
}
// 下面干的事就是在 targetMap 中设置相应 key 和依赖函数
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (dep === void 0) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(effect)) {
dep.add(effect)
effect.deps.push(dep)
if (effect.onTrack) {
effect.onTrack({
effect,
target,
type,
key
})
}
}
}
}
// 回顾之前的代码let value1, value2
const target = { num: 0 }
const state = reactive(target)
let reactiveEffect1 = effect(() => {
value1 = state.num // 依赖收集
})
let reactiveEffect2 = effect(() => {
value2 = state.num // 依赖收集
})
可以看到,依赖收集有个前置条件:拿到 effectStack 栈顶的 reactiveEffect 实例。所以如果你不在 effect 回调内访问 state.num 是不会触发依赖收集的。
依赖收集 主要做的就是建立并保存 key 和 reactiveEffect 之间的联系,这种联系全部保存在 targetMap 中。
targetMap: {
target: depsMap { num: dep=Set(reactiveEffect1, reactiveEffect2) }
}
⚠️ 看着很眼熟不是?Vue 3.0 响应式的核心就是发布/订阅模式,它的订阅由 Proxy get(track) 实现,发布由 Proxy set(trigger) 实现。
trigger
function trigger(target, type, key, extraInfo) {
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
// never been trackedreturn
}
const effects = new Set()
const computedRunners = new Set()
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
// addRunners 主要判断 effect.computed 来考虑 effect 属于 effects/computedRunners
addRunners(effects, computedRunners, depsMap.get(key))
}
// ADD | DELETE 操作,也会触发 track 中的迭代(ownKeys)操作。
if (type === 'add' /* ADD */ || type === 'delete' /* DELETE */) {
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
const run = effect => {
scheduleRun(effect, target, type, key, extraInfo)
}
// set 值时要确保 computed 相关的 ReactiveEffect 先执行
computedRunners.forEach(run)
effects.forEach(run)
}
function scheduleRun(effect, target, type, key, extraInfo) {
if ( effect.options.onTrigger) {
const event = {
effect,
target,
key,
type
};
effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event);
}
if (effect.options.scheduler !== void 0) {
effect.options.scheduler(effect);
}
else {
effect();
}
}
// 回顾之前的代码
state.num++ // 触发依赖更新
此处触发依赖更新的 key 是 num,这时我们就会从 targetMap 中找到 num 对应的 reactiveEffect 实例(上面依赖收集建立了联系),即 reactiveEffect1,reactiveEffect2,随后会遍历调用。
这里会有一种情况是某个 reactiveEffect 内可能会依赖着计算属性,对于计算属性的更新会始终会先于普通的 reactiveEffect,确保取值正确性。
computed
// 本质就是传入lazy/computed: true 的 effect 函数
function computed(getterOrOptions) {
let getter
let setter
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = () => {
console.warn('Write operation failed: computed value is readonly')
}
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
let dirty = true
let value
const runner = effect(getter, {
lazy: true,
// mark effect as computed so that it gets priority during trigger
computed: true,
scheduler: () => {
dirty = true
}
})
return {
_isRef: true,
// expose effect so computed can be stopped
effect: runner,
get value() {
if (dirty) {
value = runner()
dirty = false
}
// When computed effects are accessed in a parent effect, the parent
// should track all the dependencies the computed property has tracked.
// This should also apply for chained computed properties.
trackChildRun(runner)
return value
},
set value(newValue) {
setter(newValue)
}
}
}
// 回顾之前的代码const cValue = computed(() => state.num + 1)
console.log(value1, value2, cValue.value)
computed 函数返回一个标准对象,封装了 effect 函数调用。
可以看到内部 runner 函数就是 effect 函数调用,即 computedReactiveEffect,这个东东也被依赖收集进入了 targetMap 对应 num 中。(因为访问 state.num 触发了依赖收集)
看看这个 effect 传入的额外属性:
- lazy: true,决定 computed 回调不会同步执行,而是在访问 value 值时执行。
- computed: true,决定了该 computedReactiveEffect 在之前 trigger 过程中作为计算属性会优先计算;
- scheduler 属性,在之前 trigger 内部调用 scheduleRun 时,会判断传入的 scheduler 如果存在就会调用,这里 一旦 state.num 值更改触发 trigger,scheduler 就会调用,这里将 dirty 设置为 true,下次访问计算属性就会重新计算。
dirty: true,决定 computedReactiveEffect 调用获取计算值,可以看到第一次访问 value 时会触发调用,之后改为 false,依赖属性更新也会重新设置为 true;
dirty: false,决定此后访问都会直接返回之前的 value 值,达到缓存效果。