八小时实现迷你版vuejs四 实现compiler和directive

这一篇,我们要实现一个事件绑定的功能:

1
<div @click=“sayHello”></div>

那么为了实现这个功能,我们需要三步:

  1. 实现 compileDirectives 方法, 可以从attrs中读取directive的配置,这里称之为 descriptor
  2. 实现Directive类
  3. 实现一个自定指令: v-on

v-on 为例,如果碰到这样一个属性 v-on:click=“hello” 指令的初始化流程如下?

  1. 遍历DOM
  2. 对其中每一个Element,获取所有的attributes
  3. 遍历 attributes,如果 namev-on 开头,那么就是一个绑定事件的指令,我们把相关的配置都存在 descriptor 中
  4. 遍历DOM结束,得到一个 descriptors 列表,其中存放了我们创建directive时需要的所有参数,比如name, value, el 等
  5. 在 bind 阶段,遍历 descriptors
  6. 对每一个descriptor,创建一个Directive
  7. bind 结束,directive 初始化完成。

关于watcher的实现以及如何在directive中监听数据的变动,我们放到下一章来讲。

compileDirectives

在vue v1.0 版本及以前,是通过DOM API直接获取attributes的,并通过对name的匹配来判断是哪一类directive。在vue2中显然是通过virtual DOM的API来做的。

那么我们按照上面的步骤来说,compileDirectives 负责其中 1-4 步,即从attributes中生成一个 descriptors 列表:

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
const onRE = /^v-on:|^@/
const modelRE = /^v-model/
const textRE = /^v-text/
const dirAttrRE = /^v-([^:]+)(?:$|:(.*)$)/

export const compileDirectives = function (el, attrs) {
if (!attrs) return undefined
const dirs = []

let i = attrs.length

while (i--) {
const attr = attrs[i]
const name = attr.name
const value = attr.value
let arg = name
if (name.match(dirAttrRE)) {
if (onRE.test(name)) {
arg = name.replace(onRE, '')
pushDir('on', dirOn)
} else if (modelRE.test(name)) {
arg = name.replace(modelRE, '')
pushDir('model', dirModel)
} else if (textRE.test(name)) {
arg = name.replace(textRE, '')
pushDir('text', dirText)
}
}

function pushDir(dirName, def) {
dirs.push({
el: el,
name: dirName,
rawName: name,
def: def,
arg: arg,
value: value,
rawValue: value,
expression: value
})
}
}
if (dirs.length) return makeNodeLinkFn(dirs)
}

上面这些代码都在 compile.js

Directive 类

获取了 descriptors 之后,我们就可以逐个创建directive了,那么显然,我们需要实现一个 Directive类,他会接收这些 descriptor 并生成一个 directive 实例。

这里需要重点说明一下,vuejs对每一个指令都会生成一个 directive 的实例,那么既然有 Directive 类了,那么 directives 下面的那么多指令是怎么回事呢?

我们可以把 Directive 类当做一个父类,他定义了所有指令通用的方法:_bind, _update 以及生成 watcher。我们自己定义的比如 v-on 指令的实现,其实可以看做是他的一个子类,其中的 bind 和 update 分别会被 _bind_update 调用。当然,在语法上其实并不是父类和子类的关系,语法上来说,Directive会调用我们自定义的回调函数(update/bind) 仅此而已。

Directive 类需要实现这几个功能:

  • constructor 从 descriptor 中抽取所需的参数,比如 el, value, expression 等
  • bind 阶段调用 bind 生成一个 _update 函数,负责调用 update
  • 创建 this._watcher

我们直接贴上相关代码:

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
export default function Directive (descriptor, vm, el) {
this.descriptor = descriptor
this.vm = vm
this.el = el
this.expression = descriptor.expression
}

Directive.prototype._bind = function () {

var def = this.descriptor.def
if (typeof def === 'function') {
this.update = def
} else {
extend(this, def)
}

if (this.bind) this.bind()
if (this.update) this.update()

if (this.update) {
const dir = this
this._update = function (val, oldVal) {
dir.update(val, oldVal)
}
} else {
this._update = function () {}
}

var watcher = this._watcher = new Watcher(
this.vm,
this.expression,
this._update
)
// v-model with inital inline value need to sync back to
// model instead of update to DOM on init. They would
// set the afterBind hook to indicate that.
if (this.update) {
this.update(watcher.value)
}
}

这里注意创建watcher的代码,其中第三个参数 this._update 是watcher发现所观察的对象有更新的时候会触发的回调。

因为我们还未实现 Watcher 类,所以这里可以先把 Watcher 相关的代码注释掉。让我们实现一个自己的指令 v-on 吧。

这个指令最精简的实现非常简单:在 bind 的时候调用 addEventListener 绑定一个回调即可。

1
2
3
4
5
6
7
8
export default {
bind () {
const el = this.descriptor.el
if (this.descriptor.arg === 'click') {
el.addEventListener('click', this.vm[this.descriptor.value].bind(this.vm))
}
}
}

显然这么简单的处理是很不妥的,比如addEventListener不支持怎么办?如果用户更新了回调函数怎么办? 什么时候应该注销?,没关系,我们这里只是最精简版,暂时不用考虑这么全面。

调用 compile 以及new Directive 其实都是在 lifecycle 中完成的,代码很简单大家直接去源码中看吧。
赶紧绑定一个事件试试吧。