Vue 的响应式原理
Vue2 响应式原理
Vue 的最独特之处就是它的响应式系统,对数据进行修改时,视图自动更新。
简单来说,在编译时,template 变成 render 函数,然后在页面渲染时,调用 render 函数,其中依赖的变量就会被 getter 监测到并存储在组件对应的 watcher 中,当依赖数据发生变化时,setter 就会通知 watcher , watcher 重新触发渲染,使得所有关联组件重新渲染。
追踪数据变化
在 vue2 中,对于单文件组件的 data 选项中每一个对象,vue 会遍历对象的所有 property,使用 Object.defineProperty 把所有属性转为 getter/setter, 在内部,vue 通过它们来追踪依赖。
每个组件实例都对应一个 watcher实例, 它在组件渲染中,把所"接触(touch)" 过的的数据的property记录为依赖。之后,当依赖项的 setter 被触发时,会通知 watcher ,从而使关联的组件重新渲染。
监测变化的注意事项
对于对象
由于Object.defineProperty是单个属性级别的监听,Vue 无法检测到对象属性的添加和移除
Vue 在初始化的时候,把 data 选项中的对象转换为响应式对象。对于已经创建的实例,vue 提供了api 向对象添加响应式属性, 比如 Vue.set(vm.someObject, 'b', 2) 或 this.$set(this.someObject,'b',2)
如果说同时要添加多个属性,直接使用Object.assign(this.someObject, { a: 1, b: 2 }), 相当于直接添加新属性,vue 是监测不到的。这种情况应该 this.someObject = { ...this.someObject, a: 1, b: 2 } ,如此一来,因为 Vue 对 someObject 也有 setter 追踪(这相当于 data 的一个属性嘛),把新对象赋值给原来的响应式变量,vue 会自动把新对象变为响应式对象,支持后序的依赖追踪和视图更新。
对于数组
Vue 无法监测到:
- 基于索引,修改数组的某个元素
- 修改数组的
length属性
我们可以遍历数组的所有 key ,然后使用 Object.defineProperty 去进行数据劫持,实现索引的监测,length的监测,但随着数组长度增阿吉,这会带来很大的性能损耗,毕竟并非所有数组的所有元素都需要劫持监测。
Vue 的解决方案是重写了数组的 7 个变异方法 push pop shift unshift splice sort reverse
如果要修改下标为 i 的响应式数组,你可以:
Vue.set(myArr, i, newVal)myArr.splice(i, 1, newVal)
如果要更改数组长度:你可以:
myArr.splice(newLength)splice方法只有一个参数的时候,这个索引开始的后边的元素都会被删除。
声明响应式 property
vue2 不允许动态创建根级的响应式 property,也就是不允许动态创建响应式变量,所以必须在初始化组件实例前,提前声明所有的根级响应式变量
异步更新队列
Vue 在更新 DOM 时时异步执行的。
当监测到变化时,Vue 会开启一个队列,缓冲在同一事件循环中发生的所有数据变更。同一个 watcher 被多次触发,但只会被推入队列一次。
在下一个事件循环“tick”中,Vue 刷新队列并进行更新操作。Vue 在异步队列上做了向下兼容,使用 Promise.then MutationObserver,setImmediate或setTimeout(fn, 0)。
Vue 不推荐直接操作 DOM ,推荐采用数据驱动的方式,由 Vue 接管 DOM 操作,但在某些应用下涉及 DOM 操作。所以 Vue.nextTick(callback) 支持了在 Vue 的一次 Tick 完成之后( DOM更新后 )立即执行回调。
Vue3 响应式原理
proxy
Vue3 采用 Proxy 来创建reactive响应式对象,仅将 getter 和 setter 用于 ref
// 这里是官方文档的代码,写得太简单了只有一层的监听, 实际上reactive 和 ref 都是深层监听的
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key) // 在这里收集依赖
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key) // 在这里触发更新
}
})
}
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}reactive 的局限性
将响应式对象的某个属性赋值到 or 解构到某个变量时,该变量会是非响应式的,它不再触发原来的代理
当然如果这个属性是对象,那么响应式会得到保留。比如下例的const b = data.info 或 const { info: b } = data
<script>
const data = reactive({
info: {
id: 1,
addres: 'd12'
}
})
const b = data.info // b 指向 data.info 依然是响应式
// const { info: b } = data // b 指向 data.info , 依然是响应式
// const b = {...data.info} // 不是响应式的
const handleBchange = () => {
b.id = b.id + 1
b.addres = (Math.random() * 10).toString()
}
</script>
<template>
<h2>{{ data.info.id }}</h2>
<h2>{{ data.info.addres }}</h2>
<button @click="handleBchange">Testing</button>
<h2>{{ b.id }}</h2>
<h2>{{ b.addres }}</h2>
</template>依赖收集与视图更新
track 函数内部,首先看当前是不是有正在运行的副作用函数,如果有,维护了一个 set,这个 set 中存储了该属性的订阅者,把这个副作用函数作为订阅者加入到 set。
副作用订阅被存在一个全局 WeakMap<target, Map<key, Set<effect>>> 数据结构中。
trigger 内部,查找该 target,该 key 的 Set<effect>, 然后从集合中取出副作用函数并执行。
下面是我自己写的一个依赖追踪 + 视图更新的实现。
<body>
<h1 id="dom1"></h1>
<h2 id="dom2"></h2>
<h2 id="dom3"></h2>
<script>
// Active effective function
let activeEffect = null
// Global WeakMap<target, Map<key, Set<effect>>>
const effectStore = new WeakMap()
function findEffects(target, key) {
// defend
if (!effectStore.get(target))
effectStore.set(target, new Map())
if (!effectStore.get(target).get(key))
effectStore.get(target).set(key, new Set())
return effectStore.get(target).get(key)
}
/////// 注册 effectFunction //////////////////////////////////
function useEffect(fn) {
const effect = ()=>{ // wrapped effect
activeEffect = effect
fn()
activeEffect = null
}
effect()
}
/////// 核心三函数 //////////////////////////////////
function track(target, key) {
// 依赖收集
if (activeEffect) {
const effectSets = findEffects(target, key)
effectSets.add(activeEffect)
}
}
function trigger(target, key, newVal) {
// 视图更新
const effectSets = findEffects(target, key)
for (let effect of effectSets) {
effect()
}
}
function reactive(obj) {
if (typeof obj !== 'object' || obj === null) return obj
let val
return new Proxy(obj, {
get(target, key) {
track(target, key)
val = reactive(Reflect.get(target, key))
return val
},
set(target, key, newVal) {
val = reactive(newVal)
const res = Reflect.set(target, key, newVal)
trigger(target, key, newVal)
return res
}
})
}
/////// Testing //////////////////////////////////
// DOMS
const dom1 = document.querySelector('#dom1')
const dom2 = document.querySelector('#dom2')
const dom3 = document.querySelector('#dom3')
// Normal Data
const originObj = {
name: 'wang',
age: 12,
courses: ['chinese', 'math', 'english'],
info: {
id: '001',
address: 'Beijing',
family: [
{
relate: 'father',
name: 'wang ji'
},
{
relate: 'mother',
name: 'zhou yang'
},
]
}
}
// Reactive data
const pxy = reactive(originObj)
// Register effective function
useEffect(() => {
dom1.textContent = 'name : ' + pxy.name;
dom2.textContent = 'age : ' + pxy.courses;
const members = pxy.info.family.map(mem => mem.relate)
dom3.textContent = 'family members :' + members
})
// Change reactive data, update view automatically
setTimeout(() => {
pxy.name = 'dajiang' // name change
}, 1000);
setTimeout(() => {
pxy.courses.push('football') // courses add
}, 1500)
setTimeout(() => {
pxy.info.family.splice(1, 1) // member miss
}, 2000);
</script>
</body>在具体实现上
在响应式系统的具体实现上, Vue 用了那么几个核心部件 Observer Dep Watcher Scheduler

