【Vue】自定义指令directives && 指令钩子 && IntersectionObserver

文章目录


自定义指令

一、基本使用:directives

除了 Vue 内置的一系列指令 (比如 v-modelv-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)。

在不使用 <script setup> 的情况下,自定义指令需要通过 directives 选项注册:

  1. 注册:

    javascript 复制代码
       // main.js文件
       
       app.directive('指令名', {
         mounted(el) {
           // el: 指令所在的DOM元素
         }
       })
  2. 使用:

    javascript 复制代码
       <p v-指令名></p>

💥注意事项: 元素挂载后 (成为DOM树的一部分时) 自动执行 mounted 钩子

代码示例:(当页面加载时,让元素获取焦点)

main.js文件:

javascript 复制代码
app.directive('focus', {
  mounted(el) {
    console.log(el) // 拿到input元素
    el.focus()
  }
})

App.vue文件:

javascript 复制代码
<script setup></script>

<template>
  <div class="app">
    <input type="text" v-focus />
  </div>
</template>

二、什么是指令钩子

上述 mounted 指的是 指令钩子函数 ,和组件的生命周期钩子同名但不是一回事。

  • 组件生命周期钩子 :围绕 组件实例
  • 指令钩子 :围绕 指令绑定的 DOM 元素

常见的指令钩子如下表所示:

阶段 钩子名 说明
绑定 created 指令第一次绑定到元素时调用(元素还没插入 DOM)
挂载 beforeMount 元素即将插入 DOM 时调用
挂载完成 mounted 元素插入 DOM 后调用(常用,比如 el.focus())
更新前 beforeUpdate 元素所在组件更新前调用
更新后 updated 元素所在组件更新后调用
卸载前 beforeUnmount 元素所在组件卸载前调用
卸载后 unmounted 元素卸载后调用

三、指令钩子的参数

一个指令的定义对象可以提供几种钩子函数 (都是可选的):

javascript 复制代码
const myDirective = {
  // 指令第一次绑定到元素时调用(元素还没插入 DOM)
  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) {}
}
  1. el:指令当前绑定到的元素。这可以用于直接操作 DOM。
  2. binding:一个对象,包含以下属性:
    1. value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2
    2. oldValue:之前的值,仅在 beforeUpdateupdated 中可用。无论值是否更改,它都可用。
    3. arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    4. modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    5. instance:使用该指令的组件实例。
    6. dir:指令的定义对象。
  3. vnode:代表当前绑定元素的底层 VNode。用于了解绑定的虚拟 DOM 信息,一般用得不多。
  4. prevVnode:代表之前的渲染中指令所绑定元素的 VNode(只在 beforeUpdateupdated 中有用)。

💥注意事项: 除了 el 外,其他参数都是只读的,不要更改它们

