JS實現(xiàn)一個簡易版的vue

上一篇簡析vue實現(xiàn)原理殃恒,我們知道在 Vue 的 MVVM 設計中,我們主要針對 Observer(數(shù)據(jù)劫持)、Dep(發(fā)布訂閱)、Watcher(數(shù)據(jù)監(jiān)聽)和 Compile(模板編譯)幾個部分來實現(xiàn)唧龄,下面來實現(xiàn)它們。

// MVVM.js稼病,入口文件选侨,整合上面的幾部分

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

        if (this.$el) {
            // $data數(shù)據(jù)劫持
            new Observer(this.$data);

            // 將數(shù)據(jù)代理到實例上 vm.message = "hello"
            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;
                }
            });
        });
    }
}

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

class Compile {
    constructor(el, vm) {
        //dom
        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é)點轉移至文檔碎片
    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;

        // 轉換成數(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.from(attrs).forEach(attr => {
            // 獲取屬性名,判斷屬性是否為指令逝钥,即含 v-
            let attrName = attr.name;

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

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

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

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

        // 創(chuàng)建匹配 {{}} 的正則表達式
        //let reg = /\{\{([^}+])\}\}/g;
        //“.”表示任意字符艘款〕旨剩“+”表示前面表達式一次乃至多次』┡兀“?”表示匹配模式是非貪婪的蜘欲。
        let reg = /\{\{(.+?)\}\}/g;

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

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

CompileUtil = {};

// 更新Dom節(jié)點方法
CompileUtil.updater = {
    // 文本更新
    textUpdater(node, value) {
        node.textContent = value;
    },
    // 輸入框更新
    modelUpdater(node, value) {
        node.value = value;
    }
};

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

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

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

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

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

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

    // 獲取 data 中對應的變量的值
    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);
    });

    // 第一次設置值
    updateFn && updateFn(node, value);
};


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

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

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

    // 第一次設置值
    updateFn && updateFn(node, value);
};

// Watcher.js
Watcher 類內(nèi)部要做的事情剧罩,獲取更改前的值存儲起來,并創(chuàng)建一個 update 實例方法座泳,在值被更改時去執(zhí)行實例的 callback 以達到視圖的更新惠昔。

class Watcher {
    constructor(vm, exp, callback) {
        this.vm = vm; // MVVM 的實例
        this.exp = exp; // 模板綁定數(shù)據(jù)的變量名 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,防止重復添加
        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(newValue);
        }
    }
}

// Dep.js
發(fā)布訂閱將要執(zhí)行的函數(shù)統(tǒng)一存儲在一個數(shù)組中管理镇防,當達到某個執(zhí)行條件時潮饱,循環(huán)這個數(shù)組并執(zhí)行每一個成員来氧。

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

// Observer.js 數(shù)據(jù)劫持 Observer 類

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

        Object.keys(data).forEach(key => {
            // 劫持(實現(xiàn)數(shù)據(jù)響應式)
            this.defineReactive(data, key, data[key]);
            this.observe(data[key]); // 深度劫持
        });
    }
    // 數(shù)據(jù)響應式
    defineReactive (object, key, value) {
        let _this = this;
        // 每個變化的數(shù)據(jù)都會對應一個數(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 屬性中設置的值適合啦扬,更改獲取的屬性的值
                if(newValue !== value) {
                    _this.observe(newValue); // 重新賦值如果是對象進行深度劫持
                    value = newValue;
                    dep.notify(); // 通知所有人數(shù)據(jù)更新了
                }
            }
        });
    }
}

來驗證下我們寫的MVVM扑毡。

<!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="./Watcher.js"></script>
<script src="./Observer.js"></script>
<script src="./Compile.js"></script>
<script src="./CompileUtil.js"></script>
<script src="./Dep.js"></script>
<script src="./MVVM.js"></script>
<script>
    let vm = new MVVM({
        el: '#app',
        data: {
            message: 'hello world!'
        }
    })
</script>
</body>
</html>
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末泉褐,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子泣矛,更是在濱河造成了極大的恐慌疲眷,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件您朽,死亡現(xiàn)場離奇詭異狂丝,居然都是意外死亡换淆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門几颜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來倍试,“玉大人,你說我怎么就攤上這事蛋哭∠叵埃” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵谆趾,是天一觀的道長躁愿。 經(jīng)常有香客問我,道長沪蓬,這世上最難降的妖魔是什么彤钟? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮怜跑,結果婚禮上样勃,老公的妹妹穿的比我還像新娘。我一直安慰自己性芬,他們只是感情好,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布剧防。 她就那樣靜靜地躺著植锉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪峭拘。 梳的紋絲不亂的頭發(fā)上俊庇,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音鸡挠,去河邊找鬼辉饱。 笑死,一個胖子當著我的面吹牛拣展,可吹牛的內(nèi)容都是我干的彭沼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼备埃,長吁一口氣:“原來是場噩夢啊……” “哼姓惑!你這毒婦竟也來了?” 一聲冷哼從身側響起按脚,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤于毙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后辅搬,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體唯沮,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了介蛉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片萌庆。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖甘耿,靈堂內(nèi)的尸體忽然破棺而出踊兜,到底是詐尸還是另有隱情,我是刑警寧澤佳恬,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布捏境,位于F島的核電站,受9級特大地震影響毁葱,放射性物質(zhì)發(fā)生泄漏垫言。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一倾剿、第九天 我趴在偏房一處隱蔽的房頂上張望筷频。 院中可真熱鬧,春花似錦前痘、人聲如沸凛捏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坯癣。三九已至,卻和暖如春最欠,著一層夾襖步出監(jiān)牢的瞬間示罗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工芝硬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蚜点,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓拌阴,卻偏偏與公主長得像绍绘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子皮官,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354