成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專欄INFORMATION COLUMN

模擬 Vue 手寫一個 MVVM

sf_wangchong / 3435人閱讀

摘要:我們模擬這種模式的時候也構(gòu)建一個類,名字就叫,在使用時同框架類似,需要通過指令創(chuàng)建的實例并傳入。文本節(jié)點的內(nèi)容有可能存在,正則匹配默認是貪婪的,為了防止第一個和最后一個進行匹配,所以在正則表達式中應(yīng)使用非貪婪匹配。


閱讀原文


MVVM 的前世今生

MVVM 設(shè)計模式,是由 MVC(最早來源于后端)、MVP 等設(shè)計模式進化而來,M - 數(shù)據(jù)模型(Model),VM - 視圖模型(ViewModel),V - 視圖層(View)。

在 MVC 模式中,除了 Model 和 View 層以外,其他所有的邏輯都在 Controller 中,Controller 負責顯示頁面、響應(yīng)用戶操作、網(wǎng)絡(luò)請求及與 Model 的交互,隨著業(yè)務(wù)的增加和產(chǎn)品的迭代,Controller 中的處理邏輯越來越多、越來越復(fù)雜,難以維護。為了更好的管理代碼,為了更方便的擴展業(yè)務(wù),必須要為 Controller “瘦身”,需要更清晰的將用戶界面(UI)開發(fā)從應(yīng)用程序的業(yè)務(wù)邏輯與行為中分離,MVVM 為此而生。

很多 MVVM 的實現(xiàn)都是通過數(shù)據(jù)綁定來將 View 的邏輯從其他層分離,可以用下圖來簡略的表示:

使用 MVVM 設(shè)計模式的前端框架很多,其中漸進式框架 Vue 是典型的代表,并在開發(fā)使用中深得廣大前端開發(fā)者的青睞,我們這篇就根據(jù) Vue 對于 MVVM 的實現(xiàn)方式來簡單模擬一版 MVVM 庫。


MVVM 的流程分析

在 Vue 的 MVVM 設(shè)計中,我們主要針對 Compile(模板編譯)、Observer(數(shù)據(jù)劫持)、Watcher(數(shù)據(jù)監(jiān)聽)和 Dep(發(fā)布訂閱)幾個部分來實現(xiàn),核心邏輯流程可參照下圖:

類似這種 “造輪子” 的代碼毋庸置疑一定是通過面向?qū)ο缶幊虂韺崿F(xiàn)的,并嚴格遵循開放封閉原則,由于 ES5 的面向?qū)ο缶幊瘫容^繁瑣,所以,在接下來的代碼中統(tǒng)一使用 ES6 的 class 來實現(xiàn)。


MVVM 類的實現(xiàn)

在 Vue 中,對外只暴露了一個名為 Vue 的構(gòu)造函數(shù),在使用的時候 new 一個 Vue 實例,然后傳入了一個 options 參數(shù),類型為一個對象,包括當前 Vue 實例的作用域 el、模板綁定的數(shù)據(jù) data 等等。

我們模擬這種 MVVM 模式的時候也構(gòu)建一個類,名字就叫 MVVM,在使用時同 Vue 框架類似,需要通過 new 指令創(chuàng)建 MVVM 的實例并傳入 options。

// 文件:MVVM.js
class MVVM {
    constructor(options) {
        // 先把 el 和 data 掛在 MVVM 實例上
        this.$el = options.el;
        this.$data = options.data;

        // 如果有要編譯的模板就開始編譯
        if (this.$el) {
            // 數(shù)據(jù)劫持,就是把對象所有的屬性添加 get 和 set
            new Observer(this.$data);

            // 將數(shù)據(jù)代理到實例上
            this.proxyData(this.$data);

            // 用數(shù)據(jù)和元素進行編譯
            new Compile(this.el, this);
        }
    }
    proxyData(data) { // 代理數(shù)據(jù)的方法
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                get() {
                    return data[key];
                }
                set(newVal) {
                    data[key] = newVal;
                }
            });
        });
    }
}

