上一篇我们已经可以捕获数据的更改,下面为了让将数据的更改与页面绑定,需要通过Compiler
解析模板。
模板规则
首先 UI 模板需要定义自己的模板语法,定义数据如何在页面上显示。我们定义一下规则:
表达式模板
使用{{}}
输出表达式。例如<div>{{ name }}</div>
会输出作用域上的name
变量。
当然也支持变量的属性和方法调用
<div>{{ name.toUpperCase() }}</div>
。
表达式除了文本,也可以在属性上使用。
<div class="{{name?'has-name':''}}">{{ name.toUpperCase() }}</div>
条件表达式也是支持的
{{ user ? user.name : ''}}
,{{ user && user.name }}
。
可以将括号中的表达式理解成赋值表达式的右侧部分,所以下面这种不支持。
{{ var name = user ? user.name : ''; return name}}
DOM 属性
有时候我们需要操作 dom 节点的一些内置属性,例如按钮button
组件有内置的disabled
属性。
如果我们在属性上通过模板语法
1 | <button disabled="{{submiting?true:false}}">提交</button> |
会发现这个按钮始终处于禁用状态。因为 html 上属性值是字符串且默认是为true
的,所以只要在 html 上定义了disabled
,内置的disabled
属性始终都不会为false
。所以想要真正的控制内置属性,只能通过button.disabled = false
来实现。
简单的通过表达式模板来控制有些属性是不行的,所以我们需要定义额外的规则来实现。
属性语法
我们在属性前加入一个:
号来表示这个属性是绑定在节点的 dom 属性中,而不是绑定 html 模板的属性中。具体区别在于,在submiting
属性触发更改后,会通过button.disabled = false
而不是button.setAttribute('disabled', false)
来更改属性。
1 | <button :disabled="submiting">提交</button> |
值得一提的是,属性语法不需要{{}}
包裹。
表达式经常会有
<div>姓名:{{ name }}</div>
这种与普通文本混合的情况,所以需要使用{{}}
标记隔离,而属性语法通常不会有这种情况。
事件回调
事件相对而言比较简单,仅需要将 html 的onxxx
中的on
换成@
符号即可。
1 | <button onclick="submit">提交</button> |
onclick
绑定的是全局的submit
方法,而@click
绑定的只会绑定到当前作用域的submit
方法上。
当然,仅仅传入一个回调函数还不能满足我们的需求,因为回调函数仅仅能收到一个事件参数。在很多业务场景下,回调函数应该要支持传入变量和数据。
1 | <button @click="submit(false, name)">实名提交</button> |
1 | submit(event:Event, isAnonymous:boolean, name:string){ |
我们约定事件的event
对象始终作为第一个参数,其余参数会按顺序写在后面。
实现
Compiler
解析模板需要关联具体的作用域对象,
1 | interface IOwner { |
1 | class Compiler { |
owner
是一个双向绑定的作用域,它的内部会维护Watcher
和Compiler
,这个下一篇再说。
init
方法调用后,开始从 owner 的根节点开始解析整个模板。
1 | init() { |
解析属性
在扫描每一个节点前,我们先遍历节点的属性attribute
。
首先解析属性的原因是,属性包含了
directive(指令)
,指令可能会有自己的子作用域并告知Compiler
中止解析。
1 | private traversAttr(node: HTMLElement) { |
模板字符串解析
首先要提一下解析模板字符串的方法。parseTemplateAndSet
传入一个模板字符串和一个回调函数。
1 | private parseTemplateAndSet( |
方法会把模板字符串编译成一个计算函数,同时获取函数依赖的变量,并通过$watcher
监听所有依赖变量的更改。每当依赖的变量有改动时则执行计算函数,并将计算的结果作为参数触发回调。
比如
1 | <div>姓名:{{name}},年龄:{{age}}岁</div> |
上面的模板中,姓名:{{name}},年龄:{{age + '岁'}}
这个模板字符串会解析成四个部分。
- 姓名:
- name
- ,年龄:
- age + ‘岁’
其中2依赖了变量name
,4依赖了变量age
。整个模板字符串依赖了name
和age
2个变量,所以编译后会同时监听这2个变量的修改,并触发编译好的4个部分的计算函数,将4个部分计算的结果拼接后得到完整的结果,最后触发回调。
DOM 属性
DOM 属性很简单,仅仅需要给对应的属性值赋值即可,需要注意的是需要将true
,false
等字符串转换成真实的值。
1 | if (attr.name.startsWith(':')) { |
回调函数
回调函数的处理相对而言要复杂一点。在有参数的情况下,需要将每一个参数作为模板字符串单独进行解析,并在回调执行时,执行所有参数的监听函数并获取模板字符串的值作为参数,最终将得到的参数和回调的原始Event
对象一起传入作用域的回调函数。
1 | if (attr.name.startsWith('@')) { |
普通属性
普通的attribute
直接赋值即可。
1 | let cb = (val: string) => { |
模板一览
让我们看看使用上面规则写出来的模板是什么样子吧。
1 | <h2>信息</h2> |
下一步
通过Watcher
和Compiler
我们已经将模板和数据绑定起来了,甚至已经可以通过 2 者写一个简单的 demo 进行测试了!后续我们将实现上文开始提到的MVVM
对象,将他们的绑定关系进行进一步封装,实现更加便利的双绑关系。