MVVM開(kāi)發(fā)模式,并將其實(shí)現(xiàn)

開(kāi)發(fā)模式是大頭乾忱。但真正掌握了讥珍,對(duì)自己的整個(gè)開(kāi)發(fā)思想是大大的提高。

曾幾何時(shí)窄瘟,為自己的面向過(guò)程而沉迷著衷佃。興嘆好多代碼呀!

但你又曾知道這樣的代碼出錯(cuò)是有多容易蹄葱,解決錯(cuò)誤是有多麻煩氏义。同事看你代碼是有多惡心。

所以图云,兄弟呀惯悠,學(xué)學(xué)開(kāi)發(fā)模式知識(shí),讓你少掉幾根頭發(fā)吧竣况。

MVVM 的前世今生

MVVM 設(shè)計(jì)模式克婶,是由 MVC(最早來(lái)源于后端)、MVP 等設(shè)計(jì)模式進(jìn)化而來(lái)丹泉,M - 數(shù)據(jù)模型(Model)情萤,VM - 視圖模型(ViewModel),V - 視圖層(View)嘀掸。

在 MVC 模式中紫岩,除了 Model 和 View 層以外,其他所有的邏輯都在 Controller 中睬塌,Controller 負(fù)責(zé)顯示頁(yè)面泉蝌、響應(yīng)用戶操作、網(wǎng)絡(luò)請(qǐng)求及與 Model 的交互揩晴,隨著業(yè)務(wù)的增加和產(chǎn)品的迭代勋陪,Controller 中的處理邏輯越來(lái)越多、越來(lái)越復(fù)雜硫兰,難以維護(hù)诅愚。為了更好的管理代碼,為了更方便的擴(kuò)展業(yè)務(wù)劫映,必須要為 Controller “瘦身”违孝,需要更清晰的將用戶界面(UI)開(kāi)發(fā)從應(yīng)用程序的業(yè)務(wù)邏輯與行為中分離,MVVM 為此而生泳赋。

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

MVVM 的流程分析

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

<figure style="padding: 0px; margin: 0px;">
image

<figcaption style="padding: 0px; margin: 0px;"></figcaption>

</figure>

類似這種 “造輪子” 的代碼毋庸置疑一定是通過(guò)面向?qū)ο缶幊虂?lái)實(shí)現(xiàn)的,并嚴(yán)格遵循開(kāi)放封閉原則邪驮,由于 ES5 的面向?qū)ο缶幊瘫容^繁瑣莫辨,所以,在接下來(lái)的代碼中統(tǒng)一使用 ES6 的 class 來(lái)實(shí)現(xiàn)耕捞。

MVVM 類的實(shí)現(xiàn)

在 Vue 中衔掸,對(duì)外只暴露了一個(gè)名為 Vue 的構(gòu)造函數(shù),在使用的時(shí)候 new 一個(gè) Vue 實(shí)例俺抽,然后傳入了一個(gè) options 參數(shù)敞映,類型為一個(gè)對(duì)象,包括當(dāng)前 Vue 實(shí)例的作用域 el磷斧、模板綁定的數(shù)據(jù) data 等等振愿。

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

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

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

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

            // 用數(shù)據(jù)和元素進(jìn)行編譯
            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;
                }
            });
        });
    }
}

通過(guò)上面代碼档桃,我們可以看出,在我們 new 一個(gè) MVVM 的時(shí)候憔晒,在參數(shù) options 中傳入了一個(gè) Dom 的根元素節(jié)點(diǎn)和數(shù)據(jù) data 并掛在了當(dāng)前的 MVVM 實(shí)例上藻肄。

當(dāng)存在根節(jié)點(diǎn)的時(shí)候,通過(guò) Observer 類對(duì) data 數(shù)據(jù)進(jìn)行了劫持拒担,并通過(guò) MVVM 實(shí)例的方法 proxyDatadata 中的數(shù)據(jù)掛在當(dāng)前 MVVM 實(shí)例上嘹屯,同樣對(duì)數(shù)據(jù)進(jìn)行了劫持,是因?yàn)槲覀冊(cè)讷@取和修改數(shù)據(jù)的時(shí)候可以直接通過(guò) thisthis.$data从撼,在 Vue 中實(shí)現(xiàn)數(shù)據(jù)劫持的核心方法是 Object.defineProperty州弟,我們也使用這個(gè)方式通過(guò)添加 gettersetter 來(lái)實(shí)現(xiàn)數(shù)據(jù)劫持。

