数据绑定

上次分析了模板字符串的替换,今天分析数据绑定技术。数据绑定是MVVM框架非常吸引人的一个特性,相信大家都不陌生,如果你没有听过,你可以先阅读一下这篇文章谈谈JavaScript中的双向数据绑定,对背景知识做一个了解。

数据绑定

模板字符串的替换是一种单向数据绑定:

1
2
3
<div id="my_view">
{{ name }} is {{ age }} years old.
</div>

将这里的 nameage 绑定上对应的数据。这一部分的实现方法,昨天已经分析过了。

具体的思路是首先进行正则匹配将模板字符串捕获到,替换成 span 标签,然后监听输入对象,做一个访问控制,当给变量赋值的时候,就去更新对应 span 里面的值。

今天要做的数据绑定是下面这种形式。

1
2
3
4
5
6
<div id="test" sd-on-click="changeMessage | .button">
<p sd-text="msg | capitalize"></p>
<p sd-show="something">YOYOYO</p>
<p class="button" sd-text="msg"></p>
<p sd-class-red="error" sd-text="hello"></p>
</div>

将 id 为 test 的这个 div 的内容做绑定。

<p sd-text="msg | capitalize"></p> 表示 P 绑定变量 msg 的值,并且通过一个 capitalize 的过滤器。

<p sd-show="something">YOYOYO</p> 表示如果 something 变量有值,则这个 p 显示,否则就不显示。

<p sd-class-red="error" sd-text="hello"></p> 表示如果 error 有值,则 perror 类,并且这个 p 的内容是 hello 变量的值。

<div id="test" sd-on-click="changeMessage | .button"> 表示click事件触发 changeMessage 事件,并且通过一个过滤器,这个过滤器表示有 button 类的元素才能触发这个事件,在这里就是<p class="button" sd-text="msg"></p>,并且这个 p 里面的内容绑定 msg 变量。

指令

我们把标签上的 sd- 前缀的标志称为指令,这里有4条指令:sd-text, sd-show, sd-class-red, sd-on-click。针对4这条指令我们要编写4个对应的规则处理这四条指令。

对于sd-text指令, 指定DOM元素el和值value

1
2
3
text: function(el, value){
el.textContent == value || '';
}

对于sd-show指令,指定DOM元素el和值value

1
2
3
show: function(el, value){
el.style.display == value ? '' : 'none';
}

对于sd-class指令,与前面两个不同,它有三个参数,DOM元素,值value,类名className

1
2
3
class: function(el, value, className){
el.classList[value? 'add': 'remove'](className);
}

sd-on指令最为复杂,这是一个事件处理事件指令,有一个参数是事件处理函数 handle,和前面指令的 value 一样。有一个 event 元素,表明要绑定的事件名称。 此外还有一个过滤器,可以过滤事件绑定的对象,过滤器的输入是一个选择器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
on: {
update: function(el, handle, event){
if(handle){
handle.bind(el) // 将事件的this绑定到元素上
el.addEventListener(event,handle);
}
},
customFilter: function(handler, selector){
return function(e){
var match = e.target.webkitMatchesSelector(selector);

if(match) handler.apply(this,arguments);
}
}
}

我们还需要将已经绑定的事件解绑,并且过滤器的 selector 也有可能不只一个,所以更完整的写法如下。

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
on: {
update: function(el, handle, event, directive){
if(!directive.handles){
directive.handles = {}
}
var handles = directive.handles;
if(handles[event]){
el.removeEventListener(event, handles[event]);
}
if(handle){
handle.bind(el) // 将事件的this绑定到元素上
el.addEventListener(event,handle);
handles[event] = handle;
}
},
customFilter: function(handler, selectors){
return function(e){
var match = selectors.every(function(selector){
return e.target.webkitMatchesSelector(selector);
})

if(match) handler.apply(this,arguments);
}
}
}

这里directive对象,存储指令相关的一些参数。

过滤器

需要一个 capitalize 过滤器, 将传入的值首字母大写。

1
2
3
4
capitalize: function(value){
value = value.toString();
return value.charAt(0).toUpperCase() + value.slice(1);
}

绑定

分析完指令和过滤器之后,接下来分析绑定过程,也是最重要的一部分。

绑定的整个流程如下:

  1. 提取需要数据绑定的元素
  2. 提取这些元素中数据绑定的属性
  3. 解析这个属性
  4. 将属性的参数绑定到指令上
  5. 设置访问器属性
  6. 如果有过滤器要调用过滤器来处理

提取元素

