自定义指令相信大家平常或多或少都用过,但是它的实现原理估计也有很多人不太了解,只是停留在会使用的程度,接下来带大家揭秘下原理,让大家有更深刻的理解。
前提
定义全局指令:
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
bind: function (el) {
// 聚焦元素
setTimeout(()=>{
el.focus()
})
console.log('bind');
},
inserted:function(el){
console.log('inserted');
},
update:function(el){
console.log('update');
},
componentUpdated:function(el){
console.log('componentUpdated');
},
unbind:function(el){
console.log('unbind');
},
})
...
// App.vue
<template>
<div>
<input v-focus v-if="a%2">
<div @click="add">add</div>
</div>
</template>
<script>
export default {
name: "TestWebpackTest",
data(){
return {
a:1
}
},
methods:{
add(){
this.a++
}
}
};
</script>
我们先来看下执行Vue.directive的过程,该方法会调用在initGlobalAPI(Vue)中调用initAssetRegisters(Vue)方法:
// ASSET_TYPES = ["component", "directive", "filter"]
function initAssetRegisters(Vue) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(function (type) {
// @ts-expect-error function is not exact same type
Vue[type] = function (id, definition) {
...
else {
...
if (type === 'directive' && isFunction(definition)) {
definition = { bind: definition, update: definition };
}
this.options[type + 's'][id] = definition;
return definition;
}
};
});
}
该方法在全局的options新增了directive的focus方法:
接下来看看App.vue解析后的render方法:
var render = function render() {
var _vm = this,
_c = _vm._self._c
return _c("div", [
_c("input", { directives: [{ name: "focus", rawName: "v-focus" }] }),
])
}
v-focus被解析为{ directives: [{ name: “focus”, rawName: “v-focus” }] ,接下来就看看渲染的时候是怎么处理该对象的。
bind触发时机
bind的意思是绑定,我们的v-focus是绑定在input的dom上,所以触发时机应该在创建元素的时候。我们直接来到_update的方法中的createElm方法:
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
...
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
...
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
if (data && data.pre) {
creatingElmInVPre--;
}
}
...
}
该方法中根据input的tag创建了elm,然后看invokeCreateHooks(vnode, insertedVnodeQueue):
function invokeCreateHooks(vnode, insertedVnodeQueue) {
for (var i_2 = 0; i_2 < cbs.create.length; ++i_2) {
cbs.create[i_2](emptyNode, vnode);
}
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (isDef(i.create))
i.create(emptyNode, vnode);
if (isDef(i.insert))
insertedVnodeQueue.push(vnode);
}
}
这个方法是根据input的vnode上的属性来做一些操作,当执行到updateDirectives的时候会调用_update方法,directives的所有绑定的函数都会调用这个函数:
function updateDirectives(oldVnode, vnode) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode);
}
}
...
function _update(oldVnode, vnode) {
var isCreate = oldVnode === emptyNode;
var isDestroy = vnode === emptyNode;
var oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context);
var newDirs = normalizeDirectives(vnode.data.directives, vnode.context);
var dirsWithInsert = [];
var dirsWithPostpatch = [];
var key, oldDir, dir;
for (key in newDirs) {
oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {
// new directive, bind
callHook(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(dir, 'update', vnode, oldVnode);
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir);
}
}
}
if (dirsWithInsert.length) {
var callInsert = function () {
for (var i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode);
}
};
if (isCreate) {
mergeVNodeHook(vnode, 'insert', callInsert);
}
else {
callInsert();
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch', function () {
for (var 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
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
}
}
}
}
其中newDirs是根据我们的directives做了一些封装:
oldDir为空,因为是空节点。此时执行callHook(dir, ‘bind’, vnode, oldVnode)方法:
function callHook(dir, hook, vnode, oldVnode, isDestroy) {
var fn = dir.def && dir.def[hook];
if (fn) {
try {
fn(vnode.elm, dir, vnode, oldVnode, isDestroy);
}
catch (e) {
handleError(e, vnode.context, "directive ".concat(dir.name, " ").concat(hook, " hook"));
}
}
}
该方法直接找到bind函数然后执行bind。insertd方法也会执行mergeVNodeHook把insert放入data的hook里:
随后执行将要插入的vnode放入insertedVnodeQueue。为后面执行insert方法做铺垫。
insertd
在_update过程中如果有insertd的hook,那么dir会被放进dirsWithInsert,同时会执行mergeVNodeHook函数去添加insert的hook。首先是一个callInsert(循环执行inserted的hooks)=>wrappedHook(执行后移除该函数,只执行inserte一次)=>createFnInvoker(添加异常捕获)。
insertd是已经插入父元素的意思。按理说当input的父级elm在执行插入所有的子元素后会执行。但是insert是和父组件一起插入的,此时将执行quene给了vnode.parent.data.pendingInsert,要等到test的template下的elm插入到test时才会执行该函数。接下来我们看看在哪里执行的,再来到执行完成子组件的el生成完成后的代码:
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode))
invokeDestroyHook(oldVnode);
return;
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
...
else {
...
else {
...
// replacing existing element
var oldElm = oldVnode.elm;
var parentElm = nodeOps.parentNode(oldElm);
// create new node
createElm(vnode, insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));
// update parent placeholder node element, recursively
}
...
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm;
};
此时test组件渲染了vnode成elm时我发现并没有执行,而是在test的父组件将所有的vnode渲染后会执行invokeInsertHook函数:
function invokeInsertHook(vnode, queue, initial) {
// 判断是否有父节点
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue;
}
else {
for (var i_6 = 0; i_6 < queue.length; ++i_6) {
queue[i_6].data.hook.insert(queue[i_6]);
}
}
}
该函数执行vnode.parent.data.pendingInsert = queue将quene放入了父级的事件中。此时的父级是test组件。开始执行invokeInsertHook方法:
此时quene有两个vnode,一个是自定义的insert的vnode,一个是组件本身的insert。首先执行input的insert方法,随后执行组件的insert方法:
// 组件本身
function (vnode) {
var context = vnode.context, componentInstance = vnode.componentInstance;
if (!componentInstance._isMounted) {
componentInstance._isMounted = true;
callHook$1(componentInstance, 'mounted');
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance);
}
else {
activateChildComponent(componentInstance, true /* direct */);
}
}
}
此时主要执行callHook$1(componentInstance, ‘mounted’),表示组件已经挂载完毕。
undate、componentUpdate
update是更新的意思,所以在patchVnode的时候会执行:
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
if (isDef((i = data.hook)) && isDef((i = i.update)))
i(oldVnode, vnode);
}
...
function _update(oldVnode, vnode) {
var isCreate = oldVnode === emptyNode;
var isDestroy = vnode === emptyNode;
var oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context);
var newDirs = normalizeDirectives(vnode.data.directives, vnode.context);
var dirsWithInsert = [];
var dirsWithPostpatch = [];
var key, oldDir, dir;
for (key in newDirs) {
oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {
// new directive, bind
callHook(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(dir, 'update', vnode, oldVnode);
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir);
}
}
}
if (dirsWithInsert.length) {
var callInsert = function () {
for (var i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode);
}
};
if (isCreate) {
mergeVNodeHook(vnode, 'insert', callInsert);
}
else {
callInsert();
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch', function () {
for (var 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
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
}
}
}
}
此时有oldDirs,表示是更新操作,所以不会再执行bind,所以执行update方法。此时会执行mergeVNodeHook(vnode, ‘postpatch’):
componentUpdated会执行该方法。
总结
- bind的执行时机是在被插入到父元素之前执行。
- insertd的执行时机是当前组件的template下的elm全部完成插入后会放入insertedVnodeQueue队列等待整个组件elm被插入body,然后和组件级的insertd一起触发invokeInsertHook。
- update的执行是在patchVnode阶段,此时是在更新节点信息前执行,执行完以后再开始元素的一些操作
- componentUpdated的执行是在执行完元素的操作后执行,并不是指字面的组件更新完毕。
- unbind的执行是在元素被移除的时候执行也就是updateChildren的时候,此时input元素已经不在了。在removeVnodes时会执行invokeDestroyHook去执行updateDirectives方法。