指令是Vue提供的复用手段之一,除了内置的v-if、v-show、v-text等还支持自定义指令。Vue中代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。本文是通过源码来梳理指令的执行流程,从而加深对指令的理解。
实际上自定义指令需要清楚相关钩子函数的时机,从而集合实际场景做相关处理,钩子函数如下:
结合这些钩子函数可以更好的理解指令相关逻辑。实际上指令的处理逻辑跟内置组件transition有一些相似的,这里具体聊聊相似的逻辑。实际上Vue在pacth阶段会做diff算法以达到尽可能复用节点,在这个过程中会对相关props等属性做一些处理逻辑。而Vue中对每一类属性对应是一个module对象,这里先看下所涉及到的module,如下:
var platformModules = [
attrs,
klass,
events,
domProps,
style,
transition
];
var baseModules = [
ref,
directives
];
var modules = platformModules.concat(baseModules);
// 创建patch函数,其中会有上面module的hook的处理
var patch = createPatchFunction({
nodeOps: nodeOps, modules: modules });
从源码中看到涉及到module对应有:attrs、class、events、domProps、style、transition、ref、directives。实际上基本跟Vue数据对象对应的,上面一些module对象实际上都是定义的hook函数以便在patch合适时机执行(包含更新逻辑等)。例如attrs module和klass module的定义:
// attrs module
var attrs = {
create: updateAttrs,
update: updateAttrs
};
// klass module
var klass = {
create: updateClass,
update: updateClass
};
这些module相关hook都是在patch阶段用于更新相关属性值的,在transition这篇文章实际上分析了transition module的触发时机,实际上就是patch阶段invokeCreateHooks触发create hook的。directives module触发时机也是如此,下面是directives module定义:
var directives = {
create: updateDirectives,
update: updateDirectives,
destroy: function unbindDirectives (vnode) {
updateDirectives(vnode, emptyNode);
}
};
从directives module的定义可以很清楚的知道都是调用updateDirectives函数来处理相关逻辑的,接下面就具体看看该函数的逻辑。
function updateDirectives (oldVnode, vnode) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode);
}
}
directives是所有指令的集合中心,只有存在directives才会执行_update方法,_update方法才是指令执行的核心,其具体逻辑如下:
实际上_update函数逻辑简要概括就是:指定条件下调用相关hook。核心逻辑如下:
for (key in newDirs) {
oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {
// new directive, bind
callHook$1(dir, 'bind', vnode, oldVnode);
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir);
}
} else {
// existing directive, update
dir.oldValue = oldDir.value;
dir.oldArg = oldDir.arg;
callHook$1(dir, 'update', vnode, oldVnode);
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir);
}
}
}
上面是_update函数中新指令集合的遍历处理的逻辑,其具体逻辑概括为:
比较新旧节点:
- 对于新指令调用其bind钩子函数,并收集存在inserted钩子函数的指令
- 对于已有指令调用其update钩子函数并更新相关,并收集存在componentUpdated钩子函数的指令
_update之后的逻辑就是处理其他Hook,具体逻辑如下:
// 处理inserted钩子函数
if (dirsWithInsert.length) {
var callInsert = function () {
for (var i = 0; i < dirsWithInsert.length; i++) {
callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
}
};
isCreate ? mergeVNodeHook(vnode, 'insert', callInsert) : callInsert();
}
// 处理componentUpdated钩子函数
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch', function () {
for (var i = 0; i < dirsWithPostpatch.length; i++) {
callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
}
});
}
// 对比旧节点中不存于新节点中的指令,调用其unbind钩子函数
if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind
callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
}
}
}
实际上_update函数整体处理逻辑是非常清晰的,关键在于涉及到的相应VNode Hook和Module Hook的触发,这部分逻辑是非常复杂的,之后会专门梳理Hook相关的处理逻辑。