关于模板语法{{}}
如果大家曾经使用过Vue.js,那么在起步的教程中就有教大家如何使用模板语法来将数据渲染到dom中,类似下面的代码段:1
2
3
4
5
6
7
8
9
10
11
12<div id="app">
    <p>{{message}}</p>
</div>
<script>
    var app = new Vue({
        el:"#app",
        data:{
            message:"Hello,Vue"
        }
    })
<script>
如果让你来实现{{}}这种模板语法,你有什么好的idea吗?最主要的一点就是将外部数据的变化与dom节点上渲染的数据相关联起来,
简单的实现
我们先只考虑非常简单的实现,就是将数据替换到我们的模板中。外部的数据与文本节点上的需要渲染的值依靠键值关联起来即可。如下图:
简单来说,就是我们有两个对象,一个外部的data对象,一个内部的bindings对象,bindings对象持有与data对象键值关联的dom元素。data对象通过某种方式与bindings关联起来。
我们可以根据这样的工作流程来:以下面的html文档为例:1
2
3
4
5
6
7
8
9
10
11
12
<html>
    <body>
        <div id="app">
            <p>{{msg1}}</p>
            <p>{{msg1}}</p>
            <p>{{msg1}}</p>
            <p>{{msg2}}</p>
            <p>{{msg3}}</p>
        </div>
    <body>
<html>
- 通过正则表达式替换需要进行模板渲染的<p>节点成为我们能查找到的样式,即给他添加一个键值相关的自定义属性。
- bind过程:通过dom api查找到上一步与键值相关的节点,并使用Object.defineProperty()方法重新定义赋值过程,这个过程中可以同时将相关联的dom节点进行更新。
代码实现
结合上面的分析,我们知道了大体的方向,下面我们进行编码,如下: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
40
41
42
43
44
45
46
47function Element(id, initData){
    var bindingMark = "data-element-binding"
    var self = this,
        el = document.getElementById(id),
        bindings = {},
        data = self.data = {},
        content = el.innerHTML.replace(/<(.*)>\{\{(.*)\}\}<\/(.*)>/g, markToken)
    // 替换p节点为带bindingMark标记的节点
    el.innerHTML = content
    for(var variable in bindings){
        bind(variable)
    }
    if(initData){
        for(var variable in initData){
            data[variable] = initData[variable]
        }
    }
    function markToken(match,front,variable,end){
        bindings[variable] = {}
        return `<${front} ${bindingMark}="${variable}"></${end}>`
    }
    function bind(variable){
        // 关联对应的dom节点
        bingdings[variable].els = el.querySelectorAll(`[${bindingMark}=${variable}]`);
        [].forEach.call(bindings[variable].els, function(e){
            e.removeAttribute(bindingMark)
        })
        // 关联data与bindings中对应的dom节点
        Object.defineProperty(data,variable,{
            set:function(newVal){
                [].forEach.call(bindings[variable].els, function(e){
                    bindings[variable].value = e.textContent = newVal
                })
            },
            get:function(){
                return bindings[variable].value
            }
        })
    }
}
var app = new Element("app", {
    msg1:"hello",
    msg2:"data",
    msg3:"binding~"
})
那么上面的代码主要是做了两件事:
- 将<p></p>替换为<p data-element-binding="msg1"></p>
- 根据上面的自定义属性查询到关联的dom元素,并关联到对应的键值上,使用Object.defineProperty()重新定义了data对象的set和get属性,从而在更新值时可以同时更新文本节点。
到此,我们就实现了一个简单的使用模板进行数据绑定,本文的示例代码是基于Vue.js源码做了部分修改,参见commit:
871ed91,有兴趣的同学可以check到这笔提交看看。