本身實現MVVM(Vue源碼解析)

前言

本文會帶你們手動實現一個雙向綁定過程(僅僅涵蓋一些簡單的指令解析,如:v-textv-model,插值),固然借鑑的是Vue1的源碼,相信你們在閱讀完本文後對Vue1會有一個更好的理解,源代碼放到了github,因爲本人水平有限,理解不到位的地方還請你們指出。css

MVVM

MVVM使開發能夠更加關注於數據,減小了很大的工做量,也使代碼可讀性,可維護性更高,MVVM核心的思想就是視圖是狀態的函數:View = ViewModel(Model),因此當Model發生改變時,ViewModel會來操做View來怎麼作,而非是本身寫代碼來作。不管是雙向綁定仍是單向綁定,都是符合MVVM思想的。Vue提倡的是雙向綁定,也就是容許View到Model的變化,其實這個場景出如今的也就是表單操做上,看個例子,例子中分別利用了Vue和React實現了一下表單value變化,影響頁面與其相關的dom節點發生變化,能夠發現的是雙向綁定的Vue是inputvalue發生變化則h1innerText就發生了變化,變化是由View->Model,而提倡單向數據流的React須要手動監聽事件,事件觸發後,更改Model的值,從而使inputvalue發生了變化。看了Vue的源碼後不難發現Vue的雙向綁定的實現也就是在表單元素上添加了input事件,能夠說雙向綁定是單向綁定的一個語法糖。html

實現思路

圖片描述

上圖是一個大致的流程,下面按照流程來實現下:node

  • 利用observerdata進行了監聽,而且提供訂閱某個數據項的變化的能力git

這點的實現,須要藉助的是Object.defineProperty()來爲對象的屬性綁定get/set特性(因爲利用了Object.defineProperty(),因此Vue不支持ie8),observer須要將data的全部屬性都綁定get/set,很容易想到的就是利用遞歸來實現,具體代碼就不貼出,請參見這裏github

  • 利用Compile對模板進行解析app

這點實現的是將咱們的模板轉化爲html,過程當中會將數據與View中的節點相關聯起來,最終會將編譯好的html頁面替換到頁面上。首先來看解析,首先從根節點開始,根據不一樣的節點類型採用不一樣的解析方式:dom

function compileNode(node, vm) {
    const type = node.nodeType;
    if (type === 1 && !isScript(node)) {
        compileElement(node, vm);
    } else if (type === 3 && node.data.trim()) {
        compileTextNode(node, vm);
    } else {
        return null;
    }
}

對於文本節點來講,可能存在狀況只有兩種異步

  1. 與數據不相關不用操做函數

  2. 含有插值,須要與數據進行關聯優化

    • {{}}文本插值

    • {{{}}}html插值

利用下面正就能夠將插值找出:

/\{\{\{(.*?)\}\}\}|\{\{(.*?)\}\}/g

採用下面函數來對文本節點的內容解析:

function parseText(node) {
    var text = node.wholeText;
    if (!tagRE.test(text)) {
        return void 0;
    }
    const tokens = [];
    var lastIndex = tagRE.lastIndex = 0,
        match, index, html, value;
    while (match = tagRE.exec(text)) {
        index = match.index;
        if (index > lastIndex) {
            tokens.push({
                value: text.slice(lastIndex, index)
            })
        }
        html = htmlRE.test(match[0]);
        value = html ? match[1] : match[2];
        tokens.push({
            value: value,
            tag: true,
            html: html
        });
        lastIndex = index + match[0].length;
    }
    if (lastIndex < text.length) {
        tokens.push({
            value: text.slice(lastIndex)
        })
    }
    return tokens;
}

返回了tokens,裏面存儲了每個塊內容,一個插值or一個普通文本,tag來標記是否爲插值,html來標記是否爲純html插值。遍歷返回的tokens,根據不一樣的類型,來採用不一樣的方式將其添加到其父節點上:

function compileTextNode(node, vm) {
    const tokens = parseText(node);
    if (tokens == null) return void 0;
    var frag = document.createDocumentFragment();
    tokens.forEach(token => {
        var el;
        if (token.tag) {
            if (token.html) {
                el = document.createDocumentFragment();
                el.$parent = node.parentNode;
                el.$oneTime = true;
                dirCollection["html"](el, vm, token.value);
            } else {
                el = document.createTextNode(" ");
                dirCollection["text"](el, vm, token.value);
            }
        } else {
            el = document.createTextNode(token.value);
        } 
        el && frag.appendChild(el);
    });
    return replace(node, frag);
}

dirCollection是一個指令集合,也就是決定了如何初始化以及如何更新該節點。對於nodeType1的節點來講,指令所有存儲在其屬性中,遍歷屬性,倘若指令中含有v-html,v-model,v-text,則中止遍歷其子樹,直接將調用相應指令便可,不然,則須要遍歷其子節點,對其子節點應用compileNode進行解析:

function compileNodeList(nodes, vm) {
    for (let val of nodes) {
        compileNode(val, vm);
    }
}
function compileElement(node, vm) {
    var flag = false;
    const attrs = Array.prototype.slice.call(node.attributes);
    attrs.forEach((val) => {
        const name = val.name,
            value = val.value;
        if (dirRE.test(name)) {
            var dir;
            // 事件指令
            if (
                (dir = name.match(eventRE)) && 
                (dir = dir[1])
            ) {
                dirCollection["eventDir"](node, dir, vm, value);
            } else {
                dir = name.match(dirRE)[1];
                dirCollection[dir](node, vm, value);
            }
            // 指令中爲v-html or v-text or v-model終止遞歸
            flag = flag || 
                name === vhtml || 
                name === vtext;    
            node.removeAttribute(name);
        }    
    });
    const childs = node.childNodes;
    if (!flag && childs && childs.length) {
        compileNodeList(childs, vm);
    }
}

dirCollections中還會作的就是將數據與View的dom節點相關聯,利用的就是DepWatcher,頁面上每個與數據相關聯的節點都含有一個Watcher,當數據發生變化是Watcher用於計算,是否須要更新該節點;數據的每個屬性都有一個Dep,當該屬性發生變化時,Dep會通知與該數據相關聯的Watcher來進行計算是否須要更新對應頁面。Dep代碼Watcher代碼

  • 異步更新隊列

異步更新隊列,是一個優化,將更新dom的操做變爲異步的,放到下一個事件循環來作,這樣作能夠減小沒必要要的dom更新,看下面狀況:

vm.value++;
vm.value++;
vm.value++;

三次數據改變,倘若同步更新的話,則每次數據改變會當即更新dom,而異步更新的話,能夠先將更新推入一個隊列中,因爲是異步,也能夠保證每個Watcher只被推入到一次,這樣就避免了沒必要要的更新,異步更新主要利用的是nextTick,這個函數會優先使用Promise,不兼容則利用MutationObserver,再不兼容的話會利用setTimeout

寫在後面

看過了Vue的源碼不得不感嘆Vue的優美,而Vue2又增長了虛擬dom,這樣就能夠作到服務端渲染,給了咱們更多的可能!

這篇博客最好配合着源碼來看,關於源碼歡迎star

相關文章
相關標籤/搜索