响应式:已有对象属性的依赖收集
先理解原理和示例,再练习完整口述与追问。
回答要点
getter 收集、setter 触发、分支依赖清理、停止订阅。
原理拆解
简化响应式先定义当前活跃 effect,读取属性时登记,写入变化时触发;实际系统还需要依赖清理、嵌套 effect、循环保护、调度、代理缓存与集合处理。若分支变化后不清理旧依赖,effect 会继续被不再使用的字段触发。
示例与推演
effect 读取 enabled?left:right:初次 enabled 为 true 时依赖 left,切为 false 后应不再因 left 改变而触发。嵌套 effect 时需要恢复外层收集目标;同一次写入若集合遍历期间又重新订阅,也需避免无限重复。
易错点
教学实现不等于 Vue 完整源码。只解释 Object.defineProperty 或 Proxy,不解释依赖如何建立与失效,原理仍不完整。
参考回答可直接练习口述
用 activeEffect 记录当前执行函数,getter 把它放进属性依赖集合,setter 变化后重新执行集合里的函数。每次执行先清理旧依赖,再收集本轮依赖,解决条件分支切换后的无效订阅。下面是同步教学实现,支持普通对象的已有、可写、可配置、可枚举字符串数据属性,不实现数组、动态增删、批量调度和完整 Vue 生命周期。
let activeEffect = null
const observedObjects = new WeakSet()
function effect(fn) {
function cleanup() {
for (const dep of runner.deps) dep.delete(runner)
runner.deps.length = 0
}
function runner() {
if (!runner.active) return fn()
cleanup()
const previous = activeEffect
activeEffect = runner
try { return fn() }
finally { activeEffect = previous }
}
runner.deps = []
runner.active = true
runner.stop = () => { cleanup(); runner.active = false }
runner()
return runner
}
function observe(object) {
if (object === null || typeof object !== 'object') return object
const prototype = Object.getPrototypeOf(object)
if (Array.isArray(object) ||
(prototype !== Object.prototype && prototype !== null)) {
throw new TypeError('仅支持普通对象')
}
if (observedObjects.has(object)) return object
observedObjects.add(object)
for (const key of Object.keys(object)) {
const descriptor = Object.getOwnPropertyDescriptor(object, key)
if (!descriptor.configurable || !descriptor.writable || !('value' in descriptor)) {
throw new TypeError('属性必须是可写、可配置的数据属性')
}
let value = observe(descriptor.value)
const dep = new Set()
Object.defineProperty(object, key, {
enumerable: true,
configurable: true,
get() {
if (activeEffect && !dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
return value
},
set(next) {
if (Object.is(next, value)) return
value = observe(next)
for (const runner of [...dep]) {
if (runner !== activeEffect) runner()
}
}
})
}
return object
}
示例:const state=observe({flag:true,a:1,b:2}),执行 effect(()=>state.flag?state.a:state.b),切换 flag 后不再订阅 a。返回的 runner.stop() 可取消自动追踪。同步触发不包含 Vue 的批量去重;相互写入造成的循环副作用也不自动解决。初始化与属性数相关,单次触发与依赖及执行工作量相关;应只传可写数据对象,不用于只读或带复杂描述符的实例。