看了多篇Vue內核文章原理仍是不太明白,看看這篇試試?

    做者:殷榮檜@騰訊javascript

    建議你先把倉庫中的代碼clone下來跑一遍,執行git checkout aec6a75 切換到倉庫的第一個commit,本身運行運行,嘗試着去看一看代碼,本身先理解理解,斷點調試調試。而後再來看這篇文章代碼中寫的註釋,遇到不太理解的再來看看這篇文章,看看是否是可以更易於理解些。代碼真的已經簡化到不能再簡化,總計才150行左右,仔細看你必定能理解。第一個commit實現了雙向綁定功能,後面每一個commit都只實現一個完整的細小的功能(如v-model、computed、watch。method等),並且代碼量都儘量的少,你若是想看必定能看懂。html

    Vue最精華的部分就是雙向綁定,在此基礎上,又添加了computed,watch, methods等方法。因此要看懂Vue內核,那第一步確定就是要了解Vue雙向綁定的原理,可是說實話,看了網上不少,好多代碼都是通過重構優化後的代碼,失去了代碼原始的面貌,不太易於理解。因此決定寫一個原始一點可是又儘量簡潔一點的,可是原理絕對是Vue雙向綁定的原理,確保你看懂這篇文章,就可以瞭解Vue內核。採用最少的代碼,來實現一個個功能。有什麼寫的不妥的地方,煩請在倉庫issue中指出,我好及時修正。   這個項目的github地址爲build-your-own-vue 歡迎starvue

若是你對當前流行的輪子的原理感興趣,下面這些都是我用盡量少的代碼,儘量易於理解的方式實現的框架的原理,這些你也能夠看看,有疑問歡迎在各個倉庫下留言:java

build-your-own-reactnode

build-your-own-vuexreact

build-your-own-reduxgit

build-your-own-fluxgithub

接下來所講的這些就爲了實現下面這個簡單的雙向綁定:正則表達式

<div id="app">
    {{name}}
</div>
<script type="text/javascript">
    let vue = new Vue({
        el: '#app',
        data: {
            name: 'jackieyin'
        }
    })
    window.vue = vue;
</script>
複製代碼

在chrome devtools控制檯中經過this.vue.name = 'willen'能夠自動更新頁面中的name爲’willen‘。看看結果:vuex

雙向綁定結果

(1)從最容易的Dependency.js開始說。

先來看代碼:

let Watcher = null; // 用來代表有沒有監視器實例,這會你可能不懂,下面會遇到它,而後講解
class Dep { // 把與一個變量相關的監聽器都存在subs這個變量中
    constructor() {
        this.subs = []; // 定義一個subs容器
    }
    notify() {
        // 執行全部與變量相關的回調函數,容器中的watcher一個個都執行掉(看不懂watcher不要緊,第二結中就會講解)
        this.subs.forEach(sub => sub.update());
    }
    addSub(watcher) { // 將一個一個的watcher放入到sub的容器中(看不懂watcher不要緊,第二結中就會講解)
        // 添加與變量相關的訂閱回調
        this.subs.push(watcher);
    }
}
複製代碼

從代碼看下來,Dep就是subs容器,是一個數組,將一個個的watcher都放到subs容器中。watcher就是一個個的回調函數,都放在subs的容器中等待觸發。addSub中的this.subs.push(watcher)就是將一個個的watcher回調函數放入到其中。notify就是用來將subs中的watcher都觸發掉。watcher中就是一個一個更新頁面中對應的變量的函數。這個下面會說到。

(2)接下來就看看這個watcher是什麼?

