(八)指令解析

指令解析

指令的解析和之前的文本解析差不多,我们都需要遍历节点最后执行compileNode进行解析,这里我们加一个判断如果节点是元素我们就进入我们的指令解析compileDirectives

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function compileNode (node) {
var type = node.nodeType
if (type === 1) {
return compileElement(node)
} else if (type === 3 && node.data.trim()) {
compileTextNode(node)
}
}

function compileElement (el) {
var hasAttrs = el.hasAttributes()
var attrs = hasAttrs && toArray(el.attributes)
compileDirectives(el, attrs)
}

重点是这个compileDirectives,对于v-on指令和v-bind指令由于他们有特殊的绑定格式所以需要单独拿出来,其中bind指令对于style和class的绑定的处理也不一样,都需要分别进行处理,最后才是通用的指令解析。

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
var onRE = /^v-on:|^@/
var bindRE = /^v-bind:|^:/
var dirAttrRE = /^v-([^:]+)(?:$|:(.*)$)/
function compileDirectives (el, attrs) {
var i = attrs.length
var dirs = []
var attr, name, rawName, rawValue, value, dirName, arg, dirDef, matched
while (i--) {
attr = attrs[i]
name = rawName = attr.name
value = rawValue = attr.value
var descriptor = {
attr: rawName,
raw: rawValue,
expression: value
}
if (onRE.test(name)) {
descriptor.name = 'on'
arg = name.replace(onRE, '')
dirDef = publicDirectives.on
} else if (bindRE.test(name)) {
descriptor.name = 'bind'
arg = dirName = name.replace(bindRE, '')
if (dirName === 'style' || dirName === 'class') {
dirDef = internalDirectives[dirName]
} else {
dirDef = publicDirectives.bind
}
} else if ((matched = name.match(dirAttrRE))) {
dirName = matched[1]
arg = matched[2]
dirDef = publicDirectives[dirName]
}
if (dirDef) {
descriptor.arg = arg
descriptor.def = dirDef
new Directive(descriptor, data, el)
}
}
}

下面我们看看这些指令的具体行为:

v-on指令

on指令的话比较简单,在descriptor中arg就是我们事件名称,值对应vm中的函数名称,当这个值发生变化的时候首先重置如果之前添加了事件则先移除旧的事件,然后添加事件侦听。

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
function on (el, event, cb, useCapture) {
el.addEventListener(event, cb, useCapture)
}

function off (el, event, cb) {
el.removeEventListener(event, cb)
}

var publicDirectives = {
on: {
update (handler) {
if (!this.descriptor.raw) {
handler = function () {}
}
var vm = this.vm
// reset
if (this.handler) {
off(this.el, this.arg, this.handler)
}
this.handler = function (e) {
return handler.call(vm, e)
}
on(this.el, this.arg, this.handler)
},
}
}

查看DEMO

v-bind指令

对于一般的bind指令

  1. 对于value|checked|selected等属性我们不仅需要修改属性的值同时我们还需要修改元素节点上对应的值

  2. 对于draggable|contenteditable|spellcheck这种枚举型的值我们只允许truefalse

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
var attrWithPropsRE = /^(?:value|checked|selected|muted)$/
var enumeratedAttrRE = /^(?:draggable|contenteditable|spellcheck)$/
var publicDirectives = {
bind: {
update (value) {
var attr = this.arg
if (this.arg) {
this.handleSingle(attr, value)
}
},
handleSingle (prop, value) {
if (attrWithPropsRE.test(attr) && attr in el) {
var attrValue = attr === 'value'
? value == null ? '' : value
: value
if (el[attr] !== attrValue) {
el[attr] = attrValue
}
}
if (enumeratedAttrRE.test(attr)) {
el.setAttribute(attr, value ? 'true' : 'false')
} else if (value != null && value !== false) {
el.setAttribute(attr, value === true ? '' : value)
} else {
el.removeAttribute(attr)
}
}
}
}

对于style指令

