主题
深入响应式
Vue 响应式原理
Vue 的响应式的核心就是“拦截”
在 js 中,有两种劫持 property 访问的方式:
- Vue 2:使用
Object.defineProperty完全是出于支持旧版本浏览器的限制。 - Vue 3:使用
Proxy来创建响应式对象(IE 不支持)。
但需要注意,两者都不能直接让视图同步更新。
Proxy:拦截“行为”
劫持对象
js
const state = { count: 0 }
const p = new Proxy(state, {
get(t, k) {
console.log('get:', k) // 任何读取都会触发
return t[k]
},
set(t, k, v) {
console.log('set:', k, v) // 任何写入都会触发
t[k] = v
return true
},
})
p.count // get: count
p.count = 1 // set: count 1
p.count++ // get: count、set: count 1- 行为级拦截:不需要逐个定义属性。
- 新增/删除、数组下标 也能够感知。
数组下标和长度
Proxy 对于下标写入与 length 变化都能拦截。
js
const list = ['a']
const p = new Proxy(list, {
set(t, k, v) {
console.log('set:', k, v)
t[k] = v
return true
},
})
p[0] = 'b' // set: 0 b
p.push('c') // set: 1 c、set: length 2Object.defineProperty:拦截“属性”
劫持对象
js
const state = { count: 0 }
Object.defineProperty(state, 'count', {
get() {
console.log('get: count') // 读取时触发
return value
},
set(v) {
console.log('set: count', v) // 写入时触发
value = v
},
})
state.count // get: count
state.count = 1 // set: count 1
state.count++ // get: count、set: count 1- 只能拦截 已有属性,且需要 逐个定义。
- 新增/删除属性、数组下标 无法感知。
数组下标和长度
Object.defineProperty 需要 覆盖每个下标 或者 改写方法。
依赖追踪(依赖收集)
为什么需要依赖追踪?
虽然 Proxy、Object.defineProperty 都能劫持数据访问,但此时仅实现了对数据的“拦截”,此时还没有 同步数据变化到视图 的能力。
于是就引入了一种新的机制:依赖追踪。
依赖追踪的核心问题
数据变化 !== 视图自动更新
需要建立 数据 -> 依赖它的副作用函数 的映射关系
解决问题的和核心思路
副作用函数:会读取响应式数据,并在数据变化时重新执行的函数。
- 依赖收集(track):当 effect 读取了某个属性时,把它登记到属性的依赖集合中。
- 派发更新(trigger):当属性变化时,从依赖集合中找到所有的 effect,并依次执行。
依赖收集(track)
什么时候发生?
在某个副作用执行期间,如果读取了响应式数据,就把这个副作用记录到数据对应的依赖集合中。
- 目的:建立响应式数据 -> 副作用函数 的映射关系
- 关键点:只有在有
activeEffect的情况下,才会把依赖收集起来。
巧记:谁在用我?我记下来
派发更新(trigger)
什么时候发生?
当某个响应式数据被修改时,找到它的依赖集合,把里面的副作用函数全部重新执行一次。
- 目的:让“用过这个数据的地方”重新运行,从而更新视图或执行其他逻辑。
- 关键点:精准触发,只更新真正受影响的副作用。
巧记:我变了?通知用我的人重新执行
执行流程
采取"发布/订阅模式"
- effect(render):
- 设置 activeEffect = render
- 执行 render 时读取 state.count -> track 把 render 存到 count 的依赖集合
- 修改 state.count:
- 触发 Proxy 的 set -> trigger 从 count 的依赖集合取出 render 并执行
依赖追踪的实现
js
let activeEffect = null // 当前正在执行的副作用函数
const bucket = new WeakMap() // 依赖收集(存储副作用函数的容器),target -> key -> effects
function effect(fn) {
activeEffect = fn
fn() // 执行时触发 get,从而完成依赖收集
activeEffect = null
}
const state = new Proxy(
{ count: 0 },
{
get(t, k) {
// === 依赖收集(track) ===
if (activeEffect) {
let depsMap = bucket.get(t)
if (!depsMap) {
bucket.set(t, (depsMap = new Map()))
}
let deps = depsMap.get(k)
if (!deps) {
depsMap.set(k, (deps = new Set()))
}
deps.add(activeEffect)
}
return t[k]
},
set(t, k, v) {
t[k] = v
// === 触发更新(trigger) ===
const depsMap = bucket.get(t)
if (!depsMap) return
const effects = depsMap.get(k)
effects && effects.forEach((fn) => fn())
return true
},
},
)