# 什么是变化侦测
变化侦测的作用是侦测数据的变化。当数据变化时,会通知视图进行相应的更新。变化侦测是响应式系统的核心,没有它就没有重新渲染。框架在运行时,视图就无法随着状态的变化而变化。
Vue中可以进行更细粒度的更新,因为粒度越细,每个状态所绑定的依赖就越多,依赖追踪再内存上的开销就会越大。因为Vue2.0开始引入了虚拟DOM,将粒度调整为中等粒度,即一个状态所绑定的依赖不再是具体的DOM节点,而是一个组件。这样状态变化后,会通知到组件,组件内部再使用虚拟DOM对比。这样可以大大降低依赖数量,从而降低依赖追踪所消耗的内存。
# 如何追踪变化
JS中如何侦测一个对象的变化?有两种方法可以侦测到变化:
- Object.defineProperty
- Proxy
这里我们先说通过Object.defineProperty这种方式追踪变化
function defineReactvie(data, key ,val) {
Object.defineProperty(data, key, {
get() {
return val
},
set(newVal) {
if (val === newVal) {
return
}
val = newVal
}
})
}
# 如何收集依赖
前面我们定义了defineReactvie方法,可以看出当我们读取属性的时候会调用getter方法,给属性设置值的时候会调用setter方法,我们可以在这两个方法里收集、触发依赖
# 依赖收集在哪里
因为每个数据属性对应的依赖可能并不是一个,可以在getter中用数组来存储每个可以的key的依赖,当调用属性的setter方法时,依次触发依赖,更新下界面
export default class Dep {
constructor () {
// 存储依赖
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (window.target) {
this.addSub(window.target)
}
}
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) {
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
// 收集依赖
dep.depend()
return val
},
set: function (newVal) {
if(val === newVal){
return
}
val = newVal
// 触发依赖
dep.notify()
}
})
}
# 依赖是谁
<div id="app">
<p>{{name}}</p>
<p>{{name}}</p>
<p>{{name}}</p>
</div>
如上name属性可能在多个地方被多次调用,如果当name发生变化的时候我们逐个去通知未免太繁琐,Vue中使用了Watcher类,由它负责通知,我们收集的依赖也就是Watcher。
# 什么是Watcher
Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
// 执行this.getter(),就可以读取data.name的内容
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get() {
window.target = this
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
上述代码为window.target赋值了this也就是Watcher的实例,当我们读取data.name时就会触发getter方法,在getter中会调用dep.dend方法将这个watcher添加到Dep中,当data.name属性值发生变化的时候会触setter方法,执行dep.notify()方法依次执行依赖列表中所有watcher的update方法
# 递归侦测所有key
export class Observer {
constructor (value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
function defineReactive (data, key, val) {
// 新增,递归子属性
if (typeof val === 'object') {
new Observer(val)
}
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend()
return val
},
set: function (newVal) {
if(val === newVal){
return
}
val = newVal
dep.notify()
}
})
}
Observer类会附加到每一个被侦测的object上。一旦被附加上,Observer会将object的所有属性转换为getter/setter的形式来收集属性的依赖,并且当属性发生变化时会通知这些依赖。
walk会将每一个属性都转换成getter/setter的形式来侦测变化 这个方法只有在数据类型为Object时被调用。
# 关于Object的问题
var vm = new Vue({
el: '#app',
data: {
obj: {
name: 'f'
}
},
methods: {
del() {
delete this.obj.name
},
add() {
this.obj.age = 18
}
}
})
Vue无法检测到上述代码中age和name属性的增删,因为:
Vue通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。因为JS在ES6之前没有元编程的能力,无法侦测到一个属性是被添加了还是被删除了。为了解决这个问题,Vue提供了两个API: vm.$set和vm.$delete
# 总结
Data通过Observer转化成了geter/setter的形式来追踪变化。当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
当数据发生变化时,会触发setter,从而向Dep中的依赖(Watcher)发送通知。
Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
← Vue.js简介 Array的变化侦测 →