如何追踪变化
当你把一个普通的JavaScript对象传入Vue实例作为data的选项,Vue将会遍历该对象的所有属性,并且使用Object.defineProperty()方法将对象上的属性全部变成访问器属性getter/setter,这样就实现了对对象的 响应式化。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
一 对象的响应式化
首先定义一个 defineReactive 函数,这个方法通过 Object.defineProperty 来实现对对象的「响应式」化,入参是一个 obj(需要绑定的对象)、key(obj的某一个属性),val(具体的值)。经过 defineReactive 函数处理之后,obj的某一个属性在被读取时就会触发 getter 方法,在属性被写入时就会触发 setter 方法。
function defineReactive(obj,key,value) {
Object.defineProperty(obj,key,{
configurable: true,
enumerable: true,
get: function(){
return value
},
set: function(newVal){
if(value === newVal) {
return
}
value = newVal
}
})
}
以上方法只能对obj的一个属性进行响应式化,我们需要将对象中的所有属性都进行响应式化,所以封装一个Observer 类,将对象的所有属性都转换成 getter/setter 的形式
class Observer{
constructor(value) {
this.value = value
if(!Array.isArray(value)) {
this.walk(value)
}
}
/*
* walk会将对象的每一个属性都转换成getter/setter的形式来将属性响应式化
* 这个方法只有在数据类型为Object时调用(即非数组),因为对象的响应式化与
* 数组不同,这里只讨论非组数的情况
*/
walk(obj){
Object.keys(obj).forEach( (key) => {
defineReactive(obj,key,obj[key])
})
}
}
此时函数 defineReactive 需要作出一些变化,来递归对象的所有子属性,这样就可以将对象的所有属性都响应式化。
function defineReactive(obj,key,value) {
/* 递归子属性,变成响应式的*/
if(typeof value === 'object'){
new Observer(value)
}
Object.defineProperty(obj,key,{
configurable: true,
enumerable: true,
get: function(){
return val
},
set: function(newVal){
if(val === newVal) {
return
}
val = newVal
}
})
}
二 依赖收集
为什么要收集依赖? 我们之所以要观测数据,是为了当数据发生了变化,可以通知那些曾经使用了该数据的地方,以便数据发送变化时,视图能够得到更新。
<template>
<div>
{{ title }}
</div>
</template>
以上模板中使用了title,故当title的值发生变化时,需要通知使用了它的地方,依赖收集会让 title 属性知道有个地方在依赖我的数据,我发生变化时需要通知它。
在如何追踪变化那里已经讲过,每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖;之后当依赖项的 setter 触发时,会通知 watcher,watcher 又会通知外界,从而使它关联的组件重新渲染。
由于组件在渲染的过程中会去读取相应的属性,即会触发getter,那么只要在getter中收集依赖就可以了。而当属性的值发生变化时会触发setter,所以只要在setter时触发依赖,以更新对应的视图即可。
那么依赖收集在哪里呢? 我们将依赖收集的代码封装成一个Dep类,这个类用于管理依赖。使用这个类,可以添加依赖,删除依赖或者向依赖发送通知等。代码如下:
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub)
}
notify() {
// stabilize the subscriber list first
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)
}
}
}
此时将 defineReactive 改成如下:
function defineReactive(obj,key,value) {
let dep = new Dep()
/* 递归子属性,变成响应式化*/
if(typeof value === 'object'){
new Observer(value)
}
Object.defineProperty(obj,key,{
configurable: true,
enumerable: true,
get: function(){
/* 收集依赖,Dep.target是Watcher实例*/
dep.addSub(Dep.target)
return val
},
set: function(newVal){
if(val === newVal) {
return
}
val = newVal
/* 通知依赖以更新视图*/
dep.notify()
}
})
}
可以看到将依赖收集到了Dep中的subs属性。
上面说了那么多,我们仍然不知道依赖是什么😈? 也就是说我们需要收集谁,收集谁,换句话说,就是当属性发生变化时,需要通知谁。
当然通知的是属性所用到的地方咯,而使用这个属性的地方有很多,而且类型还不一样,极有可能是模板,也有可能是用户写的一个watch,这时候需要抽象出一个能集中处理这些情况的类。然后在依赖收集阶段将这个类的实例收集进来,通知也只通知他一个,它在负责通知其他地方,类似于一个中介的角色,称它为 Watcher。
综上我们收集的是Watcher实例,当属性发送变化时通知的也是Watcher实例,它再负责通知其它地方以更新视图。在如何追踪变化那里说过,每个组件实例对应一个Watcher实例。
class Watcher{
constructor(){
/* new一个Watcher对象时会将该实例赋值给Dep.target*/
Dep.target = this
}
/* 更新视图的方法 */
update(){
console.log(`视图更新了!`)
}
}
最后封装一个Vue类:
class Vue{
constructor(options){
this.$data = options.data
/* 使数据对象变成响应式的*/
new Observer(this.$data)
/* 新建一个Watcher观察者对象,这时候Dep.target会指向Watcher实例 */
new Watcher()
/* 在这里模拟render function的过程,为了触发name属性的getter函数以进行依赖收集 */
console.log(`开始render`,this.$data.name)
}
}
这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行化
let app = new Vue({
data: {
name: 'jack',
address: {
addOne: `china`,
addTwo: `Japan`
}
}
})
总结
defineReactive 函数中的Dep实例用来收集依赖,即Watcher实例。在对象被读时,会触发getter,此时将Watcher实例对象(存放在Dep.target)收集到Dep类中的subs中去。以后当对象的属性被重写之后,就会触发setter,调用Dep类上的notify方法通知所有的Watcher实例并触发该实例上的update方法以更新视图。