模仿vue本身動手寫響應式框架(五)終章 - v-for指令實現

概述

上一篇中,咱們實現了vue對象的構建,而且已經初步實現了變量的綁定和事件綁定,如今咱們就剩下一個問題須要解決,就是v-for指令的實現,這也是本系列中最難的部分。javascript

難點

實現v-for有如下幾個難點html

  • 表達式解析,v-for有兩種語法item in items(item,index) in items,第二種能夠獲取到序號,程序須要解析這兩種語法
  • 編譯v-for內的元素,雖然已經有了compile函數,可是v-for循環內的上下文和vue並不一致,什麼意思呢,compile裏面綁定的值和變量是vue,vue是全局的,但v-for內綁定的變量是循環內的,每次都不同

編譯

在compile中,若是遇到v-for會先將v-for內的節點所有生成好,再做爲子節點append到父節點上,所以第一步就是判斷是否包含v-for指令vue

function isLoop(element) {
        return element.attributes && element.attributes['v-for'];
    }

compile函數遞歸編譯子節點從java

for (let i = 0; i < node.childNodes.length; ++i) {
            element.appendChild(compile(node.childNodes[i]));
        }

修改成node

for (let i = 0; i < node.childNodes.length; ++i) {
            let child = node.childNodes[i];
            if (isLoop(child)) {
                let ns = compileLoop(child, element);
                for (let j = 0; j < ns.length; ++j) {
                    element.appendChild(ns[j]);
                }
            } else {
                element.appendChild(compile(child));
            }
        }

compileLoop會對v-for節點進行編譯,而且返回節點數組,父節點對返回的節點進行append。vuex

解析

編譯的第一步就是解析,須要解析三部分的內容數組

  • 循環的數組變量
  • 循環過程當中變量名
  • 循環過程當中元素下標
let vfor = element.attributes['v-for'].value;
let itemName;
let indexName;
let varName;
let loopExp1 = /\(([^,]+),([^\)]+)\)\s+in\s+(.*)/g;
let loopExp2 = /(\w+)\s+in\s+(.*)/g;
let m;
if (m = loopExp1.exec(vfor)) {
    itemName = m[1];
    indexName = m[2]
    varName = m[3];
} else if (m = loopExp2.exec(vfor)) {
    itemName = m[1];
    varName = m[2];
}

直接用正則進行解析,loopExp1和loopExp2分別對應兩種語法,varName:數組名,itemName:循環變量名,indexName:循環下標閉包

元素生成

解析完成後就能夠開始生成元素app

var directive = {
    origin: element.cloneNode(true),
    attr: 'v-for',
    exp: {
        varName: varName,
        indexName: indexName,
        itemName: itemName
    }
}
element.attributes.removeNamedItem('v-for');
let arrays = vue[varName];
let elements = [];
for (let i = 0; i < arrays.length; ++i) {
    vue[itemName] = arrays[i];
    vue[indexName] = i;
    elements.push(compile(element.cloneNode(true), false));
}
if (!loopElement[varName]) {
    let loop = {};
    loop.elements = elements;
    loop.parent = parent;
    loopElement[varName] = loop;
}
  • 定義了一個變量directive,把v-for一些語法也作了保存,下次能夠直接用,無需再次解析
  • 由於是用clone生成,所以須要移除掉v-for標籤,否則會進入死循環
  • 遞歸調用compile生成新元素,在每一次循環都將當前變量和下標放到vue中,保證了編譯的時候程序能夠找到變量
for (let i = 0; i < arrays.length; ++i) {
    vue[itemName] = arrays[i];
    vue[indexName] = i;
    elements.push(compile(element.cloneNode(true), false));
}
  • 將結果保存到loopElement中,保存的目的是,當綁定的數組發生變化時,須要刪除當前相關節點從新生成新的節點

指令

directive.change = function (name, value) {
    let ele = loopElement[name];
    for (let i = 0; i < ele.elements.length; ++i) {
        ele.elements[i].remove();
    }
    let newEles = [];
    let arrays = vue[this.exp.varName];
    for (let i = 0; i < arrays.length; ++i) {
        vue[this.exp.itemName] = arrays[i];
        vue[this.exp.indexName] = i;
        let node = compile(this.origin.cloneNode(true));
        newEles.push(node);
    }
    loopElement[name].elements = newEles;
    for (let j = 0; j < newEles.length; ++j) {
        ele.parent.appendChild(newEles[j]);
    }
}
addSubscriber(varName, directive);
  • 先對當前元素進行移除
  • 和上面的邏輯同樣,生成新的元素
  • 經過以前保存的parent進行append
  • addSubscriber建立訂閱者將指令註冊到訂閱者中

完整的compileLoop代碼以下框架

