指令分析-sd-each

代码与效果

通过标签上”sd-“开头的指令绑定数据与操作。sd-each是众多指令中的一个,可以实现标签的循环输出。例如 sd-each = "todo:todos" 就是要将 todos 数组中的每一个对象都输出。

概述

有下面一段HTML片段,要将对应的对象的值正确的绑定到对应的标签当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app" sd-controller="TodoList" sd-on="click:changeMessage | delegate .button">
<p sd-text="msg | capitalize"></p>
<p sd-text="msg | uppercase"></p>
<p sd-on="click:remove">bye</p>
<p sd-text="total | money"></p>
<p class="button">Change Message</p>
<p sd-class="red:error" sd-show="error">Error</p>
<ul sd-show="todos">
<li class="todo"
sd-controller="Todo"
sd-each="todo:todos"
sd-class="done:todo.done"
sd-on="click:changeMessage, click:todo.toggle"
sd-text="todo.title"
></li>

</ul>
</div>

数据绑定的原理,我们上一节已经分析过了。总体来说分为以下几个步骤,首先是提取标签中的指令,分析指令,将指令中的参数提取出来,为这些指令和标签做绑定操作,通过绑定操作可以实现对象的属性与标签的值进行绑定,或者将事件处理函数与标签的事件进行绑定。

回到上面的片段,ul中的li有一个sd-each指令,是将todos数组的内容与标签相绑定。我们如何去实现呢?

功能设计

sd-each 节点也和普通的节点一样,可以绑定诸如 sd-text,sd-class等指令。它的不同之处在于它的数目等于绑定的数组的长度。

现在,整个框架的生命周期分为两个阶段。一个是绑定阶段,一个是更新数据阶段。框架首先将指令收集起来,然后再在数据赋值的时候更新DOM节点。

针对sd-each,首先在绑定阶段在li这个位置做一个标记,之后在更新阶段在标记位置插入绑定了数据的节点。

除去sd-each指令,这个节点与普通节点一样,我们循环将其他指令进行正确的绑定,输出正确的DOM节点,插入指定位置。

Seed对象在传入节点和数据之后可以生成绑定了数据的节点,因此,我们可以将sd-each的节点作为Seed对象的输入,生成一个绑定了对象的节点输出。

代码设计

通过文字解答有些复杂,直接看each指令的代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
each: {
bind: function () {
this.el.removeAttribute(config.prefix + '-each')
this.prefixRE = new RegExp('^' + this.arg + '.')
var ctn = this.container = this.el.parentNode
this.marker = document.createComment('sd-each-' + this.arg + '-marker')
ctn.insertBefore(this.marker, this.el)
ctn.removeChild(this.el)
this.childSeeds = []
},
update: function (collection) {
if (this.childSeeds.length) {
this.childSeeds.forEach(function (child) {
child.destroy()
})
this.childSeeds = []
}
watchArray(collection, this.mutate.bind(this))
var self = this
collection.forEach(function (item, i) {
self.childSeeds.push(self.buildItem(item, i, collection))
})
console.log('collection creation done.')
},
mutate: function (mutation) {
console.log(mutation)
},
buildItem: function (data, index, collection) {
var Seed = require('seed/src/seed.js'),
node = this.el.cloneNode(true)
var spore = new Seed(node, data, {
eachPrefixRE: this.prefixRE,
parentSeed: this.seed
})
this.container.insertBefore(spore.el, this.marker)
collection[index] = spore.scope
return spore
}
}

bind函数,是在绑定阶段运行的钩子函数。创建一个注释节点作为标记,在数据更新之后将节点插入标记节点之前。

还有几个细节,在bind函数中提取了sd-each指令的参数todo,todo是其他指定绑定值的一个前缀。创建了一个childSeeds节点,存放绑定后的Seed对象,在下一次绑定的时候要先将这些节点清除。

update函数,是在更新阶段调用的函数。collection参数是传入的数据数组,对collection中的每一个元素,调用buildItem函数去生成绑定数据的节点。

buildItem函数,将数据以及sd-each节点进行绑定,生成绑定后的对象spore,对象的el属性是要输出的DOM节点,插入标记节点之前,并且将spore对象存入childSeeds数组。

嵌套作用域

sd-each指令在上一节进行了详细的分析,主要思想就是将sd-each节点根据数据数组进行数据绑定。

如果节点绑定的sd-text不是todo.title而是父级作用域中的msg,需要怎么处理?

在给msg赋值的时候,sd-each的节点要随之数据进行更新。但是如果我们的sd-each的赋值操作在msg的赋值操作之后,那么我们就无法正确的解析sd-each节点,并且绑定上数据。

所以,sd-each指令的数据更新工作要比其他的指令要早,在其他指令的绑定阶段,我们的sd-each节点就要进行数据的更新。因为sd-each节点的数据更新阶段才会解析指令。

总结

sd-each指令的实现,要通过调用Seed构造函数生成节点,并且在更新阶段才调用这一构造函数,因此为了实现作用域的嵌套,我们要在其他数据的绑定阶段就将这一指令的数据进行更新。