vue2 vue3 中指令总结

vue2 vue3 中指令总结

vue 内置了一些指令,也提供了自定义指令的接口。

指令的作用:可把一些可复用的逻辑封装成指令,以实现逻辑复用,尤其是需要直接操作 DOM 时,可把这些操作封装成指令,能极大提高代码复用性和可维护性。

指令按照使用范围看,分为全局指令局部指令(在某个组件内部使用的)。

指令和组件一样,具有一些在特定时期执行的函数,叫作指令钩子,就是通过它们定义指令的。

使用方式

有一个 v-test 指令。

html 复制代码
<template>
  <div v-test:disabled.foo="'directive'">
    <h1>{{ msg }}</h1>
  </div>
</template>

: 之后的是指令参数,类似 v-on:keyup 中的 keyup
. 之后的时指令修饰符,foo 是修饰符。

参数只能有一个,修饰符可有多个。

v-test:disabled:boo.foo.zoo="msg"

在指令内部,可获取到这样的对象:

js 复制代码
{
  arg: "disabled:boo"
  modifiers: {
    foo: true,
    zoo: true
  }
}

希望不同情况下绑定不同的参数,可使用动态参数

v-test:[argu].foo.zoo="msg"

参数必须在修饰符之前。

指令的等号后面是指令表达式,其值对应 binding 对象的 value 属性。

binding 一个对象:

js 复制代码
{
  arg: "disabled",
  expression: "msg",
  modifiers: {
    foo: true
  },
  name: "test",
  value: "你好",
}

vue2 指令定义方式

以插件的形式定义一个全局 v-click-outside :

js 复制代码
export const clickOutside = (Vue, options) => {
  Vue.directive('clickOutside', {
    inserted(el, binding, vnode) {
      const {
        value
      } = binding
      // const _this = vnode.context
      // NOTE 技巧:处理函数挂载在元素上,方便解绑时移出事件监听
      el.onClick = ({
        target
      }) => {
        if (el.contains(target)) {
          // 点击内部
          console.log('clickInside')
        } else {
          // 点击外部
          console.log('clickOutside')
          value && value()
        }
      }
      document.addEventListener('click', el.onClick, false)
    },
    unbind(el, binding, vnode) {
      document.removeEventListener('click', el.onClick, false)
    },
  })
}

注册插件:

main.js

js 复制代码
Vue.use(clickOutside)

使用:

html 复制代码
<template>
  <div>
    <h3>测试 v-click-outside</h3>
    <div v-click-outside="onClickOutside">
      <p>测试点击外部</p>
      <p>测试点击外部</p>
      <p>测试点击外部</p>
      <p>测试点击外部</p>
    </div>

  </div>
</template>

<script>
  export default {

    name: 'ClickOutsideDemo',
    data() {
      return {
        msg: 'Hello web components in stencil!',
      }
    },
    methods: {
      onClickOutside() {
        console.log('onClickOutside')
        console.log(this.msg)
      },
    },

  }
</script>

点击到div外部时,就会执行onClickOutside

js 复制代码
Vue.directive('directive-name', options)

options 是一个对象,包含一些生命周期钩子,这些生命周期都是可选的。

按照执行顺序可:

bash 复制代码
bind # 只调用一次,指令绑定到元素时调用,父元素可能不存在。做初始化工作
⬇️
inserted # 只调用一次,被绑定的元素插入到父节点,父节点存在,此时可能被绑定元素还每插入文档中。
⬇️
update # 此时组件还没更新完毕,拿不到最新的数据
⬇️
componentUpdated # 此时组件已经更新完毕 能拿到最新
⬇️
unbind # 只调用一次,指令和元素解绑,可做一些收尾工作

简写方式:

js 复制代码
Vue.directive('color-swatch', function(el, binding) {
  el.style.backgroundColor = binding.value
})

在 bind 和 update 时触发相同行为,而不关心其它的钩子,此时推荐使用函数方式定义指令。

钩子的参数

js 复制代码
bind(el, binding, vnode)
inserted(el, binding, vnode)
update(el, binding, vnode, oldVnode) //  update componentUpdated 还有额外的 oldVnode
componentUpdated(el, binding, vnode, oldVnode)
unbind(el, binding, vnode)

指令钩子函数的参数,主要关注 elbinding

el 是绑定指令的元素,可对其进行 DOM 操作。

