实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
因此接下去我们执行以下4个步骤,实现数据的双向绑定:

1.    实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就拿到最新值并通知订阅者。

2.    实现一个订阅者Watcher,连接ObserverCompile。可以订阅并收到每个属性的变化通知并执行指令绑定的相应函数,从而更新视图。

3.    实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板替换数据,以及绑定相应的更新函数。

4.    mvvm入口函数,整合以上三者。

接下来用流程图+代码的方式讲解

实现Observe

1.实现数据的双向绑定

数据变动  --->  视图更新

视图更新  --->  数据变动 

要想实现当数据变动时视图更新,首先要做的就是如何知道数据变动了,可以通过Object.defineProperty()函数监听data对象里的数据,当数据变动了就会触发set()方法。所以我们需要实现一个数据监听器Observe,来对数据对象中的所有属性进行监听,当某一属性数据发生变化时,拿到最新的数据通知绑定了该属性的订阅器,订阅器再执行相应的数据更新回调函数,从而实现视图的刷新。

function observe(data){
if (typeof data != 'object') {
return ;
}
return new Observe(data);
}

function Observe(data){
this.data = data;
this.walk(data);
}

Observe.prototype = {
walk: function(data){
let _this = this;
for (key in data) {
if (data.hasOwnProperty(key)){
let value = data[key];
if (typeof value == 'object'){
observe(value);
}
_this.defineReactive(data,key,data[key]);
}
}
},
defineReactive: function(data,key,value){
Object.defineProperty(data,key,{
enumerable: true,//可枚举
configurable: false,//不能再define
get: function(){
console.log('你访问了' + key);return value;
},
set: function(newValue){
console.log('你设置了' + key);
if (newValue == value) return;
value = newValue;
observe(newValue);//监听新设置的值
}
})
}
}

 

实现一个订阅器(Dep

要想通知订阅者,首先得要有一个订阅器(统一管理所有的订阅者)。为了方便管理,会为每一个data对象的属性都添加一个订阅器(new Dep)。

订阅器里存着的是订阅者Watcher(后面会讲到),由于订阅者可能会有多个,就需要建立一个数组来维护。一旦数据变化,就会触发订阅器的notify()方法,订阅者就会调用自身的update方法实现视图更新。

function Dep(){
this.subs = [];
}
Dep.prototype = {
addSub: function(sub){this.subs.push(sub);
},
notify: function(){
this.subs.forEach(function(sub) {
sub.update();
})
}
}

每次响应属性的set()函数调用的时候,都会触发订阅器,

Observe.prototype = {
//省略的代码未作更改
defineReactive: function(data,key,value){
let dep = new Dep();//创建一个订阅器,会被闭包在key属性的get/set函数内,因此每个属性对应唯一一个订阅器dep实例
Object.defineProperty(data,key,{
enumerable: true,//可枚举
configurable: false,//不能再define
get: function(){
console.log('你访问了' + key);
return value;
},
set: function(newValue){
console.log('你设置了' + key);
if (newValue == value) return;
value = newValue;
observe(newValue);//监听新设置的值
dep.notify();//通知所有的订阅者
}
})
}
}

 

实现Watcher

那么实例化watcher会进行哪些操作,又会和gettersetter有什么交互呢

 

Observe()函数实现data对象的属性劫持,并在属性值改变时触发订阅器的notify()通知订阅者Watcher,订阅者就会调用自身的update方法实现视图更新。

Compile()函数负责解析模板,初始化页面,并且为每个data属性新增一个监听数据的订阅者(new Watcher

Watcher订阅者作为ObserverCompile之间通信的桥梁,主要做的事情是:

1.在自身实例化时往订阅器(dep)里面添加自己

2.自身必须有一个update方法,此方法用来当监听到值发生变化时,将新的值替换旧的旧的值

3.待属性变动dep.notify()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,其实在数据劫持过后的值进行变化时调用set方法时会调用notify方法

以此来通知所有人数据进行更新了。

//Watcher
function Watcher(vm, exp, cb) {
this.vm = vm;
this.cb = cb;
this.exp = exp;
this.value = this.get();//初始化时将自己添加进订阅器
};

Watcher.prototype = {
update: function(){
this.run();
},
run: function(){
const value = this.vm[this.exp];
//console.log('me:'+value);
if (value != this.value){
this.value = value;
this.cb.call(this.vm,value);
}
},
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm[this.exp] // 访问自己,执行defineProperty里的get函数
Dep.target = null; // 释放自己
return value;
}
}

//这里列出Observe和Dep,方便理解
Observe.prototype = {
defineReactive: function(data,key,value){
let dep = new Dep();
Object.defineProperty(data,key,{
enumerable: true,//可枚举
configurable: false,//不能再define
get: function(){
console.log('你访问了' + key);
//说明这是实例化Watcher时引起的,则添加进订阅器
if (Dep.target){
//console.log('访问了Dep.target');
dep.addSub(Dep.target);
}
return value;
},
})
}
}

Dep.prototype = {
addSub: function(sub){this.subs.push(sub);
},
}

Observe()函数执行时,即为每个属性都添加了一个订阅器dep,而这个dep被闭包在属性的get/set函数内。所以,可以在实例化Watcher时调用this.get()函数访问data.name属性,这会触发defineProperty()函数内的get函数,get方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcher实例就能收到更新通知。

那么Watcher()函数中的get()函数内Dep.taeger = this又有什么特殊的含义呢?如果想实例化Watcher时将相应的Watcher实例添加一次进dep订阅器即可,而不希望在以后每次访问data.name属性时都加入一次dep订阅器。就需要在实例化执行this.get()函数时用Dep.target = this来标识当前Watcher实例,当添加进dep订阅器后设置Dep.target=null