Vue源码之指令细节

前言

指令是Vue提供的复用手段之一,除了内置的v-if、v-show、v-text等还支持自定义指令。Vue中代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。本文是通过源码来梳理指令的执行流程,从而加深对指令的理解。

指令执行流程

实际上自定义指令需要清楚相关钩子函数的时机,从而集合实际场景做相关处理,钩子函数如下:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用
  • unbind:只调用一次,指令与元素解绑时调用

结合这些钩子函数可以更好的理解指令相关逻辑。实际上指令的处理逻辑跟内置组件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函数来处理相关逻辑的,接下面就具体看看该函数的逻辑。

updateDirectives函数
function updateDirectives (oldVnode, vnode) {
     
	if (oldVnode.data.directives || vnode.data.directives) {
     
    	_update(oldVnode, vnode);
    }
}

directives是所有指令的集合中心,只有存在directives才会执行_update方法,_update方法才是指令执行的核心,其具体逻辑如下:

Vue源码之指令细节_第1张图片
实际上_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相关的处理逻辑。

你可能感兴趣的:(Vue相关,Vue源码,指令)