simpleTemplate.js 中列表渲染的改进

无关痛痒的碎碎念

在写上一篇博文的时候,我一边写文字一边看代码,同时将编写代码的思维写进博文。这个过程显然让我从跟编程时不同的角度来审视代码,由此也能发现代码中新的bug。由此可见,写博文对于重新理清思路,理解代码有非常大的帮助,是另一种意义上的“代码审查”。

另外在解释一些代码的时候,本以为自己是明白为何这样写的,但是一旦将这个“为何”形成文字,再看一遍,顿时就觉得其实并不是真的“懂”了,而是只是脑子里觉得“啊差不多是那个样子”,手就自然出代码了。然而一旦要解释给别人,要不就错漏百出要不就哑口无言。

也不知道现在搞技术的人有多少能写出详细的文章出来了,似乎很多理工科人都非常讨厌一直以来的语文教育,进而讨厌写文章,于是更不可能写出长篇幅、细致的文章来。

开始正文吧

上一篇文章中,simpleTemplate.js已经得到了极大的改造,能够识别和渲染标志位和列表。在文章的最后提到了实际上列表的渲染还没有完成,充其量只是实现了一个循环而已,于是本文就来继续完善。

要解决什么问题

如何在列表渲染中得到跟本次渲染相关的临时数据。

在渲染,也就是执行_render函数的时候,可供渲染的数据源是指向固定但是内容未知的。数据源的指向取决于第一次执行_render时传入的参数(形参是scope),在函数内部递归调用的时候也是将这个数据传递给下一次调用执行。

然而,当在渲染列表的时候,渲染函数是已经确切得到了数据源的某一个数组型数据的(不然就无法根据这个数组进行递归),因此是绝对有能力提供这个数组的临时数据的。

什么是临时数据?这个名词是我自己编的。其实就是指遍历数组的过程中所产生的下标和下标所对应的数据。并不是什么很复杂的东西。

很简单,举个例子。比如有这么一个数据:

1
2
3
{
name: ['Ada', 'Brown', 'Cindy']
}

配合这么一个模板:

1
2
3
4
5
6
7
<ul>
{@name}
<li>

</li>
{-name}
</ul>

如果要求是输出数组name中的所有名字,那么<li></li>之间应该写什么?

好吧实际上什么也写不了。

注意到在列表渲染的过程中,调用函数进行递归的时候,传入的数据跟当前正在执行的函数被调用时传入的数据是一样的,临时产生的数据根本就不能传过去。这难道就是世界上最遥远的距离?

怎么给原始的数据添加进临时数据呢?

首先想到也是立刻抛弃的想法就是直接在原始数据源中添加,因为之前也说过了,原始数据源是未知的。无论新添加的数据取什么样的键值(原始数据源是一个对象),都有可能会覆盖原来已经存在的数据。

将原始数据源和临时数据包装起来成一个新的数据,如何?

且不论这样会大大增加渲染函数的复杂性,若是出现列表多层嵌套,这数据都成卷心菜了……

作用域

其实这个功能的实现的确有点像作用域。

在循环的外部,是一个作用域,称为A;在循环的内部,是另一个作用域,称为B。B可以访问A内定义的变量(数据),而A不应该访问B内定义的变量(数据)。

于是这里就是javascript中的原型链大显身手的时候了。(其实我也是借用了AngularJS中$scope的实现思路。)

将A作用域中的变量(数据)作为B作用域中的变量(数据)的原型(prototype),那么B中建立的数据就不会实质改写A中的数据(注意,只是大部分情况下。有某些特殊情况还是可以改写的),而在B中也能够读取到A中的数据。

强烈建议不明白原型链的人好好去了解一下。这个跟OOP中的继承是不同的概念,很多人可能会混淆了。

如何构造这个关系?让我们来站在巨人的肩膀上,使用大师Douglas Crockford的代码。

1
2
3
4
5
6
7
8
// Thank you, Douglas Crockford.
if ( typeof Object.create !== 'function' ) {
Object.create = function ( o ) {
function F() {}
F.prototype = o;
return new F();
};
}

经典的一段代码,出现在大师这篇文章

简单来说,用法是这样的A = Object.create( B ),这样B就成为了A的原型。

在代码中的顶层空间中添加上大师的代码后,在渲染函数内找到渲染列表的代码中加入两行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case '@': // begin of list
if ( Object.prototype.toString.call( data ) === '[object Array]' ) {
for ( var loopIndex = 0; loopIndex < data.length; loopIndex++ ) {
// expend scope
newScope = Object.create( scope );
newScope['*'] = data[loopIndex];

// recursively render
tempFragment.push( _render.call( this, newScope, i + 1, functions['loop'][i]) );
}
}

// reset index
i = functions['loop'][i];
break;

在新建的数据源newScope中,使用一个原则上不太可能会用来作为键值的*符号作为临时数据的键值。

于是之前的模板写成这样就达到要求。

1
2
3
4
5
6
7
<ul>
{@name}
<li>
{*}
</li>
{-name}
</ul>

别忘了修改用于识别的正则表达式!

1
/\{\s*([@|\-|!]?)(([\w\d]+|\*)(\.[\w\d]+)*)\s*\}/gm

或许这里可能有人会吐槽说,如果原始的数据源中有一个键值是*的数据怎么办?这样不就被循环中的新数据源覆盖了吗?关于这点我想说的是如果产生原始数据源的程序员使用了这么奇怪无意义的键值,就真是脑残了。这个问题基本可以不讨论。