概述
在上一篇中,我們實現了vue物件的構建,並且已經初步實現了變數的繫結和事件繫結,現在我們就剩下一個問題需要解決,就是v-for
指令的實現,這也是本系列中最難的部分。
難點
實現v-for有以下幾個難點
- 表示式解析,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指令
function isLoop(element) {
return element.attributes && element.attributes['v-for'];
}
compile函式遞迴編譯子節點從
for (let i = 0; i < node.childNodes.length; ++i) {
element.appendChild(compile(node.childNodes[i]));
}
修改為
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。
解析
編譯的第一步就是解析,需要解析三部分的內容
- 迴圈的陣列變數
- 迴圈過程中變數名
- 迴圈過程中元素下標
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:迴圈下標
元素生成
解析完成後就可以開始生成元素
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程式碼點選這裡檢視
參考
點選以下連結,檢視該系列其他文章