Observer
Observer 专门负责把一个对象转换为响应式对象。
在组件的生命周期中,发生在 beforeCreate 之后,created 之前。
数组的变异方法重写,对象的 set/delete。
Dep
Vue 会为响应式对象本身及其属性都创建 Dep 实例
Dep 实例的任务就是记录依赖 dep.depend() + *通知更新 dep.notify()*
注意, 如果模板里 用到了 obj.a,而我们给 obj 新增了一个属性 b ,这也会触发更新,因为触发了 obj 的 setter
Watcher
getter 中是如何知道是哪个函数在访问对象的? Watcher 解决的就是这个问题。
vue 把渲染函数交给 watcher 去执行,当 watcher 去执行(渲染)函数时,他首先会把更改一个全局变量,让这个全局变量指向自己,然后再执行函数。
在函数执行过程中,如果访问到某个响应式数据,那么就触发了他的 getter 函数,getter 函数内部就可以通过这个全局变量知道现在正在执行的 watcher 实例,Dep 就把这个 watcher 给记录到依赖中去; 后序当数据发生变化时,Dep 就通知 watcher 进行更新。
每个组件至少对应一个watcher,组件的 render 函数本身就由 watcher 去托管运行
Scheduler
每次 setter 都会进行 dep.notify,当一次同步代码里多次修改了对象,就会多次通知 wathcer 进行更新。
所以需要有个调度器来节流,来控制渲染的实际执行。
调度器维护一个执行队列,同一个 watcher 只会存在一次,队列中的 watcher 不是立即执行,而是通过 nextTick ,作为一个微任务,异步执行
nextTick 的回调到底是何时执行?
vue 中,所有的视图更新都是在 scheduler 中通过 nextTick 作为微任务异步更新的,也就是我们可调用的 nextTick API
视图更新作为一个任务,通过 nextTick(updateView) 加入异步队列
同理,我们 nextTick(myFn) 也是把 myFn 给加入异步队列
所以:nextTick 执行顺序就是加入队列的顺序。
下面的例子体现了这点:
// <h1 id="id">{{data.info.id}}<id>
const handleBchange = () => {
nextTick(() => {
const idDom = document.querySelector('#id')
console.log('before id update: ',idDom.innerHTML) // 未更新的 id
})
data.info.id *= 10 // 把更新 idDom 放入异步队列
data.info.addres = (Math.random() * 10).toFixed(3)
nextTick(() => {
const idDom = document.querySelector('#id')
console.log('after id update: ',idDom.innerHTML) // 最新的 id
})
}实现一个组件对象的异步渲染机制(nextTick)
const sfc = {
_data: {
name: 'wang'
},
tickCallback: new Set(),
pending: false,
init() {
this.r = this.render.bind(this)
// 初始化响应式数据
this.data = new Proxy(this._data, {
get(target, key) {
return Reflect.get(target, key)
},
set: (target, key, newValue) => {
Reflect.set(target, key, newValue)
this.nextTick(this.r)
this.flush()
}
})
},
nextTick(fn) {
this.tickCallback.add(fn)
this.flush()
},
flush() {
if (this.pending === false) {
this.pending = true
Promise.resolve().then(() => {
const cbs = [...this.tickCallback]
this.tickCallback.clear()
this.pending = false
for (let cb of cbs) {
cb()
}
// 当 nextTick 中又新增了nextTick, 也就是在 nextTick 的 callback 中 又修改了数据,那需要派发下一次 flucsh
if (this.tickCallback.size > 0) {
this.flush()
}
})
}
},
render() {
console.log('dom render, name is ', this._data.name)
}
}
sfc.init()
sfc.nextTick(() => {
console.log('start render')
})
sfc.data.name = 'dj1'
sfc.data.name = 'dj2'
sfc.data.name = 'dj3'
sfc.nextTick(() => {
console.log('end render')
sfc.data.name = 'not'
})
/* log:
start render
dom render, name is dj7
end render,
dom render, name is not
*/