概述
在上一篇中,我们实现了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代码点击这里查看
参考
点击以下链接,查看该系列其他文章
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。