通過上面代碼,我們可以看出,在我們 new 一個 MVVM 的時候,在參數(shù) options 中傳入了一個 Dom 的根元素節(jié)點和數(shù)據(jù) data 并掛在了當前的 MVVM 實例上。

當存在根節(jié)點的時候,通過 Observer 類對 data 數(shù)據(jù)進行了劫持,并通過 MVVM 實例的方法 proxyDatadata 中的數(shù)據(jù)掛在當前 MVVM 實例上,同樣對數(shù)據(jù)進行了劫持,是因為我們在獲取和修改數(shù)據(jù)的時候可以直接通過 thisthis.$data,在 Vue 中實現(xiàn)數(shù)據(jù)劫持的核心方法是 Object.defineProperty,我們也使用這個方式通過添加 gettersetter 來實現(xiàn)數(shù)據(jù)劫持。

最后使用 Compile 類對模板和綁定的數(shù)據(jù)進行了解析和編譯,并渲染在根節(jié)點上,之所以數(shù)據(jù)劫持和模板解析都使用類的方式實現(xiàn),是因為代碼方便維護和擴展,其實不難看出,MVVM 類其實作為了 Compile 類和 Observer 類的一個橋梁。


模板編譯 Compile 類的實現(xiàn)

Compile 類在創(chuàng)建實例的時候需要傳入兩個參數(shù),第一個參數(shù)是當前 MVVM 實例作用的根節(jié)點,第二個參數(shù)就是 MVVM 實例,之所以傳入 MVVM 的實例是為了更方便的獲取 MVVM 實例上的屬性。

Compile 類中,我們會盡量的把一些公共的邏輯抽取出來進行最大限度的復(fù)用,避免冗余代碼,提高維護性和擴展性,我們把 Compile 類抽取出的實例方法主要分為兩大類,輔助方法和核心方法,在代碼中用注釋標明。

1、解析根節(jié)點內(nèi)的 Dom 結(jié)構(gòu)
// 文件:Compile.js
class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;

        // 如過傳入的根元素存在,才開始編譯
        if (this.el) {
            // 1、把這些真實的 Dom 移動到內(nèi)存中,即 fragment(文檔碎片)
            let fragment = this.node2fragment(this.el);
        }
    }

    /* 輔助方法 */
    // 判斷是否是元素節(jié)點
    isElementNode(node) {
        return node.nodeType === 1;
    }

    /* 核心方法 */
    // 將根節(jié)點轉(zhuǎn)移至文檔碎片
    node2fragment(el) {
        // 創(chuàng)建文檔碎片
        let fragment = document.createDocumentFragment();
        // 第一個子節(jié)點
        let firstChild;

        // 循環(huán)取出根節(jié)點中的節(jié)點并放入文檔碎片中
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
}

上面編譯模板的過程中,前提條件是必須存在根元素節(jié)點,傳入的根元素節(jié)點允許是一個真實的 Dom 元素,也可以是一個選擇器,所以我們創(chuàng)建了輔助方法 isElementNode 來幫我們判斷傳入的元素是否是 Dom,如果是就直接使用,是選擇器就獲取這個 Dom,最終將這個根節(jié)點存入 this.el 屬性中。

解析模板的過程中為了性能,我們應(yīng)取出根節(jié)點內(nèi)的子節(jié)點存放在文檔碎片中(內(nèi)存),需要注意的是將一個 Dom 節(jié)點內(nèi)的子節(jié)點存入文檔碎片的過程中,會在原來的 Dom 容器中刪除這個節(jié)點,所以在遍歷根節(jié)點的子節(jié)點時,永遠是將第一個節(jié)點取出存入文檔碎片,直到節(jié)點不存在為止。

2、編譯文檔碎片中的結(jié)構(gòu)

