vue響應式資料的實現原理解析

newbeehh發表於2018-07-11

今天講下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響應式資料的實現原理解析


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雙向繫結的實現原理(這篇文章寫得很好)


相關文章