directive自定义指令实现原理
# 概述
vue3
中内置了很多丰富实用的指令,如v-show
、v-if/v-else
或v-model
等,但是实际开发中可能我们还需要某些统一的处理,比如交互按钮的防抖,输入框的自动focus
等,这时我们就可以通过vue3
的directive
注册自定义指令。
# 指令
# 指令钩子
vue3
的自定义指令通常情况下是由一个包含类似组件生命周期钩子函数的对象,在DOM
节点不同的时期,执行不同的钩子函数,而我们就可以在对应的钩子函数中接受到DOM
节点、实例instance
等待处理一些业务逻辑。
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode) {},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 自定义防抖指令
# 定义指令
一般地,我们只需要在 mounted
中或updated
中去处理,一个防抖指令如下:
export default {
mounted(el, binding) {
if (typeof binding.value !== 'function') {
throw new Error("debounce指令的参数必须是一个函数,延时为1500ms")
return
}
// 初始化时监听
el.addEventListener('click', () => {
if (!el.disabled) {
el.disabled = true;
const timer = setTimeout(() => {
el.disabled = false;
binding.value()
clearTimeout(timer)
}, 1000) // 1s间隔
}
});
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
上面的防抖指令debounce
实现的就是click
事件延迟一秒触发,当然时间间隔也可以当作参数传递,使用的时候:
# 指令的注册
大多数情况还是会选择全局注册指令,挂载到 App 的全局上下文中去
app.directive("debounce", debounce);
# 使用指令
<el-button v-debounce="() => search(formRef)" type="primary">
搜索
</el-button>
2
3
时间间隔使用参数传递,我们也可以这样写:
<el-button
v-debounce="{ event: () => search(formRef), time: 1500 }"
type="primary"
>
搜索
</el-button>
2
3
4
5
6
指令对应修改如下:
export default {
mounted(el, binding) {
const {event,time}=binding.value;
if (typeof event !== 'function') {
throw new Error("debounce指令的参数必须是一个函数,延时为1500ms")
return
}
// 初始化时监听
el.addEventListener('click', () => {
if (!el.disabled) {
el.disabled = true;
const timer = setTimeout(() => {
el.disabled = false;
event()
clearTimeout(timer)
}, time) // 1s间隔
}
});
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 注意事项
官网上说:"只有当所需功能只能通过直接的 DOM
操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用 v-bind
这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。” 毫无疑问,自定义指令是会带来性能的消耗,使用时应该有所权衡。
# directive
源码分析
vue3
使用自定义指令就三步:1.定义指令,构造特殊的对象 2.指令注册 3.指令绑定到DOM
节点。
那directive
的源码就从全局注册开始
# 指令注册
当调用app.directive
全局注册自定义指令时,会执行下面这个directive
函数,将指令挂载到全局上下文context
上去,所有的自定义指令保存在context.directives
中。
directive(name: string, directive?: Directive) {
if (!directive) {
return context.directives[name]
}
context.directives[name] = directive
return app
},
2
3
4
5
6
7
# 指令绑定
指令绑定在DOM
或者Node
节点上,肯定就会涉及到模板的编译。
vue3
首先会通过@vue/runtime-dom
的registerRuntimeCompiler
方法将编译器注册到运行时,这样vue
就可以在运行时编译模板字符串。
runtimeDom.registerRuntimeCompiler(compileToFunction);
编译器compileToFunction
的作用就是将模板字符串编译成渲染函数,而其内部会调用@vue/compiler-dom
模块的compile
函数将字符串模板template
编译为代码。compile
接受template
作为其中一个参数,返回baseCompile
函数的执行结果,而baseCompile
函数的第二个参数是个对象,它会有一个属性directiveTransforms
,初始时它的值是包含和DOM
节点有关的transform
方法,如下:
const DOMDirectiveTransforms = {
cloak: noopDirectiveTransform,
html: transformVHtml,
text: transformVText,
model: transformModel,
// override compiler-core
on: transformOn,
// override compiler-core
show: transformShow,
};
2
3
4
5
6
7
8
9
10
在baseCompile
函数中会进一步合并directiveTransforms
,主要包括on
、bind
、model
, 如上注释会重写覆盖DOMDirectiveTransforms
的on
和model
对应的的transform
函数,此外如果baseCompile
的第一个参数source
是字符串,就还会调用baseParse
解析函数,它会将source
解析成ast
语法树,其中也包括自定义的指令相关字符串。接着,会调用transform
函数,在这个函数就会调用一个递归函数traverseNode
会去遍历其子节点以及遍历执行对应的nodeTransforms
,而nodeTransforms
就是和directiveTransforms
同时传进来的,而指令的逻辑就在其中的transformElement
中进行的。
transformElement
接受两个参数 node
和 context
。当 node
的属性 props
长度不为 0 时,就会调用 buildProps
函数,buildProps
函数顾名思义就是获取 node
上的属性进行相应操作,而针对指令,就会先判断是不是自定义指令,如果是自定义指令则将 prop
放进 runtimeDirectives
;如果不是自定义指令,则调用指令自身的 transform
。
buildProps
函数调用完成后,返回一个名为 propsBuildResult
对象,而指令集都包含在 propsBuildResult.directives
中,然后调用 createVNodeCall
, 在 createVNodeCall
中就会调用 withDirectives
方法,这个方法会将指令结构出来的arg
、exp
、modifiers
等 添加到 VNode
的 dirs
上,下面是它的实现:
function withDirectives(vnode, directives) {
// 先判断当前渲染实例是否存在,若不存在,则直接返回vnode
if (currentRenderingInstance === null) {
return vnode;
}
// 获取当前渲染实例的暴露的代理
const instance = getExposeProxy(currentRenderingInstance)
// 获取vnode上的指令数组dirs
const bindings = vnode.dirs || (vnode.dirs = []);
// 遍历指令集directives
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i];
if (dir) {
// 判断dir是否是函数
if (shared.isFunction(dir)) {
dir = {
mounted: dir, //挂载后
updated: dir, // 更新后
};
}
// 判断dir的deep是否为true,
if (dir.deep) {
// 调用traverse进行深度遍历
reactivity.traverse(value);
}
// 将指令集中的内容放到vnode的dirs中
bindings.push({
dir,
instance,
value,
oldValue: void 0,
arg,
modifiers,
});
}
}
return vnode;
}
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
# 指令执行
指令的执行就简单多了,指令中对应created
、mounted
等生命周期函数,那么只需要在VNode
对应的生命周期时去判断该节点有没有指令即其dirs
是否有值,若有值则执行对应的invokeDirectiveHook
,比如如下代码就是mountElement
中的一个片段,mountElement
会在element
或是VNode
挂载时执行
dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
而invokeDirectiveHook
函数地定义如下,会根据参数name
从vnode.dirs
中取指令对应地生命周期函数,并传参,调用callWithAsyncErrorHanding
执行,实际就是执行hook(vnode.el,binding,vnode,preVNode)
function invokeDirectiveHook(vnode, prevVNode, instance, name) {
const bindings = vnode.dirs;
const oldBindings = prevVNode && prevVNode.dirs;
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i];
if (oldBindings) {
binding.oldValue = oldBindings[i].value;
}
let hook = binding.dir[name];
if (hook) {
pauseTracking();
// 停止追踪
callWithAsyncErrorHandling(hook, instance, 8 /* DIRECTIVE_HOOK */, [
vnode.el,
binding,
vnode,
prevVNode,
]);
// 恢复之前的追踪状态
resetTracking();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
invokeDirectiveHook
的name
对应组件的生命周期钩子,也是对应指令的周期钩子函数,其值可能为created
/beforeMount
/mounted
/beforeUpdate
/updated
/beforeUnmount
/unmounted
。
但是需要注意的一点是,在withDirectives
中解析自定义指令时,若自定义指令是一个函数,则只有mounted
和updated
钩子是注册了,即组件只有触发mounted
或updated
时,才会触发自定义指令函数,其余钩子函数被触发时,自定义指令函数不会被执行;而当自定义指令是一个对象时,组件触发,会间接触发自定义指令对象中对应的同名函数。
至此vue3
中关于directive
指令就介绍到这里了,后续会根据理解的加深予以补充修正。