在 Vue 中的模板編譯的主要就是兩部分,也是瀏覽器無法解析的部分,元素節(jié)點中的指令和文本節(jié)點中的 Mustache 語法(雙大括號)。

// 文件:Compile.js —— 完善
class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;

        // 如過傳入的根元素存在,才開始編譯
        if (this.el) {
            // 1、把這些真實的 Dom 移動到內(nèi)存中,即 fragment(文檔碎片)
            let fragment = this.node2fragment(this.el);

            // ********** 以下為新增代碼 **********
            // 2、將模板中的指令中的變量和 {{}} 中的變量替換成真實的數(shù)據(jù)
            this.compile(fragment);

            // 3、把編譯好的 fragment 再塞回頁面中
            this.el.appendChild(fragment);
            // ********** 以上為新增代碼 **********
        }
    }

    /* 輔助方法 */
    // 判斷是否是元素節(jié)點
    isElementNode(node) {
        return node.nodeType === 1;
    }

    // ********** 以下為新增代碼 **********
    // 判斷屬性是否為指令
    isDirective(name) {
        return name.includes("v-");
    }
    // ********** 以上為新增代碼 **********

    /* 核心方法 */
    // 將根節(jié)點轉(zhuǎn)移至文檔碎片
    node2fragment(el) {
        // 創(chuàng)建文檔碎片
        let fragment = document.createDocumentFragment();
        // 第一個子節(jié)點
        let firstChild;

        // 循環(huán)取出根節(jié)點中的節(jié)點并放入文檔碎片中
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment;
    }

    // ********** 以下為新增代碼 **********
    // 解析文檔碎片
    compile(fragment) {
        // 當前父節(jié)點節(jié)點的子節(jié)點,包含文本節(jié)點,類數(shù)組對象
        let childNodes = fragment.childNodes;

        // 轉(zhuǎn)換成數(shù)組并循環(huán)判斷每一個節(jié)點的類型
        Array.from(childNodes).forEach(node => {
            if (this.isElementNode(node)) { // 是元素節(jié)點
                // 遞歸編譯子節(jié)點
                this.compile(node);

                // 編譯元素節(jié)點的方法
                this.compileElement(node);
            } else { // 是文本節(jié)點
                // 編譯文本節(jié)點的方法
                this.compileText(node);
            }
        });
    }
    // 編譯元素
    compileElement(node) {
        // 取出當前節(jié)點的屬性,類數(shù)組
        let attrs = node.attributes;
        Array.form(attrs).forEach(attr => {
            // 獲取屬性名,判斷屬性是否為指令,即含 v-
            let attrName = attr.name;

            if (this.isDirective(attrName)) {
                // 如果是指令,取到該屬性值得變量在 data 中對應(yīng)得值,替換到節(jié)點中
                let exp = attr.value;

                // 取出方法名
                let [, type] = attrName.split("-");

                // 調(diào)用指令對應(yīng)得方法
                CompileUtil[type](node, this.vm, exp);
            }
        });

    }
    // 編譯文本
    compileText(node) {
        // 獲取文本節(jié)點的內(nèi)容
        let exp = node.contentText;

        // 創(chuàng)建匹配 {{}} 的正則表達式
        let reg = /{{([^}+])}}/g;

        // 如果存在 {{}} 則使用 text 指令的方法
        if (reg.test(exp)) {
            CompileUtil["text"](node, this.vm, exp);
        }
    }
    // ********** 以上為新增代碼 **********
}

上面代碼新增內(nèi)容得主要邏輯就是做了兩件事:

調(diào)用 compile 方法對 fragment 文檔碎片進行編譯,即替換內(nèi)部指令和 Mustache 語法中變量對應(yīng)的值;

將編譯好的 fragment 文檔碎片塞回根節(jié)點。