最后使用 Compile 類對(duì)模板和綁定的數(shù)據(jù)進(jìn)行了解析和編譯低零,并渲染在根節(jié)點(diǎn)上婆翔,之所以數(shù)據(jù)劫持和模板解析都使用類的方式實(shí)現(xiàn),是因?yàn)榇a方便維護(hù)和擴(kuò)展掏婶,其實(shí)不難看出啃奴,MVVM 類其實(shí)作為了 Compile 類和 Observer 類的一個(gè)橋梁。

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

Compile 類在創(chuàng)建實(shí)例的時(shí)候需要傳入兩個(gè)參數(shù)气堕,第一個(gè)參數(shù)是當(dāng)前 MVVM 實(shí)例作用的根節(jié)點(diǎn)纺腊,第二個(gè)參數(shù)就是 MVVM 實(shí)例,之所以傳入 MVVM 的實(shí)例是為了更方便的獲取 MVVM 實(shí)例上的屬性茎芭。

Compile 類中揖膜,我們會(huì)盡量的把一些公共的邏輯抽取出來(lái)進(jìn)行最大限度的復(fù)用,避免冗余代碼梅桩,提高維護(hù)性和擴(kuò)展性壹粟,我們把 Compile 類抽取出的實(shí)例方法主要分為兩大類,輔助方法和核心方法宿百,在代碼中用注釋標(biāo)明趁仙。

1、解析根節(jié)點(diǎn)內(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;

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

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

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

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

上面編譯模板的過(guò)程中痊焊,前提條件是必須存在根元素節(jié)點(diǎn)盏袄,傳入的根元素節(jié)點(diǎn)允許是一個(gè)真實(shí)的 Dom 元素,也可以是一個(gè)選擇器薄啥,所以我們創(chuàng)建了輔助方法 isElementNode 來(lái)幫我們判斷傳入的元素是否是 Dom辕羽,如果是就直接使用,是選擇器就獲取這個(gè) Dom垄惧,最終將這個(gè)根節(jié)點(diǎn)存入 this.el 屬性中刁愿。

解析模板的過(guò)程中為了性能,我們應(yīng)取出根節(jié)點(diǎn)內(nèi)的子節(jié)點(diǎn)存放在文檔碎片中(內(nèi)存)到逊,需要注意的是將一個(gè) Dom 節(jié)點(diǎn)內(nèi)的子節(jié)點(diǎn)存入文檔碎片的過(guò)程中代乃,會(huì)在原來(lái)的 Dom 容器中刪除這個(gè)節(jié)點(diǎn)谅辣,所以在遍歷根節(jié)點(diǎn)的子節(jié)點(diǎn)時(shí),永遠(yuǎn)是將第一個(gè)節(jié)點(diǎn)取出存入文檔碎片,直到節(jié)點(diǎn)不存在為止闹获。

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

在 Vue 中的模板編譯的主要就是兩部分授账,也是瀏覽器無(wú)法解析的部分倘是,元素節(jié)點(diǎn)中的指令和文本節(jié)點(diǎn)中的 Mustache 語(yǔ)法(雙大括號(hào))。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • 調(diào)用 compile 方法對(duì) fragment 文檔碎片進(jìn)行編譯,即替換內(nèi)部指令和 Mustache 語(yǔ)法中變量對(duì)應(yīng)的值呢蔫;
  • 將編譯好的 fragment 文檔碎片塞回根節(jié)點(diǎn)切心。

在第一個(gè)步驟當(dāng)中邏輯是比較繁瑣的,首先在 compile 方法中獲取所有的子節(jié)點(diǎn)片吊,循環(huán)進(jìn)行編譯绽昏,如果是元素節(jié)點(diǎn)需要遞歸 compile,傳入當(dāng)前元素節(jié)點(diǎn)俏脊。在這個(gè)過(guò)程當(dāng)中抽取出了兩個(gè)方法全谤,compileElementcompileText 用來(lái)對(duì)元素節(jié)點(diǎn)的屬性和文本節(jié)點(diǎn)進(jìn)行處理。

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

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

在調(diào)用指令的方法時(shí)都是調(diào)用的 CompileUtil 下對(duì)應(yīng)的方法窑睁,我們之所以單獨(dú)把這些指令對(duì)應(yīng)的方法抽離出來(lái)存儲(chǔ)在 CompileUtil對(duì)象下的目的是為了解耦挺峡,因?yàn)楹竺嫫渌念愡€要使用。

3担钮、CompileUtil 對(duì)象中指令方法的實(shí)現(xiàn)

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

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

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

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

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

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

