我们都知道 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
指令比较特殊,相对于其他指令更加复杂。所以后续我们将单独实现它。