模擬一個(gè)簡(jiǎn)單的Vue

Vue是現(xiàn)在前端非常流行的一個(gè)前端框架了,了解它的實(shí)現(xiàn)原理現(xiàn)在基本已經(jīng)快成為前端開發(fā)一個(gè)必備的基本功了,這篇文章將嘗試寫一個(gè)簡(jiǎn)單的Vue框架。

Vue數(shù)據(jù)監(jiān)聽架構(gòu)

Vue主要架構(gòu)分為三個(gè)部分Compile、ObserverWatcher結(jié)構(gòu)圖如下:

Vue數(shù)據(jù)監(jiān)聽架構(gòu)

Obserer負(fù)責(zé)監(jiān)聽Vue中的數(shù)據(jù),Compile負(fù)責(zé)Vue中涉及dom節(jié)點(diǎn)的渲染,Compile和Observer通過Watcher關(guān)聯(lián),當(dāng)Observer監(jiān)聽到數(shù)據(jù)變化會(huì)通過watcher使Compile更新頁面,反之亦然。
下邊就一部分一部分拆解Vue數(shù)據(jù)監(jiān)聽架構(gòu)。

Vue函數(shù)

這里簡(jiǎn)單模擬Vue函數(shù),el為Vue作用的dom節(jié)點(diǎn)鉤子,data為Vue主要監(jiān)聽的數(shù)據(jù),option為Vue中dom交互事件函數(shù)放置的地方。

class Vue {
    constructor(el, data, option) {
        this.$el = el;
        this.$data = data;
        this.$option = option; // 綁定方法放在這里
        if (this.$el) {
            new Observer(this.$data)
            new Compile(this.$el, this);
        }
    }
}

Compile

構(gòu)造函數(shù)

compile負(fù)責(zé)Vue數(shù)據(jù)在頁面上的渲染,首先看構(gòu)造函數(shù):

constructor(el, vm) {
        this.vm = vm;
        if (el && el.nodeType === 1) {
            this.$el = el;
        } else {
            this.$el = document.querySelector(el);
        }

        const fragment = this.createFragment(this.$el);
        this.compile(fragment);
        this.$el.appendChild(fragment);
    }

createFragment(el) {
        const fragment = document.createDocumentFragment();
        while (el.firstChild) {
            fragment.appendChild(el.firstChild);
        }
        return fragment;
    }

都是比較簡(jiǎn)單的功能,首先在Vue構(gòu)造函數(shù)中將el與vue實(shí)例通過構(gòu)造函數(shù)傳遞進(jìn)來,其他值得一說的就是為了減少dom結(jié)構(gòu)變化造成的重排,使用了fragment,先將el子節(jié)點(diǎn)緩存在fragment中,然后compile后一次性插入el子節(jié)點(diǎn)中。

compile

compile(fragment) {
        fragment.childNodes.forEach((childNode) => {
            if (childNode && childNode.nodeType === 1) {
                this.compileElement(childNode)
            } else {
                this.compileText(childNode)
            }
            if (childNode && childNode.childNodes.length > 0) {
                this.compile(childNode);
            }
        })
    }

遍歷子節(jié)點(diǎn),發(fā)現(xiàn)如果是element節(jié)點(diǎn)進(jìn)行子節(jié)點(diǎn)的遞歸調(diào)用,這里簡(jiǎn)單處理為子節(jié)點(diǎn)只有element與text類型節(jié)點(diǎn)。分別針對(duì)element與text節(jié)點(diǎn)做編譯處理。

編譯text與element類型子節(jié)點(diǎn)

 compileElement(node) {
        const attributes = Array.from(node.attributes);
        attributes.forEach((attribute) => {
            const {name, value} = attribute;
            if (this.isDirective(name)) {
                const [, directive] = name.split('-');
                const [directiveName, eventName] = directive.split(':');
                CompileUtil[directiveName](node, value, this.vm, eventName);
            }
        })
    }

    compileText(node) {
        if (node.textContent && node.textContent.includes('{{')) {
            CompileUtil['text'](node, node.textContent, this.vm)
        }
    }

    isDirective(name) {
        if (typeof name !== 'string') {
            return false;
        }
        return name.startsWith('v-');
    }
編譯element節(jié)點(diǎn)

編譯element節(jié)點(diǎn)首先遍歷節(jié)點(diǎn)屬性,找出v-開頭的屬性,簡(jiǎn)單假定這些就是vue框架渲染節(jié)點(diǎn)的鉤子屬性。
然后拆分鉤子屬性獲取到expr(獲取data值的屬性表達(dá)式),綁定的事件名稱,然后開始渲染頁面。
渲染頁面部分是個(gè)很獨(dú)立的一塊工作,所以這里封裝了一個(gè)工具對(duì)象。

