成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

模擬 Vue 手寫一個(gè) MVVM

sf_wangchong / 3390人閱讀

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


閱讀原文


MVVM 的前世今生

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

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

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

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


MVVM 的流程分析

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

類似這種 “造輪子” 的代碼毋庸置疑一定是通過面向?qū)ο缶幊虂韺?shí)現(xiàn)的,并嚴(yán)格遵循開放封閉原則,由于 ES5 的面向?qū)ο缶幊瘫容^繁瑣,所以,在接下來的代碼中統(tǒng)一使用 ES6 的 class 來實(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 框架類似,需要通過 new 指令創(chuàng)建 MVVM 的實(shí)例并傳入 options。

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

        // 如果有要編譯的模板就開始編譯
        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;
                }
            });
        });
    }
}

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

當(dāng)存在根節(jié)點(diǎn)的時(shí)候,通過 Observer 類對(duì) data 數(shù)據(jù)進(jìn)行了劫持,并通過 MVVM 實(shí)例的方法 proxyDatadata 中的數(shù)據(jù)掛在當(dāng)前 MVVM 實(shí)例上,同樣對(duì)數(shù)據(jù)進(jìn)行了劫持,是因?yàn)槲覀冊(cè)讷@取和修改數(shù)據(jù)的時(shí)候可以直接通過 thisthis.$data,在 Vue 中實(shí)現(xiàn)數(shù)據(jù)劫持的核心方法是 Object.defineProperty,我們也使用這個(gè)方式通過添加 gettersetter 來實(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ì)盡量的把一些公共的邏輯抽取出來進(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;

        // 如過傳入的根元素存在,才開始編譯
        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;
    }
}

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

解析模板的過程中為了性能,我們應(yīng)取出根節(jié)點(diǎn)內(nèi)的子節(jié)點(diǎn)存放在文檔碎片中(內(nèi)存),需要注意的是將一個(gè) Dom 節(jié)點(diǎn)內(nèi)的子節(jié)點(diǎn)存入文檔碎片的過程中,會(huì)在原來的 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 中的模板編譯的主要就是兩部分,也是瀏覽器無法解析的部分,元素節(jié)點(diǎn)中的指令和文本節(jié)點(diǎn)中的 Mustache 語法(雙大括號(hào))。

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

        // 如過傳入的根元素存在,才開始編譯
        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 再塞回頁面中
            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 語法中變量對(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è)過程當(dāng)中抽取出了兩個(gè)方法,compileElementcompileText 用來對(duì)元素節(jié)點(diǎn)的屬性和文本節(jié)點(diǎn)進(jìn)行處理。

compileElement 中的核心邏輯就是處理指令,取出元素節(jié)點(diǎn)所有的屬性判斷是否是指令,是指令則調(diào)用指令對(duì)應(yīng)的方法。compileText 中的核心邏輯就是取出文本的內(nèi)容通過正則表達(dá)式匹配出被 Mustache 語法的 “{{ }}” 包裹的內(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)的方法,我們之所以多帶帶把這些指令對(duì)應(yīng)的方法抽離出來存儲(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;
    }
};

這部分的整個(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è)方法 getVal、getTextValsetVal 掛在了 CompileUtil 對(duì)象下。

// 文件: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 對(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];
    }, vm.$data);
}

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

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

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

// 文件:CompileUtil.js —— model 方法
// 處理 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)聽
    node.addEventListener("input", e => {
        // 獲取輸入的新值
        let newValue = e.target.value;

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

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

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

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

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

    // 通過正則替換,將取到數(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)到頁面上。

所以我們的實(shí)現(xiàn)方式是給這個(gè)綁定了 v-model 的表單元素監(jiān)聽了 input 事件,并在事件中實(shí)時(shí)的將新的 value 值更新到 data 中,至于 data 中的改變后響應(yīng)到頁面中需要另外三個(gè)類 WatcherObserverDep 共同實(shí)現(xiàn),我們下面就來實(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ǔ)起來,并創(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();
        }
    }
}

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

使用 get 方法獲取舊值得時(shí)候?yàn)槭裁匆獙?dāng)前的實(shí)例掛在 Dep 上,在獲取值后為什么又清空了;

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

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


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

其實(shí)發(fā)布訂閱說白了就是把要執(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ù)組,用來管理每一個(gè) watcher,即 Watcher 類的實(shí)例,而 addSub 就是用來將 watcher 添加到 subs 數(shù)組中的,我們看到 notify 方法就解決了上面的一個(gè)疑問,Watcher 類的 update 方法是怎么執(zhí)行的,就是這樣循環(huán)執(zhí)行的。

接下來我們整合一下盲點(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 上,在獲取值后為什么又清空了。

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


數(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ù)通過 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)聽
    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)聽到
        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,我們把劫持的邏輯多帶帶抽取成 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)行劫持,還記得在模板編譯的過程中,遇到模板中綁定的變量,就會(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)化,即判斷重新賦的值與舊值是否相等,如果相等就不重新渲染頁面,不等的情況有兩種,如果原來這個(gè)被改變的值是基本數(shù)據(jù)類型沒什么影響,如果是引用類型,我們需要對(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ì)更新頁面。

MVVM 類將 data 的屬性掛在 MVVM 實(shí)例上并劫持與通過 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ā)布訂閱的邏輯。

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


驗(yàn)證 MVVM

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





    
    MVVM


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

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

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

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

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


總結(jié)

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


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

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/98285.html

相關(guān)文章

  • 2019-我的前端面試題

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

    iKcamp 評(píng)論0 收藏0
  • VUE - MVVM - part12 - props

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

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

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

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

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

    khs1994 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<