上次分析了模板字符串的替换,今天分析数据绑定技术。数据绑定是MVVM框架非常吸引人的一个特性,相信大家都不陌生,如果你没有听过,你可以先阅读一下这篇文章谈谈JavaScript中的双向数据绑定,对背景知识做一个了解。
数据绑定
模板字符串的替换是一种单向数据绑定:
1 | <div id="my_view"> |
将这里的 name
和 age
绑定上对应的数据。这一部分的实现方法,昨天已经分析过了。
具体的思路是首先进行正则匹配将模板字符串捕获到,替换成 span
标签,然后监听输入对象,做一个访问控制,当给变量赋值的时候,就去更新对应 span
里面的值。
今天要做的数据绑定是下面这种形式。
1 | <div id="test" sd-on-click="changeMessage | .button"> |
将 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
有值,则 p
有 error
类,并且这个 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 | text: function(el, value){ |
对于sd-show指令,指定DOM元素el和值value
1 | show: function(el, value){ |
对于sd-class指令,与前面两个不同,它有三个参数,DOM元素,值value,类名className
1 | class: function(el, value, className){ |
sd-on指令最为复杂,这是一个事件处理事件指令,有一个参数是事件处理函数 handle,和前面指令的 value 一样。有一个 event 元素,表明要绑定的事件名称。 此外还有一个过滤器,可以过滤事件绑定的对象,过滤器的输入是一个选择器。
1 | on: { |
我们还需要将已经绑定的事件解绑,并且过滤器的 selector 也有可能不只一个,所以更完整的写法如下。
1 | on: { |
这里directive对象,存储指令相关的一些参数。
过滤器
需要一个 capitalize 过滤器, 将传入的值首字母大写。
1 | capitalize: function(value){ |
绑定
分析完指令和过滤器之后,接下来分析绑定过程,也是最重要的一部分。
绑定的整个流程如下:
- 提取需要数据绑定的元素
- 提取这些元素中数据绑定的属性
- 解析这个属性
- 将属性的参数绑定到指令上
- 设置访问器属性
- 如果有过滤器要调用过滤器来处理
提取元素
利用属性选择器提取元素,选择器的值为[sd-text],[sd-show],[sd-class],[sd-on]
1 | var prefix = 'sd', |
处理DOM节点
通过上一步得到元素节点之后,我们需要将他们一一处理,这一步需要做的是将元素中的属性提取出来,并且将属性绑定到指令上。
1 | function processNode(el){ |
处理属性
处理属性是将属性的参数提取出来,存在 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。
我们需要为谁绑定控制器呢?
如果是为 binding
的 value
绑定控制器,那么每一次设置这个值的时候都会调用set函数,但是用户并不会直接去设置这个值,也不能直接去设这个值,因为在数据绑定的时候这些对用户来说都是不可见的。
所以要为用户传入的对象绑定控制器。这里我们约定用户传入的对象为 scope
,那么为 scope
绑定访问控制器。
1 | Object.defineProperty(scope, directive.key, { |
应用过滤器
经过上面的分析,整个流程已经很清楚了。下面分析最后一步,为有过滤器的指令绑定过滤器。
过滤器分两种,一种是定义在Filter中的capitalize过滤器,一种是定义在指令on中的customFilter过滤器。针对这两种过滤器我们进行分类处理。
1 | function applyFilters (value, directive) { |
这里解释一下customFilter, 这个filter返回一个函数,这个函数是事件处理函数。
1 |
|
这个filter接收的第一个参数是事件处理函数,是用户通过scope传入的,通过闭包将其存起来,然后特定的元素调用这个事件处理函数。
总结
对于这个单向数据绑定,总结一下整个处理流程
- 获取绑定了指令的dom节点
- 提取这个dom上的属性
- 分析属性,将属性的值存在directive中
- 将directive根据他的key放在binding中
- 如果这个directive有filters,那么调用过滤器
- 将用户传入的scope对象设置访问控制器
- 用户设置参数值,就可以绑定到dom上
源码可以看vue naive implementation,
运行component命令安装插件,然后将dev.html的js指向build文件夹的build.js。