Vue.js源码分析之自定义指令详解

前言

除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。

官网介绍的比较抽象,显得很高大上,我个人对自定义指令的理解是:当自定义指令作用在一些DOM元素或组件上时,该元素在初次渲染、插入到父节点、更新、解绑时可以执行一些特定的操作(钩子函数()

自定义指令有两种注册方式,一种是全局注册,使用Vue.directive(指令名,配置参数)注册,注册之后所有的Vue实例都可以使用,另一种是局部注册,在创建Vue实例时通过directives属性创建局部指令,局部自定义指令只能在当前Vue实例内使用

自定义指令可以绑定如下钩子函数:

·bind      ;只调用一次,元素渲染成DOM节点后,执行directives模块的初始化工作时调用,在这里可以进行一次性的初始化设置。
·inserted       ;被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
·update       ;所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。
·componentUpdated ;指令所在组件的 VNode 及其子 VNode 全部更新后调用。
·unbind       ;只调用一次,指令与元素解绑时调用。

每个钩子函数可以有四个参数,分别是el(对应的DOM节点引用)、binding(一些关于指令的扩展信息,是个对象)、vnode(该节点对应的虚拟VN哦的)和oldVnode(之前的VNode,仅在update和componentUpdated钩子中可用)

bind钩子函数执行的时候该DOM元素被渲染出来了,但是并没有插入到父元素中,例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="vue.js"></script>
</head>
<body>
    <div id="d"><input type="" name="" v-focus></div>
    <script>
        Vue.directive('focus', {
            bind:function(el){console.log(el.parentElement);},                      //打印父节点
            inserted: function (el) {console.log(el.parentElement);el.focus()}      //打印父节点,并将当前元素处于聚焦状态
        })
        var app = new Vue({el:"#d"})
    </script>
</body>
</html>

输出如下:

可以看到input元素自动获得焦点了,控制台输出如下:

可以看到对于bind()钩子来说,它的父节点是获取不到的,因为Vue内部会在执行bind()钩子后才会将当前元素插入到父元素的子节点里

源码分析

在解析模板将DOM转换成AST对象的时候会执行processAttrs()函数,如下:

function processAttrs (el) {                     //解析Vue的属性
  var list = el.attrsList;
  var i, l, name, rawName, value, modifiers, isProp;
  for (i = 0, l = list.length; i < l; i++) {             //遍历每个属性
    name = rawName = list[i].name;
    value = list[i].value;
    if (dirRE.test(name)) {                                 //如果该属性以v-、@或:开头,表示这是Vue内部指令
      // mark element as dynamic
      el.hasBindings = true;
      // modifiers
      modifiers = parseModifiers(name);
      if (modifiers) {
        name = name.replace(modifierRE, '');
      }
      if (bindRE.test(name)) { // v-bind                          //bindRD等于/^:|^v-bind:/ ,即该属性是v-bind指令时
        /*v-bind的分支*/
      } else if (onRE.test(name)) { // v-on
        /*v-on的分支*/
      } else { // normal directives
        name = name.replace(dirRE, '');                         //去掉指令前缀,比如v-show执行后等于show
        // parse arg
        var argMatch = name.match(argRE);
        var arg = argMatch && argMatch[1];
        if (arg) {
          name = name.slice(0, -(arg.length + 1));
        }
        addDirective(el, name, rawName, value, arg, modifiers); //执行addDirective给el增加一个directives属性
        if ("development" !== 'production' && name === 'model') {
          checkForAliasModel(el, value);
        }
      }
    } else {
      /*非Vue指令的分支*/
    }
  }
}

addDirective会给AST对象上增加一个directives属性保存指令信息,如下:

function addDirective (                         //第6561行 指令相关,给el这个AST对象增加一个directives属性,值为该指令的信息
  el,
  name,
  rawName,
  value,
  arg,
  modifiers
) {
  (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });
  el.plain = false;
}

例子里的p元素执行到这里时对应的AST对象如下:

接下来在generate生成rendre函数的时候,会执行genDirectives()函数,将AST转换成一个render函数,如下:

with(this){return _c('div',{attrs:{"id":"d"}},[_c('input',{directives:[{name:"focus",rawName:"v-focus"}],attrs:{"type":"","name":""}})])}

最后等渲染完成后会执行directives模块的create钩子函数,如下:

var directives = {                 //第6173行 directives模块
  create: updateDirectives,             //创建DOM后的钩子
  update: updateDirectives,
  destroy: function unbindDirectives (vnode) {
    updateDirectives(vnode, emptyNode);
  }
}

function updateDirectives (oldVnode, vnode) {         //第6181行   oldVnode:旧的Vnode,更新时才有 vnode:新的VNode
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode);
  }
}

_updat 就是处理指令初始化和更新的,如下:

