今天講下vue的響應式資料,也就是mvvm雙向繫結模式,主要的目的是要讓大家瞭解該模式在vue中是如何實現的,所以將以極簡的程式碼進行示例。
我們先假設這樣的一個使用情景:
<div id="app">
<input type="text" v-model="text">
{{text}}
</div>複製程式碼
let vm=new Vue({
el:'app',
data:{
text:'hello world'
}
});複製程式碼
這裡就涉及到了vue的雙向繫結。
接下來我就用一些非常簡單程式碼實現以上功能。
首先,我們得解析vue中的v-model
指令,也就是html中的自定義屬性,以及插入元件中的變數{{text}}
。這兩者都與響應式資料text
有關。
vue2.x後就使用了虛擬dom和diff演算法,但今天的目的是理解雙休繫結的原理,為了方便大家的理解,這裡使用vue1.x用過document Fragment
來代替。document Fragment
是一個節點片段,對它的進行dom操作不會導致頁面的重排和重繪。它就是一個優化dom操作的物件。具體的大家可以參考下MDN教程。
/**
* 節點轉換成節點片段,優化動態改變節點的效能
* @param root - vue的根節點
* @param vm - vue例項
*/
function nodeToFramge(root,vm) {
let df=document.createDocumentFragment();
let node;
//不斷的把vue要管理的dom元素放到document Fragment中。
while(node=root.firstChild){
//優化dom操作效能問題。
df.appendChild(node);
//進行dom的解析,也可以理解成編譯吧
compile(node,vm);
}
return df;
}複製程式碼
如何解析呢,一般就是遍歷dom節點了,然後判斷其中的節點型別,是元素節點就獲取其中的屬性節點,然後再進行遍歷,最後獲取到v-model
屬性,簡單示例:
/**
*編譯模板
* @param node - vue管理的節點
* @param vm - vue例項
*/複製程式碼
function compile(node,vm) {
//節點型別為元素
if(node.nodeType===1){
let attr=node.attributes;
//遍歷解析html的屬性
for(let i=0;i<attr.length;i++){
//v-m的資料響應
if(attr[i].nodeName==='v-model'){
//獲取html屬性的值,也就是響應式資料的鍵名
let name=attr[i].nodeValue;
//初始化輸入控制元件的資料
node.value=vm[name];
//監聽資料的變化,實現v-m的資料響應
node.addEventListener('input',function (e) {
vm[name]=e.target.value;
});
//刪除v-model自定義屬性
node.removeAttribute('v-model');
}
}
}
}複製程式碼
而當遍歷到文字節點時:
//節點為文字型別
else if(node.nodeType===3){
//識別響應式資料的正規表示式
let reg=/\{\{(.*)\}\}/;
//找出響應式資料
if(reg.test(node.nodeValue)){
//從正規表示式的子表示式中獲取響應式資料的鍵名
let name=RegExp.$1.trim();
//建立觀察者
new Watcher(vm,node,name);
}
}複製程式碼
然後我們開始為每個插入dom的中資料實現一個觀察者:
/**
* 觀察者
* @param vm
* @param node
* @param name
* @constructor
*/
function Watcher(vm,node,name) {
//標誌變數。判斷是否要進行觀察者的註冊。
Moniter.target=this;
//要改變的節點
this.node=node;
//響應式資料的鍵名
this.name=name;
//vue例項
this.vm=vm;
//初始化資料和註冊觀察者
this.update();
//註冊完成,取消標誌變數
Moniter.target=null;
}
//更新資料
Watcher.prototype.update=function () {
//第一次呼叫時就是觸發資料的get方法去初始化資料和註冊觀察者。之後時更新資料
this.node.nodeValue=this.vm[this.name];
};複製程式碼
然後資料劫持,註冊觀察者。資料劫持主要用到了Object.defineProperty方法,具體的同學可以看MDN的教程。
/**
* 資料劫持
* @param vm
*/
function defineReactive(vm) {
Object.keys(vm.data).forEach(function (name) {
//儲存未被訪問器屬性覆時,資料屬性的值。
let value=vm.data[name];
//註冊監聽者
let mo=new Moniter();
//資料劫持
Object.defineProperty(vm,name,{
set:function (newValue) {
if(value===newValue) return;
//觸發觀察者實現資料更新
value=newValue;
mo.dispatch();
},
get:function () {
//判斷是否時初始化資料,然後註冊觀察者
if(Moniter.target) mo.addWatcher(Moniter.target);
return value;
}
})
})
}複製程式碼
併為每個響應式的屬性實現一個監聽者:
/**
* 監聽者
* @constructor
*/
function Moniter() {
//儲存觀察者的陣列
this.watchers=[];
}
//觸發觀察者
Moniter.prototype.dispatch=function () {
this.watchers.forEach(function (watcher) {
watcher.update();
})
};
//註冊觀察者
Moniter.prototype.addWatcher=function (target) {
this.watchers.push(target);
}; 複製程式碼
vue的觀察者並不是一個函式,而是一個對像,如watcher物件。每個屬性都有一個監聽者,就是儲存觀察者的陣列。觀察者和監聽者之間又個全域性標準,判斷是否要實現資料監聽。view到model方向的資料變化是js的事件監聽實現的,也算是內建的觀察者模式吧,在編譯模板的時候就已經實現觀察者的註冊,mode到view方向的資料變化是自定義的觀察者模式。在編譯模板中建立觀察者,在為資料建立訪問器屬性時建立堅監聽者,在get方法中註冊觀察者,在set方法中觸發監聽器。
整體來說就是元素提取,模板編譯,事件監聽。
/**
* vue類
* @param options - 配置的資料
* @constructor
*/
function Vue(options) {
//將響應式資料與vue例項關聯
this.data=options.data;
//獲取vue的根節點
let root=document.getElementById(options.el);
//資料劫持
defineReactive(this);
//編譯模板
root.appendChild(nodeToFramge(root,this));
}複製程式碼
這篇文章感覺涉及的東西有點多,而且有點繞,一直想不好該怎麼寫才能讓大家更好的理解,因此,我只好把幾乎每句程式碼都寫上了註釋,希望大家能夠理解並且有所收穫吧。
參考:Vue.js雙向繫結的實現原理(這篇文章寫得很好)