# Object 的变化侦测
# 变化侦测及实现
Vue.js 自动通过状态生成 DOM,并将其显示到页面,这个过程叫渲染。
在运行时应用内部的状态会不断发生变化,需要不停地渲染。确定状态发生了什么变化通过“变化侦测”实现,一般分“推”和“拉”两种。
Vue.js 的变化侦测属于“推”(push),相对于“拉”,“推”知道的信息更多,可以更细粒度的更新。当然,粒度越细,意味每个状态绑定的依赖就越多,依赖追踪在内存上的开销就会越大。所以,在后来的 Vue.js 2.0 开始,引入了虚拟 DOM,将粒度调整为中等粒度,即一个状态所绑定的依赖不再是具体 DOM,而是一个组件(状态变化后会通知组件,组件内部再使用虚拟 DOM 进行比对)。
JavaScript 中有两种方法可以侦测到变化:Object.defineProperty
和 Proxy
。在 Vue.js 3.0 之前都是使用第一种方法。
/**
* 对 Object.defineProperty 的封装
* @param {*} data
* @param {*} key
* @param {*} val
*/
function defineReactive (data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('get value')
return val
},
set: function (newVal) {
console.log('change value')
if (newVal === val) {
return
}
val = newVal
}
})
}
const a = { name: 'default' }
defineReactive(a, 'name', a.name)
console.log(a.name) // get value
// default
a.name = 'new name' // change value
console.log(a.name) // get value
// new name
a.name = 'other name' // change value
console.log(a.name) // get value
// other nam
# 收集依赖
侦测数据变化的目的是在数据变化时,及时地通知使用到数据的地方,进行对应的操作。
Vue.js 的实现方法是,将用到数据的地方都收集起来,等到数据变化时,对所有依赖触发一次通知。简单说,即在 getter
中收集依赖,在 setter
中触发依赖。
function defineReactive (data, key, val) {
const dep = []
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('get value')
dep.push(window.target) // 收集依赖
return val
},
set: function (newVal) {
console.log('set value')
if (newVal === val) {
return
}
// 通知依赖
for (let i = 0; i < dep.length; i ) {
dep[i](newVal, val)
}
val = newVal
}
})
}
window.target = (newVal, oldVal) => {
console.log('change value', oldVal, '->', newVal)
}
const a = { name: 'default' }
defineReactive(a, 'name', a.name)
console.log(a.name) // 触发 getter 进行依赖收集
// get value
// default
a.name = 'new name' // 触发 setter 进行依赖通知
// set value
// change value default -> new name
console.log(a.name)
// get value
// new name
可以将依赖收集的逻辑封装成 Dep
类,专门用于管理依赖。
class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (dataConsumer) {
this.addSub(dataConsumer)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i ) {
subs[i].update()
}
}
}
function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
function defineReactive (data, key, val) {
const dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend()
return val
},
set: function (newVal) {
if (newVal === val) {
return
}
val = newVal
dep.notify()
}
})
}
const dataConsumer = {
data: null,
update: function () {
console.log('update')
}
}
const a = { name: 'default' }
defineReactive(a, 'name', a.name)
dataConsumer.data = a.name
a.name = 'new name' // update
# 依赖是谁
收集依赖,是在收集什么?收集的是用到数据的地方,用于在数据发生变化时及时通知使用方。用到数据的地方可能是模板,也可能是用户写的一个 watch
.
为了方便集中处理这些情况,抽象出一个类,在收集阶段只收集封装好的这个类的实例进来,通知时也直接通知它,然后由它去通知其他地方。
# Watcher
Watcher
作为一个中介角色,数据发生变化时通知它,它再通知其他地方。
class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn) // 执行 this.getter 可以读取到 data 中的值
this.cb = cb
this.value = this.get()
}
get () {
dataConsumer = this
let value = this.getter.call(this.vm, this.vm) // 触发依赖收集
dataConsumer = undefined
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
const bailRE = /[^w.$]/
function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i ) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
# 递归侦测所有的 key
之前的实现中只侦测数据中的一个属性,需要将所有属性都侦测,可以封装一个 Observer
类。作用是将一个数据内的所有属性转换成 getter/setter
的形式,然后去追踪变化。