从源码看vue(v2.7.10)中的自定义指令的原理

自定义指令相信大家平常或多或少都用过,但是它的实现原理估计也有很多人不太了解,只是停留在会使用的程度,接下来带大家揭秘下原理,让大家有更深刻的理解。

前提

定义全局指令:

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会执行该方法。

总结

  1. bind的执行时机是在被插入到父元素之前执行。
  2. insertd的执行时机是当前组件的template下的elm全部完成插入后会放入insertedVnodeQueue队列等待整个组件elm被插入body,然后和组件级的insertd一起触发invokeInsertHook。
  3. update的执行是在patchVnode阶段,此时是在更新节点信息前执行,执行完以后再开始元素的一些操作
  4. componentUpdated的执行是在执行完元素的操作后执行,并不是指字面的组件更新完毕。
  5. unbind的执行是在元素被移除的时候执行也就是updateChildren的时候,此时input元素已经不在了。在removeVnodes时会执行invokeDestroyHook去执行updateDirectives方法。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Young soul2

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值