概述

在上一篇中,咱们实现了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代码点击这里查看

参考

点击以下链接,查看该系列其余文章

  • 模拟vue本人入手写响应式框架(一) – Vue实现todo利用
  • 模拟vue本人入手写响应式框架(二) – Vue对象创立用
  • 模拟vue本人入手写响应式框架(三) – dom解析
  • 模拟vue本人入手写响应式框架(四) – Vue对象构建
  • 模拟vue本人入手写响应式框架(五)终章 – v-for指令实现