class Watch {
    constructor(vue, exp, cb) {
        this.vue = vue; // 將vue實例傳入到watcher中
        this.exp = exp; // 須要對那個表達式進行監控,好比對上例中的'name'進行監控,那麼這裏的exp就是'name'
        this.cb = cb; // 一但監聽到上述exp表達式子的值發生變化,須要通知到的cb(callback)回調函數
        this.hasAddedAsSub = false; // 有沒有被添加到Dep中的Subscriber中去,有的話就不須要重複添加
        this.value = this.get(); // 獲得當前vue實例上對應表達式exp的最新的值
    }
    get() {
        Watcher = this; // 這邊的Watcher爲何須要放入this,並在下面又置空,你須要繼續向下看,暫且先記着,這邊把如今的watcher實例放到了Watcher中了。
            var value = this.vue[this.exp]; // 獲得表達式的值,就是獲得'name'表達式的值爲‘willen’(經過chrome devtools控制檯中經過this.vue.name = 'willen'修改了name爲’willen‘。)
        Watcher = null; // 將Watcher置空,讓給下一個值
        return value; // 將獲取到的表達式的值返回出去
    }
    update() {
        let value = this.get(); // 經過get()函數獲得當前的watcher監聽的表達式的值,例如上面的‘willen’
        let oldVal = this.value; // 獲取舊的值
        if(value !== oldVal) {  // 對比新舊錶達式‘name’的值,發現修改前爲'jackieyin',修改後爲'willen',說明須要更新頁面
            this.value = value; // 把如今的值記錄下來,用於和下次比較。
            this.cb.call(this.vue, value); // 用如今的值willen去執行回調函數,其實就是更新一下頁面中的{{name}}從‘jackieyin’ 爲‘willen’
        }
    }
}
複製代碼

(3) 接下來看一下Observer,這個類是作什麼工做的。

class Observer {
    constructor(data) {
        this.defineReactive(data); // 將用戶自定義的data中的元素都進行劫持觀察,從而來實現雙向綁定
    }
    defineReactive(data) { // 開始對用戶定義的數據進行劫持
        var dep  = new Dep(); //這個就是第一節中說起到的Dependency類。用來收集雙向綁定的各個數據變化時都有的依賴watcher
        Object.keys(data).forEach(key => { // 遍歷用戶定義的data,其實如今也就一個‘name’字段
            var val = data[key]; // 獲得data['name']的值爲jackieyin
            Object.defineProperty(data, key, {
                get() { // 使用get對data中的name字段進行劫持
                    if(Watcher) { // 這個就是第二結中說起的Watcher了,(第二結中Watcher = this賦值後這邊纔會進入if)
                        if(!Watcher.hasAddedAsSub) { // 對於已經添加到訂閱列表中的監視器則無需再重複添加了,防止將watcher重複添加到subs容器中,沒有意義,由於一下子更新{{name}}從‘jackieyin’到‘willen’,更新兩三次也還仍是一個結果
                            dep.addSub(Watcher); // 將監視器watcher添加到subs訂閱列表中
                            Watcher.hasAddedAsSub = true;  // 代表這個結果已經添加到subs容器中了
                        }
                    }
                    return val; // 將name中的值返回出去
                },
                set(newVal) { // 對this.vue.name = 'willen'這個set行爲進行劫持
                    if(newVal === val) { // 新值(例如仍是this.vue.name = 'jackieyin')與以前的值相同,不作任何修改
                        return;
                    }
                    val = newVal; // 將vue實例上對應的值(name的值)修改成新的值
                    dep.notify(); // 通知subs中watcher都觸發來對頁面進行更新,將頁面中的{{name}}處的‘jackieyin’更新爲'willen'
                }
            })
        });
    }
}
複製代碼

(4) 最後再一塊兒來看看編譯類Compile,這個是用來對{{name}}進行編譯,說白了就是在你的實例的data對象中,找到name: 'jackieyin',而後在頁面上將{{name}}替換爲‘jackieyin’