// 獲取文本 {{}} 中變量在 data 對(duì)應(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) => {
        // 如果當(dāng)前歸并的為數(shù)組的最后一項(xiàng),則將新值設(shè)置到該屬性
        if(currentIndex === exp.length - 1) {
            return prev[next] = newVal
        }

        // 繼續(xù)歸并
        return prev[next];
    });
}

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

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

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

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

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

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

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

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

    // 第一次設(shè)置值
    updateFn && updateFn(vm, value);
};
復(fù)制代碼

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

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

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

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

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

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

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

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

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

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

        // 更改前的值
        this.value = this.get();
    }
    get() {
        // 將當(dāng)前的 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 對(duì) dom 進(jìn)行更新
        if(newValue !== oldValue) {
            this.callback();
        }
    }
}
復(fù)制代碼

看到上面代碼一定有兩個(gè)疑問(wèn):

  • 使用 get 方法獲取舊值得時(shí)候?yàn)槭裁匆獙?dāng)前的實(shí)例掛在 Dep 上袱巨,在獲取值后為什么又清空了阁谆;
  • update 方法內(nèi)部執(zhí)行了 callback 函數(shù),但是 update 在什么時(shí)候執(zhí)行愉老。

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

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

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

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

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

接下來(lái)我們整合一下盲點(diǎn):

  • Dep 實(shí)例在哪里創(chuàng)建聲明引有,又是在哪里將 watcher 添加進(jìn) subs 數(shù)組的瓣颅;
  • Depnotify 方法應(yīng)該在哪里調(diào)用;
  • Watcher 內(nèi)容中譬正,使用 get 方法獲取舊值得時(shí)候?yàn)槭裁匆獙?dāng)前的實(shí)例掛在 Dep 上宫补,在獲取值后為什么又清空了。

這些問(wèn)題在最后一個(gè)類 Observer 實(shí)現(xiàn)的時(shí)候都將清晰曾我,下面我們重點(diǎn)來(lái)看最后一部分核心邏輯粉怕。

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

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

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

        // 要對(duì)這個(gè) data 數(shù)據(jù)將原有的屬性改成 set 和 get 的形式
        // 要將數(shù)據(jù)一一劫持稚晚,先獲取到 data 的 key 和 value
        Object.keys(data).forEach(key => {
            // 劫持(實(shí)現(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;
        // 每個(gè)變化的數(shù)據(jù)都會(huì)對(duì)應(yīng)一個(gè)數(shù)組崇堵,這個(gè)數(shù)組是存放所有更新的操作
        let dep = new Dep();

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

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

defineReactive 方法中幔摸,創(chuàng)建了 Dep 的實(shí)例,并對(duì) data 的數(shù)據(jù)使用 getset 進(jìn)行劫持拍顷,還記得在模板編譯的過(guò)程中,遇到模板中綁定的變量塘幅,就會(huì)解析昔案,并創(chuàng)建 watcher,會(huì)在 Watcher 類的內(nèi)部獲取舊值电媳,即當(dāng)前的值踏揣,這樣就觸發(fā)了 get,在 get 中就可以將這個(gè) watcher 添加到 Depsubs 數(shù)組中進(jìn)行統(tǒng)一管理匾乓,因?yàn)樵诖a中獲取 data 中的值操作比較多捞稿,會(huì)經(jīng)常觸發(fā) get,我們又要保證 watcher 不會(huì)被重復(fù)添加拼缝,所以在 Watcher 類中娱局,獲取舊值并保存后,立即將 Dep.target 賦值為 null咧七,并且在觸發(fā) get 時(shí)對(duì) Dep.target 進(jìn)行了短路操作衰齐,存在才調(diào)用 DepaddSub 進(jìn)行添加。

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

MVVM 類將 data 的屬性掛在 MVVM 實(shí)例上并劫持與通過(guò) Observer 類對(duì) data 的劫持還有一層聯(lián)系橙垢,因?yàn)檎麄€(gè)發(fā)布訂閱的邏輯都是在 datagetset 上,只要觸發(fā)了 MVVM 中的 getset 內(nèi)部會(huì)自動(dòng)返回或設(shè)置 data 對(duì)應(yīng)的值伦糯,就會(huì)觸發(fā) datagetset柜某,就會(huì)執(zhí)行發(fā)布訂閱的邏輯。

通過(guò)上面長(zhǎng)篇大論的敘述后敛纲,這個(gè) MVVM 模式用到的幾個(gè)類的關(guān)系應(yīng)該完全敘述清晰了喂击,雖然比較抽象,但是細(xì)心琢磨還是會(huì)明白之間的關(guān)系和邏輯淤翔,下面我們就來(lái)對(duì)我們自己實(shí)現(xiàn)的這個(gè) MVVM 進(jìn)行驗(yàn)證翰绊。