在第一個步驟當中邏輯是比較繁瑣的,首先在 compile 方法中獲取所有的子節(jié)點,循環(huán)進行編譯,如果是元素節(jié)點需要遞歸 compile,傳入當前元素節(jié)點。在這個過程當中抽取出了兩個方法,compileElementcompileText 用來對元素節(jié)點的屬性和文本節(jié)點進行處理。

compileElement 中的核心邏輯就是處理指令,取出元素節(jié)點所有的屬性判斷是否是指令,是指令則調(diào)用指令對應(yīng)的方法。compileText 中的核心邏輯就是取出文本的內(nèi)容通過正則表達式匹配出被 Mustache 語法的 “{{ }}” 包裹的內(nèi)容,并調(diào)用處理文本的 text 方法。

文本節(jié)點的內(nèi)容有可能存在 “{{ }} {{ }} {{ }}”,正則匹配默認是貪婪的,為了防止第一個 “{” 和最后一個 “}” 進行匹配,所以在正則表達式中應(yīng)使用非貪婪匹配。

在調(diào)用指令的方法時都是調(diào)用的 CompileUtil 下對應(yīng)的方法,我們之所以多帶帶把這些指令對應(yīng)的方法抽離出來存儲在 CompileUtil 對象下的目的是為了解耦,因為后面其他的類還要使用。

3、CompileUtil 對象中指令方法的實現(xiàn)

CompileUtil 中存儲著所有的指令方法及指令對應(yīng)的更新方法,由于 Vue 的指令很多,我們這里只實現(xiàn)比較典型的 v-model 和 “{{ }}” 對應(yīng)的方法,考慮到后續(xù)更新的情況,我們統(tǒng)一把設(shè)置值到 Dom 中的邏輯抽取出對應(yīng)上面兩種情況的方法,存放到 CompileUtilupdater 對象中。

// 文件:CompileUtil.js
CompileUtil = {};

// 更新節(jié)點數(shù)據(jù)的方法
CompileUti.updater = {
    // 文本更新
    textUpdater(node, value) {
        node.textContent = value;
    },
    // 輸入框更新
    modelUpdater(node, value) {
        node.value = value;
    }
};

這部分的整個思路就是在 Compile 編譯模板后處理 v-model 和 “{{ }}” 時,其實都是用 data 中的數(shù)據(jù)替換掉 fragment 文檔碎片中對應(yīng)的節(jié)點中的變量。因此會經(jīng)常性的獲取 data 中的值,在更新節(jié)點時又會重新設(shè)置 data 中的值,所以我們抽離出了三個方法 getValgetTextValsetVal 掛在了 CompileUtil 對象下。

// 文件:CompileUtil.js —— 取值方法
// 獲取 data 值的方法
CompileUtil.getVal = function (vm, exp) {
    // 將匹配的值用 . 分割開,如 vm.data.a.b
    exp = exp.split(".");

    // 歸并取值
    return exp.reduce((prev, next) => {
        return prev[next];
    }, vm.$data);
};

// 獲取文本 {{}} 中變量在 data 對應(yīng)的值
CompileUtil.getTextVal = function (vm, exp) {
    // 使用正則匹配出 {{ }} 間的變量名,再調(diào)用 getVal 獲取值
    return exp.replace(/{{([^}]+)}}/g, (...args) => {
        return this.getVal(vm, args[1]);
    });
};

// 設(shè)置 data 值的方法
CompileUtil.setVal = function (vm, exp, newVal) {
    exp = exp.split(".");
    return exp.reduce((prev, next, currentIndex) => {
        // 如果當前歸并的為數(shù)組的最后一項,則將新值設(shè)置到該屬性
        if(currentIndex === exp.length - 1) {
            return prev[next] = newVal
        }

        // 繼續(xù)歸并
        return prev[next];
    }, vm.$data);
}

