Vue.js雙向繫結的實現原理
vuejs是如何實現雙向資料繫結的呢?
Vue.js 最核心的功能有兩個,一是響應式的資料繫結系統,二是元件系統。本文僅探究雙向繫結是怎樣實現的。先講涉及的知識點,再用簡化得不能再簡化的程式碼實現一個簡單的 hello world 示例。
一、訪問器屬性
訪問器屬性是物件中的一種特殊屬性,它不能直接在物件中設定,而必須通過 defineProperty() 方法單獨定義。
var obj = { };
// 為obj定義一個名為 hello 的訪問器屬性
Object.defineProperty(obj, "hello", {
get: function () {return sth},
set: function (val) {/* do sth */}
})
obj.hello // 可以像普通屬性一樣讀取訪問器屬性
訪問器屬性的"值"比較特殊,讀取或設定訪問器屬性的值,實際上是呼叫其內部特性:get和set函式。
obj.hello // 讀取屬性,就是呼叫get函式並返回get函式的返回值
obj.hello = "abc" // 為屬性賦值,就是呼叫set函式,賦值其實是傳參
get 和 set 方法內部的 this 都指向 obj,這意味著 get 和 set 函式可以操作物件內部的值。另外,訪問器屬性的會"覆蓋"同名的普通屬性,因為訪問器屬性會被優先訪問,與其同名的普通屬性則會被忽略。
二、極簡雙向繫結的實現
此例實現的效果是:隨文字框輸入文字的變化,span 中會同步顯示相同的文字內容;在js或控制檯顯式的修改 obj.hello 的值,檢視會相應更新。這樣就實現了 model => view 以及 view => model 的雙向繫結。
以上就是 Vue 實現雙向繫結的基本原理。
三、分解任務
上述示例僅僅是為了說明原理。我們最終要實現的是:
首先將該任務分成幾個子任務:
1、輸入框以及文字節點與 data 中的資料繫結
2、輸入框內容變化時,data 中的資料同步變化。即 view => model 的變化。
3、data 中的資料變化時,文字節點的內容同步變化。即 model => view 的變化。
要實現任務一,需要對 DOM 進行編譯,這裡有一個知識點:DocumentFragment。
四、DocumentFragment
DocumentFragment(文件片段)可以看作節點容器,它可以包含多個子節點,當我們將它插入到 DOM 中時,只有它的子節點會插入目標節點,所以把它看作一組節點的容器。使用 DocumentFragment 處理節點,速度和效能遠遠優於直接操作 DOM。Vue 進行編譯時,就是將掛載目標的所有子節點劫持(真的是劫持,通過 append 方法,DOM 中的節點會被自動刪除)到 DocumentFragment 中,經過一番處理後,再將 DocumentFragment 整體返回插入掛載目標。
# 勘誤:flag.append() 應為 flag.appendChild()。下同。在 Chrome 中用 append() 竟然正常,沒報錯。
五、資料初始化繫結
以上程式碼實現了任務一,我們可以看到,hello world已經呈現在輸入框和文字節點中。
六、響應式的資料繫結
再來看任務二的實現思路:當我們在輸入框輸入資料的時候,首先觸發 input 事件(或者 keyup、change 事件),在相應的事件處理程式中,我們獲取輸入框的 value 並賦值給 vm 例項的 text 屬性。我們會利用 defineProperty 將 data 中的 text 設定為 vm 的訪問器屬性,因此給 vm.text 賦值,就會觸發 set 方法。在 set 方法中主要做兩件事,第一是更新屬性的值,第二留到任務三再說。
任務二也就完成了,text 屬性值會與輸入框的內容同步變化:
七、訂閱/釋出模式(subscribe&publish)
text 屬性變化了,set 方法觸發了,但是文字節點的內容沒有變化。如何讓同樣繫結到 text 的文字節點也同步變化呢?這裡又有一個知識點:訂閱釋出模式。
訂閱釋出模式(又稱觀察者模式)定義了一種一對多的關係,讓多個觀察者同時監聽某一個主題物件,這個主題物件的狀態發生改變時就會通知所有觀察者物件。
釋出者發出通知 => 主題物件收到通知並推送給訂閱者 => 訂閱者執行相應操作
之前提到的,當 set 方法觸發後做的第二件事就是作為釋出者發出通知:“我是屬性 text,我變了”。文字節點則是作為訂閱者,在收到訊息後執行相應的更新操作。
八、雙向繫結的實現
回顧一下,每當 new 一個 Vue,主要做了兩件事:第一個是監聽資料:observe(data),第二個是編譯 HTML:nodeToFragement(id)。
在監聽資料的過程中,會為 data 中的每一個屬性生成一個主題物件 dep。
在編譯 HTML 的過程中,會為每個與資料繫結相關的節點生成一個訂閱者 watcher,watcher 會將自己新增到相應屬性的 dep 中。
我們已經實現:修改輸入框內容 => 在事件回撥函式中修改屬性值 => 觸發屬性的 set 方法。
接下來我們要實現的是:發出通知 dep.notify() => 觸發訂閱者的 update 方法 => 更新檢視。
這裡的關鍵邏輯是:如何將 watcher 新增到關聯屬性的 dep 中。
在編譯 HTML 過程中,為每個與 data 關聯的節點生成一個 Watcher。Watcher 函式中發生了什麼呢?
首先,將自己賦給了一個全域性變數 Dep.target;
其次,執行了 update 方法,進而執行了 get 方法,get 的方法讀取了 vm 的訪問器屬性,從而觸發了訪問器屬性的 get 方法,get 方法中將該 watcher 新增到了對應訪問器屬性的 dep 中;
再次,獲取屬性的值,然後更新檢視。
最後,將 Dep.target 設為空。因為它是全域性變數,也是 watcher 與 dep 關聯的唯一橋樑,任何時刻都必須保證 Dep.target 只有一個值。
至此,hello world 雙向繫結就基本實現了。文字內容會隨輸入框內容同步變化,在控制器中修改 vm.text 的值,會同步反映到文字內容中。