模仿vue自己動手寫響應式框架(三) - dom解析

wls1036發表於2020-07-19

背景

上一篇中,我們保證了程式碼不報錯,並且引數也成功傳進去了,但未實現任何的邏輯,這一章我們的工作就是完成dom的解析,在vue中,透過構建虛擬dom結構實現dom的高效更新和渲染,模仿這個思路,我們構建一個超級簡單虛擬的dom結構,本著讓所有人都能看得懂的目的,這次構建的dom結構不考慮效能,不考慮設計,以能實現功能為目的。

domParser(dom解析)

基本的思路:

  • 從引數el指定的節點開始遍歷
  • 解析節點
  • 解析子節點
  • 遞迴的方式完成所有節點的解析
(function (global, factory) {
    global.Vue = factory
})(this, function (options) {

    //1. 解析dom結構
    domParser();

    function domParser() {
        let el = options.el;
         //配置的el以'#'開頭
        if (el.startsWith("#")) {
            el = el.substr(1);
        }
        let app = document.getElementById(el);
        let virtualDom = document.createDocumentFragment();
        for (let i = 0; i < app.childNodes.length; ++i) {
            let node = compile(app.childNodes[i]);
            virtualDom.appendChild(node);
        }
        app.innerHTML = '';
        app.appendChild(virtualDom);
    }
})
  • createDocumentFragment建立虛擬dom的頂層節點
  • 先不考慮compile的實現,在compile中我們將實現變數的解析,事件掛載等,但這裡先不管
  • app.innerHTML = '';暴力的將原始內容全部清除
  • app.appendChild(virtualDom);插入虛擬dom結構

compile(節點編譯)

在dom結構中,每一個節點都是一個Node,Node有不同的型別,雖然標準中型別較多,但常用的就兩種,元素型別(Node.ELEMENT_NODE)和文字型別(Node.TEXT_NODE),比如<p>hello</p>包含兩種型別,<p>為元素型別,hello為文字型別。ELEMENT_NODE值為1,TEXT_NODE為3,在compile中,這兩種型別的解析方式不一樣,TEXT只需解析文字即可,ELEMENT型別需要解析屬性。

function compile(node) {
        let element = node.cloneNode(false);
        for (let i = 0; i < node.childNodes.length; ++i) {
            element.appendChild(compile(node.childNodes[i]));
        }
        if (element.nodeType == 3) {
            //文字型別解析
            let vars = parseVariable(element.textContent);
            for (let i = 0; i < vars.length; ++i) {
                let directive = Directive(element);
                addSubscriber(vars[i], directive);
            }
        } else if (element.nodeType == 1 && element.attributes) {
            //元素型別解析
            let attrs = element.attributes;
            for (let i = 0; i < attrs.length; ++i) {
                let name = attrs[i].name;
                if (name.startsWith("v-bind") || name.startsWith(":")) {
                    let directive = Directive(element, name, attr[i].value);
                    addSubscriber(vars[i], directive);
                }
            }
        }
        return element;
    }
  • cloneNode(false)克隆當前節點,false表示不復制子節點,因為我們需要對子節點進行逐一的解析
  • parseVariable解析文字中變數,也就是類似{{param}}字串
  • Directive內容見下文
  • Subscriber內容見下文
  • 對於元素型別,逐一遍歷屬性,如果屬性名稱以v-bind或者:開頭就認為繫結了變數

parseVariable(變數解析)

function parseVariable(content) {
        var variables = {};
        let m;
        let variableRegExp = new RegExp("\{\{([^\}]+\)}\}", "g");
        while (m = variableRegExp.exec(content)) {
            if (!variables[m[1]]) {
                variables[m[1]] = true;
            }
        }
        var items = [];
        for (k in variables) {
            items.push(k);
        }
        return items;
    }

這裡直接用正則對變數進行解析。

Directive(指令)

這裡是借鑑了vue的概念,在vue中指令可以完成一系列特定的功能,比如指令v-model可以將值繫結到變數,v-for可以對變數進行迴圈。我們這次只完成頁面上使用到指令,我們在這個系列中將實現以下指令

  • v-model:實現雙向繫結
  • v-on: 實現事件繫結
  • title:設定title屬性
  • style:設定style屬性
function Directive(node, attr, expression) {
        var directive = {
            node: node
        }
        if (node.nodeType == 3) {
            directive.change = function (value) {
                this.node.textContent = value;
            }
        } else if (node.nodeType == 1) {
            if (attr === 'title') {
                directive.change = function (value) {
                    this.node.title = value;
                }
            } else if (attr === 'v-model') {
                directive.change = function (value) {
                    this.node.value = value;
                }
                node.addEventListener(('input', function (e) {
                    valueTrigger(expression, e.target.value);
                }))
            } else if (attr === 'style') {
                directive.change = function(name, value) {
                    this.node.style = this.origin.replace("\{\{" + name + "\}\}", value);
                }
            }
        }
        return directive;
    }

定義了一個directive的物件,該物件包含以下屬性

  • node:指令目標節點
  • change:指定具體邏輯

對於文字型別節點(node.nodeType == 3),直接將變數的內容複製給節點,當然這個肯定是不對的,會將其他內容覆蓋,但請放心,我們下一節會解決這個問題,我只不過不想給這一篇引入太多內容,導致大家消化不良。對於元素型別(node.nodeType == 1),如果是雙向繫結,會給節點加上一個input的事件,監聽節點值的變化,並執行valueTrigger的邏輯,該函式邏輯下一篇再講

Subscriber(訂閱者)

如果一個節點繫結了變數,那麼這個節點就是一個Subscriber(訂閱者),變數值發生變化會呼叫節點相關指令(Directive),我們宣告一個subscriber的變數用於存放訂閱者資訊,以變數的名稱作為key,指令作為值,比如有A和B兩個節點繫結了變數name,那麼subscriber的結構如下

{
    name:[
    {
        node:A,
        change:function(){...}
    },{
        node:B,
        change:function(){...}
    }]
}

addSubscriber程式碼如下:

var subscriber = Object.create(null);
function addSubscriber(variableName, directive) {
        let item = subscriber[variableName];
        if (!item) {
            item = [];
        }
        item.push(directive);
        subscriber[variableName] = item;
}

總結


解析節點---->解析變數---->根據繫結的型別關聯指令--->新增訂閱者
                     |
                     |--->雙向繫結監聽值變化--->值變化觸發事件

點選這裡檢視程式碼和效果

參考

點選餘下連結,檢視該系列其他文章

相關文章