編譯text節(jié)點(diǎn)
    compileText(node) {
        if (node.textContent && node.textContent.includes('{{')) {
            CompileUtil['text'](node, node.textContent, this.vm)
        }
    }

文本類型節(jié)點(diǎn)主要判斷出是否是{{template }}類型的節(jié)點(diǎn),然后將textConten傳遞給CompileUtil渲染到頁面。

CompileUtil
結(jié)構(gòu)圖

CompileUtil結(jié)構(gòu)圖

首先針對(duì)vue的幾個(gè)常用指令v-text、v-html、v-modal與v-on對(duì)應(yīng)了幾個(gè)操作方法,update是對(duì)應(yīng)渲染到頁面方法的工具對(duì)象。
首先從text方法來開始看:

text(node, expr, vm) {
        let value = null;
        if (expr.includes('{{')) {
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                new Watch(args[1], vm, (newValue) => {
                    this.update.textUpdate(node, newValue);
                });
                return this.getValue(args[1], vm);
            })
        } else {
            value = this.getValue(expr, vm);
            new Watch(expr, vm, (newValue) => {
                this.update.textUpdate(node, newValue);
            });
        }
        this.update.textUpdate(node, value);
    },

首先通過expr區(qū)分出是模版渲染還是v-text渲染,如果是模版渲染就用replace抽取出表達(dá)式,然后通過公用的表達(dá)式獲取值方法拿到值渲染到頁面。
watch類通過表達(dá)式關(guān)聯(lián)vm中的對(duì)象變化,然后通過回調(diào)函數(shù)重新渲染頁面。
getValue方法很簡(jiǎn)單,表達(dá)式通過‘.’拆分為數(shù)組,進(jìn)行reduce操作,然后將vue實(shí)例中的data作為起始值。

getValue(expr, vm) {
        return expr.split('.').reduce((data, attr) => {
            return data[attr];
        }, vm.$data)
    },

Watch與Dep

Dep

Dep類非常簡(jiǎn)單

class Dep {
    constructor() {
        this.subs = [];
    }

    add(watcher) {
        this.subs.push(watcher);
    }

    notify() {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }

}

Dep對(duì)象中負(fù)責(zé)添加watcher,在需要的時(shí)候發(fā)起通知,讓watcher更新頁面

watch

class Watch {
    constructor(expr, vm, callBack) {
        this.expr = expr;
        this.vm = vm;
        this.callBack = callBack;
        this.oldValue = this.getOldValue();
    }

    update() {
        const newValue = CompileUtil.getValue(this.expr, this.vm);
        if (this.oldValue !== newValue) {
            this.callBack(newValue);
            this.oldValue = newValue;
        }
    }

    getOldValue() {
        Dep.target = this; // 用這種方式就不能Dep類與Watch類分在兩個(gè)文件,webpack打包target值會(huì)丟掉
        const oldValue = CompileUtil.getValue(this.expr, this.vm); // 獲取data中的值,在get中添加Watch入Dep
        Dep.target = null;
        return oldValue;
    }
}

watch類中在構(gòu)造函數(shù)中傳遞expr vm 與跟新的回調(diào)函數(shù),最重要的是getOldValue函數(shù),在這里邊在Dep類中添加了target屬性,屬性值存了Watch實(shí)例對(duì)象,這里的關(guān)鍵思想是在這里通過CompileUtil.getValue獲取Vue中data值,并在Dep中上存了一個(gè)watch,獲取data屬性值的時(shí)候會(huì)調(diào)用這個(gè)屬性的get方法,如果Dep對(duì)象上target有值,就在Dep對(duì)象上添加一個(gè)watch。
update方法通過CompileUtils.getValue獲取watch中表達(dá)式值如果新值不等于老值就調(diào)用callback跟新頁面

Observer

Observer類是核心對(duì)象,這里通過構(gòu)造函數(shù)傳遞Vue需要監(jiān)聽的對(duì)象

class Observer {
    constructor(data) {
        this.observe(data);
    }

    observe(data) {
        if (data && typeof data === 'object') {
            for (const key of Object.keys(data)) {
                this.defineReactive(data, key, data[key]);
            }
        }
    }

    defineReactive(data, key, value) {
        this.observe(value);
        const dep = new Dep();
        Object.defineProperty(data, key, {
            configurable: false,
            enumerable: true,
            get: () => {
                Dep.target && dep.add(Dep.target)
                return value;
            },
            set: (v) => {
                this.observe(v);
                if (v !== value) {
                    value = v;
                    dep.notify();
                }
            }
        })
    }
}