binding 一个对象:

js 复制代码
{
  arg: "disabled",
  expression: "msg",
  modifiers: {
    foo: true
  },
  name: "test",
  value: "你好",
}

updatecomponentUpdated 钩子函数,binding 对象还有 oldArgoldValue 属性。

指令钩子和组件生命周期的执行顺序是怎样的?

挂载组件

bash{7, 复制代码
beforeCreate
⬇️
created
⬇️
beforeMount
⬇️
bind # 指令 绑定到元素时调用,父元素可能不存在
⬇️
inserted # 指令 被绑定元素插入父元素时调用,父元素一定存在
⬇️
mounted

更新组件

bash{3,5} 复制代码
beforeUpdate
⬇️
update # 指令,此时组件还没更新完毕,拿不到最新的数据
⬇️
componentUpdated # 指令 此时组件已经更新完毕 能拿到最新的数据
⬇️
updated

销毁组件

bash 复制代码
beforeDestroy
⬇️
unbind # 指令 组件在销毁之前调用,仍然能拿到组件的数据
⬇️
destroyed

重建组件时

bash 复制代码
beforeCreate
⬇️
created (重建)
⬇️
beforeMount
⬇️
bind # 注意这里,指令绑定这个钩子函数,将会拿不到重建后的最新数据
⬇️
beforeDestroy (组件销毁)
⬇️
unbind # 指令
⬇️
destroyed
⬇️
inserted # 指令 使用该钩子函数,能拿到重建后的数据
⬇️
mounted

结论:只有 insertedcomponentUpdated 生命周期钩子,在执行时组件的 DOM 已经更新完毕,可放心使用。它们可获取到组件更新后的数据,指令绑定的元素的父元素也已经存在。
如何在指令生命周期中使用 this 或者访问组件实例?

直接使用 this 为 undefined,可使用 vnode.context 获取:

js 复制代码
inserted(el, binding, vnode) {
    // setTile 是组件 methods 里的方法
    vnode.context.setTile(el)
  },
  componentUpdated(el, binding, vnode, oldVnode) {
    vnode.context.setTile(el)
  },

vue3 中的指令

vue3 的指令钩子、钩子的参数和定义方式有变化。

定义方式

全局指令

js 复制代码
app.directive('directive-name', directiveOptions)

简写形式:

js 复制代码
app.directive('directive-name', (el, binding) => {}) // mounted 和 updated 操作相同

局部指令

script setup 使用 v 开头命名。

html 复制代码
<script setup>
  // 注册一个局部的自定义指令,需要以小写v开头
  const vFocus = {
    mounted(el, binding, vNode) {
      console.log(el)
      console.log(binding)
      console.log(vNode)
      // 获取input,并调用其focus()方法
      el.focus()
    },
  }
</script>

<template>
  <input v-focus />
</template>

setup 函数方式:

html 复制代码
<script>
  export default {
    directives: {
      focus: {
        mounted(el, binding, vNode) {
          // 获取input,并调用其focus()方法
          el.focus()
        },
      },
    },
    setup() {},
  }
</script>

指令钩子的变化:

js 复制代码
const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode) {},
  // 在元素被插入到 DOM 前调用, 可执行一些初始化工作
  beforeMount(el, binding, vnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用,收尾工作
  beforeUnmount(el, binding, vnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode) {},
}

常用的钩子:

bash 复制代码
beforeMount # 类似 vue2 bind
mounted # 类似 vue2 inserted
updated # 类似 vue2 componentUpdated
beforeUnmount # 类似 vue2 unbind

binding 的参数有变化:增加了 instance 属性用于获取组件实例。

创建一个检测点击 div 外部的指令:

ts 复制代码
import type { App } from 'vue'

export const clickOutside = (app: App, options: any) => {
  app.directive('clickOutside', {
    mounted(el, binding) {
      const { value, instance } = binding
      // NOTE 技巧:处理函数挂载在元素上,方便解绑时移出事件监听
      el.onClick = (event: Event) => {
        if (el.contains(event.target)) {
          console.log('clickInside')
        } else {
          // 点击外部
          console.log('clickOutside')
          // console.log(instance)// 组件实例
          value && value()
        }
      }
      document.addEventListener('click', el.onClick, false)
    },
    beforeUnmount(el) {
      console.log('beforeUnmount')
      document.removeEventListener('click', el.onClick, false)
    },
  })
  return app
}