驗(yàn)證 MVVM

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

<!-- index.html 文件 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MVVM</title>
</head>
<body>
    <div id="app">
        <!-- 雙向數(shù)據(jù)綁定 靠的是表單 -->
        <input type="text" v-model="message">
        <div>{{message}}</div>
        <ul>
            <li>{{message}}</li>
        </ul>
        {{message}}
    </div>

    <!-- 引入依賴的 js 文件 -->
    <script src="./js/Watcher.js"></script>
    <script src="./js/Observer.js"></script>
    <script src="./js/Compile.js"></script>
    <script src="./js/CompileUtil.js"></script>
    <script src="./js/Dep.js"></script>
    <script src="./js/MVVM.js"></script>
    <script>
        let vm = new MVVM({
            el: '#app',
            data: {
                message: 'hello world!'
            }
        });
    </script>
</body>
</html>

打開(kāi) Chrom 瀏覽器的控制臺(tái),在上面通過(guò)下面操作來(lái)驗(yàn)證:

  • 輸入 vm.message = "hello" 看頁(yè)面是否更新旁壮;
  • 輸入 vm.$data.message = "hello" 看頁(yè)面是否更新监嗜;
  • 改變文本輸入框內(nèi)的值,看頁(yè)面的其他元素是否更新抡谐。

總結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末羔沙,一起剝皮案震驚了整個(gè)濱河市放仗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌撬碟,老刑警劉巖诞挨,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異呢蛤,居然都是意外死亡惶傻,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門其障,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)银室,“玉大人,你說(shuō)我怎么就攤上這事◎诟遥” “怎么了辜荠?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)抓狭。 經(jīng)常有香客問(wèn)我伯病,道長(zhǎng),這世上最難降的妖魔是什么否过? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任午笛,我火速辦了婚禮,結(jié)果婚禮上苗桂,老公的妹妹穿的比我還像新娘药磺。我一直安慰自己,他們只是感情好煤伟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布癌佩。 她就那樣靜靜地躺著,像睡著了一般便锨。 火紅的嫁衣襯著肌膚如雪围辙。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,182評(píng)論 1 299
  • 那天鸿秆,我揣著相機(jī)與錄音酌畜,去河邊找鬼怎囚。 笑死卿叽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的恳守。 我是一名探鬼主播考婴,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼催烘!你這毒婦竟也來(lái)了沥阱?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤伊群,失蹤者是張志新(化名)和其女友劉穎考杉,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體舰始,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡崇棠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丸卷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片枕稀。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出萎坷,到底是詐尸還是另有隱情凹联,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布哆档,位于F島的核電站蔽挠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏虐呻。R本人自食惡果不足惜象泵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望斟叼。 院中可真熱鬧偶惠,春花似錦、人聲如沸朗涩。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)谢床。三九已至兄一,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間识腿,已是汗流浹背出革。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留渡讼,地道東北人骂束。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像成箫,于是被迫代替她去往敵國(guó)和親展箱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • vue理解淺談 一 理解vue的核心理念 使用vue會(huì)讓人感到身心愉悅,它同時(shí)具備angular和react的優(yōu)點(diǎn)...
    ambeer閱讀 24,126評(píng)論 2 18
  • 本文是lhyt本人原創(chuàng)蹬昌,希望用通俗易懂的方法來(lái)理解一些細(xì)節(jié)和難點(diǎn)混驰。轉(zhuǎn)載時(shí)請(qǐng)注明出處。文章最早出現(xiàn)于本人github...
    lhyt閱讀 2,211評(píng)論 0 4
  • 這方面的文章很多皂贩,但是我感覺(jué)很多寫的比較抽象栖榨,本文會(huì)通過(guò)舉例更詳細(xì)的解釋。(此文面向的Vue新手們明刷,如果你是個(gè)大牛...
    Ivy_2016閱讀 15,390評(píng)論 8 64
  • 上周在大數(shù)據(jù)的趨勢(shì)和特點(diǎn)中婴栽,說(shuō)到了人類這次面臨的問(wèn)題不是問(wèn)題無(wú)法解決,而是問(wèn)題過(guò)于復(fù)雜遮精。采用機(jī)械思維居夹,其速度和效率...
    唯生物閱讀 2,607評(píng)論 0 0
  • 一.java系統(tǒng)變量: 1.“JAVA_HOME”准脂,而變量值是裝jdk時(shí)的路徑 2.接下來(lái)找到“CLASSPATH...
    WY長(zhǎng)河閱讀 265評(píng)論 0 0