function _update (oldVnode, vnode) {                 //第6187行 初始化/更新指令
  var isCreate = oldVnode === emptyNode;                                                     //是否为初始化
  var isDestroy = vnode === emptyNode;
  var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
  var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);                 //调用normalizeDirectives$1()函数规范化参数

  var dirsWithInsert = [];
  var dirsWithPostpatch = [];

  var key, oldDir, dir;
  for (key in newDirs) {                                     //遍历newDirs
    oldDir = oldDirs[key];                                         //oldVnode上的key指令信息
    dir = newDirs[key];                                            //vnode上的key指令信息
    if (!oldDir) {                                                 //如果oldDir不存在,即是新增指令
      // new directive, bind
      callHook$1(dir, 'bind', vnode, oldVnode);                     //调用callHook$1()函数,参数2为bind,即执行v-focus指令的bind函数
      if (dir.def && dir.def.inserted) {                            //如果有定义了inserted钩子函数
        dirsWithInsert.push(dir);                                     //则保存到dirsWithInsert数组里
      }
    } else {
      // existing directive, update
      dir.oldValue = oldDir.value;
      callHook$1(dir, 'update', vnode, oldVnode);
      if (dir.def && dir.def.componentUpdated) {
        dirsWithPostpatch.push(dir);
      }
    }
  }
  if (dirsWithInsert.length) {                                    //如果dirsWithInsert存在(即有绑定了inserted钩子函数)
    var callInsert = function () {                                  //定义一个callInsert函数,该函数会执行dirsWithInsert里的每个函数
      for (var i = 0; i < dirsWithInsert.length; i++) {
        callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
      }
    };
    if (isCreate) {                                                 //如果是初始化
      mergeVNodeHook(vnode, 'insert', callInsert);                    //则调用mergeVNodeHook()函数
    } else {
      callInsert();
    }
  }

  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]) {
        // no longer present, unbind
        callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
      }
    }
  }
}

writer by:大沙漠 QQ:22969969

对于bind钩子函数来说是直接执行了,而对于inserted钩子函数则是把函数保存到dirsWithInsert数组里,再定义了一个callInsert函数,该函数内部通过作用域访问dirsWithInsert变量,并遍历该变量依次执行每个inserted钩子函数

mergeVNodeHook()钩子函数的作用是把insert作为一个hooks属性保存到对应的Vnode的data上面,当该Vnode插入到父节点后会调用该hooks,如下:

function mergeVNodeHook (def, hookKey, hook) {      //第2074行  合并VNode的钩子函数 def:一个VNode hookKey:(事件名,比如:insert) hook:回调函数
  if (def instanceof VNode) {                           //如果def是一个VNode
    def = def.data.hook || (def.data.hook = {});          //则将它重置为VNode.data.hook,如果VNode.data.hook不存在则初始化为一个空对象 注:普通节点VNode.data.hook是不存在的。
  }
  var invoker;
  var oldHook = def[hookKey];

  function wrappedHook () {
    hook.apply(this, arguments);                            //先执行hook函数
    // important: remove merged hook to ensure it's called only once
    // and prevent memory leak
    remove(invoker.fns, wrappedHook);                       //然后把wrappedHook从invoker.fns里remove掉,以且包只执行一次
  }

  if (isUndef(oldHook)) {                               //如果oldHook不存在,即之前没有定义hookKey这个钩子函数
    // no existing hook
    invoker = createFnInvoker([wrappedHook]);               //直接调用createFnInvoker()返回一个闭包函数,参数为执行的回调函数
  } else {
    /* istanbul ignore if */
    if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
      // already a merged invoker
      invoker = oldHook;
      invoker.fns.push(wrappedHook);
    } else {
      // existing plain hook
      invoker = createFnInvoker([oldHook, wrappedHook]);
    }
  }

  invoker.merged = true;
  def[hookKey] = invoker;                               //设置def的hookKey属性指向新的invoker
}

createFnInvoker就是v-on指令对应的那个函数,用到了同一个API,执行完后,我们就把invoker插入到input对应的VNode.data.hook里了,如下:

最后等到该VNode插入到父节点后就会执行invokeCreateHooks()函数,该函数会遍历VNode.hook.insert,依次执行每个函数,也就执行到我们自定义定义的inserted钩子函数了。

总结

