我们都知道 html 元素是通过标签属性来控制其效果的,例如video的control属性,虽然只是一个简单的值,但是内部会有很复杂的实现:添加了video播放的视频的控制功能。而很多情况下,html 内置的属性无法满足千变万化的需求,于是指令(directive)便应运而生。指令最直观的理解就是扩展 html 属性,从而实现内置属性不支持功能。
生命周期
指令有初始化->更新->销毁三个生命周期。
初始化 bind
bind 发生下模板初始化时,解析到指令后就会将指令进行初始化。
更新 update
当指令绑定的值发生变化时,指令可以接收到更新的值,并进行对应的操作。
销毁 unbind
当模板卸载时,模板上面的指令也会一并销毁,如果指令中存在事件绑定,定时器等,应该在销毁时进行清理。
实现
指令的声明
指令在整个应用的模板上都应该是有效的,所以指令的声明要在全局使用,因此我们选择在 MVVM 对象上暴露一个静态方法全局声明指令。
1 | class MVVM { |
声明一个foo指令:
1 | MVVM.directive('foo', { |
在多数情况下,指令并不需要在卸载时进行操作,为了方便起见,我们也支持传入一个函数简写,替换bind和update。在指令bind和update生命周期触发时,这个函数都会触发。
1 | MVVM.directive('bar', (el: any, binding) {}, |
代码实现
指令的实现并不复杂,原理和Complier类似,都是解析表达式,然后通过Watcher对数据进行监听,并在数据变动时触发对应的生命周期回调。
构造函数
构造函数接受以下参数
owner- MVVM 上下文el- dom 节点exp- 表达式config- 上文中的指令的声明配置
1 | class Directive implements IDestroy { |
解析表达式,并监听所有依赖项的更改事件,添加到上下文的$watcher中。并将计算的值作为value传递给指令的update生命周期。
1 | const { expression, dependencies } = parseExpression(exp, 'this.$owner'); |
在构造函数中触发指令的bind函数。
1 | this.config.bind.call(this, el, { |
销毁
实现IDestroy接口,注销在$watcher上的事件监听,并触发unBind生命周期。
1 | destroy() { |
指令前缀
为了区分指令与原生属性,我们需要加入x-作为前缀。
Compiler 中处理指令
在Complier一文中,由于还未接触指令,我们并没有处理,现在让我们补全吧。
在属性遍历时,如果需要属性以约定的前缀开头,则认为是指令。
1 | attributes.forEach(attr => { |
将指令的属性从 html 标签上移除,让标签看起来干净点。从指令配置表中找到这个指令的配置后,进行初始化,并将识别到的指令保存在Compiler的指令集合中,当Complier销毁时,内部的指令也需要一并销毁。
1 | private initDirective(node: any, attr: Attr) { |
优先级与子作用域
有些特殊类型的指令,例如if会控制一个模板块的销毁和重建,因此它的优先级是高于其他指令的。
同时,由于模板块需要重建和销毁,而模板的数据是来自当前 MVVM 作用域的,因此需要创建一个子作用域来控制销毁和重建,而子作用域的数据来自于父作用域。
如果
if不销毁模板块,而只是控制 Dom 节点的渲染,那么模板块中的指令和组件可能会出现令人困惑的情况:页面上元素不存在却没有销毁(可能仍然在内存中)。而且这样与show指令功能似乎有点重叠。
通过加入指令的配置项scoped,我们告诉Compiler不要继续解析这个指令所在元素的子元素了。
1 | if (directive && directive.$scoped) { |
而子作用域除了实现与作用域的相同接口外,监听了父级所有数据的更改,并触发内部的更改。
1 | class ChildScope implements IOwner { |
实现常用指令
show
用于控制元素的显隐。
实现非常简单,由于不需要在销毁时做额外处理,所以用简写形式实现。
1 | MVVM.directive('show', (el, binding) => { |
model
用于实现表单元素与数据的绑定。
1 | MVVM.directive('model', { |
if
1 | MVVM.directive('if', { |
使用指令
下面模拟了应用的三步提交过程。
1 | <div id="app"> |
下一步
大家会发现一直没有提及一个非常常用的指令 - 循环指令for。因为for指令比较特殊,相对于其他指令更加复杂。所以后续我们将单独实现它。