Vue中directives的指令的五个属性是怎么实现的

438 阅读3分钟

那天去了一场面试,问到了这五个属性分别在什么时候调用对应的钩子函数,我就按照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方法,调用钩子函数最开始就是从这里开始的

微信截图_20210623235645.png

看一下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钩子能在自身以及其子节点都更新完了才执行