style指令支持字符串、数组、对象。如果是字符串直接修改cssText,如果是数组则先把所有属性合并成一个对象,最后处理和对象的处理是一样的。

handleObject将指令绑定对象的所有的属性缓存起来,只有当发生变化的属性才做处理,同时如果属性被移除了也要做相应的处理。

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
var internalDirectives = {
style: {
update (value) {
if (typeof value === 'string') {
this.el.style.cssText = value
} else if (Array.isArray(value)) {
this.handleObject(value.reduce(extend, {}))
} else {
this.handleObject(value || {})
}
},
handleObject (value) {
// cache object styles so that only changed props
// are actually updated.
var cache = this.cache || (this.cache = {})
var name, val
for (name in cache) {
if (!(name in value)) {
this.handleSingle(name, null)
delete cache[name]
}
}
for (name in value) {
val = value[name]
if (val !== cache[name]) {
cache[name] = val
this.handleSingle(name, val)
}
}
},
handleSingle (prop, value) {
// cast possible numbers/booleans into strings
if (value != null) value += ''
if (value) {
this.el.style[prop] = value
} else {
this.el.style[prop] = ''
}
}
}
}

对于class指令

如果绑定值为空了,那么我们把之前的class也清空,如果绑定值是字符串,我们根据空格分隔转化为className数组然后设置为新的class,对于数组或对象的形式我们先执行normalize转换成相应的className数组然后设置为新的class。

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
var internalDirectives = {
class: {
update (value) {
if (!value) {
this.cleanup()
} else if (typeof value === 'string') {
this.setClass(value.trim().split(/\s+/))
} else {
this.setClass(normalize(value))
}
},
setClass (value) {
this.cleanup(value)
for (var i = 0, l = value.length; i < l; i++) {
var val = value[i]
if (val) {
apply(this.el, val, addClass)
}
}
this.prevKeys = value
},
cleanup (value) {
const prevKeys = this.prevKeys
if (!prevKeys) return
var i = prevKeys.length
while (i--) {
var key = prevKeys[i]
if (!value || value.indexOf(key) < 0) {
apply(this.el, key, removeClass)
}
}
}
}
}

normalize对于数组的每一项先判断是否是字符串,如果是字符串那么直接加入结果,如果是对象的形式则遍历每一项加入,对于对象则直接遍历加入。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
function normalize (value) {
const res = []
if (isArray(value)) {
for (var i = 0, l = value.length; i < l; i++) {
const key = value[i]
if (key) {
if (typeof key === 'string') {
res.push(key)
} else {
for (var k in key) {
if (key[k]) res.push(k)
}
}
}
}
} else if (isObject(value)) {
for (var key in value) {
if (value[key]) res.push(key)
}
}
return res
}

function setClass (el, cls) {
if (isIE9 && !/svg$/.test(el.namespaceURI)) {
el.className = cls
} else {
el.setAttribute('class', cls)
}
}

function addClass (el, cls) {
if (el.classList) {
el.classList.add(cls)
} else {
var cur = ' ' + getClass(el) + ' '
if (cur.indexOf(' ' + cls + ' ') < 0) {
setClass(el, (cur + cls).trim())
}
}
}

function removeClass (el, cls) {
if (el.classList) {
el.classList.remove(cls)
} else {
var cur = ' ' + getClass(el) + ' '
var tar = ' ' + cls + ' '
while (cur.indexOf(tar) >= 0) {
cur = cur.replace(tar, ' ')
}
setClass(el, cur.trim())
}
if (!el.className) {
el.removeAttribute('class')
}
}

function apply (el, key, fn) {
key = key.trim()
if (key.indexOf(' ') === -1) {
fn(el, key)
return
}
// The key contains one or more space characters.
// Since a class name doesn't accept such characters, we
// treat it as multiple classes.
var keys = key.split(/\s+/)
for (var i = 0, l = keys.length; i < l; i++) {
fn(el, keys[i])
}
}

Demo

查看DEMO