2017-07-25
{{dataBinding}}
利用双花括号完成数据绑定的思路
此篇接上一篇 利用 @click 绑定事件
上一篇完成了如下代码:
00// root 是根节点 
01// config 是传入配置 上面写着对应事件的 handler
02function tplParser(root, config){
03    Array.from(root.children).forEach(child => {
04        // child 的属性 attributes 
05        Array.from(child.attributes).forEach(attr => {
06            if (attr.name.startsWith('@') && attr.value in config){
07                let eventName = attr.name.slice(1); 
08
09                child.addEventListener(
10                    // such as "click"
11                    eventName,
12                    // such as () => console.log('clicking!') 
13                    config[attr.value]
14                );
15            }
16        }); 
17
18        // Just Regard Child As A Tree's Root 
19        tplParser(child, config); 
20    })
21}
本篇将会在第 02 行行尾插入如下代码以完成属性结点和文本结点的数据绑定。
00dataBinding(root, config);

# replacer

数据绑定的关键点在于从字符串中匹配出 {{ name }},类似的原理也可以作用于模版渲染,可以把 HTML 当作是一段模版,然后当数据的setter被触发的时候,再把新渲染的数据重写回 DOM 里。
关于如何从模版里提取出花括号,我使用了正则来完成:
00var exp = /{{(.*?)}}/g;
利用 match 方法,可以判断是否是数据模版:
00"hello, {{ name }}".match(exp); 
01// => 
02// ["{{ name }}"]
03
04"there is no data binding".match(exp); 
05// =>
06// null
进一步的,我们需要从中将 name 替换成 config.data 里面的属性,这时候需要用到 String.prototype.replace 了。
通过 MDN 可以知道,replace 的用法可能比想象中的复杂的多:
00str.replace(RegExp | String, Function | String);
第一个参数可以是正则也可以是普通字符串,第二个参数也类似 ———— 可以是函数或者字符串。特别的,当第二个参数也是字符串的时候,这时候 replace 是一阶函数,当第二个参数是函数的时候,replace 将成为高阶函数。
先来看看作为一阶函数调用的情况:
00"hello, {{ name }}".replace("hello", "world"); 
01// => replace(String, String)
02// "world, {{ name }}"
03
04"hello, {{ name }}".replace(exp, "nginx"); 
05// => replace(RegExp, String)
06// "hello, nginx"
07
08"hello, {{ name }}, {{ name2 }}".replace(exp, "nginx"); 
09// => replace(RegExp, String)
10// "hello, nginx, nginx"
可以发现,他们都不能把 name 这个内串提取出来,为了弥补这个缺陷,才会有作为高阶函数调用的 replace 形式:
  1. replace(String, Function);
  2. replace(RegExp, Function);
这个被传进去的函数姑且称作 replacer 把,它的参数长度是不定的:
00function replacer(match, p1, p2, p3, p4, p5, ...., offset, string){
01
02}
p1 p2 p3 表示第 n 个括号匹配的字符串,这里的正则只有一个括号,因此 p1 指的就是匹配到的字符串,即 name
00"{{ welcome }} {{ to }} {{ ecznBlog }}".replace(exp, (match, p1) => {
01    console.log(p1); 
02}); 
03// => 
04// welcome 
05// to 
06// ecznBlog
因此渲染一段带双括号的函数如下所示:
00var render = template => data => {
01    var exp = /{{(.*?)}}/g;
02
03    return template.replace(exp, (_, p1) => {
04        return data[p1.trim()];
05    }); 
06}
使用:
00var tpl = `
01    <header>
02        <h1>Hello, {{ name }}</h1>
03        <div class="title">{{ title }}</div>
04    </header>
05`; 
06
07var headerGenerator = render(tpl); 
08
09console.log(
10    headerGenerator({
11        name: "varEczn", 
12        title: "Personal, Blog"
13    })
14)
15// => 
16// <header>
17//     <h1>Hello, varEczn</h1>
18//     <div class="title">Personal, Blog</div>
19// </header>
之后再利用这个渲染器,进行 settergetter 的重写即可。

# 完整实现

00function dataBinding(root, config){
01    Array.from(root.childNodes).concat(
02        Array.from(root.attributes)
03    ).filter(
04        e => e.nodeType === 3 || e.nodeType === 2
05    ).forEach(node => {
06        if (node.nodeType === 3){
07            var matches = node.wholeText.match(exp);
08            var tpl = node.wholeText
09            // 渲染器
10            var render = () => node.data = tpl.replace(exp, replacer.bind(config.data)); 
11        } else {
12            var matches = node.value.match(exp); 
13            var tpl = node.value; 
14            // 渲染器
15            var render = () => node.value = tpl.replace(exp, replacer.bind(config.data)); 
16        }
17        
18        // 无绑定 结束函数 
19        if (!matches) return; 
20
21        //  首次渲染
22        render();
23        
24        //  根据 matches 里的匹配对象进行 setter getter 绑定 
25        matches.map(e => e.slice(2, -2).trim()).forEach(varName => {
26            // 原先值 
27            var preVal = config.data[varName]; 
28            Object.defineProperty(config.data, varName, {
29                get(){
30                    return preVal; 
31                }, 
32                set(newVal){
33                    preVal = newVal; 
34                    render(); 
35                    return newVal; 
36                }
37            });
38        }); 
39    })
40}




回到顶部