獲取和設(shè)置 data 的值兩個方法 getValsetVal 思路相似,由于獲取的變量層級不定,可能是 data.a,也可能是 data.obj.a.b,所以都是使用歸并的思路,借用 reduce 方法實現(xiàn)的,區(qū)別在于 setVal 方法在歸并過程中需要判斷是不是歸并到最后一級,如果是則設(shè)置新值,而 getTextVal 就是在 getVal 外包了一層處理 “{{ }}” 的邏輯。

在這些準備工作就緒以后就可以實現(xiàn)我們的主邏輯,即對 Compile 類中解析的文本節(jié)點和元素節(jié)點指令中的變量用 data 值進行替換,還記得前面說針對 v-model 和 “{{ }}” 進行處理,因此設(shè)計了 modeltext 兩個核心方法。

CompileUtil.model 方法的實現(xiàn):

// 文件:CompileUtil.js —— model 方法
// 處理 v-model 指令的方法
CompileUtil.model = function (node, vm, exp) {
    // 獲取賦值的方法
    let updateFn = this.updater["modelUpdater"];

    // 獲取 data 中對應(yīng)的變量的值
    let value = this.getVal(vm, exp);

    // 添加觀察者,作用與 text 方法相同
    new Watcher(vm, exp, newValue => {
        updateFn && updateFn(node, newValue);
    });

    // v-model 雙向數(shù)據(jù)綁定,對 input 添加事件監(jiān)聽
    node.addEventListener("input", e => {
        // 獲取輸入的新值
        let newValue = e.target.value;

        // 更新到節(jié)點
        this.setVal(vm, exp, newValue);
    });

    // 第一次設(shè)置值
    updateFn && updateFn(vm, value);
};

CompileUtil.text 方法的實現(xiàn):

// 文件:CompileUtil.js —— text 方法
// 處理文本節(jié)點 {{}} 的方法
CompileUtil.text = function (node, vm, exp) {
    // 獲取賦值的方法
    let updateFn = this.updater["textUpdater"];

    // 獲取 data 中對應(yīng)的變量的值
    let value = this.getTextVal(vm, exp);

    // 通過正則替換,將取到數(shù)據(jù)中的值替換掉 {{ }}
    exp.replace(/{{([^}]+)}}/g, (...args) => {
        // 解析時遇到了模板中需要替換為數(shù)據(jù)值的變量時,應(yīng)該添加一個觀察者
        // 當變量重新賦值時,調(diào)用更新值節(jié)點到 Dom 的方法
        new Watcher(vm, arg[1], newValue => {
            // 如果數(shù)據(jù)發(fā)生變化,重新獲取新值
            updateFn && updateFn(node, newValue);
        });
    });

    // 第一次設(shè)置值
    updateFn && updateFn(vm, value);
};

上面兩個方法邏輯相似,都獲取了各自的 updater 中的方法,對值進行設(shè)置,并且在設(shè)置的同時為了后續(xù) data 中的數(shù)據(jù)修改,視圖的更新,創(chuàng)建了 Watcher 的實例,并在內(nèi)部用新值重新更新節(jié)點,不同的是 Vue 的 v-model 指令在表單中實現(xiàn)了雙向數(shù)據(jù)綁定,只要表單元素的 value 值發(fā)生變化,就需要將新值更新到 data 中,并響應(yīng)到頁面上。

所以我們的實現(xiàn)方式是給這個綁定了 v-model 的表單元素監(jiān)聽了 input 事件,并在事件中實時的將新的 value 值更新到 data 中,至于 data 中的改變后響應(yīng)到頁面中需要另外三個類 WatcherObserverDep 共同實現(xiàn),我們下面就來實現(xiàn) Watcher 類。


觀察者 Watcher 類的實現(xiàn)