到此这篇关于Vue.js源码分析之自定义指令的文章就介绍到这了,更多相关Vue.js自定义指令内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 如何使用vue自定义指令构建拖放插件

    我们都知道html5的拖放特性,利用它可以很方便的实现拖拽和放置功能,比如一些选择类操作的使用场景,让用户去拖拽比鼠标点击更容易接受和理解.今天我们就利用这一特性,结合vue的自定义指令,来实现一个简单但是实用的拖放插件. 为什么叫它插件?因为我们的目标不是开发一个vue组件,而是两个vue的自定义指令,并且最终会把这两个自定义指令封装到一个es6的class里,在实际项目中引入就可以很方便的使用了. 大部分的拖放使用场景都是把一些待选元素从A区域拖放到B区域.这里就涉及到两个概念,一个是可拖拽

  • Vue实现指令式动态追加小球动画组件的步骤

    1. 小球组件 我们希望可以封装一个通用的小球动画组件,这个组件可以在任何地方调动,而且小球组件可以通过this.$ball({...props})这样的方式调用,让他在用法上接近element-ui template模板 <template> <div class="ball-wrapper"> <transition @before-enter="beforeEnter" @enter="enter" @afte

  • Vue自定义v-has指令,做按钮权限判断的步骤

    应用场景 以后台管理系统为例,每个用户所拥有的按钮权限不一样.管理员配置权限之后,用户登录时,从接口拿到按钮权限列表,然后根据后台数据判断显示哪些按钮. 简单说一下,自定义指令 Vue.js官网关于自定义指令的讲解 cn.vuejs.org/v2/guide/cu- 基础概念 Vue除了核心功能默认内置的指令(v-model和v-show)还可以注册自定义指令. 在Vue2.0中,代码复用和抽象的主要形式是组件.但有的情况下,仍需要对普通DOM元素进行底层操作,这时候就会用到自定义指令. 譬如,

  • vue封装自定义指令之动态显示title操作(溢出显示,不溢出不显示)

    vue项目开发的过程中,如果需要文字溢出的dom元素鼠标悬浮时才显示title文字的话,可以封装一个自定义的指令. 方法一. 组件内代码如下: directives: { title: { inserted(el) { const { clientWidth, scrollWidth, title } = el; if (!title && scrollWidth > clientWidth) el.title = el.innerText; } } }, 使用: <artic

  • Vue基本指令实例图文讲解

    Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架.与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用.Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合.另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动. 一. v-on指令 1. 基础用法 v-on是事件监听的指令, 下面来看简单用法 <!DOCTYPE html> <html lang="en"&

  • Vue 3自定义指令开发的相关总结

    什么是指令(directive) 在Angular和Vue中都有Directive的概念,我们通常讲Directive 翻译为"指令". 在计算机技术中,指令是由指令集架构定义的单个的CPU操作.在更广泛的意义上,"指令"可以是任何可执行程序的元素的表述,例如字节码. 那么在前端框架Vue中"指令"到底是什么,他有什么作用呢? 在Vue开发中我们在模板中经常会使用v-model和v-show等以v-开头的关键字,这些关键字就是Vue框架内置的指令

  • Vue自定义v-has指令实现按钮权限判断

    应用场景 以后台管理系统为例,每个用户所拥有的按钮权限不一样.管理员配置权限之后,用户登录时,从接口拿到按钮权限列表,然后根据后台数据判断显示哪些按钮. 简单说一下,自定义指令 Vue.js官网关于自定义指令的讲解 cn.vuejs.org/v2/guide/cu- 基础概念 Vue除了核心功能默认内置的指令(v-model和v-show)还可以注册自定义指令. 在Vue2.0中,代码复用和抽象的主要形式是组件.但有的情况下,仍需要对普通DOM元素进行底层操作,这时候就会用到自定义指令. 譬如,

  • vue3.0自定义指令(drectives)知识点总结

    在大多数情况下,你都可以操作数据来修改视图,或者反之.但是还是避免不了偶尔要操作原生 DOM,这时候,你就能用到自定义指令. 举个例子,你想让页面的文本框自动聚焦,在没有学习自定义指令的时候,我们可能会这么做. const app = Vue.createApp({ mounted(){ this.$refs.input.focus(); }, template: `<input type="text" ref="input" />`, }); 在mou

  • 8个非常实用的Vue自定义指令

    本文在github做了收录 github.com/Michael-lzg- demo源码地址 github.com/Michael-lzg- 在 Vue,除了核心功能默认内置的指令 ( v-model 和 v-show ),Vue 也允许注册自定义指令.它的作用价值在于当开发人员在某些场景下需要对普通 DOM 元素进行操作. Vue 自定义指令有全局注册和局部注册两种方式.先来看看注册全局指令的方式,通过 Vue.directive( id, [definition] ) 方式注册全局指令.然后

  • 详解Vue自定义指令及使用

    一.什么是指令 学习 vue 的时候肯定会接触指令,那么什么是指令呢? 在 vue 中提供了一些对于页面和数据更为方便的输出,这些操作就叫做指令,以 v-xxx 表示,比如 html 页面中的属性 <div v-xxx ></div> 比如在 angular 中 以 ng-xxx 开头的就叫做指令 指令中封装了一些 DOM 行为,结合属性作为一个暗号,暗号有对应的值,根据不同的值,会进行相关 DOM 操作的绑定,即可以进行一些模板的操作 vue 中常用的一些内置 v- 指令 v-t

随机推荐