Vue源码解析15-自定义指令

Vue可以自定义局部和全局指令,具体用法参考官网即可,这篇文章讲述其内部实现。

parse

在上篇指令概述中已有大概说到,在processAttrs函数中会处理自定义指令,这里详细说下。

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
function processAttrs(el) {
// attrsList结构示范:[{name:'id',value:'app'}]
const list = el.attrsList;
let i, l, name, rawName, value, modifiers, isProp;
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name;
value = list[i].value;
// v- 或 @ 或 : 开头的属性名
if (dirRE.test(name)) {
// mark element as dynamic
el.hasBindings = true;
// modifiers修饰符, 即.xxx,若存在则返回一个对象, {m1: true, m2:true}
modifiers = parseModifiers(name);
if (modifiers) {
name = name.replace(modifierRE, ''); // 去除修饰符
}
// 其他处理...

// normal directives,普通指令, v-xxx
name = name.replace(dirRE, '');
// parse arg,解析指令参数
const argMatch = name.match(argRE);
const arg = argMatch && argMatch[1];
if (arg) {
name = name.slice(0, -(arg.length + 1));
}
// 添加el.directives数组元素
// el.directives.push({ name, rawName, value, arg, modifiers })
addDirective(el, name, rawName, value, arg, modifiers);

// v-model处理...
}
}
}

直接看代码有点抽象,我们用一个例子:

1
<p v-mydir:myarg.foo.bar="msg">111</p>

经过parseModifiers处理后得到的modifiers

1
2
3
4
{
foo: true,
bar: true,
}

然后经过replace(modifierRE, '')replace(dirRE, '')后,我们得到的name就是mydir:myarg了。其中myarg是指令的参数,指令是可以接收参数的,参考官网教程。之后的argMatch就是用来解析指令的参数,最终我们得到的name就是mydirarg是就是myarg

addDirectiveel.directives塞入上述得到的几个属性值:

1
2
3
4
export function addDirective(el: ASTElement, name: string, rawName: string, value: string, arg: ?string, modifiers: ?ASTModifiers) {
(el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers });
el.plain = false;
}

所以最终我们的el.directives为:

1
2
3
4
5
6
7
8
9
[
{
arg: 'myarg',
modifiers: { foo: true, bar: true },
name: 'mydir',
rawName: 'v-mydir:myarg.foo.bar',
value: 'msg',
},
];

generate

依然如上篇文章所提,在genData中会首先调用genDirectives处理自定义指令:

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
function genDirectives(el: ASTElement, state: CodegenState): string | void {
const dirs = el.directives; // 节点上的普通指令
if (!dirs) return;
let res = 'directives:[';
let hasRuntime = false;
let i, l, dir, needRuntime;
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i];
needRuntime = true;
// state.directives目前包含v-on、v-bind、v-cloak、v-html、v-model、v-text
const gen: DirectiveFunction = state.directives[dir.name];
if (gen) {
// compile-time directive that manipulates AST.
// returns true if it also needs a runtime counterpart.
needRuntime = !!gen(el, dir, state.warn);
}
if (needRuntime) {
hasRuntime = true;
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''}${
dir.arg ? `,arg:"${dir.arg}"` : ''
}${dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''}},`;
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']';
}
}

经过处理,我们拿到的字符串为:

1
2
3
4
5
6
7
8
9
10
11
`
directives:[
{
name:"mydir",
rawName:"v-mydir:myarg.foo.bar",
value:(msg),expression:"msg",
arg:"myarg",
modifiers:{"foo":true,"bar":true}
}
]
`;

之后经过render生成vnode,这些都会放到vnode.data.directives上。

patch

上篇概述里已提到_update中会在不停阶段调用指令的不同选项:

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
function _update(oldVnode, vnode) {
const isCreate = oldVnode === emptyNode; // 第一次创建,oldVnode为空
const isDestroy = vnode === emptyNode; // 销毁时,vnode为空。
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context);
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context);

const dirsWithInsert = [];
const dirsWithPostpatch = [];

let key, oldDir, dir;
for (key in newDirs) {
oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {
// new directive, bind
callHook(dir, 'bind', vnode, oldVnode); // 调用指令的bind方法
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir);
}
} else {
// existing directive, update
dir.oldValue = oldDir.value;
callHook(dir, 'update', vnode, oldVnode); // 调用指令的update方法
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir);
}
}
}

if (dirsWithInsert.length) {
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
// 调用指令的inserted方法
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode);
}
};
if (isCreate) {
// 将callInsert合并到vnode.data.hook[‘insert’]数组中,不会立即调用
mergeVNodeHook(vnode, 'insert', callInsert);
} else {
callInsert();
}
}

if (dirsWithPostpatch.length) {
// 将callInsert合并到vnode.data.hook[‘postpatch’]数组中,不会立即调用
mergeVNodeHook(vnode, 'postpatch', () => {
for (let i = 0; i < dirsWithPostpatch.length; i++) {
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
}
});
}

if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind,调用指令的unbind方法
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
}
}
}
}

normalizeDirectives用于获取自定义指令的具体选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function normalizeDirectives(dirs: ?Array<VNodeDirective>, vm: Component): { [key: string]: VNodeDirective } {
const res = Object.create(null);
if (!dirs) {
return res;
}
let i, dir;
for (i = 0; i < dirs.length; i++) {
dir = dirs[i];
if (!dir.modifiers) {
dir.modifiers = emptyModifiers;
}
res[getRawDirName(dir)] = dir;
dir.def = resolveAsset(vm.$options, 'directives', dir.name, true);
}
return res;
}

function getRawDirName(dir: VNodeDirective): string {
return dir.rawName || `${dir.name}.${Object.keys(dir.modifiers || {}).join('.')}`;
}

返回的res对象中存放着指令的定义,key为指令名,value为指令的选项对象,通常包含bindinsertupdate等钩子函数。resolveAsset方法就是获取指令的定义,会将指令名转为驼峰、中划线的各种形式来尝试获取

拿到指令定义后,如果是新增的指令,则执行callHook(dir, 'bind', vnode, oldVnode)调用指令的bind方法:

1
2
3
4
5
6
7
8
9
10
function callHook(dir, hook, vnode, oldVnode, isDestroy) {
const fn = dir.def && dir.def[hook];
if (fn) {
try {
fn(vnode.elm, dir, vnode, oldVnode, isDestroy);
} catch (e) {
handleError(e, vnode.context, `directive ${dir.name} ${hook} hook`);
}
}
}

如果不是第一次绑定,则调用update钩子函数。

之后如果vnode是第一次创建,isCreatetrue,会把dirsWithInsert数组中的回调合并到 vnode.data.hook.insert中。,如果不是则直接执行dirsWithInsert中的回调。insert钩子的执行时机应该是dom节点已经插入到页面中。

剩下的逻辑都很简单了,这里不再赘述。