CompileUtil 對象的方法中創(chuàng)建 Watcher 實例的時候傳入了三個參數(shù),即 MVVM 的實例、模板綁定數(shù)據(jù)的變量名 exp 和一個 callback,這個 callback 內(nèi)部邏輯是為了更新數(shù)據(jù)到 Dom,所以我們的 Watcher 類內(nèi)部要做的事情就清晰了,獲取更改前的值存儲起來,并創(chuàng)建一個 update 實例方法,在值被更改時去執(zhí)行實例的 callback 以達到視圖的更新。

// 文件:Watcher.js
class Watcher {
    constructor(vm, exp, callback) {
        this.vm = vm;
        this.exp = exp;
        this.callback = callback;

        // 更改前的值
        this.value = this.get();
    }
    get() {
        // 將當前的 watcher 添加到 Dep 類的靜態(tài)屬性上
        Dep.target = this;

        // 獲取值觸發(fā)數(shù)據(jù)劫持
        let value = CompileUtil.getVal(this.vm, this.exp);

        // 清空 Dep 上的 Watcher,防止重復(fù)添加
        Dep.target = null;
        return value;
    }
    update() {
        // 獲取新值
        let newValue = CompileUtil.getVal(this.vm, this.exp);
        // 獲取舊值
        let oldValue = this.value;

        // 如果新值和舊值不相等,就執(zhí)行 callback 對 dom 進行更新
        if(newValue !== oldValue) {
            this.callback();
        }
    }
}

看到上面代碼一定有兩個疑問:

使用 get 方法獲取舊值得時候為什么要將當前的實例掛在 Dep 上,在獲取值后為什么又清空了;

update 方法內(nèi)部執(zhí)行了 callback 函數(shù),但是 update 在什么時候執(zhí)行。

這就是后面兩個類 Depobserver 要做的事情,我們首先來介紹 Dep,再介紹 Observer 最后把他們之間的關(guān)系整個串聯(lián)起來。


發(fā)布訂閱 Dep 類的實現(xiàn)

其實發(fā)布訂閱說白了就是把要執(zhí)行的函數(shù)統(tǒng)一存儲在一個數(shù)組中管理,當達到某個執(zhí)行條件時,循環(huán)這個數(shù)組并執(zhí)行每一個成員。

// 文件:Dep.js
class Dep {
    constructor() {
        this.subs = [];
    }
    // 添加訂閱
    addSub(watcher) {
        this.subs.push(watcher);
    }
    // 通知
    notify() {
        this.subs.forEach(watcher => watcher.update());
    }
}

Dep 類中只有一個屬性,就是一個名為 subs 的數(shù)組,用來管理每一個 watcher,即 Watcher 類的實例,而 addSub 就是用來將 watcher 添加到 subs 數(shù)組中的,我們看到 notify 方法就解決了上面的一個疑問,Watcher 類的 update 方法是怎么執(zhí)行的,就是這樣循環(huán)執(zhí)行的。

接下來我們整合一下盲點:

Dep 實例在哪里創(chuàng)建聲明,又是在哪里將 watcher 添加進 subs 數(shù)組的;

Depnotify 方法應(yīng)該在哪里調(diào)用;

Watcher 內(nèi)容中,使用 get 方法獲取舊值得時候為什么要將當前的實例掛在 Dep 上,在獲取值后為什么又清空了。

這些問題在最后一個類 Observer 實現(xiàn)的時候都將清晰,下面我們重點來看最后一部分核心邏輯。


數(shù)據(jù)劫持 Observer 類的實現(xiàn)

還記得實現(xiàn) MVVM 類的時候就創(chuàng)建了這個類的實例,當時傳入的參數(shù)是 MVVM 實例的 data 屬性,在 MVVM 中把數(shù)據(jù)通過 Object.defineProperty 掛到了實例上,并添加了 gettersetter,其實 Observer 類主要目的就是給 data 內(nèi)的所有層級的數(shù)據(jù)都進行這樣的操作。