class Compile {
    constructor(el, vue) {
        this.$vue = vue; // 拷貝vue實例,之因此加$符號,表示暴露給用戶的,常常在Vue中看到這種帶$標誌的,說明是暴露給用戶使用的。
        this.$el = document.querySelector(el); // 獲取到dom對象,其實就是document.querySelector('#app'); 
        if(this.$el) { // 若是存在能夠掛在的實例
            // 在$fragment中操做,比this.$el中操做節省不少性能,因此要賦值給fragment
            let $fragment = this.node2Fragment(this.$el); // 將獲取到的el的地方使用片斷替代,這是爲了便於在內存中操做,使得更新頁面更加快速
            this.compileText($fragment.childNodes[0]); // 將模板中的{{}}替換成對應的變量,如{{name}}替換爲'jackieyin'
            this.$el.appendChild($fragment); // 將el獲取到的dom節點使用內存中的片斷進行替換
        }
    }
    node2Fragment(el) { // 用來把dom中的節點賦值到內存fragment變量中去
        // 將node節點都放到fragment中去
        var fragment  = document.createDocumentFragment();
        fragment.appendChild(el.firstChild);// 將el中的元素放到fragment中去,並刪除el中原有的,這個是appendChild自帶的功能
        return fragment;
    }

    compileText(node) {
        // 對包含可能出現vue標識的部分進行編譯,主要是將{{xxx}}替換成對應的值,這邊是用正則表達式檢測{{}}進行替換
        var reg = /\{\{(.*)\}\}/; // 用來判斷有沒有vue的雙括號的
        if(reg.test(node.textContent)) {
            let matchedName = RegExp.$1;
            node.textContent = this.$vue[matchedName];
            new Watch(this.$vue, matchedName, function(value) { // 對當前的表達式‘name’添加watcher監聽器,其實後來就是把這個watcher放入到了dep中的subs的數組中了。當'name'更新爲‘willen’後,其實就是執行了這邊的node.textContent = value就把頁面中的jackieyin替換成了willen了。這就是雙向綁定了。node其實就是剛纔存放在內存中的$fragement的節點,因此至關於直接操做了內存,因此更新頁面就比修改DOM更新頁面快多了。
                node.textContent = value;
            });
        }
    }
}
複製代碼

(5)這個時候就能夠來組裝出一個咱們本身的小型的Vue了。

class Vue {
    constructor(options) {
        let data = this._data = options.data || undefined;
        this._initData(); // 將data中的數據都掛載到this上去,使得this.name 至關於就是獲得了this._data.name
        new Observer(data); // 將data中的數據進行劫持
        new Compile(options.el, this); // 將{{name}}用data中的’jackieyin‘數據替換掉
    }
    _initData() {
        // 這個函數的功能很簡單,就是把用戶定義在data中的變量,都掛載到Vue實例(this)上
        let that = this;
	        Object.keys(that._data).forEach((key) => {
	            Object.defineProperty(that, key, {
	                get: () => {
	                    return that._data[key];
	                },
	                set: (newVal) => {
	                    that._data[key] = newVal;
	                }
	            })
	        });
	    }
	}
複製代碼

(6)大功告成,把咱們所寫的零件組裝在一塊兒試一下咱們的小型的vue是否工做正常。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="app">
        {{name}}
    </div>
    <script src="./js/Dependency.js"></script>
    <script src="./js/Observer.js"></script>
    <script src="./js/Watch.js"></script>
    <script src="./js/Compile.js"></script>
    <script src="./js/Vue.js"></script>
    <script type="text/javascript">
        let vue = new Vue({
            el: '#app',
            data: {
                name: 'jackie'
            }
        })
        window.vue = vue;
    </script>
</body>
</html>
複製代碼

雙向綁定結果

怎麼樣,搞定了,其實,這只是Vue的冰山一角(下圖中的綠色框框的部分),在這個倉庫中還實現了一系列vue的功能,若是你有興趣能夠一個commit一個commit的往上看,每一個commit都只實現一個完整的細小的功能,並且代碼量都儘量的少,你若是想看必定能看懂。這倉庫都是沒有使用虛擬DOM來實現,更新顆粒度細,如今的Vue下降了更新的顆粒度,用了虛擬DOM,可是Vue中雙向綁定的原理始終未變,因此這篇文章仍是須要看懂的,老弟。之後有時間我再研究研究虛擬DOM寫個倉庫。

123

如發現文章有什麼錯誤,能夠在 個人github中進行評論留言。若是你覺的文章寫的還能夠, 歡迎star

文章vue內核倉庫地址

相關文章
相關標籤/搜索