从vue3 watch开始理解Vue的响应式原理
前言
vue源代码的内容非常之多,包括模板解析(把vue文件的写法解析成一个函数用来插入dom或者调整dom),依赖收集,响应式,事件处理,插槽,组件,指令,等等等。刚开始看时不知道从何看起,这里分享一个个人的学习思路,我们可以从一些关键的api开始逐步学习,这里推荐 reactive,watch。明白了这两个api时如何工作的,那vue的响应式系统就基本理解了。
let`s start
js
// demo.js
import { reactive, watch } from './vue-core.js'
const user = reactive({
name: 'Alice',
age: 25
})
watch(user, (newValue, oldValue) => {
console.log(
`User changed from ${oldValue.name},${oldValue.age} to ${newValue.name},${newValue.age}`
)
})
setTimeout(() => {
user.name = 'Bob'
user.age = 30
}, 1000)
朴素的示例,如果user发生了变化。那么就会触发watch的回调。脱离vue框架,我们需要如何设计呢,首先想到的简单逻辑就是拦截user的set和get。无论是通过Proxy还是Object的set,原理是一样的。
vue-core.js
let activeSub = null // 当前活跃的订阅者
const targetMap = new WeakMap()
export function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key)
return res
}
})
}
// 简化版watch实现,只考虑监听一个reactive对象(类似上面的user例子)的变化
export function watch(source, cb) {
const getter = () => {
for (const key in source) {
source[key]
}
return source
}
let oldValue
const effect = new ReactiveEffect(getter)
const job = () => {
const newValue = effect.run()
//todo 对比newValue和oldValue是否有变化
cb(newValue, oldValue)
oldValue = newValue
}
effect.schedule = job
oldValue = effect.run()
console.log('old', oldValue)
}
// 示例解释:搜集user中每个key的下游依赖,当user的name或age变化时,触发对应的依赖更新,从而调用watch的回调函数。
function track(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Dep()))
}
dep.track()
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.trigger()
}
}
function Dep() {
this.subs = new Set()
this.track = () => {
// 借助全局变量activeSub记录当前活跃的订阅者
if (activeSub) {
this.subs.add(activeSub)
}
}
this.trigger = () => {
this.subs.forEach((sub) => {
sub.notify()
})
}
}
function ReactiveEffect(fn) {
this.fn = fn
this.notify = null // 回调函数, 可类比于watch的cb用来辅助理解
this.schedule = null
this.run = () => {
// 标记当前活跃的订阅者为this 通过fn触发每个ref或者reactive对象的getter
// 从而触发track函数,将当前活跃的订阅者this添加到dep的subs中
const preActiveSub = activeSub
activeSub = this
const value = this.fn()
activeSub = preActiveSub
return value
}
// 上游的ref或者reactive对象触发setter时,会通过收集者调用trigger函数,从而触发schedule函数,在watch中就是cb函数
this.trigger = () => {
this.schedule?.()
}
this.notify = () => {
this.trigger()
}
}
从上面的demo中我们初步实现了user变动时调用一个回调,deps表示"依赖",通俗的理解就是有多少个下游正在依赖user,例如computed,watch,render函数等等这些的最终结果都是根据上游来变动的。

这里面其实就两个概念需要理解,一个是依赖收集者 Dep,一个是响应副作用管理器 ReactiveEffect。借助例子,Dep 理解为用来管理user中每个key都有哪些watch正在监听。而 ReactiveEffect 理解为当user的某个key发生变化时,需要调用哪些watch的回调函数。
调用watch的过程其实关注的就是 oldValue = effect.run() 这一段代码。在创建watch时,会立即调用effect.run(),从而触发user的getter,从而触发track函数,将当前活跃的订阅者effect添加到user.name和user.age的dep中。这样,当user.name或user.age发生变化时,就会触发trigger函数,从而调用effect的notify函数,从而调用watch的回调函数。
组件渲染其实就是一个特殊的watch,watch的回调函数就是渲染函数。当user的name或age发生变化时,就会触发渲染函数,从而更新dom。