// 文件:Observer.js
class Observer {
    constructor (data) {
        this.observe(data);
    }
    // 添加數(shù)據(jù)監(jiān)聽
    observe(data) {
        // 驗證 data
        if(!data || typeof data !== "object") {
            return;
        }

        // 要對這個 data 數(shù)據(jù)將原有的屬性改成 set 和 get 的形式
        // 要將數(shù)據(jù)一一劫持,先獲取到 data 的 key 和 value
        Object.keys(data).forEach(key => {
            // 劫持(實現(xiàn)數(shù)據(jù)響應(yīng)式)
            this.defineReactive(data, key, data[key]);
            this.observe(data[key]); // 深度劫持
        });
    }
    // 數(shù)據(jù)響應(yīng)式
    defineReactive (object, key, value) {
        let _this = this;
        // 每個變化的數(shù)據(jù)都會對應(yīng)一個數(shù)組,這個數(shù)組是存放所有更新的操作
        let dep = new Dep();

        // 獲取某個值被監(jiān)聽到
        Object.defineProperty(object, key, {
            enumerable: true,
            configurable: true,
            get () { // 當取值時調(diào)用的方法
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set (newValue) { // 當給 data 屬性中設(shè)置的值適合,更改獲取的屬性的值
                if(newValue !== value) {
                    _this.observe(newValue); // 重新賦值如果是對象進行深度劫持
                    value = newValue;
                    dep.notify(); // 通知所有人數(shù)據(jù)更新了
                }
            }
        });
    }
}

在的代碼中 observe 的目的是遍歷對象,在內(nèi)部對數(shù)據(jù)進行劫持,即添加 gettersetter,我們把劫持的邏輯多帶帶抽取成 defineReactive 方法,需要注意的是 observe 方法在執(zhí)行最初就對當前的數(shù)據(jù)進行了數(shù)據(jù)類型驗證,然后再循環(huán)對象每一個屬性進行劫持,目的是給同為 Object 類型的子屬性遞歸調(diào)用 observe 進行深度劫持。

defineReactive 方法中,創(chuàng)建了 Dep 的實例,并對 data 的數(shù)據(jù)使用 getset 進行劫持,還記得在模板編譯的過程中,遇到模板中綁定的變量,就會解析,并創(chuàng)建 watcher,會在 Watcher 類的內(nèi)部獲取舊值,即當前的值,這樣就觸發(fā)了 get,在 get 中就可以將這個 watcher 添加到 Depsubs 數(shù)組中進行統(tǒng)一管理,因為在代碼中獲取 data 中的值操作比較多,會經(jīng)常觸發(fā) get,我們又要保證 watcher 不會被重復(fù)添加,所以在 Watcher 類中,獲取舊值并保存后,立即將 Dep.target 賦值為 null,并且在觸發(fā) get 時對 Dep.target 進行了短路操作,存在才調(diào)用 DepaddSub 進行添加。

data 中的值被更改時,會觸發(fā) set,在 set 中做了性能優(yōu)化,即判斷重新賦的值與舊值是否相等,如果相等就不重新渲染頁面,不等的情況有兩種,如果原來這個被改變的值是基本數(shù)據(jù)類型沒什么影響,如果是引用類型,我們需要對這個引用類型內(nèi)部的數(shù)據(jù)進行劫持,因此遞歸調(diào)用了 observe,最后調(diào)用 Depnotify 方法進行通知,執(zhí)行 notify 就會執(zhí)行 subs 中所有被管理的 watcherupdate,就會執(zhí)行創(chuàng)建 watcher 時的傳入的 callback,就會更新頁面。

MVVM 類將 data 的屬性掛在 MVVM 實例上并劫持與通過 Observer 類對 data 的劫持還有一層聯(lián)系,因為整個發(fā)布訂閱的邏輯都是在 datagetset 上,只要觸發(fā)了 MVVM 中的 getset 內(nèi)部會自動返回或設(shè)置 data 對應(yīng)的值,就會觸發(fā) datagetset,就會執(zhí)行發(fā)布訂閱的邏輯。

通過上面長篇大論的敘述后,這個 MVVM 模式用到的幾個類的關(guān)系應(yīng)該完全敘述清晰了,雖然比較抽象,但是細心琢磨還是會明白之間的關(guān)系和邏輯,下面我們就來對我們自己實現(xiàn)的這個 MVVM 進行驗證。


驗證 MVVM

我們按照 Vue 的方式根據(jù)自己的 MVVM 實現(xiàn)的內(nèi)容簡單的寫了一個模板如下:





    
    MVVM


    
{{message}}
  • {{message}}
{{message}}

打開 Chrom 瀏覽器的控制臺,在上面通過下面操作來驗證:

輸入 vm.message = "hello" 看頁面是否更新;

輸入 vm.$data.message = "hello" 看頁面是否更新;

改變文本輸入框內(nèi)的值,看頁面的其他元素是否更新。


總結(jié)

通過上面的測試,相信應(yīng)該理解了 MVVM 模式對于前端開發(fā)重大的意義,實現(xiàn)了雙向數(shù)據(jù)綁定,實時保證 View 層與 Model 層的數(shù)據(jù)同步,并可以讓我們在開發(fā)時基于數(shù)據(jù)編程,而最少的操作 Dom,這樣大大提高了頁面渲染的性能,也可以使我們把更多的精力用于業(yè)務(wù)邏輯的開發(fā)上。


文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://hztianpu.com/yun/98285.html

相關(guān)文章