📌 举个例子:带参数和修饰符的自定义指令

  • 背景色蓝色(arg = "blue"
  • 加粗(modifiers.bold = true
  • 绑定值 msg 也可以用来动态控制颜色。
javascript 复制代码
app.directive('highlight', {
  mounted(el, binding) {
    console.log(binding)

    // 默认颜色
    let color = 'yellow'

    // 如果传了参数(比如 :blue)
    if (binding.arg) {
      color = binding.arg
    }

    // 如果有修饰符,比如 .bold
    if (binding.modifiers.bold) {
      el.style.fontWeight = 'bold'
    }

    el.style.backgroundColor = color
  }
})

使用:

javascript 复制代码
<p v-highlight:blue.bold="msg">Hello Vue!</p>

四、绑定数据

1. 需求

实现一个 color 指令:传入不同的颜色,给标签设置文字颜色

2. 语法

  1. 在绑定指令时,可以通过 "等号" 的形式为指令绑定具体的参数值

    javascript 复制代码
       <div v-color="colorStr">Some Text</div>
  2. 通过 binding.value 可以拿到指令值,指令值修改会触发 updated 钩子

    javascript 复制代码
       app.directive('指令名', {
         // 挂载后自动触发一次
         mounted(el, binding) { },
         // 数据更新, 每次都会执行
         updated(el, binding) { }
       })

3. 代码示例

main.js文件:

javascript 复制代码
// 
app.directive('color', {
  mounted(el, binding) {
    el.style.color = binding.value
  },
  updated(el, binding) {
    el.style.color = binding.value
  }
})

App.vue文件:

javascript 复制代码
<script setup>
    import { ref } from 'vue'
    const colorStr = ref('red') // 颜色
</script>

<template>
  <p v-color="colorStr"></p>
</template>

4. 简化写法

对于自定义指令来说,一个很常见的情况是仅仅需要在 mountedupdated 上实现相同的行为。这种情况下我们可以直接用一个箭头函数来定义指令,如下所示:

javascript 复制代码
app.directive('color', (el, binding) => {
  // 这会在 mounted 和 updated 时都调用
  el.style.color = binding.value
})

👉 这种写法其实就是 语法糖

Vue 规定: 如果你注册指令时传入的是一个函数,而不是对象,那么它会自动把这个函数同时当作 mountedupdated 两个钩子。

案例:图片懒加载

实际开发过程中,如果项目中图片过多,我们不会一次加载所有的图片,而是当图片出现在可视区的时候才去加载,比如京东、淘宝等都采用了这种方案。

解决方案:封装一个 v-lazyload 自定义指令,实现图片懒加载,从而节省资源、提高性能。

认识 IntersectionObserver

IntersectionObserver 接口(从属于Intersection Observer API)为开发者提供了一种可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段。

完整代码

App.vue文件:

javascript 复制代码
<script setup>
    const imgList = [
        'https://img1.baidu.com/it/u=14492133,1259363498&fm=253&fmt=auto&app=138&f=JPEG?w=667&h=500',
        'https://img0.baidu.com/it/u=1764531212,1643995922&fm=253&fmt=auto&app=120&f=JPEG?w=750&h=500',
        'https://img1.baidu.com/it/u=3461494820,2726880132&fm=253&fmt=auto&app=138&f=JPEG?w=773&h=500',
        'https://img1.baidu.com/it/u=2991964469,2851730176&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500',
        'https://img2.baidu.com/it/u=1519104236,3241953583&fm=253&fmt=auto&app=138&f=JPEG?w=781&h=500',
        'https://img0.baidu.com/it/u=3431675376,3243768390&fm=253&fmt=auto&app=138&f=JPEG?w=712&h=447',
        'https://img1.baidu.com/it/u=2111075854,406597938&fm=253&fmt=auto&app=138&f=JPEG?w=888&h=500',
        'https://img0.baidu.com/it/u=1615464091,2840643412&fm=253&fmt=auto?w=945&h=605',
        'https://img0.baidu.com/it/u=3926979850,631936366&fm=253&fmt=auto&app=120&f=JPEG?w=785&h=500',
        'https://img1.baidu.com/it/u=469866567,781924764&fm=253&fmt=auto&app=120&f=JPEG?w=750&h=500'
    ]
</script>

<template>
    <div class="container">
        <img v-for="item in imgList" v-lazyload="item"
            width="600" height="320" />
    </div>
</template>

<style lang="scss">
* {
    margin: 0;
}
.container {
    width: 600px;
    display: flex;
    flex-direction: column;
    margin: 0 auto;
}
</style>

main.js文件中:

javascript 复制代码
app.directive('lazyload', (el, binding) => {
    const io = new IntersectionObserver(([entry]) => {
        // entry:交叉状态对象
        if(entry.isIntersecting) {
            // 到这说明图片与可视区发送交叉,说明要渲染出来
            el.src = binding.value

            // 监听图片加载错误事件
            el.addEventListener('error', (error) => {
                console.log('图片加载失败', error);
            })

            // 停止监听,关闭监听
            io.unobserve(el)
            io.disconnect()
        }
    })
    // 开启监视
    io.observe(el)
})
相关推荐
禅思院44 分钟前
AI对话前端从入门到崩溃:一个长对话引发的五层优化战争【引子】
前端·面试·架构
TrisighT1 小时前
Electron 鸿蒙 PC 上点外链唤醒应用,我试了 6 种写法只有 1 种能跑
前端·electron·harmonyos
天才熊猫君2 小时前
配置与数据分离:一种可视化搭建的属性编辑方案
前端·javascript
林希_Rachel_傻希希2 小时前
web性能之相关路径——AI总结
前端·javascript·面试
不好听6132 小时前
从零搭建一个 RAG 语义搜索系统 —— DEMO的初始阶段
javascript·面试·llm
何时梦醒2 小时前
上下文工程(Context Engineering):AI 应用开发的新范式 —— 从理论到实战全解析
javascript
竹林8182 小时前
用 wagmi v2 踩坑两天,我终于搞懂了多链钱包切换在 DeFi 前端中的正确姿势
前端·javascript
用户2136610035723 小时前
Vue项目搜索功能与面包屑导航
前端·javascript
星栈3 小时前
LiveView 的实时通信,爽是爽,但 PubSub 和广播也最容易把自己绕晕
前端·前端框架·elixir
用户2930750976693 小时前
告别关键词匹配,拥抱向量语义 —— RAG 搜索从零到一
前端