Vue指令的剖析及拓展

概述

在日常的开发工作中,Vue指令的使用非常常见,而Vue指令对于我们的来说也非常的有用且高效。下面我将按照知识图谱的展示介绍与剖析Vue指令。

指令简介

什么是指令

什么是Vue中指令?

指令 (Directives) 是带有 v- 前缀的特殊 attribute。指令 attribute 的值预期是单个 JavaScript 表达式 。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。

v- 前缀作为一种视觉提示,用来识别模板中 Vue 特定的 attribute。当你在使用 Vue.js 为现有标签添加动态行为 (dynamic behavior) 时,v- 前缀很有帮助,然而,对于一些频繁用到的指令来说,就会感到使用繁琐。同时,在构建由 Vue 管理所有模板的单页面应用程序 (SPA - single page application) 时,v- 前缀也变得没那么重要了。

在 Vue 中存在一个全局 APIVue.directive,下面我们来进行介绍与学习。

  • 用法
bash 复制代码
Vue.directive( id, [definition] )
  • 参数

    • {string} id
    • {Function | Object} [definition]
  • 作用:注册或获取全局指令

js 复制代码
//注册
Vue.directive("my-directive", {
  bind: function () {},
  instered: function () {},
  update: function () {},
  componentUpdate: function () {},
  unbind: function () {},
});
//注册指令函数
Vue.directive("my-directive", function () {
  //这里将会被bind和update调用
});

//getter 方法,返回已注册的指令
let myDirective = Vue.directive("my-directive");

这里需要强调 Vue.directive 方法的作用是注册或获取全局指令,而不是让指令生效。其区别是注册指令需要做的事是将指令保存在某个位置,而让指令生效是指将指令从某个位置拿出来执行它。

注册指令的实现并不难,代码如下

js 复制代码
Vue.options = Object.create(null)
Vue.options['directives'] = Object.create(null)

Vue.directive = function(id,definition) {
    if(!definition) {
        return this.options
    } else {
        if(typeof definition === 'function') {
            definition = {bind:fubction,update:function}
        }
        this.options['directive'][id] = definition
        return definition
    }
}

我们在 Vue 构造函数上创建了 options 属性来存放选项,并在选项上新增了 directive 方法用于存放指令。

指令原理概述

Vue.directive全局API可以创建自定义指令并获取全局指令,但它并不能让指令生效,而指令的相关知识贯穿Vue.js内部的各个核心技术点,所以我们来介绍一下指令的原理。

在模板解析阶段,我们将指令解析到AST ,然后使用AST生成代码字符串的过程中实现某些内置指令的功能,最后在虚拟DOM渲染的过程中触发自定义指令的钩子函数使指令生效。

下图给出了指令生效的全过程。在模板解析阶段,会将节点上的指令解析出来并添加到AST的directives属性中。

随后directives数据会传递到VNode中,接着就可以通过vnode.data.directives获取一个节点所绑定的指令。

最后当虚拟DOM进行修补时,会根据节点的对比结果触发一些钩子函数。更新指令的程序会监听create、update和destory钩子函数,并在这三个钩子函数触发时对VNode和oldVNode进行对比,最终根据对比结果触发指令的钩子函数。(使用自定义指令时,可以监听5种钩子函数:bind,inserted,uodate,componentUpdate与unbind。)指令的钩子函数出发后,就说明指令生效了。

有一些内置指令是在模板编译阶段实现的。在代码生成时,通过生成一个特殊的代码字符串来实现指令的功能。例如,在模板中使用v-if指令

html 复制代码
<li v-if="has">if</li>
<li v-else>else</li>

在模板编译的代码生成阶段会生成这样的代码字符串:

js 复制代码
(has)?_c('li',[_v('if')]):_c('li',[_v('else')])

为了方便观察,我们将字符串格式化:

js 复制代码
(has)
  ? _c('li',[_v('if')])
  : _c('li',[_v('else')])

这样一段代码字符串在最终被执行时,会根据has变量的值来选择创建哪个节点。诸如此类的还有v-for指令,而除此之外还有复杂的v-on指令的原理在这里不做详细的介绍。

内置指令

内置指令指的就是Vue自带指令,开箱即用。

Vue一共有16个自带指令,包括了:

v-textv-htmlv-showv-ifv-elsev-else-ifv-forv-onv-bindv-modelv-slotv-prev-cloakv-oncev-memov-is,其中v-memo3.2新增的,v-is3.1.0中废弃。

内置指令的使用想必大家都非常熟悉了,这里就不一一列举具体的使用了。

内置指令的使用

自定义指令

自定义指令简介

除了核心功能默认的内置指令外,Vue.js 也允许注册自定义指令。虽然代码复用和抽象的主要形式是组件,但是有些情况下,仍然要对普通 DOM 元素进行底层操作,这时就会用到自定义指令。

自定义指令的内部原理

我们知道,虚拟DOM通过算法对比两个VNode之间的差异并更新真实的DOM节点。在更新真实的DOM节点时,有可能是创建新的节点,或者更新一个已有的节点,还有可能是删除一个节点等。虚拟DOM在渲染时,除了更新DOM内容外,还会触发钩子函数。例如,在更新节点时,除了更新节点的内容外,还会触发update钩子函数。这是因为标签上通常会绑定一些指令、事件或属性,这些内容也需要在更新节点时同步被更新。因此,事件、指令、属性等相关处理逻辑只需要监听钩子函数,在钩子函数触发时执行相关处理逻辑即可实现功能。

指令的处理逻辑分别监听了create、update与destroy,其代码如下

js 复制代码
export default {
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives(vnode){
    updateDirectives(vnode, emptyNode)
  }
}

虚拟DOM在触发钩子函数时,上面代码中对应的函数会被执行。但无论哪个钩子函数触发,最终都会执行一个叫作updateDirectives的函数。从代码中可以得知,指令相关的处理逻辑都在updateDirectives函数中实现,该函数的代码如下:

js 复制代码
function updateDirectives (oldVnode, vnode){
  if(oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode)
  }
}

可以看到,不论oldVnode还是vnode,只要其中一个有一个虚拟节点存在directives,那么就会执行_update函数处理指令。

说明:在模板解析时,directives会从模板的属性中解析出来并最终设置到VNode中。

自定义指令的使用

如何注册自定义指令

要想使用自定义指令,我们必须先提前把它注册好,就好比我们的组件一样,得先注册,才能使用。

注册指令也分为全局注册和局部注册,和全局注册组件和局部注册组件一个道理。全局注册的指令可以在任何组件中直接使用,局部注册的指令只能在注册的地方使用。

全局注册

全局注册顾名思义。自定义指令注册好后,在项目的所有组件内都可以直接使用。 vue提供了一个directive方法给我们注册自定义指令,我们在main.js中注册一个全局的自定义指令。 代码如下:

js 复制代码
Vue.directive('name',{
  bind(){},
  inserted(){},
  update(){},
  componentUpdated(){},
  unbind(){}
})

上段代码中我们就直接调用了Vue提供的directive方法来注册全局的自定义指令,该方法接收两个参数:指令名称.包含指令钩子函数的对象。

指令注册完毕后,我们就可以在项目中任意组件中的元素上使用"v-指令名称"的形式使用指令了。

需要注意的是,指令钩子函数不是必须的,大家可以把它与vue的生命周期钩子函数做类比,它们的作用就是用来让指令在不同的过程中做不同的事情。

局部注册

通常来说,如果自定义指令不是每个组件都会用到的话,我们一般局注册自定义指令就好了。

js 复制代码
export default {
  name: "App",
  directives: {
    resize: {
      bind() {},
      inserted() {},
      update() {},
      componentUpdated() {},
      unbind() {},
    },
  },
};

如上所示,Vue提供了一个directives选项供我们注册自定义指令,它与data、methods同级别,上段代码中我们注册了一个名叫resize的自定义指令,该指令只允许在组件内部使用。

很多时候我们不需要用到自定义指令中的所有钩子函数,常用的就那么几个,所以官方给我们提供了一种简写的方式。

js 复制代码
resize(el, binding)
console.log("我是简写的自定义指令",binding.value);

上面代码的写法让我们的指令变得很简洁,上段代码的意思就是把bind和update钩子函数合二为一了,并且只会执行这两个方法,通常我们想要这两个钩子函数做同样的事的时候使用。

自定义指令参数详解

上面简单介绍了局部注册自定义指令和全局注册自定义指令,可以看到指令里面有几个钩子函数,我们的操作逻辑主要在这几个钩子函数当中,所以我们有必要介绍下这几个钩子函数。

bind: 只调用一次。指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被播入文档中)。
update: 所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
componentUpdated: 指令所在组件的VNode及其子VNode全部更新后调用。
unbind: 只调用一次,指令与元素解绑时调用。

上面5个就是自定义指令的全部钩子函数,每个钩子函数都是可选的,视情况而定。大家可以简单理解钩子函数顺序;指令绑定到元素时(bind)、元素插入时(inserted),组件更新时(update)、组件更新后(componentUpdated)、指令与元素解绑时(unbind)。这些和组件的生命周期函数有点类似。

钩子函数参数介绍

为了方便我们的逻辑操作,每个钩子函数都会接收参数,我们可以用这些参数做我们想做的事。

el: 指令所绑定的元素,可以用来直接操作DOM.

binding: 一个对象,包含以下属性:

  • name:指令名,不包括v-前缀。
  • value :指令的绑定值。例如: v-my-directivem-1 + 1"书,绑定值为2。
  • oldvalue :指令绑定的前一个值,仅在update和componentupdated钩子中可用。无论值是否改变都可用。
  • expression:字符串形式的指令表达式。例如v-my-directivem"1 + 1"中,表达式为"1+1".
  • arg ∶传给指令的参数,可选。例如v-my-directive:foo中,参数为"foo".
  • modifiers ∶一个包含修饰符的对象。例如: v-my-directive.foo.bar中,修饰符对象为(foo: true, bar: true }。

vnode:

Vue编译生成的虚拟节点。

oldVnode:

上一个虚拟节点,仅在update和componentUpdated钩子中可用。

在使用的时候,el和binding参数是我们使用得最平凡的,有了这些参数,我们的操作就变得简单起来。

几个常见使用的自定义指令

需求:实现长按,用户需要按下并按住按钮几秒钟,触发相应的事件

思路:

  • 创建一个计时器, 2 秒后执行函数
  • 当用户按下按钮时触发 mousedown 事件,启动计时器;用户松开按钮时调用 mouseout 事件。
  • 如果 mouseup 事件 2 秒内被触发,就清除计时器,当作一个普通的点击事件
  • 如果计时器没有在 2 秒内清除,则判定为一次长按,可以执行关联的函数。
  • 在移动端要考虑 touchstart,touchend 事件
js 复制代码
const longpress = {
  bind: function (el, binding, vNode) {
    if (typeof binding.value !== 'function') {
      throw 'callback must be a function'
    }
    // 定义变量
    let pressTimer = null
    // 创建计时器( 2秒后执行函数 )
    let start = (e) => {
      if (e.type === 'click' && e.button !== 0) {
        return
      }
      if (pressTimer === null) {
        pressTimer = setTimeout(() => {
          handler()
        }, 2000)
      }
    }
    // 取消计时器
    let cancel = (e) => {
      if (pressTimer !== null) {
        clearTimeout(pressTimer)
        pressTimer = null
      }
    }
    // 运行函数
    const handler = (e) => {
      binding.value(e)
    }
    // 添加事件监听器
    el.addEventListener('mousedown', start)
    el.addEventListener('touchstart', start)
    // 取消计时器
    el.addEventListener('click', cancel)
    el.addEventListener('mouseout', cancel)
    el.addEventListener('touchend', cancel)
    el.addEventListener('touchcancel', cancel)
  },
  // 当传进来的值更新的时候触发
  componentUpdated(el, { value }) {
    el.$value = value
  },
  // 指令与元素解绑的时候,移除事件绑定
  unbind(el) {
    el.removeEventListener('click', el.handler)
  },
}

export default longpress

需求:实现点击元素外部时,触发指定事件,完成相关操作。

思路:

  • 判断点击的元素是否是本身,是本身则返回。
  • 判断指令中是否绑定了函数,如果绑定了函数则调用那个函数。
  • 监听元素的点击事件,执行处理函数。
js 复制代码
Vue.directive('clickOutside', {
  // 初始化指令
  bind(el, binding) {
    function clickHandler(e) {
      // 这里判断点击的元素是否是本身,是本身则返回。
      if (el.contains(e.target)) return false
      // 判断指令中是否绑定了函数
      if (binding.expression) {
        // 如果绑定了函数 则调用那个函数,此处的binding.value就是handleClose方法
        binding && binding.value(e)
      }
    }
    // 给当前元素绑定一个私有变量,方便在unbind中可以接触事件的监听。
    el.__vueClickOutside__ = clickHandler
    document.addEventListener('click', clickHandler)
  },
  update() {},
  unbind(el) {
    // 解除事件监听
    document.removeEventListener('click', el.__vueClickOutside__)
    delete el.__vueClickOutside__
  }
})