function compileLoop(element, parent) {
    let vfor = element.attributes['v-for'].value;
    let itemName;
    let indexName;
    let varName;
    let loopExp1 = /\(([^,]+),([^\)]+)\)\s+in\s+(.*)/g;
    let loopExp2 = /(\w+)\s+in\s+(.*)/g;
    let m;
    if (m = loopExp1.exec(vfor)) {
        itemName = m[1];
        indexName = m[2]
        varName = m[3];
    } else if (m = loopExp2.exec(vfor)) {
        itemName = m[1];
        varName = m[2];
    }
    var directive = {
        origin: element.cloneNode(true),
        attr: 'v-for',
        exp: {
            varName: varName,
            indexName: indexName,
            itemName: itemName
        }
    }
    element.attributes.removeNamedItem('v-for');
    let arrays = vue[varName];
    let elements = [];
    for (let i = 0; i < arrays.length; ++i) {
        vue[itemName] = arrays[i];
        vue[indexName] = i;
        elements.push(compile(element.cloneNode(true), false));
    }
    if (!loopElement[varName]) {
        let loop = {};
        loop.elements = elements;
        loop.parent = parent;
        loopElement[varName] = loop;
    }
    directive.change = function (name, value) {
        let ele = loopElement[name];
        for (let i = 0; i < ele.elements.length; ++i) {
            ele.elements[i].remove();
        }
        let newEles = [];
        let arrays = vue[this.exp.varName];
        for (let i = 0; i < arrays.length; ++i) {
            vue[this.exp.itemName] = arrays[i];
            vue[this.exp.indexName] = i;
            let node = compile(this.origin.cloneNode(true));
            newEles.push(node);
        }
        loopElement[name].elements = newEles;
        for (let j = 0; j < newEles.length; ++j) {
            ele.parent.appendChild(newEles[j]);
        }
    }
    addSubscriber(varName, directive);
    return elements;
}

事件響應

上一篇中咱們的事件響應是這麼寫的

function addEvent(element, event, method) {
    element.addEventListener(event, function(e) {
        let params = [];
        let paramNames = method.params;
        if (paramNames) {
            for (let i = 0; i < paramNames.length; ++i) {
                params.push(vue[paramNames[i]]);
            }
        }
        vue[method.name].apply(vue, params);
    })
}

這麼寫對於循環有個問題,由於每次循環都會重置下標和循環變量,下標和循環變量都是保存在vue對象中的,因此當事件觸發時,params.push(vue[paramNames[i]]);這行代碼是取不到值的由於上下文已經發生變化。解決這個問題的辦法就是閉包,經過閉包保存當時環境信息,不至於運行時丟失,只需將獲取數據移到外面就行。

function addEvent(element, event, method) {
    let params = [];
    let paramNames = method.params;
    if (paramNames) {
        for (let i = 0; i < paramNames.length; ++i) {
            params.push(vue[paramNames[i]]);
        }
    }
    element.addEventListener(event, function (e) {
        vue[method.name].apply(vue, params);
    })
}

到這裏就能夠實現v-for指令,但以前的一些遺留還未修復,咱們在dom解析這篇中提到目前對於文本節點值發生變化只是簡單的文本替換,以下:

if (node.nodeType == 3) {
    directive.change = function(name, value) {
        this.node.textContent = this.origin.replace("\{\{" + name + "\}\}", value);
    }
}

若是有多個變量或者相似todo.text這種多級變量結果就會出錯,這裏寫了一個專門用來解析表達的函數

if (node.nodeType == 3) {
    directive.change = function (name, value) {
        this.node.textContent = evaluteExpression(this.origin);
    }
}
  • evaluteExpression
function evaluteExpression(text) {
    let vars = parseVariable(text);
    for (let i = 0; i < vars.length; ++i) {
        let value = getVariableValue(vars[i]);
        text = text.replace("\{\{" + vars[i] + "\}\}", value);
    }
    return text;
}
  • 先對變量進行解析
  • 循環獲取變量值,經過調用getVariableValue
  • 循環替換
  • getVariableValue
function getVariableValue(name) {
    let value;
    if (name.indexOf(".")) {
        let ss = name.split(".");
        value = vue[ss[0]];
        if (value) {
            for (let i = 1; i < ss.length; ++i) {
                value = value[ss[i]];
                if (value == undefined) {
                    break;
                }
            }
        }
    } else {
        value = vue[name];
    }
    if (value == undefined || value == null) {
        value = "";
    }
    return value;
}
  • 相似item.text的多級變量進行循環獲取值
  • 若是未定義設置爲空字符串

效果

如下是實現的效果圖,也能夠點擊這裏進行查看

完整js代碼點擊這裏查看

參考

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

相關文章
相關標籤/搜索