此篇接上一篇
利用 @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 形式:- replace(String, Function);
- 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>
之后再利用这个渲染器,进行
setter
和 getter
的重写即可。# 完整实现 ↵
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}