需求:当我们需要快速复制一段文本到剪切板时,手动选中复制后粘贴稍显麻烦,通过自定义指令可以实现点击文本时自动将文本复制到剪贴板,直接粘贴完成需求。

思路:

  • 获取与指令绑定的值。
  • 创建一个input元素,将获取到的值赋于input元素的value属性。
  • 利用input元素的select方法选中value,然后再使用浏览器提供的方法将选中的指添加到剪切板。
js 复制代码
Vue.directive('copy', {
  bind(el, binding) {
    el.__vueCopy__ = binding.value
    // 给目标元素定义点击事件
    el.clickHandler = () => {
      if (!el.__vueCopy__) {
        console.log('没有可复制的数据!')
      }
      return false
    }
    /* 
    在获取到与指令绑定的值之后如何将内容添加到剪切板呢?
    核心思想是:
    首先创建一个不可见的input或者textarea元素将获取到的值添加给元素的value属性
    然后利用input元素的select方法选中value
    然后再使用浏览器提供的方法将选中的值添加到剪切板
    */
    let inputElement = document.createElement('input')
    inputElement.setAttribute('value', el.__vueCopy__)
    inputElement.style.position = 'absolute'
    inputElement.style.left = '-9999px'
    document.body.appendChild(inputElement)
    inputElement.select()

    // 将选中内容添加至剪切板目前有两种方法
    /* 
     1.使用 document.execCommand 但是此特性已不推荐使用,所以尽量不要使用该特性。
      https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand
    */
    /*
      2.使用 Clipboard Clipboard API 可用于实现剪切、复制和粘贴功能,系统剪贴板暴露于全局属性 Navigator.clipboard 之中。
      如果用户没有适时使用 Permissions API 授予相应权限和"clipboard-read"或 "clipboard-write"权限,调用Clipboard对象的方法不会成功。
      所有剪贴板API 方法都是异步的;它们返回一个 Promise 对象,在剪贴板访问完成后被执行。如果剪贴板访问被拒绝,promise 也会被拒绝。
      https://developer.mozilla.org/zh-CN/docs/Web/API/Clipboard
    */
    if (navigator.clipboard) {
      navigator.clipboard.writeText(el.__vueCopy__).then(
        // 复制成功callback
        function() {
          console.log('复制成功!')
        },
        // 复制失败callback
        function() {
          console.log('复制失败!')
        }
      )
    } else {
      document.execCommand('copy')
      console.log('复制成功!')
    }
    document.body.removeChild(inputElement)
    // 给目标元素添加点击事件
    el.addEventListener('click', el.clickHandler)
  },
  // 当传进来的值更新的时候触发
  componentUpdated(el, { value }) {
    el.__vueCopy__ = value
  },
  // 指令与元素解绑时,移除点击事件绑定。
  unbind(el) {
    el.removeEventListener('click', el.clickHandler)
  }
})

需求:根据用户的权限,动态展示用户可以看到的模块。

思路:根据用户的权限,展示或移除对应的DOM元素。

js 复制代码
let checkPermission = function(el, binding) {
  const { value } = binding
  const userIds = [1, 2, 3] // 这里用来获取用户的权限列表,可以动态获取或者存储在vuex。
  try {
    let flag
    if (value && value instanceof Array) {
      flag = value.some((item) => userIds.includes(item))
    } else {
      flag = userIds.includes(value)
    }
    // 隐藏无权限内容
    if (!flag) el.parentNode && el.parentNode.removeChild(el)
  } catch (error) {
    throw new Error('check v-')
  }
}

// 检查用户是否具备权限,绑定元素接收数组,用户权限涵盖数组内的任意一项就允许展示。
Vue.directive('permission', {
  inserted(el, binding) {
    checkPermission(el, binding)
  },
  update(el, binding) {
    checkPermission(el, binding)
  }
})

需求:配合animate.css,对于即将要出现在可视区的元素添加动画特效。

