[TOC]
___
#### 一、單項綁定與雙向綁定
> 單項綁定: 即我們將Model(也就是數據) 綁定到view (前端一般為網頁) ,當我們用JavaScript代碼更新Model時,View就會自動更新
> 雙向綁定: 數據模型(Module)和視圖(View)之間的雙向綁定, 也就是用戶在視圖上的修改會自動同步到數據模型中去,同樣的,如果數據模型中的值發生了變化,也會立刻同步到視圖中去。
#### 二、Vue 中實現的雙向數據綁定的思路
vue 中實現是通過 `數據劫持 + 發布訂閱者模式` 來實現雙向綁定的。接下來我們先整體大概來了解一下vue中的實現雙向綁定的整體思路。然后在上代碼。
vue雙向綁定的整體思路可以用一下圖片來展示:

實現`vue` MVVM模式的具體思路如下:
1. 實現Observer 監聽器:
`Observer` 監聽器主要作用是使用 ` Object.defineProperty() `來實現對屬性的劫持并監聽,也就是給vue data對象中的每一個屬性添加 `getter` 和 `setter`, 在屬性觸發get時,添加訂閱者,在觸發set時,通知訂閱者
2. 實現 Dep 消息訂閱器:
Dep 消息訂閱器的作用是存儲所有訂閱者,同時接受Observer 監聽器發出的消息,接受到消息后并對訂閱者做一些處理(是添加訂閱者還是批量通知訂閱者)
3. 實現 Watcher 訂閱者
Watcher 訂閱者是Watcher訂閱者作為Observer和Compile之間通信的橋梁,它主要的做的一些事是
(1)在自身實例化時往屬性訂閱器(dep)里面添加自己;
(2)自身必須有一個update()方法;
(3)待屬性變動dep.notice()通知時,能調用自身的update()方法,并觸發Compile中綁定的回調,并更新視圖。
4. 實現 Compile 解析器:
Compile 解析器的作用主要是解析模板指令,將模板中的變量(如 {{ name }})替換成數據或一些其他指令(如 v-show v-for v-if 等),然后初始化渲染頁面視圖,并將每個指令對應的節點綁定更新函數,添加監聽數據的訂閱者。這里也就是解析模板,并對模板中的每一個數據添加一個數據監聽器對象(Watcher)。
5. 將以上幾種方法整合,暴露出接口,生成可以實例化的入口函數(如 new Vue())
其實上邊第一步就是vue所做的數據劫持,第二步和第三步就是實現發布訂閱者模式,第四部是解析vue語法模板用的。
#### 二、Vue 中雙向數據綁定的具體實現
1. **Observer 監聽器的實現**
```JavaScript
function defineReactive(data,key,val){
observe(val) // 遞歸遍歷對象
let dep = new Dep() // 創建Dep 實例
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
set(newVal){
if(val === newVal){
return
}
val = newVal
dep.notify(); // 通知Dep 發布消息
},
get(){
if(Dep.target){ // 判斷觀察者Watcher 是否存在
dep.addSub(Dep.target) // 添加觀察者
}
return val
}
})
}
function observe(data){
if(!data || typeof data !== "object"){ // 判斷傳入的data是否為對象
return
}
Object.keys(data).forEach(key=>{ // 遍歷data對象中一級屬性
defineReactive(data,key,data[key])
})
}
```
2. **Dep 消息訂閱器 實現**
```JavaScript
function Dep(){
this.subs = []; // 存儲Watcher觀察者
}
Dep.prototype = {
addSub(sub){ // 添加觀察者
this.subs.push(sub)
},
notify(){ // 通知相關的所有訂閱者
this.subs.forEach(watch=>{
watch.updata();
})
}
}
```
`Dep` 主要是收集訂閱者的,并可以監聽器的消息,來完成添加訂閱者或是通知所有訂閱者更新數據。
3. **Watcher 訂閱者的實現**
```JavaScript
function Watcher(vm,key,callback){
this.callback = callback; // 存儲觀察者的回調函數
this.vm = vm; // 存儲入口函數的實例this
this.key = key;
this.value = this.get() // 儲存所觀察對象的初始值
}
Watcher.prototype = {
updata(){
this.run();
},
run(){
let value = this.vm.data[this.key] //獲取變化后的屬性值
let oldVal = this.value // 獲取舊屬性值
if(value !== oldVal){
this.callback(value) // 調用回調函數并傳入變化后的值更新視圖
}
},
get(){ // 該方法可以在初始化時,將Watcher 觀察者初始化并添加到訂閱其中
Dep.target = this;
// 獲取初始值時(這是實際是獲取的vue實例中data中的初始值),會觸發Observer監聽器中的get方法
let value = this.vm.data[this.key]
Dep.target = null;// 這里釋放了Wahter 防止Watcher二次添加到訂閱器中
return value;
}
}
```
關于Watcher 訂閱者我們需要注意的是,除了發布的消息并及時更新視圖外,他還有一個get方法,該方法的作用就是在初始化時將每一個訂閱者添加到訂閱器中收集起來。而觸發訂閱器添加(也就是觸發Dep的addSub方法)觀察者的方法是需要觸發監聽其中的get方法。也就是需要獲取相應的屬性值(對應上邊 `let value = this.vm.data[this.key]` ),這時就會觸發get方法,并在訂閱者初始化時將自己添加到Dep 訂閱器中,添加完成后通過 `Dep.target = null` 將Dep.target釋放,下次再執行該屬性下的get方法時,不會重復添該屬性對應的加觀察者。同時Watcher還是Compile編譯器和Observer 監聽者之間的橋梁,通過Watcher可以把監聽后的屬性和Compil編譯后的模板中的數據綁定起來,并在屬性值變化時,更新模板中對應的數據。
4. **Compile 編譯器的實現**
```JavaScript
function Compile(el,vm){
this.vm = vm; // 存儲vue實例this
this.el = el // 儲存dom 根節點,vue實際上并非這么簡單,還做的一些其他判斷,判斷是否是dom節點或是字符串
if(el){
this.$frag = this.nodeToFragment(el) // 將DOM節點轉換為fragment 虛擬節點
this.compileElement(this.$frag) // 編譯解析模板
}
el.appendChild(this.$frag) // 將編譯后的虛擬DOM 添加到根節點
return this.$frag;
}
Compile.prototype = {
nodeToFragment(node){ //建一個fragment片段,將需要解析的dom節點存入fragment片段里再進行處理(提高性能)
let fragment = document.createDocumentFragment();
let child = node.firstChild
while(child){ // 循環將DOM節點添加到fragment 虛擬節點中
fragment.appendChild(child)
child = node.firstChild
}
return fragment;
},
// 編譯解析模板,這里只解析了{{}}這樣的模板,在該方法中還可以解析v-show v-if v-for v-module 等等一些模板
compileElement(elFrag){
let reg = /\{\{(.*)\}\}/;// 解析{{}} 這里只是簡單的解析{{name}},并沒有處理包含js簡單代碼的情況
[].slice.call(elFrag.childNodes).forEach((node)=>{
if(node.nodeType === 3){ // 節點類型為文本text類型
if(reg.test(node.nodeValue)){
this.compileText(node,reg.exec(node.nodeValue)[1])
}
}
})
},
compileText(node,exp){ // 將數據初始化,并生成訂閱器實例(訂閱者),并更新視圖
this.updateView(node,this.vm[exp])
new Watcher(this.vm,exp,(value)=>{ // 當數據變化時將會執行回調函數,并更新視圖
this.updateView(node,value)
})
},
updateView(node,value){ // 更新視圖
node.textContent = typeof value == 'undefined' ? '' : value;
}
}
```
需要說明的是,這里的 `Complie`編譯器只是簡單的實現解析{{}}的模板,并且只解析了掛載點下的子節點,如果子節點中還含有{{}},這里不能實現,這里重點說明vue數據的雙向綁定原理。
5. **入口文件的實現**
```JavaScript
function SelfVue (obj){ // 入口文件 類似 new Vue()
this.data = obj.data
let el = document.querySelector(obj.el)
this.$el = el
// 這里我們目的是將data下的數據綁定到SelfVue實例上,通過實例可以直接訪問
Object.keys(obj.data).forEach(key=>{
this.proxyDataKeys(key)
})
observe(obj.data) // 給data中的所有屬性添加get和set
new Compile(el,this) //編譯初始模塊
return this
}
/*
將SelfVue實例data屬性上的數據綁定到SelfVue實例上
例如:
let selfVue = new SelfVue({
el:"#root",
data:{
name:'King',
},
})
我們訪問name屬性,需要這樣訪問 selfVue.data.name
將SelfVue實例data屬性上的數據綁定到SelfVue實例上后就可以這樣訪問 selfVue.name
*/
SelfVue.prototype = {
proxyDataKeys(key){
let self = this;
Object.defineProperty(this,key,{
enumerable:true,
configurable:true,
get(){
return self.data[key]
},
set(newVal){
self.data[key] = newVal
}
})
}
}
```
6. **[所有代碼實例](https://github.com/webxiaoma/vue-demos/tree/master/two_way_binding)**
#### 推薦文章
1. [剖析vue實現原理](https://github.com/DMQ/mvvm#_2)