前言
由于JS为Object和Array提供的方法机制不同,所以Vue针对Object和Array,采用了两套不同的变化侦测机制。本章,我们先来详细介绍一下Object的变化侦测。
Object变化如何侦测
Vue变化侦测机制的关键点在于观测数据变化,那么我们如何观测Object的变化呢?
Vue为我们定义了Observer类。通过Observer类,可以将一个正常的数据转换成可观测的数据。
例如:
let apple = new Observer({
'weight':'1斤',
'price':10
})
这样,apple的两个属性都变得可观测了。
那么,observer类究竟是怎样的一个存在呢?我们来看看源码:
/**
* Observer类会通过递归的方式把一个对象的所有属性包括子属性都转化成可观测对象
*/
export class Observer {
constructor (value) {
this.value = value
// 给value新增一个__ob__属性,值为该value的Observer实例
// 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
def(value,'__ob__',this)
if (Array.isArray(value)) {
// 当value为数组时的逻辑
// ...
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
看到这里,大家可能还是不知道Observer类究竟是如何监测数据变化,实现数据可观测呢?
我们再把上述代码中的defineReactive方法代码放出来,大家就明白了。
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj,key,val) {
// 如果只传了obj和key,那么val = obj[key]
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
if(val === newVal){
return
}
console.log(`${key}属性被修改了`);
val = newVal;
}
})
}
defineReactive方法用到了JS一个非常重要的原生方法Object.defineProperty。通过Object.defineProperty()方法,可以给数据对象定义一个属性(key),并把这个属性的读和写分别使用get()和set()进行拦截,每当该属性进行读或写操作的时候就会触发get()和set(),从而使得数据变化可以观测。
谁使用了数据成为依赖
现在,我们知道了数据什么时候发生变化,该去通知视图去更新了。但是,视图那么大,我们到底该通知谁去更新?总不能一个数据变化了,把整个视图全部更新一遍吧。
理想的情况是:视图里谁用到了这个数据、谁依赖了这个数据,就更新谁!
Vue当然是这么做的。
那么,Vue究竟怎么用代码的形式,来描述这个“谁”呢?请看以下代码:
/**
* @param { vm } vue对象实例
* @param { expOrFn } 对象的属性
* @param { cb } 真正包含数据更新逻辑的函数
*/
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn) //parsePath方法把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
//初始化的时候触发数据属性的get方法
this.value = this.get()
}
// 触发数据属性的get方法,访问数据属性即可实现
get () {
// 把Watcher实例保存到Dep类的target属性上
Dep.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
Dep.target = null;
return value
}
// 当update被触发时,此时获取到的数据属性值时已经被修改后的新值
update () {
const oldValue = this.value
this.value = this.get()
// 触发传递给Watcher的更新数据的函数
this.cb.call(this.vm, this.value, oldValue)
}
}
从上述代码可以知道,在创建Watcher实例的过程中会自动的把自己添加到这个数据对应的依赖管理器中,以后这个Watcher实例就代表这个依赖,当数据变化时,我们就通知Watcher实例,由Watcher实例再去通知真正的依赖。
依赖放在哪里集中管理
一个数据可能被多处使用,产生众多依赖,这些依赖如何管理?
简单的做法是,我们给每个数据都建一个依赖数组,谁依赖了这个数据,我们就把谁放入这个依赖数组中,那么当这个数据发生变化的时候,我们就去它对应的依赖数组中,把每个依赖都通知一遍。
更好的做法是,我们为每一个数据都建立一个依赖管理器,把这个数据所有的依赖都管理起来。所以,vue创建了Dep类。
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 删除一个依赖
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
/*依赖收集,当存在Dep.target的时候添加依赖*/
depend () {
if (Dep.target) {
Dep.target.addDep(this) // *Watcher里的addDep方法,添加一个依赖关系到Deps集合中*/
}
}
/*通知所有依赖更新*/
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() //Watcher里面的调度者接口,当依赖发生改变的时候进行回调
}
}
}
/*依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。*/
Dep.target = null
const targetStack = []
/*将watcher实例设置给Dep.target,用以依赖收集。同时将该实例存入target栈中*/
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
/*将观察者实例从target栈中取出并设置给Dep.target*/
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
Object依赖何时收集何时更新
数据变化知道了,依赖是谁也知道了,依赖放哪也明确了,那么何时才能收集依赖,又是何时才能去通知依赖进行更新
function defineReactive (obj,key,val) {
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
const dep = new Dep() //实例化一个依赖管理器,生成一个依赖管理数组dep
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
dep.depend() // 在getter中收集依赖
return val;
},
set(newVal){
if(val === newVal){
return
}
val = newVal;
dep.notify() // 在setter中通知依赖更新
}
})
}
可观测的数据被获取时会触发getter属性,所以我们就可以在getter中收集这个依赖。同样,当这个数据变化时会触发setter属性,那么我们就可以在setter中通知依赖更新。
关于Object侦测的问题
前面介绍了Object类型数据的变化侦测原理,了解了数据的变化是通过getter/setter来追踪的。
但是,也正是由于这种追踪方式,有些语法中即便是数据发生了变化,Vue也追踪不到。例如:
data() {
return {
obj: {}
}
}
methods: {
addKey() { //在obj上面新增name属性
this.obj.name = 'apple'
}
deleteKey() {//在obj上面删除name属性
delete this.obj.name
}
}
在上面代码中,无论是为obj新增name属性,还是删除name属性,Vue都无法侦测到。
Vue是通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性,所以才会导致上述问题。
为了解决这一问题,Vue增加了两个全局API:Vue.set和Vue.delete。关于这两个API的实现原理,我们后续再介绍。
总结
Object通过Observer类将其所有数据包括子数据都转换成getter/setter的形式来追踪变化。
我们在getter中收集依赖,在setter中通知依赖。
收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。
所谓的依赖,其实就是Watcher。只有Watcher触发的getter来会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中,当数据发生变化时,会循环依赖列表,把所有的watcher都通知一遍。
数据与Observer、Dep、watcher之间的运转流程如下:
- 数据 通过
Observer转换成可侦测的对象。 - 当外界通过
Watcher读取数据时,会将Watcher添加到Dep中 - 当数据发生变化时,则会向
Dep中的依赖即Watcher发送通知。 Watcher接收到通知后,会向外界发送通知。外界接收到通知后进行相应的更新。