自己實現MVVM(Vue源碼解析)

vevd6292 8年前發布 | 15K 次閱讀 Vue.js MVVM模式 Vue.js開發

前言

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

MVVM

MVVM 使開發可以更加關注于數據,減少了很大的工作量,也使代碼可讀性,可維護性更高, MVVM 核心的思想就是視圖是狀態的函數: View = ViewModel(Model) ,所以當Model發生改變時,ViewModel會來操作View來怎么做,而非是自己寫代碼來做。無論是雙向綁定還是單向綁定,都是符合 MVVM 思想的。Vue提倡的是雙向綁定,也就是允許View到Model的變化,其實這個場景出現在的也就是表單操作上, 看個例子 ,例子中分別利用了Vue和React實現了一下表單 value 變化,影響頁面與其相關的 dom 節點發生變化, 可以發現的是雙向綁定的Vue是 input 的 value 發生變化則 h1 的 innerText 就發生了變化,變化是由View->Model,而提倡單向數據流的 React 需要手動監聽事件,事件觸發后,更改Model的值,從而使 input 的 value 發生了變化。看了Vue的源碼后不難發現Vue的雙向綁定的實現也就是在表單元素上添加了 input 事件,可以說雙向綁定是單向綁定的一個語法糖。

實現思路

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

  • 利用 observer 對 data 進行了監聽,并且提供訂閱某個數據項的變化的能力

這點的實現,需要借助的是 Object.defineProperty() 來為對象的屬性綁定 get/set 特性(由于利用了 Object.defineProperty() ,所以Vue不支持ie8), observer 需要將 data 的所有屬性都綁定 get/set ,很容易想到的就是利用遞歸來實現 。

  • 利用 Compile 對模板進行解析

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

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 是一個指令集合,也就是決定了如何初始化以及如何更新該節點。對于 nodeType 為 1 的節點來說,指令全部存儲在其屬性中,遍歷屬性,假若指令中含有 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 節點相關聯,利用的就是 Dep 與 Watcher ,頁面上每一個與數據相關聯的節點都含有一個 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,這樣就可以做到服務端渲染,給了我們更多的可能!

 

 

來自:https://segmentfault.com/a/1190000007741904

 

 本文由用戶 vevd6292 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!