  • 2019-我的前端面試題

    摘要:先說下我面試情況,我一共面試了家公司。篇在我面試的眾多公司里,只有同城的面問到相關(guān)問題,其他公司壓根沒問。我自己回答的是自己開發(fā)組件面臨的問題。完全不用擔心對方到時候打電話核對的問題。 2019的5月9號,離發(fā)工資還有1天的時候,我的領(lǐng)導(dǎo)親切把我叫到辦公室跟我說:阿郭,我們公司要倒閉了,錢是沒有的啦,為了不耽誤你,你趕緊出去找工作吧。聽到這話,我虎軀一震,這已經(jīng)是第2個月沒工資了。 公...

    iKcamp 評論0 收藏0
  • VUE - MVVM - part12 - props

    摘要:看這篇之前,如果沒有看過之前的文章,移步拉到文章末尾查看之前的文章。而該組件實例的父實例卻并不固定,所以我們將這些在使用時才能確定的參數(shù)在組件實例化的時候傳入。系列文章地址優(yōu)化優(yōu)化總結(jié) 看這篇之前,如果沒有看過之前的文章,移步拉到文章末尾查看之前的文章。 前言 在上一步,我們實現(xiàn) extend 方法,用于擴展 Vue 類,而我們知道子組件需要通過 extend 方法來實現(xiàn),我們從測試例...

    bluesky 評論0 收藏0
  • 關(guān)于Vue2一些值得推薦的文章 -- 五、六月份

    摘要:五六月份推薦集合查看最新的請點擊集前端最近很火的框架資源定時更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語。葉上初陽乾宿雨,水面清圓,一一風荷舉。家住吳門,久作長安旅。五月漁郎相憶否。小楫輕舟,夢入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請::點擊::集web前端最近很火的vue2框架資源;定時更新,歡迎 Star 一下。 蘇...

    sutaking 評論0 收藏0
  • 關(guān)于Vue2一些值得推薦的文章 -- 五、六月份

    摘要:五六月份推薦集合查看最新的請點擊集前端最近很火的框架資源定時更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語。葉上初陽乾宿雨,水面清圓,一一風荷舉。家住吳門,久作長安旅。五月漁郎相憶否。小楫輕舟,夢入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請::點擊::集web前端最近很火的vue2框架資源;定時更新,歡迎 Star 一下。 蘇...

    khs1994 評論0 收藏0

發(fā)表評論

0條評論

閱讀需要支付1元查看
<