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

背景

上一篇中,咱們保證了代碼不報錯,而且參數也成功傳進去了,但未實現任何的邏輯,這一章咱們的工做就是完成dom的解析,在vue中,經過構建虛擬dom結構實現dom的高效更新和渲染,模仿這個思路,咱們構建一個超級簡單虛擬的dom結構,本着讓全部人都能看得懂的目的,此次構建的dom結構不考慮性能,不考慮設計,以能實現功能爲目的。javascript

domParser(dom解析)

基本的思路:html

  • 從參數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類型須要解析屬性。vue

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

這裏直接用正則對變量進行解析。java

Directive(指令)

這裏是借鑑了vue的概念,在vue中指令能夠完成一系列特定的功能,好比指令v-model能夠將值綁定到變量,v-for能夠對變量進行循環。咱們此次只完成頁面上使用到指令,咱們在這個系列中將實現如下指令node

  • 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的對象,該對象包含如下屬性vuex

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

對於文本類型節點(node.nodeType == 3),直接將變量的內容複製給節點,固然這個確定是不對的,會將其餘內容覆蓋,但請放心,咱們下一節會解決這個問題,我只不過不想給這一篇引入太多內容,致使你們消化不良。對於元素類型(node.nodeType == 1),若是是雙向綁定,會給節點加上一個input的事件,監聽節點值的變化,並執行valueTrigger的邏輯,該函數邏輯下一篇再講express

Subscriber(訂閱者)

若是一個節點綁定了變量,那麼這個節點就是一個Subscriber(訂閱者),變量值發生變化會調用節點相關指令(Directive),咱們聲明一個subscriber的變量用於存放訂閱者信息,以變量的名稱做爲key,指令做爲值,好比有A和B兩個節點綁定了變量name,那麼subscriber的結構以下json

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

addSubscriber代碼以下:app

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

總結

解析節點---->解析變量---->根據綁定的類型關聯指令--->新增訂閱者
                     |
                     |--->雙向綁定監聽值變化--->值變化觸發事件

點擊這裏查看代碼和效果框架

參考

點擊餘下連接,查看該系列其餘文章

相關文章
相關標籤/搜索