利用属性选择器提取元素,选择器的值为[sd-text],[sd-show],[sd-class],[sd-on]

1
2
3
4
5
6
7
8
var prefix = 'sd',
Filters = require('./filters'),
Directives = require('./directives'),
selector = Object.keys(Directives).map(function(d){
return '[' + prefix + '-' + d +']';
}).join()

var els = root.querySelectorAll(selector);

处理DOM节点

通过上一步得到元素节点之后,我们需要将他们一一处理,这一步需要做的是将元素中的属性提取出来,并且将属性绑定到指令上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function processNode(el){
// 提取属性
copeAttribute(el.attributes).forEach(function(attr){
// 处理属性
var directive = parseDirective(attr)
if(directive){
// 绑定属性
bindDirective(arguments)
}
})


}

function copyAttribute(attributes){
return [].map.call(attributes, function(attr){
return {
name: attr.name,
value: attr.value
}
})
}

处理属性

处理属性是将属性的参数提取出来,存在 directive 变量当中。

假设得到 sd-on-click="changeMessage | .button" 这样一个属性,我们将属性的参数提取出来。

从属性中可以得到4个参数,on,click, changeMessage, .button,对于第一个参数on,我们需要的是前面定义的on指令。

>
attr <-> ‘sd-on-click=”changeMessage | .button”‘
key <-> changeMessage
filters <-> .button
definition <-> Directives[‘on’]
argument <-> click
update <-> definition === ‘function’ ? defination : defination.update

通过这一步返回一个 directive 对象,里面包含上述6个参数。

绑定属性指令

通过处理属性得到的指令对象要与我们的元素绑定起来directive.el = el

与绑定模板字符串时候一样,这里也需要一个bingdings对象。bingdings是一个映射表,每一个key对应一个绑定对象binding。这里的key就是处理属性得到的key,同时这个key也是你绑定的参数的值。

>
bindings[key] <-> bingding
binding: {
value: undefined (等于你传入的值)
directives: [] (key相同的指令组成的一个数组)
}

绑定访问控制器

如果你不知道什么是访问控制器, 可以参考Object.defineProperty()中的set和get

我们需要为谁绑定控制器呢?

如果是为 bindingvalue 绑定控制器,那么每一次设置这个值的时候都会调用set函数,但是用户并不会直接去设置这个值,也不能直接去设这个值,因为在数据绑定的时候这些对用户来说都是不可见的。

所以要为用户传入的对象绑定控制器。这里我们约定用户传入的对象为 scope,那么为 scope 绑定访问控制器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Object.defineProperty(scope, directive.key, {
set: function(newValue){
binding.value = newValue
binding.directives.forEach(function(directive){
if(newValue && directive.filters){
// 调用过滤器
newValue = applyFilters(value, directive)
}

// 调用指令中的方法
directive.update(
directive.el,
newValue,
directive.argument,
directive
)
})
}
})

应用过滤器

经过上面的分析,整个流程已经很清楚了。下面分析最后一步,为有过滤器的指令绑定过滤器。

过滤器分两种,一种是定义在Filter中的capitalize过滤器,一种是定义在指令on中的customFilter过滤器。针对这两种过滤器我们进行分类处理。

1
2
3
4
5
6
7
8
9
10
11
12
function applyFilters (value, directive) {
if(directive.defination.customFilter){
return directive.defination.customFilter(value,directive.filters);
}else{
directive.filters.forEach(function (filter) {
if (Filters[filter]) {
value = Filters[filter](value)
}
})
return value
}
}

这里解释一下customFilter, 这个filter返回一个函数,这个函数是事件处理函数。

1
2
3
4
5
6
7
8
9

customFilter: function(handle, selectors){
return functiion(e){
var match = selectors.every(function(selector){
e.target.webkitMatchesSelector(selector)
})
if(match) handle.apply(this, arguments)
}
}

这个filter接收的第一个参数是事件处理函数,是用户通过scope传入的,通过闭包将其存起来,然后特定的元素调用这个事件处理函数。

总结

对于这个单向数据绑定,总结一下整个处理流程

  1. 获取绑定了指令的dom节点
  2. 提取这个dom上的属性
  3. 分析属性,将属性的值存在directive中
  4. 将directive根据他的key放在binding中
  5. 如果这个directive有filters,那么调用过滤器
  6. 将用户传入的scope对象设置访问控制器
  7. 用户设置参数值,就可以绑定到dom上

源码可以看vue naive implementation,
运行component命令安装插件,然后将dev.html的js指向build文件夹的build.js。