那天去了一场面试,问到了这五个属性分别在什么时候调用对应的钩子函数,我就按照vue官网上的答案回答了
-
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
-
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
-
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有
-
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
-
unbind:只调用一次,指令与元素解绑时调用。
最后面试官问了一个我哑口无言的问题,那这些属性在源码中是怎么实现的,于是我回来看代码了
<input v-if="show" v-focus v-model="msg" />
directives: {
focus: {
// 指令的定义
inserted: function(el) {
debugger;
el.focus();
},
bind: function(el) {
debugger;
},
update: function(el) {
debugger;
},
componentUpdated: function(el) {
debugger;
},
unbind: function(el) {
debugger;
}
}
},
假如现在定义了一个directives叫focus,那么focus会被解析绑定在对应vnode.data.diretives数组的其中一项上面,def里面就是自定义的钩子函数,而每次执行patch方法都会执行一次updateDirectives方法,调用钩子函数最开始就是从这里开始的
看一下updateDirectives方法,如果vnode和oldVnode都没有定义directives,可以跳过_update这一步了
function updateDirectives (oldVnode, vnode) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode);
}
}
否则就是要执行_update方法 下面方法我尽量注释以及去掉无关紧要的方法
function _update (oldVnode, vnode) {
// isCreate是判断是否首次渲染的依据,这是在调用bind钩子的时候要用来判断的,
// 如果oldVnode是empty则认为他是isCreate
var isCreate = oldVnode === emptyNode;
// isDestroy是判断是否destroy的依据,这是在调用unbind钩子的时候要用来判断的,
// 如果vnode是empty则认为他是 isDestroy
var isDestroy = vnode === emptyNode;
//获取oldVnode上面的directives数组oldDirs
var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
//获取vnode上面的directives数组newDirs
var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);
//dirsWithInsert这个数组是用来装这个组件上的inserted方法,并将它传给父组件渲染完成的时候调用
var dirsWithInsert = [];
//dirsWithPostpatch这个数组是用来装这个组件上的componentUpdated方法,并将它传给父组件渲染完成的时候调用
var dirsWithPostpatch = [];
var key, oldDir, dir;
for (key in newDirs) { //遍历newDirs
oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {
// 如果这个指令在oldVnode上没有,则认为这个是vnode首次调用的,可以调用对应directives.def上面的自定义的bind方法
callHook$1(dir, 'bind', vnode, oldVnode); //调用bind钩子
if (dir.def && dir.def.inserted) {
//如果同时这个directives有定义inserted方法,则把这个方法先push进dirsWithInsert数组
dirsWithInsert.push(dir);
}
} else {
// 如果新旧节点都有同一个指令的话,则认为是指令的update,调用update函数
dir.oldValue = oldDir.value;
dir.oldArg = oldDir.arg;
callHook$1(dir, 'update', vnode, oldVnode);//调用update钩子
if (dir.def && dir.def.componentUpdated) {
//如果这个指令还定义了componentUpdated方法,就把这个方法push到dirsWithPostpatch中
dirsWithPostpatch.push(dir);
}
}
}
//如果dirsWithInsert有值,创建一个callInsert方法,用来调用这个vnode
的inserted钩子(在父组件渲染完的时候调用)
if (dirsWithInsert.length) {
var callInsert = function () {
for (var i = 0; i < dirsWithInsert.length; i++) {
callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
}
};
if (isCreate) {
//如果是首次渲染还需要将这个方法直接定义到对应vnode.data.hooks的insert属性上??
mergeVNodeHook(vnode, 'insert', callInsert);
} else {
callInsert();
}
}
//如果dirsWithPostpatch有值,则把他绑定到这个vnode
的postpatch属性上,在父组件PatchVnode之后调用中调用
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch', function () {
for (var i = 0; i < dirsWithPostpatch.length; i++) {
callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
}
});
}
//如果不是首次渲染
if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) {
//且vnode上没有这个directives ,则认为这个指令被移除了,会调用unbind钩子
callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
}
}
}
}
经过上面的代码
vnode在调用初次渲染阶段,会把这个vnode在directives中的inserted钩子绑定到vnode.data.hooks.insert上面,
在更新阶段,会把这个vnode在directives中的componentUpdated钩子绑定到vnode.data.hooks.postpatch上面
在createElm(vnode)的时候,会通过invokeCreateHooks(vnode, insertedVnodeQueue);这个方法的执行将vnode放到insertedVnodeQueue数组中
在patch(oldVode,vnode)的最后返回前会执行invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);这个方法,也就是把insertedVnodeQueue数组中所有的vnode执行其vnode.data.hooks.insert方法(假如没有这个钩子方法就不执行),这样就保证了这个方法将在vnode自身渲染完成后才执行
而vnode.data.hooks.postpatch方法这是在patchVnode(oldVnode,vnode)的最后阶段,也就是vnode以及vnode的chiildren都比较完了,假如这个vnode有vnode.data.hooks.postpatch方法则执行这个方法,这就是为什么componentUpdated钩子能在自身以及其子节点都更新完了才执行