模擬一個(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)聽(tīng)架構(gòu)

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

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

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

Vue函數(shù)

這里簡(jiǎn)單模擬Vue函數(shù),el為Vue作用的dom節(jié)點(diǎn)鉤子碑隆,data為Vue主要監(jiān)聽(tīng)的數(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ù)在頁(yè)面上的渲染上煤,首先看構(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í)例通過(guò)構(gòu)造函數(shù)傳遞進(jìn)來(lái),其他值得一說(shuō)的就是為了減少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á)式),綁定的事件名稱晨炕,然后開始渲染頁(yè)面。
渲染頁(yè)面部分是個(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渲染到頁(yè)面瞄勾。

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)渲染到頁(yè)面方法的工具對(duì)象愿阐。
首先從text方法來(lái)開始看:

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);
    },

首先通過(guò)expr區(qū)分出是模版渲染還是v-text渲染,如果是模版渲染就用replace抽取出表達(dá)式趾疚,然后通過(guò)公用的表達(dá)式獲取值方法拿到值渲染到頁(yè)面缨历。
watch類通過(guò)表達(dá)式關(guān)聯(lián)vm中的對(duì)象變化,然后通過(guò)回調(diào)函數(shù)重新渲染頁(yè)面糙麦。
getValue方法很簡(jiǎn)單,表達(dá)式通過(guò)‘.’拆分為數(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更新頁(yè)面

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)鍵思想是在這里通過(guò)CompileUtil.getValue獲取Vue中data值袒餐,并在Dep中上存了一個(gè)watch飞蛹,獲取data屬性值的時(shí)候會(huì)調(diào)用這個(gè)屬性的get方法须肆,如果Dep對(duì)象上target有值,就在Dep對(duì)象上添加一個(gè)watch桩皿。
update方法通過(guò)CompileUtils.getValue獲取watch中表達(dá)式值如果新值不等于老值就調(diào)用callback跟新頁(yè)面

Observer

Observer類是核心對(duì)象,這里通過(guò)構(gòu)造函數(shù)傳遞Vue需要監(jiān)聽(tīng)的對(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)聽(tīng)佛嬉,這里遍歷對(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)行頁(yè)面更新湾揽。
這里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)單通過(guò)expr獲取變量值然后渲染到頁(yè)面,watch監(jiān)聽(tīng)到變化后重新調(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就是我們常說(shuō)的雙向綁定
一樣我們先看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)聽(tīng)框架中的變量變化后更新節(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輸入框值后,頁(yè)面動(dòng)態(tài)發(fā)生變化


修改輸入框值

結(jié)語(yǔ)

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


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末刮便,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子绽慈,更是在濱河造成了極大的恐慌恨旱,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坝疼,死亡現(xiàn)場(chǎng)離奇詭異搜贤,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)钝凶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門仪芒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)唁影,“玉大人,你說(shuō)我怎么就攤上這事掂名【萆颍” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵饺蔑,是天一觀的道長(zhǎng)锌介。 經(jīng)常有香客問(wèn)我,道長(zhǎng)猾警,這世上最難降的妖魔是什么孔祸? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮发皿,結(jié)果婚禮上崔慧,老公的妹妹穿的比我還像新娘。我一直安慰自己穴墅,他們只是感情好惶室,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著封救,像睡著了一般拇涤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上誉结,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天鹅士,我揣著相機(jī)與錄音,去河邊找鬼惩坑。 笑死掉盅,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的以舒。 我是一名探鬼主播趾痘,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蔓钟!你這毒婦竟也來(lái)了永票?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤滥沫,失蹤者是張志新(化名)和其女友劉穎侣集,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兰绣,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡世分,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缀辩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片臭埋。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡踪央,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瓢阴,到底是詐尸還是另有隱情畅蹂,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布荣恐,位于F島的核電站魁莉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏募胃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一畦浓、第九天 我趴在偏房一處隱蔽的房頂上張望痹束。 院中可真熱鬧,春花似錦讶请、人聲如沸祷嘶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)论巍。三九已至,卻和暖如春风响,著一層夾襖步出監(jiān)牢的瞬間嘉汰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工状勤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鞋怀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓持搜,卻偏偏與公主長(zhǎng)得像密似,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子葫盼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容