在observe方法中遍歷data對(duì)象,然后調(diào)用核心方法defineReactive,這里注意的是在方法中首先回調(diào)了observe方法,因?yàn)閷?duì)象的屬性值可能也是個(gè)對(duì)象,所以回調(diào)了一下observe方法進(jìn)行深度監(jiān)聽,這里遍歷對(duì)象的每個(gè)屬性值,然后添加get 與set方法,get方法中與watch對(duì)象中的getOldValue進(jìn)行聯(lián)動(dòng),在set方法中因?yàn)樾略O(shè)置的值可能也是一個(gè)對(duì)象,所以也要回調(diào)一此observe方法,如果屬性設(shè)置的值與老值不同就調(diào)用dep進(jìn)行廣播所有watch進(jìn)行頁面更新。
這里set方法有個(gè)小技巧,set方法構(gòu)成一個(gè)閉包,v關(guān)聯(lián)了data的屬性值所以每次更新值都可以和data中的屬性值進(jìn)行比較。

測(cè)試

下邊簡(jiǎn)單測(cè)試一下功能
html部分的代碼

<input type="text" id="input">
  <p v-text="text.value">
    </p>
    {{text.value}}

js部分的代碼

var vue = new Vue(
    '#box',
    {
        text: {
            value: '文本'
        },
        html: '<h1>html</h1>',
        inputValue: 'input'
    },
    {
        clickButton() {
            alert(this.$data.text.value);
        }
    }
)

const input = document.getElementById('input');
input.addEventListener('input', (e) => {
    vue.$data.text.value = e.target.value;
})
為了測(cè)試效果給input綁定時(shí)間修改input值修改文本綁定的變量

測(cè)試結(jié)果

初始效果

改變input值后效果


改變變量后值

v-html效果

v-html比較簡(jiǎn)單,首先看CompileUtil部分代碼:

html(node, expr, vm) {
        const value = this.getValue(expr, vm);
        this.update.htmlUpdate(node, value);
        new Watch(expr, vm, (newValue) => {
            this.update.htmlUpdate(node, newValue);
        })
    },
...
     htmlUpdate(node, value) {
            node.innerHTML = value;
        },
...

思路很簡(jiǎn)單通過expr獲取變量值然后渲染到頁面,watch監(jiān)聽到變化后重新調(diào)用update

測(cè)試

html部分代碼:

    <button id="changeHtmlBtn">修改html</button>
    <div v-html="html">
        html
    </div>

js部分代碼


const htmlBtn = document.getElementById('changeHtmlBtn');
htmlBtn.addEventListener('click', (e) => {
    vue.$data.html = '<h2>changeHtml</h2>'
})

當(dāng)點(diǎn)擊button后修改div下的html


初始效果
點(diǎn)擊button后效果

修改后的html

v-modal

v-modal就是我們常說的雙向綁定
一樣我們先看CompileUtil部分代碼

...
  setValue(expr, vm, inputValue) {
        expr.split('.').reduce((data, currentValue, currentIndex, array) => {
            if (currentIndex === array.length - 1) {
                // 最后一個(gè)屬性值賦值input輸入的值
                data[currentValue] = inputValue;
            }
            return data[currentValue];
        }, vm.$data)
    },
...
modal(node, expr, vm) {
        node.addEventListener('input', (e) => {
            const value = e.target.value;
            this.setValue(expr, vm, value);
        }, false);
        new Watch(expr, vm, (newValue) => {
            this.update.modalUpdate(node, newValue);
        });
        this.update.modalUpdate(node, this.getValue(expr, vm));
    },
 update: {
...
               modalUpdate(node, value) {
            node.value = value;
        }
...
    }

其實(shí)也很簡(jiǎn)單給節(jié)點(diǎn)綁定一個(gè)input事件,事件回調(diào)函數(shù)給vue中的data賦值,watch監(jiān)聽框架中的變量變化后更新節(jié)點(diǎn)的value值,賦值操作封裝一個(gè)setValue方法,setValue方法和getValue方法一樣使用reduce方法,在最后一個(gè)屬性賦值inputValue

測(cè)試

html代碼
<input type="text" v-modal = 'inputValue'>
<div>{{inputValue}}</div>
inputValue初始值賦值為input

效果

初始效果

input初始值賦值為input

修改input輸入框值后,頁面動(dòng)態(tài)發(fā)生變化


修改輸入框值

結(jié)語

這里只是簡(jiǎn)單模擬vue框架,有很多地方存在缺陷,大家有選擇的閱讀思考就好,感謝閱讀。


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容