相同的功能使用 hook 实现:

js 复制代码
import {
  onMounted,
  onBeforeUnmount,
  ref
} from 'vue'

export function useOnClickOutside(DOM = null, callback) {
  const isClickOutside = ref(false)

  function handleClick(event) {
    if (DOM.value && !DOM.value.contains(event.target)) {
      callback()
      isClickOutside.value = true
    }
  }

  onMounted(() => {
    document.addEventListener('mousedown', handleClick)
  })

  onBeforeUnmount(() => {
    document.removeEventListener('mousedown', handleClick)
  })
  return isClickOutside
}

hook vs 指令

使用方式:hook 在 script 里,指令绑定到模板

定义方式:hook 更加简单,只需记住组件的生命钩子,复用更加方便,指令略微繁琐,需要额外记忆指令钩子。

hook 能返回响应是对象,指令做不到。

hook 能组合使用,指令不行。多个指令用在同一个元素上,也算一种组合?

hook 优势更大,更加灵活

指令钩子函数和组件生命周期的执行顺序

挂载阶段:

bash 复制代码
setup
onBeforeMount
created # 指令钩子
beforeMount # 指令钩子
mounted # 指令钩子
onMounted

销毁阶段:

bash 复制代码
onBeforeUnmount
beforeUnmount # 指令钩子
unmounted # 指令钩子
onUnmounted

比较好实践:只定义 mounted、updated 和 beforeUnmount 钩子。

指令和 render 函数

指令用在 jsx 上

和用到 html 上一样。

jsx 复制代码
function JSXDemo() {
  return (
    <>
      <h3>v-copy 用jsx上</h3>
      <div v-copy={'这是复制的内容'} style={{ color: 'red' }}>
        点击复制
      </div>
      <h3>v-auth</h3>
      <button v-auth:disabled={'li'} class="button">
        无权限,被禁用
      </button>
      <br />
      <button v-auth="key3">无权限删除</button>
      <br />
      <button v-auth="key">有权限</button>
      <br />
      <ElButton type="primary" v-auth="key">
        按钮
      </ElButton>
    </>
  )
}

指令和 render 函数一起使用

使用 widthDirectives 函数注册指令。

withDirectives(VNode, [[vDirective,value, arg, modifiers]])

js 复制代码
function RenderDemo() {
  // withDirectives(VNode, [[vDirective,value, arg, modifiers]])
  const Input = h('input', {
    type: 'text',
    value: 'hello'
  })
  const InputWithFocus = withDirectives(Input, [
    [directiveObj.focus, true]
  ])
  const DivWithCopy = withDirectives(h('div', '这是复制的内容'), [
    [directiveObj.copy, '这是复制的内容'],
  ])
  const BtnWithAuth = withDirectives(h('button', '无权限,被禁用'), [
    [directiveObj.auth, 'li', 'disabled'],
  ])
  return h('div', [
    h('h3', 'v-copy 用h上'),
    DivWithCopy,
    ['点击复制', h('br'), InputWithFocus],
    h('br'),
    h('h3', 'v-auth'),
    BtnWithAuth,
  ])
}

不能使用全局指令

最佳实践

  1. 传递多个值,使用对象
html 复制代码
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
js 复制代码
app.directive("demo", (el, binding) => {
  console.log(binding.value.color); // => "white"
  console.log(binding.value.text); // => "hello!"
});
  1. 不在组件上使用自定义指令

原因:当组件有多个根元素时,它不知道绑定到那个元素上。

其他问题

如何使用代码测试指令呢?

我搜索不到相关教程。

github 的一个 issue

有请大佬解答。

参考

Use Vue3 Custom Directives Like a Pro

小结

  • 指令是复用逻辑的有效方式,尤其是遇到需要直接操作 DOM 的情况。
  • vue2 中常用的指令钩子:inserted、componentUpdated 和 unbind。
  • vue3 中常用的指令钩子:mounted、updated 和 beforeUnmount。
  • vue3 中 script setup 定义局部指令使用 v 开头。
  • vue3 的 binding 对象添加了 instance 属性。
  • 相同的功能 hook 和指令都能实现,hook 优势更大。
相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
CodeClimb5 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
沈梦研5 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
轻口味6 小时前
Vue.js 组件之间的通信模式
vue.js
九酒6 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm