我是如何寫 Vue 源碼的:思路篇

看了那麼多篇文章,我發現不少文章只會告訴你他是怎麼寫的而不會告訴你他是怎麼想的。而我認爲,可否寫出代碼最主要的是如何構思的?爲何有的人能把代碼寫的很優雅而有的人寫的卻很臃腫?爲何有的人能一直寫下去而有的人卻容易「中道崩殂」?我但願你在本篇文章有所收穫,謝謝你的閱讀!javascript

逆向思惟

我不知道你有沒有試圖尋找過 Vue 源碼的入口,固然,這對熟悉代碼審計的老手來講很容易。可是若是你並無代碼審計的任何經驗,我想也你會頭疼。固然,我這裏並不講如何進行代碼審計。我要告訴你的是如何在不閱讀源碼的狀況下去實現相似的功能,我稱之爲逆向思惟html

固然在你要模仿一個東西的時候你首先要熟悉它,並且還要有十分清晰的思路。下面我就談談我是如何用最簡單的思路去實現 Vue 數據雙向綁定的:vue

<!-- html -->
<div id="app">{{name}}</div>
複製代碼
const app = new Vue({
    el: '#app',
    
    data: {
        name: 'Fish Chan'
    }
});
複製代碼

這是最簡單的 Vue 代碼,我相信只要是學過 Vue 都能看懂。上面的代碼 new 了一個 Vue 實例,且傳了一個參數(對象類型)。java

因此我新建了一個文件 core.js,內容以下:node

// 目標:我須要一個 Vue 類,構造函數能夠接收一個參
class Vue {
    constructor(options) {
        // TODO 編譯模板並實現數據雙向綁定
    }
}
複製代碼

就這樣,咱們就有了一個基礎的 Vue 類,它沒有作任何事情。接下來,咱們繼續。替換模板裏面的內容屬於_編譯_,因此我又建立了一個文件叫 compile.js(這裏模擬了 Java 的思惟,一個類一個文件,這樣每一個文件都很小巧,也很清楚每一個文件是幹嗎的):設計模式

// 目標:編譯模板,替換掉模板內容: {{name}}
class Compile {
    constructor() {
        // TODO 編譯模板
    }
}
複製代碼

仍是和上面同樣,我沒有寫任何實質性的內容,由於我始終堅持一個原則 不寫無用的代碼,用則寫,因此我寫代碼的習慣是須要用到某個數據了纔會把須要的數據傳過來。app

如今個人 Compile 須要知道從哪裏開始編譯,因而咱們傳入了第一個參數 el; 我還須要把模板內容替換成真實的數據,因此又傳了第二個參數,攜帶數據的 vue 實例:函數

class Compile {
    constructor(el, vue) {
        this.$el = document.querySelector(el);
        this.$vue = vue;
    }
    
    compileText() {
        // TODO 編譯模板,找到 {{name}} 並替換成真實數據
    }
}
複製代碼

爲了一步步的牽引思路,你會發現我在代碼中習慣用 TODO 去寫好下一步,固然這在你思路十分清晰的時候是不必這樣作的,除非你臨時有事須要離開你的電腦桌。ui

編譯模板

咱們順着思路繼續完成 compile.jsthis

class Compile {
    constructor(el, vue) {
        this.$el = document.querySelector(el);
        this.$vue = vue;
    }
    
    compileText() {
        const reg = /\{\{(.*)\}\}/; // 用於匹配 {{name}} 的正則
        
        const fragment = this.node2Fragment(this.$el); // 把操做 DOM 改爲操做文檔碎片
        const node = fragment.childNodes[0]; // 取節點_對象_
        
        if (reg.test(node.textContent)) {
            let matchedName = RegExp.$1;
            node.textContent = this.$vue._data[matchedName]; // 替換數據
            this.$el.appendChild(node); // 編譯好的文檔碎片放進根節點
        }
    }
    
    node2Fragment(node) {
        const fragment = document.createDocumentFragment();
        fragment.appendChild(node.firstChild);
        return fragment;
    }
}
複製代碼

其實,寫到這裏咱們就已經完成了模板編譯的部分。下面咱們只須要在 core.js 裏面調用它就行了:

class Vue {
    constructor(options) {
    	let data = this._data = options.data;
    	
    	const _complie = new Compile(options.el, this);
    	_complie.compileText();
    }
}
複製代碼

先運行一下看看:

成功編譯模板

數據雙向綁定

嗯,編譯模板已經實現了,如今開始實現數據雙向綁定,在這以前我但願你先去了解下設計模式之觀察者模式Object.defineProperty

新建一個 Observer 類,用於數據雙向綁定:

class Observer {
    constructor(data) {
        this.defineReactive(data);
    }
    
    defineReactive(data) {
        Object.keys(data).forEach(key => {
            let val = data[key];
            Object.defineProperty(data, key, {
                get() {
                    // TODO 監聽數據
                    return val;
                },
                set(newVal) {
                    val = newVal;
                    // TODO 更新視圖
                }
            })
        });
    }
}
複製代碼

接下來就是觀察者模式的實現了,基本上是一個固定的模板(我認爲設計模式是很好學的東西,就比如數學公式同樣):

class Dep {
    constructor(vue) {
        this.subs = []; // 存放訂閱者
    }
    
    addSubscribe(subscribe) {
        this.subs.push(subscribe);
    }
    
    notify() {
        let length = this.subs.length;
        while(length--)
        {
            this.subs[length].update();
        }
    }
}
複製代碼

接下來是訂閱者Watcher,訂閱者要作的事情就是執行某個事件:

class Watcher {
    constructor(vue, exp, callback) {
        this.vue = vue;
        this.exp = exp;
        this.callback = callback;
        this.value = this.get();
    }
    
    get() {
        Dep.target = this;
        let value = this.vue._data[this.exp];
        Dep.target = null;
        return value;
    }
    
    update() {
        this.value = this.get();
        this.callback.call(this.vue, this.value); // 將新的數據傳回,用於更新視圖;這裏保證了 this 指向 vue
    }
}
複製代碼

就這樣,照搬了觀察者模式和利用Object.defineProperty就簡單實現了一個數據雙向綁定。

完整代碼

下面把全部的 TODO 部分進行代碼替換,咱們就實現了全部的功能:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="author" content="Fish Chan">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue-demo</title>
    <script src="./Dep.js"></script>
    <script src="./Watch.js"></script>
    <script src="./Compile.js"></script>
    <script src="./Observer.js"></script>
    <script src="./core.js"></script>
</head>
<body>
    <div id="app">{{name}}</div>

    <script> const app = new Vue({ el: '#app', data: { name: 'Fish Chan' } }); </script>
</body>
</html>
複製代碼

core.js

class Vue {
    constructor(options) {
        let data = this._data = options.data;

        new Observer(data);

        const _complie = new Compile(options.el, this);

        _complie.compileText();
    }
}
複製代碼

Observer.js

class Observer {
    constructor(data) {
        this.defineReactive(data);
    }
    
    defineReactive(data) {
        let dep = new Dep();
        Object.keys(data).forEach(key => {
            let val = data[key];
            Object.defineProperty(data, key, {
                get() {
                    Dep.target && dep.addSubscribe(Dep.target);
                    return val;
                },
                set(newVal) {
                    val = newVal;
                    dep.notify();
                }
            })
        });
    }
}
複製代碼

Compile.js

class Compile {
    constructor(el, vue) {
        this.$el = document.querySelector(el);
        this.$vue = vue;
    }
    
    compileText() {
        const reg = /\{\{(.*)\}\}/; // 用於匹配 {{name}} 的正則
        
        const fragment = this.node2Fragment(this.$el); // 把操做 DOM 改爲操做文檔碎片
        const node = fragment.childNodes[0];
        
        if (reg.test(node.textContent)) {
            let matchedName = RegExp.$1;
            node.textContent = this.$vue._data[matchedName]; // 替換數據
            this.$el.appendChild(node); // 編譯好的文檔碎片放進根節點

            new Watcher(this.$vue, matchedName, function(value) {
                node.textContent = value;
                console.log(node.textContent);
            });
        }
    }
    
    node2Fragment(node) {
        const fragment = document.createDocumentFragment();
        fragment.appendChild(node.firstChild);
        return fragment;
    }
}
複製代碼

Watch.js

class Watcher {
    constructor(vue, exp, callback) {
        this.vue = vue;
        this.exp = exp;
        this.callback = callback;
        this.value = this.get();
    }
    
    get() {
        Dep.target = this;
        let value = this.vue._data[this.exp];
        Dep.target = null;
        return value;
    }
    
    update() {
        this.value = this.get();
        this.callback.call(this.vue, this.value); // 將新的數據傳回,用於更新視圖
    }
}
複製代碼

Dep.js

class Dep {
    constructor(vue) {
        this.subs = []; // 存放訂閱者
    }
    
    addSubscribe(subscribe) {
        this.subs.push(subscribe);
    }
    
    notify() {
        let length = this.subs.length;
        while(length--)
        {
            this.subs[length].update();
        }
    }
}
複製代碼

看下最終的運行圖吧:

總結

除了基本功紮實外,寫代碼必定要理清思路。思路是否清晰可能決定了你可否寫出一份優雅的代碼,也可能決定你是否能從始至終的完成一個項目。

相關文章
相關標籤/搜索