思路:通过判断元素位置,添加对应的类名,展示动画特效。

js 复制代码
Vue.directive('animate', {
  inserted(el, binding) {
    // 聚焦元素
    binding.addClass = () => {
      // 支持IntersectionObserver
      if (window.IntersectionObserver) {
        const obj = new IntersectionObserver(function(els) {
          els.forEach((item) => {
            if (
              item.isIntersecting &&
              item.target.className.indexOf(binding.value) === -1
            ) {
              item.target.className = binding.value + '' + item.target.className
              obj.unobserve(item.target)
            }
          })
        })
        // 创建观察者对象
        obj.observe(el)
      } else {
        const { top } = el.getBoundingClientRect()
        const h =
          document.documentElement.clientHeight || document.body.clientHeight
        if (top < h) {
          if (el.className.indexOf(binding.value) === -1) {
            el.className = binding.value + ' ' + el.className
          }
          if (binding.addClass) {
            window.removeEventListener('scroll', binding.addClass)
          }
        }
        window.addEventListener('scroll', binding.addClass, true)
      }
      binding.addClass()
    }
  },
  unbind(el, binding) {
    if (binding.addClass) {
      window.removeEventListener('mousewheel', binding.addClass)
    }
  }
})

自定义指令还能做什么

除了实现控制页面元素显示与隐藏、图片懒加载、输入内容过滤等功能,Vue的自定义指令还可以做很多其他的事情。例如,你可以使用自定义指令来创建复杂的模板渲染函数、封装常见的逻辑操作、实现高级的动画效果等。总之,自定义指令使得Vue更加灵活和强大,可以满足你在前端开发中的各种需求。

例如一下自定义指令,封装之后我们就可以在使用elment-uidialog组件时通过拖拽来改变dialog的位置了。

js 复制代码
Vue.directive('moveDialog', {
  bind(el) {
    const dialogHeaderEl = el.querySelector('.el-dialog__header')
    const dragDom = el.querySelector('.el-dialog')
    dialogHeaderEl.style.cssText += ';cursor:move;'

    // 获取元素原有属性
    const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)

    dialogHeaderEl.onmousedown = (e) => {
      // 鼠标按下,计算当前元素距离可视区域的高度。
      const disX = e.clientX - dialogHeaderEl.offsetLeft
      const disY = e.clientY - dialogHeaderEl.offsetTop

      // 获取到的值带 px 进行正则替换
      let styL, styT

      // 注意在ie中,第一次取到值为组件自带,50%移动之后复制为px;
      if (sty.left.includes('%')) {
        styL = +document.body.clientWidth * (+sty.left.replace(/%/g, '') / 100)
        styT = +document.body.clientHeight * (+sty.top.replace(/%/g, '') / 100)
      } else {
        styL = +sty.left.replace(/\px/g, '')
        styT = +sty.top.replace(/\px/g, '')
      }

      document.onmousemove = function(e) {
        // 通过事件委托,计算移动的距离。
        const l = e.clientX - disX
        const t = e.clientY - disY

        // 移动当前元素
        dragDom.style.left = `${l + styL}px`
        dragDom.style.top = `${t + styT}px`
      }

      document.onmouseup = function() {
        document.onmousemove = null
        document.onmouseup = null
      }
    }
  }
})

总结

Vue自定义指令是Vue框架中的一个强大功能,它允许我们创建自定义的指令来实现页面上的各种效果。通过自定义指令,我们可以封装和复用代码,提高开发效率,同时使代码更加清晰易懂。

在未来,随着Vue的持续发展和更新,自定义指令将更加灵活和强大。我们可以期待更多的自定义指令选项,如更复杂的的行为、更精细的参数控制等。同时,随着前端技术的不断发展,自定义指令将在前端开发中扮演更重要的的角色,为我们的开发工作带来更多便利和效率。

总之,通过本文的介绍和实例演示,我希望帮助读者更好地理解和使用Vue的自定义指令,从而在开发中发挥更大的创造力,实现更多以前难以实现的效果和功能。

相关推荐
Leyla6 分钟前
【代码重构】好的重构与坏的重构
前端
影子落人间9 分钟前
已解决npm ERR! request to https://registry.npm.taobao.org/@vant%2farea-data failed
前端·npm·node.js
世俗ˊ34 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92134 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_39 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人1 小时前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css