Vue3 学习教程,从入门到精通,Vue3指令知识点及使用方法详细介绍(6)

发布于:2025-07-13 ⋅ 阅读:(21) ⋅ 点赞:(0)

Vue3指令知识点及使用方法详细介绍

一、条件渲染
  1. v-if 指令

    • 语法: v-if="表达式"

    • 功能: 根据表达式值的真假,动态地渲染元素。

    • 示例代码

      <template>
        <div>
          <!-- v-if 控制元素的显示与隐藏 -->
          <p v-if="isVisible">这段文字只有在 isVisible 为 true 时才会显示</p>
          <button @click="toggleVisibility">切换显示状态</button>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            isVisible: true
          };
        },
        methods: {
          toggleVisibility() {
            this.isVisible = !this.isVisible; // 点击按钮切换 isVisible 的值
          }
        }
      };
      </script>
      
      • 说明:

        • v-if="isVisible" 根据 isVisible 的布尔值决定是否渲染 <p> 标签。
        • 点击按钮调用 toggleVisibility 方法,切换 isVisible 的值,从而实现元素的显示与隐藏。
        2.v-if / v-else / v-else-if
        <template>
          <div>
            <!-- v-if 根据条件渲染元素 -->
            <p v-if="score >= 90">优秀</p>
            <!-- v-else-if 提供额外条件分支 -->
            <p v-else-if="score >= 60">及格</p>
            <!-- v-else 必须紧跟在 v-if 或 v-else-if 后面 -->
            <p v-else>不及格</p>
            
            <!-- 使用 template 包裹多个元素 -->
            <template v-if="isLoggedIn">
              <h1>欢迎回来</h1>
              <p>您有3条新消息</p>
            </template>
          </div>
        </template>
        
        <script setup>
        import { ref } from 'vue';
        
        const score = ref(85);
        const isLoggedIn = ref(true);
        </script>
        
        3.v-show
        <template>
          <div>
            <!-- v-show 只是切换 display 属性 -->
            <p v-show="isVisible">你可以看到我</p>
            <button @click="isVisible = !isVisible">切换显示</button>
          </div>
        </template>
        
        <script setup>
        import { ref } from 'vue';
        
        const isVisible = ref(true);
        </script>
        

        v-if vs v-show:

        • v-if 是"真正"的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
        • v-if 是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时才会开始渲染条件块。
        • v-show 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
        • 一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
  2. v-else 指令

    • 语法: v-else

    • 功能: 与 v-if 配合使用,表示 v-if 条件不满足时的替代内容。

    • 示例代码

      <template>
        <div>
          <p v-if="isLoggedIn">欢迎回来,{{ username }}</p>
          <p v-else>请先登录</p>
          <button @click="toggleLoginStatus">切换登录状态</button>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            isLoggedIn: false,
            username: '张三'
          };
        },
        methods: {
          toggleLoginStatus() {
            this.isLoggedIn = !this.isLoggedIn; // 切换登录状态
          }
        }
      };
      </script>
      
      • 说明:
        • v-if="isLoggedIn" 判断用户是否登录。
        • 如果 isLoggedInfalse,则显示 v-else 的内容。
  3. v-else-if 指令

    • 语法: v-else-if="表达式"

    • 功能: 提供额外的条件判断。

    • 示例代码

      <template>
        <div>
          <p v-if="score >= 90">优秀</p>
          <p v-else-if="score >= 80">良好</p>
          <p v-else-if="score >= 60">及格</p>
          <p v-else>不及格</p>
          <button @click="changeScore(95)">设置分数为 95</button>
          <button @click="changeScore(85)">设置分数为 85</button>
          <button @click="changeScore(70)">设置分数为 70</button>
          <button @click="changeScore(45)">设置分数为 45</button>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            score: 0
          };
        },
        methods: {
          changeScore(newScore) {
            this.score = newScore; // 更新分数
          }
        }
      };
      </script>
      
二、循环渲染
  1. v-for 指令

    • 语法: v-for="(item, index) in 可迭代对象" :key="唯一标识"
    • 功能: 遍历数组、对象或字符串,动态渲染多个元素。
    语法知识点
    • v-for 用于遍历数组或对象,渲染列表。
    • 支持多种语法,如 (item, index) in items(value, key, index) in object
    使用方法
    • 通常用于渲染列表、表格等重复性内容。

    • 可以结合 :key 属性提高渲染性能。

    • 示例代码1

      <template>
        <div>
          <!-- 遍历数组 -->
          <ul>
            <li v-for="(item, index) in items" :key="index">
              {{ item }} - 索引: {{ index }}
            </li>
          </ul>
      
          <!-- 遍历对象 -->
          <div v-for="(value, key, index) in person" :key="index">
            {{ key }}: {{ value }} - 索引: {{ index }}
          </div>
      
          <!-- 遍历字符串 -->
          <div v-for="(char, index) in message" :key="index">
            字符: {{ char }} - 索引: {{ index }}
          </div>
      
          <button @click="addItem">添加项目</button>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            items: ['苹果', '香蕉', '橙子'],
            person: {
              name: '张三',
              age: 25,
              job: '前端工程师'
            },
            message: 'Hello Vue3'
          };
        },
        methods: {
          addItem() {
            this.items.push('葡萄'); // 向数组添加新项目
          }
        }
      };
      </script>
      
      • 说明:

        • v-for="(item, index) in items" 遍历数组 itemsitem 表示当前元素,index 表示索引。
        • v-for="(value, key, index) in person" 遍历对象 personkey 表示对象的键,value 表示对应的值,index 表示索引。
        • v-for="(char, index) in message" 遍历字符串 messagechar 表示当前字符,index 表示索引。

        示例代码2

        <template>
          <div>
            <h2>用户列表</h2>
            <ul>
              <li v-for="(user, index) in users" :key="user.id">
                {{ index + 1 }}. {{ user.name }} - {{ user.email }}
              </li>
            </ul>
        
            <!-- 遍历对象 -->
            <h2>用户信息</h2>
            <ul>
              <li v-for="(value, key) in userInfo" :key="key">
                {{ key }}: {{ value }}
              </li>
            </ul>
          </div>
        </template>
        
        <script>
        export default {
          data() {
            return {
              users: [
                { id: 1, name: '张三', email: 'zhangsan@example.com' },
                { id: 2, name: '李四', email: 'lisi@example.com' },
              ],
              userInfo: {
                age: 25,
                occupation: '工程师',
                location: '北京',
              },
            };
          },
        };
        </script>
        
三、数据插入(v-text 和 v-html)
  1. 文本插值

    • 语法: {{ 表达式 }}

    • 功能: 动态插入数据到模板中。

    • 示例代码

      <template>
        <div>
          <p>用户名: {{ username }}</p>
          <p>欢迎信息: {{ getWelcomeMessage() }}</p>
          <button @click="updateUsername">更新用户名</button>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            username: '张三'
          };
        },
        methods: {
          getWelcomeMessage() {
            return `欢迎回来,${this.username}`;
          },
          updateUsername() {
            this.username = '李四'; // 更新用户名
          }
        }
      };
      </script>
      
      • 说明:
        • {{ username }} 动态插入 username 的值。
        • {{ getWelcomeMessage() }} 动态插入方法返回的值。
  2. HTML 插值

    • 语法: v-html="表达式"

    • 功能: 动态插入 HTML 内容。

    • 示例代码

      <template>
        <div>
          <div v-html="htmlContent"></div>
          <button @click="updateHtmlContent">更新 HTML 内容</button>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            htmlContent: '<h3 style="color: blue;">这是动态 HTML 内容</h3>'
          };
        },
        methods: {
          updateHtmlContent() {
            this.htmlContent = '<p style="color: red;">HTML 内容已更新</p>'; // 更新 HTML 内容
          }
        }
      };
      </script>
      
      • 说明:

        • v-html="htmlContent"htmlContent 的值作为 HTML 渲染到页面上。

        案例代码

        <template>
          <div>
            <!-- v-text 会覆盖元素原有内容 -->
            <p v-text="message"></p>
            
            <!-- 双大括号插值,不会覆盖原有内容 -->
            <p>消息: {{ message }}</p>
            
            <!-- v-html 会解析 HTML 内容 -->
            <div v-html="htmlContent"></div>
            
            <!-- 双大括号中的 JavaScript 表达式 -->
            <p>{{ message.split('').reverse().join('') }}</p>
          </div>
        </template>
        
        <script setup>
        import { ref } from 'vue';
        
        const message = ref('Hello Vue3!');
        const htmlContent = ref('<strong style="color: red;">红色加粗文本</strong>');
        </script>
        
四、属性绑定
  1. v-bind 指令

    • 语法: :属性名="表达式"v-bind:属性名="表达式"

    • 功能: 动态绑定 HTML 属性。

    • 示例代码

      <template>
        <div>
          <!-- 动态绑定 class -->
          <div :class="classObject">动态绑定 class</div>
      
          <!-- 动态绑定 style -->
          <div :style="styleObject">动态绑定 style</div>
      
          <!-- 动态绑定属性 -->
          <img :src="imageSrc" :alt="imageAlt">
      
          <button @click="toggleClass">切换 class</button>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            classObject: {
              'active-class': true,
              'disabled-class': false
            },
            styleObject: {
              color: 'blue',
              fontSize: '18px'
            },
            imageSrc: 'https://via.placeholder.com/150',
            imageAlt: '示例图片'
          };
        },
        methods: {
          toggleClass() {
            this.classObject['active-class'] = !this.classObject['active-class']; // 切换 active-class 的状态
          }
        }
      };
      </script>
      
      • 说明:
        • :class="classObject" 动态绑定 class 属性,classObject 是一个对象,包含多个类名及其布尔值。
        • :style="styleObject" 动态绑定 style 属性,styleObject 是一个对象,包含多个样式属性及其值。
        • :src="imageSrc":alt="imageAlt" 动态绑定 <img> 标签的 srcalt 属性。
  2. 动态绑定多个属性

    • 语法: v-bind="对象"

    • 功能: 动态绑定多个属性。

    • 示例代码

      <template>
        <div>
          <button :disabled="isDisabled" :class="buttonClass">按钮</button>
          <button :"{disabled: isDisabled, class: buttonClass}">按钮(对象语法)</button>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            isDisabled: false,
            buttonClass: 'primary-button'
          };
        }
      };
      </script>
      
五、事件绑定
  1. v-on 指令

    • 语法: @事件名="处理函数"v-on:事件名="处理函数"

    • 功能: 绑定事件处理函数。

      语法知识点
      • v-on 用于绑定事件监听器。
      • 简写为 @
      使用方法
      • 绑定常见事件,如 click, input, change, submit 等。
      • 支持事件修饰符,如 .prevent, .stop, .once 等。
    • 示例代码1

      <template>
        <div>
          <!-- 简单事件绑定 -->
          <button @click="handleClick">点击我</button>
      
          <!-- 事件修饰符 -->
          <input @keydown.enter="handleEnter" placeholder="按回车键触发">
          <input @input="handleInput" placeholder="输入内容">
      
          <p>输入内容: {{ inputText }}</p>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            inputText: ''
          };
        },
        methods: {
          handleClick() {
            alert('按钮被点击了!');
          },
          handleEnter() {
            alert('按下回车键了!');
          },
          handleInput(event) {
            this.inputText = event.target.value; // 获取输入框的值并更新到 inputText
          }
        }
      };
      </script>
      
      • 说明:

        • @click="handleClick" 绑定点击事件到 handleClick 方法。
        • @keydown.enter="handleEnter" 使用事件修饰符 .enter,表示当按下回车键时触发 handleEnter 方法。
        • @input="handleInput" 绑定输入事件到 handleInput 方法,每当输入框内容变化时触发。

        案例代码2

        <template>
          <div>
            <!-- 绑定点击事件 -->
            <button @click="handleClick">点击我</button>
        
            <!-- 绑定提交事件并阻止默认行为 -->
            <form @submit.prevent="handleSubmit">
              <input v-model="inputValue" placeholder="输入内容" />
              <button type="submit">提交</button>
            </form>
        
            <!-- 事件修饰符 -->
            <div @click="handleDivClick">
              <button @click.stop="handleButtonClick">点击按钮不触发 div 点击事件</button>
            </div>
          </div>
        </template>
        
        <script>
        export default {
          data() {
            return {
              inputValue: '',
            };
          },
          methods: {
            handleClick() {
              alert('按钮被点击了!');
            },
            handleSubmit() {
              alert(`提交的内容是: ${this.inputValue}`);
            },
            handleDivClick() {
              console.log('div 被点击了');
            },
            handleButtonClick() {
              console.log('按钮被点击了');
            },
          },
        };
        </script>
        
  2. 事件对象

    • 语法: @事件名="处理函数($event)"

    • 功能: 在事件处理函数中获取原生事件对象。

    • 示例代码

      <template>
        <div>
          <button @click="handleClickWithEvent($event)">点击获取事件对象</button>
        </div>
      </template>
      
      <script>
      export default {
        methods: {
          handleClickWithEvent(event) {
            console.log('事件对象:', event);
            event.target.style.color = 'red'; // 修改按钮文本颜色
          }
        }
      };
      </script>
      
六、双向数据绑定
  1. v-model 指令

    • 语法: v-model="数据属性"

    • 功能: 创建双向数据绑定(常见于表单输入)。

    • 示例代码

      <template>
        <div>
          <!-- 文本输入框 -->
          <input type="text" v-model="textValue" placeholder="输入文本">
          <p>输入的文本: {{ textValue }}</p>
      
          <!-- 复选框 -->
          <input type="checkbox" v-model="isChecked">
          <label :for="'checkbox' + Math.random().toString(36).substr(2, 9)">是否同意</label>
          <p>复选框状态: {{ isChecked }}</p>
      
          <!-- 单选按钮 -->
          <input type="radio" value="选项1" v-model="radioValue" name="radioGroup">
          <label :for="'radio1' + Math.random().toString(36).substr(2, 9)">选项1</label>
          <input type="radio" value="选项2" v-model="radioValue" name="radioGroup">
          <label :for="'radio2' + Math.random().toString(36).substr(2, 9)">选项2</label>
          <p>选中的选项: {{ radioValue }}</p>
      
          <!-- 下拉框 -->
          <select v-model="selectedOption">
            <option value="苹果">苹果</option>
            <option value="香蕉">香蕉</option>
            <option value="橙子">橙子</option>
          </select>
          <p>选中的水果: {{ selectedOption }}</p>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            textValue: '',
            isChecked: false,
            radioValue: '',
            selectedOption: ''
          };
        }
      };
      </script>
      
      • 说明:
        • v-model="textValue" 在文本输入框中创建双向绑定,输入内容会实时更新到 textValue
        • v-model="isChecked" 在复选框中创建双向绑定,勾选状态会更新到 isChecked
        • v-model="radioValue" 在单选按钮中创建双向绑定,选中的值会更新到 radioValue
        • v-model="selectedOption" 在下拉框中创建双向绑定,选中的选项会更新到 selectedOption
七、插槽
  1. 默认插槽

    • 语法: <slot></slot>

    • 功能: 定义组件的插槽,允许父组件向子组件传递内容。

    • 示例代码

      <!-- 子组件: MyComponent.vue -->
      <template>
        <div class="my-component">
          <h3>自定义组件</h3>
          <slot></slot>
        </div>
      </template>
      
      <!-- 父组件 -->
      <template>
        <div>
          <my-component>
            <p>这是插入到子组件的内容</p>
          </my-component>
        </div>
      </template>
      
      <script>
      import MyComponent from './MyComponent.vue';
      
      export default {
        components: {
          MyComponent
        }
      };
      </script>
      
  2. 具名插槽

    • 语法:

      • 子组件: <slot name="插槽名"></slot>
      • 父组件: <template v-slot:插槽名><template #插槽名>
    • 功能: 为插槽指定名称,允许在子组件中定义多个具名插槽。

    • 示例代码

      <!-- 子组件: MyCard.vue -->
      <template>
        <div class="card">
          <h3>卡片标题</h3>
          <slot name="header"></slot>
          <div class="card-body">
            <slot name="body"></slot>
          </div>
          <div class="card-footer">
            <slot name="footer"></slot>
          </div>
        </div>
      </template>
      
      <!-- 父组件 -->
      <template>
        <div>
          <my-card>
            <template v-slot:header>
              <h4>自定义标题</h4>
            </template>
            <template #body>
              <p>这是卡片的正文内容</p>
            </template>
            <template v-slot:footer>
              <button>操作按钮</button>
            </template>
          </my-card>
        </div>
      </template>
      
      <script>
      import MyCard from './MyCard.vue';
      
      export default {
        components: {
          MyCard
        }
      };
      </script>
      
  3. 作用域插槽

    • 语法:

      • 子组件: <slot name="插槽名" :数据="值"></slot>
      • 父组件: <template v-slot:插槽名="插槽数据"></template>
    • 功能: 通过插槽向父组件传递数据,父组件可以访问这些数据并自定义内容。

    • 示例代码

      <!-- 子组件: TodoItem.vue -->
      <template>
        <div class="todo-item">
          <slot name="todo" :todo="todo" :index="index"></slot>
        </div>
      </template>
      
      <script>
      export default {
        props: {
          todo: Object,
          index: Number
        }
      };
      </script>
      
      <!-- 父组件 -->
      <template>
        <div>
          <todo-item v-for="(todo, index) in todos" :key="index" :todo="todo" :index="index">
            <template v-slot:todo="slotProps">
              <div>
                <span>{{ slotProps.index + 1 }}. {{ slotProps.todo.text }}</span>
                <span v-if="slotProps.todo.completed">(已完成)</span>
              </div>
            </template>
          </todo-item>
        </div>
      </template>
      
      <script>
      import TodoItem from './TodoItem.vue';
      
      export default {
        components: {
          TodoItem
        },
        data() {
          return {
            todos: [
              { text: '学习 Vue3', completed: true },
              { text: '完成项目', completed: false }
            ]
          };
        }
      };
      </script>
      
八、性能提升相关指令
  1. v-once 指令

    • 语法: v-once

    • 功能: 表示元素和组件只渲染一次。后续数据变化不会影响渲染结果。

    • 示例代码

      <template>
        <div>
          <p v-once>这个内容只渲染一次: {{ message }}</p>
          <p>这个内容会更新: {{ message }}</p>
          <button @click="updateMessage">更新消息</button>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            message: '初始消息'
          };
        },
        methods: {
          updateMessage() {
            this.message = '消息已更新'; // 更新消息
          }
        }
      };
      </script>
      
      • 说明:
        • v-once 修饰的 <p> 标签内容只在初次渲染时更新,后续数据变化不会影响它。
        • 没有 v-once<p> 标签内容会随着 message 的变化而更新。
  2. v-memo 指令 (Vue 3.3+)

    • 语法: v-memo
    • 功能: 当指令绑定的值变化时,重新渲染元素或组件。可以减少不必要的渲染。
    语法知识点
    • v-memo 用于优化渲染性能,指示 Vue 仅在依赖项变化时才重新渲染元素。
    使用方法
    • 接受一个依赖数组,类似于 computed 的依赖。

    • 仅在依赖项变化时重新渲染。

    • 示例代码

      <template>
        <div>
          <div v-memo="[name, age]">
            <p>姓名: {{ name }}</p>
            <p>年龄: {{ age }}</p>
            <p>职业: {{ job }}</p> <!-- 职业不会触发重新渲染 -->
          </div>
          <button @click="updateName">更新姓名</button>
          <button @click="updateAge">更新年龄</button>
          <button @click="updateJob">更新职业</button>
        </div>
      </template>
      
      <script>
      export default {
        data() {
          return {
            name: '张三',
            age: 25,
            job: '前端工程师'
          };
        },
        methods: {
          updateName() {
            this.name = '李四'; // 会触发重新渲染
          },
          updateAge() {
            this.age = 26; // 会触发重新渲染
          },
          updateJob() {
            this.job = '全栈工程师'; // 不会触发重新渲染
          }
        }
      };
      </script>
      
      • 说明:
        • v-memo="[name, age]" 表示只有当 nameage 发生变化时,才会重新渲染 <div> 及其子元素。
        • job 发生变化不会触发重新渲染,因为 v-memo 没有监听它。
九、自定义指令
  1. 创建与使用自定义指令

    • 语法:

      • 全局注册: app.directive('指令名', 指令配置)
      • 局部注册: directives: { '指令名': 指令配置 }
    • 功能: 创建自定义指令,扩展 Vue 的功能。

    • 示例代码

      <template>
        <div>
          <input v-focus ref="inputRef" placeholder="自动聚焦的输入框">
          <button @click="logInput">日志输出输入框</button>
        </div>
      </template>
      
      <script>
      // 全局注册自定义指令
      const app = Vue.createApp({});
      
      app.directive('focus', {
        mounted(el) {
          el.focus(); // 聚焦元素
        }
      });
      
      export default {
        name: 'App',
        methods: {
          logInput() {
            console.log(this.$refs.inputRef.value); // 输出输入框的值
          }
        }
      };
      </script>
      
      • 说明:
        • v-focus 是一个全局注册的自定义指令。
        • mounted 钩子函数在指令绑定的元素插入到 DOM 后调用,这里实现自动聚焦。
  2. 自定义指令使用场景

    • 场景 1: 实现点击按钮高亮效果

      <template>
        <div>
          <button v-highlight:yellow>黄色高亮按钮</button>
          <button v-highlight>默认颜色高亮按钮</button>
        </div>
      </template>
      
      <script>
      export default {
        name: 'App',
      
        // 局部注册自定义指令
        directives: {
          highlight: {
            mounted(el, binding) {
              // 默认背景色为蓝色
              const color = binding.arg || 'blue';
              el.style.backgroundColor = color;
              el.style.padding = '10px 20px';
              el.style.border = 'none';
              el.style.borderRadius = '4px';
              el.style.cursor = 'pointer';
      
              // 点击事件
              el.addEventListener('click', () => {
                el.style.transform = 'scale(1.05)';
                setTimeout(() => {
                  el.style.transform = 'scale(1)';
                }, 200);
              });
            }
          }
        }
      };
      </script>
      
      • 说明:
        • v-highlight:yellow 使用参数 yellow 指定背景颜色。
        • v-highlight 使用默认背景颜色。
  3. 自定义指令应用示例

    • 场景 2: 实现长按事件

      <template>
        <div>
          <button v-longpress="handleLongPress">长按我 2 秒</button>
          <p v-if="isLongPressed">已长按 2 秒!</p>
        </div>
      </template>
      
      <script>
      export default {
        name: 'App',
        data() {
          return {
            isLongPressed: false,
            pressTimer: null
          };
        },
        methods: {
          handleLongPress() {
            this.isLongPressed = true;
            setTimeout(() => {
              this.isLongPressed = false;
            }, 3000); // 3 秒后重置状态
          }
        },
        directives: {
          longpress: {
            mounted(el, binding, vnode) {
              let pressTimer;
      
              el.addEventListener('mousedown', () => {
                pressTimer = setTimeout(() => {
                  binding.value(); // 执行传递的方法
                }, 2000); // 长按 2 秒触发
              });
      
              el.addEventListener('mouseup', () => {
                clearTimeout(pressTimer);
              });
      
              el.addEventListener('mouseleave', () => {
                clearTimeout(pressTimer);
              });
      
              // 触摸设备支持
              el.addEventListener('touchstart', () => {
                pressTimer = setTimeout(() => {
                  binding.value();
                }, 2000);
              });
      
              el.addEventListener('touchend', () => {
                clearTimeout(pressTimer);
              });
      
              el.addEventListener('touchcancel', () => {
                clearTimeout(pressTimer);
              });
            }
          }
        }
      };
      </script>
      
      • 说明:
        • v-longpress="handleLongPress" 使用自定义指令实现长按事件。
        • 通过 mousedownmouseupmouseleave 以及触摸事件实现长按检测。

      4. 生命周期钩子函数

      • mounted:指令首次绑定到元素时调用。
      • updated:指令绑定的数据更新时调用。
      • unmounted:指令从元素上解绑时调用。
      案例代码
      <!-- 自定义指令 v-auto-resize -->
      <template>
        <div>
          <textarea v-auto-resize></textarea>
        </div>
      </template>
      
      <script>
      export default {
        directives: {
          autoResize: {
            mounted(el) {
              el.style.resize = 'none';
              el.addEventListener('input', () => {
                el.style.height = 'auto';
                el.style.height = el.scrollHeight + 'px';
              });
            },
            updated(el) {
              el.style.height = 'auto';
              el.style.height = el.scrollHeight + 'px';
            },
          },
        },
      };
      </script>
      

      注释:

      • v-auto-resize 指令自动调整 textarea 的高度以适应内容。
      • 使用 mountedupdated 钩子确保在初始化和更新时都调整高度。
十、综合性案例
综合示例 1: 创建一个带有过滤和排序功能的待办事项列表
<template>
  <div class="todo-app">
    <h2>待办事项列表</h2>

    <!-- 输入新待办事项 -->
    <div class="input-group">
      <input
        v-model="newTodo"
        @keyup.enter="addTodo"
        placeholder="输入新的待办事项"
      >
      <button @click="addTodo">添加</button>
    </div>

    <!-- 过滤选项 -->
    <div class="filter-options">
      <button
        v-for="(filter, index) in filters"
        :key="index"
        :class="{ active: currentFilter === filter }"
        @click="currentFilter = filter"
      >
        {{ filter }}
      </button>
    </div>

    <!-- 待办事项列表 -->
    <ul class="todo-list">
      <li
        v-for="(todo, index) in filteredTodos"
        :key="index"
        :class="{ completed: todo.completed }"
      >
        <input type="checkbox" v-model="todo.completed">
        <span>{{ todo.text }}</span>
        <button @click="deleteTodo(index)">删除</button>
      </li>
    </ul>

    <!-- 统计信息 -->
    <div class="todo-stats">
      <p>总待办事项: {{ todos.length }}</p>
      <p>已完成: {{ completedCount }}</p>
      <p>未完成: {{ pendingCount }}</p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'TodoApp',
  data() {
    return {
      newTodo: '',
      todos: [
        { text: '学习 Vue3', completed: true },
        { text: '完成项目', completed: false },
        { text: '阅读文档', completed: false }
      ],
      currentFilter: '全部',
      filters: ['全部', '已完成', '未完成']
    };
  },
  computed: {
    // 计算过滤后的待办事项列表
    filteredTodos() {
      if (this.currentFilter === '全部') {
        return this.todos;
      } else if (this.currentFilter === '已完成') {
        return this.todos.filter(todo => todo.completed);
      } else if (this.currentFilter === '未完成') {
        return this.todos.filter(todo => !todo.completed);
      }
      return this.todos;
    },
    // 计算已完成的待办事项数量
    completedCount() {
      return this.todos.filter(todo => todo.completed).length;
    },
    // 计算未完成的待办事项数量
    pendingCount() {
      return this.todos.filter(todo => !todo.completed).length;
    }
  },
  methods: {
    // 添加新的待办事项
    addTodo() {
      if (this.newTodo.trim() !== '') {
        this.todos.push({
          text: this.newTodo.trim(),
          completed: false
        });
        this.newTodo = ''; // 清空输入框
      }
    },
    // 删除待办事项
    deleteTodo(index) {
      this.todos.splice(index, 1);
    }
  }
};
</script>

<style scoped>
.todo-app {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}
.input-group {
  display: flex;
  margin-bottom: 20px;
}
.input-group input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px 0 0 4px;
  outline: none;
}
.input-group button {
  padding: 10px 15px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 0 4px 4px 0;
  cursor: pointer;
}
.filter-options {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}
.filter-options button {
  padding: 8px 12px;
  background-color: #f1f1f1;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.filter-options button.active {
  background-color: #4CAF50;
  color: white;
}
.todo-list {
  list-style-type: none;
  padding: 0;
  margin-bottom: 20px;
}
.todo-list li {
  display: flex;
  align-items: center;
  padding: 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-bottom: 8px;
  background-color: white;
}
.todo-list li.completed span {
  text-decoration: line-through;
  color: #888;
}
.todo-list input[type="checkbox"] {
  margin-right: 10px;
}
.todo-list button {
  margin-left: auto;
  padding: 5px 10px;
  background-color: #f44336;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.todo-stats {
  display: flex;
  justify-content: space-between;
  background-color: #f9f9f9;
  padding: 12px;
  border-radius: 4px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
</style>
综合示例 2: 创建一个带有主题切换和响应式布局的用户卡片组件
<template>
  <div :class="['user-card', { 'dark-theme': isDarkTheme }]" :style="cardStyle">
    <div class="card-header">
      <h3 v-if="showTitle">用户信息卡片</h3>
      <div class="theme-toggle">
        <label>
          <input type="checkbox" v-model="isDarkTheme">
          <span class="slider"></span>
        </label>
        <span>暗色模式</span>
      </div>
    </div>

    <div class="card-content">
      <div class="user-avatar">
        <img :src="user.avatar" :alt="user.name" v-if="user.avatar">
        <div class="default-avatar" v-else>
          {{ user.name.charAt(0).toUpperCase() }}
        </div>
      </div>
      <div class="user-details">
        <h4>{{ user.name }}</h4>
        <p v-if="user.email">邮箱: {{ user.email }}</p>
        <p v-if="user.phone">电话: {{ user.phone }}</p>
        <p v-if="user.address">地址: {{ user.address }}</p>
      </div>
    </div>

    <div class="card-footer">
      <button @click="showDetails = !showDetails">
        {{ showDetails ? '隐藏详情' : '显示详情' }}
      </button>
      <div class="user-more-details" v-if="showDetails">
        <p v-if="user.bio">个人简介: {{ user.bio }}</p>
        <div v-else class="no-bio">
          <p>暂无个人简介</p>
        </div>
        <p v-if="user.skills && user.skills.length > 0">
          技能:
          <span
            v-for="(skill, index) in user.skills"
            :key="index"
            class="skill-tag"
          >
            {{ skill }}
          </span>
        </p>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserCard',
  props: {
    user: {
      type: Object,
      required: true,
      default: () => ({
        name: '未知用户',
        avatar: null,
        email: null,
        phone: null,
        address: null,
        bio: null,
        skills: []
      })
    },
    showTitle: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      isDarkTheme: false,
      showDetails: false
    };
  },
  computed: {
    // 计算卡片样式
    cardStyle() {
      return {
        '--card-bg-color': this.isDarkTheme ? '#2c3e50' : '#ffffff',
        '--card-text-color': this.isDarkTheme ? '#ecf0f1' : '#34495e',
        '--card-border-color': this.isDarkTheme ? '#34495e' : '#bdc3c7',
        '--card-shadow-color': this.isDarkTheme ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.1)'
      };
    }
  }
};
</script>

<style scoped>
:root {
  --card-bg-color: #ffffff;
  --card-text-color: #34495e;
  --card-border-color: #bdc3c7;
  --card-shadow-color: rgba(0, 0, 0, 0.1);
}

.user-card {
  background-color: var(--card-bg-color);
  border-radius: 10px;
  box-shadow: 0 4px 12px var(--card-shadow-color);
  border: 1px solid var(--card-border-color);
  overflow: hidden;
  transition: all 0.3s ease;
  color: var(--card-text-color);
  width: 100%;
  max-width: 400px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  border-bottom: 1px solid var(--card-border-color);
}

.card-header h3 {
  margin: 0;
  font-size: 18px;
}

.theme-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
}

.theme-toggle label {
  position: relative;
  display: inline-block;
  width: 40px;
  height: 22px;
}

.theme-toggle input {
  opacity: 0;
  width: 0;
  height: 0;
}

.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  transition: .4s;
  border-radius: 22px;
}

.slider:before {
  position: absolute;
  content: "";
  height: 16px;
  width: 16px;
  left: 3px;
  bottom: 3px;
  background-color: white;
  transition: .4s;
  border-radius: 50%;
}

input:checked + .slider {
  background-color: #4CAF50;
}

input:checked + .slider:before {
  transform: translateX(18px);
}

.card-content {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.user-avatar {
  display: flex;
  justify-content: center;
  margin-bottom: 10px;
}

.user-avatar img {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  object-fit: cover;
}

.default-avatar {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #3498db;
  color: white;
  font-size: 24px;
  font-weight: bold;
}

.user-details h4 {
  margin-top: 0;
  margin-bottom: 10px;
  font-size: 18px;
}

.user-details p {
  margin: 5px 0;
  line-height: 1.5;
}

.card-footer {
  padding: 16px 20px;
  border-top: 1px solid var(--card-border-color);
}

.card-footer button {
  padding: 8px 16px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-bottom: 10px;
}

.card-footer button:hover {
  background-color: #2980b9;
}

.user-more-details {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px dashed var(--card-border-color);
}

.skill-tag {
  display: inline-block;
  padding: 4px 8px;
  background-color: #e0f7fa;
  color: #00838f;
  border-radius: 4px;
  font-size: 12px;
  margin-right: 5px;
  margin-bottom: 5px;
}

.no-bio {
  padding: 10px;
  border-radius: 4px;
  background-color: var(--card-border-color);
  color: var(--card-text-color);
}

@media (max-width: 480px) {
  .user-card {
    border-radius: 8px;
  }

  .user-details h4 {
    font-size: 16px;
  }

  .user-avatar img, .default-avatar {
    width: 80px;
    height: 80px;
  }
}
</style>
综合案例3:动态表单验证
<!-- DynamicForm.vue -->
<template>
  <div>
    <form @submit.prevent="handleSubmit">
      <div>
        <label>用户名:</label>
        <input v-model="form.username" v-validate="validations.username" type="text" />
        <p v-if="errors.username">{{ errors.username }}</p>
      </div>
      <div>
        <label>邮箱:</label>
        <input v-model="form.email" v-validate="validations.email" type="text" />
        <p v-if="errors.email">{{ errors.email }}</p>
      </div>
      <button type="submit">提交</button>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      form: {
        username: '',
        email: '',
      },
      errors: {},
      validations: {
        username: {
          required: true,
          minLength: 3,
        },
        email: {
          required: true,
          regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
        },
      },
    };
  },
  directives: {
    validate: {
      mounted(el, binding, vnode) {
        const validateField = () => {
          const value = el.value;
          const rules = binding.value;
          const errors = [];

          if (rules.required && !value) {
            errors.push('字段是必填的');
          }
          if (rules.minLength && value.length < rules.minLength) {
            errors.push(`字段长度必须至少为 ${rules.minLength} 个字符`);
          }
          if (rules.regex && !rules.regex.test(value)) {
            errors.push('字段格式不正确');
          }

          vnode.context.errors[vnode.key] = errors;
        };

        el.addEventListener('input', validateField);
        validateField();
      },
    },
  },
  methods: {
    handleSubmit() {
      if (Object.keys(this.errors).length === 0) {
        alert('表单提交成功');
      } else {
        alert('表单验证失败');
      }
    },
  },
};
</script>

注释:

  • 使用自定义指令 v-validate 对每个表单字段进行验证。
  • v-validate 指令根据 validations 数据定义验证规则。
  • 动态更新 errors 对象,显示相应的错误信息。
综合案例4:动态指令参数
<!-- DynamicDirective.vue -->
<template>
  <div>
    <p v-highlight="color">这是一个高亮显示的文本</p>
    <button @click="changeColor">更改颜色</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      color: 'yellow',
    };
  },
  directives: {
    highlight: {
      mounted(el, binding) {
        el.style.backgroundColor = binding.value;
      },
      updated(el, binding) {
        el.style.backgroundColor = binding.value;
      },
    },
  },
  methods: {
    changeColor() {
      this.color = this.color === 'yellow' ? 'lightblue' : 'yellow';
    },
  },
};
</script>

注释:

  • v-highlight 指令接受一个参数 color,用于设置元素的背景色。
  • 通过按钮点击更改 color 的值,动态更新背景色。
综合案例5:无限滚动列表
<template>
  <div>
    <h1>无限滚动列表</h1>
    <ul 
      v-infinite-scroll="loadMore" 
      style="height: 400px; overflow-y: auto; border: 1px solid #ccc;"
    >
      <li 
        v-for="item in items" 
        :key="item.id"
        style="padding: 10px; border-bottom: 1px solid #eee;"
      >
        {{ item.content }}
      </li>
      <li v-if="loading" style="text-align: center; padding: 10px;">
        加载中...
      </li>
      <li v-if="noMore" style="text-align: center; padding: 10px; color: #999;">
        没有更多数据了
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const items = ref([]);
const loading = ref(false);
const noMore = ref(false);
let page = 1;

// 模拟获取数据
function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      const newItems = Array.from({ length: 10 }, (_, i) => ({
        id: (page - 1) * 10 + i + 1,
        content: `项目 ${(page - 1) * 10 + i + 1} - 这是第${page}页的内容`
      }));
      resolve(newItems);
    }, 1000);
  });
}

// 加载更多数据
async function loadMore() {
  if (loading.value || noMore.value) return;
  
  loading.value = true;
  const newItems = await fetchData();
  
  if (newItems.length === 0) {
    noMore.value = true;
  } else {
    items.value = [...items.value, ...newItems];
    page++;
  }
  
  loading.value = false;
}

// 初始化数据
onMounted(() => {
  loadMore();
});

// 无限滚动指令
const vInfiniteScroll = {
  mounted(el, binding) {
    const callback = binding.value;
    
    el.addEventListener('scroll', () => {
      // 检查是否滚动到底部
      if (el.scrollHeight - el.scrollTop <= el.clientHeight + 50) {
        callback();
      }
    });
    
    // 组件卸载时移除事件监听
    onUnmounted(() => {
      el.removeEventListener('scroll');
    });
  }
};
</script>
综合案例6:图片预览指令
<template>
  <div>
    <h1>图片预览</h1>
    <div style="display: flex; flex-wrap: wrap; gap: 10px;">
      <img 
        v-for="img in images" 
        :key="img.id"
        v-preview="img.large" 
        :src="img.thumbnail" 
        style="width: 200px; height: 150px; object-fit: cover; cursor: pointer;"
      >
    </div>
    
    <!-- 预览模态框 -->
    <div v-if="previewVisible" class="preview-modal" @click="previewVisible = false">
      <img :src="previewImage" class="preview-image">
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const images = ref([
  { id: 1, thumbnail: 'https://picsum.photos/200/150?random=1', large: 'https://picsum.photos/800/600?random=1' },
  { id: 2, thumbnail: 'https://picsum.photos/200/150?random=2', large: 'https://picsum.photos/800/600?random=2' },
  { id: 3, thumbnail: 'https://picsum.photos/200/150?random=3', large: 'https://picsum.photos/800/600?random=3' },
  { id: 4, thumbnail: 'https://picsum.photos/200/150?random=4', large: 'https://picsum.photos/800/600?random=4' },
  { id: 5, thumbnail: 'https://picsum.photos/200/150?random=5', large: 'https://picsum.photos/800/600?random=5' },
  { id: 6, thumbnail: 'https://picsum.photos/200/150?random=6', large: 'https://picsum.photos/800/600?random=6' },
]);

const previewVisible = ref(false);
const previewImage = ref('');

// 图片预览指令
const vPreview = {
  mounted(el, binding) {
    el.addEventListener('click', () => {
      previewImage.value = binding.value;
      previewVisible.value = true;
    });
    
    // 组件卸载时移除事件监听
    onUnmounted(() => {
      el.removeEventListener('click');
    });
  }
};
</script>

<style>
.preview-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.preview-image {
  max-width: 90%;
  max-height: 90%;
  object-fit: contain;
}
</style>
十一、本章小结

本章节详细介绍了 Vue3 中的指令系统,包括内置指令和自定义指令,以及它们的使用方法和具体案例。主要内容包括:

  1. 条件渲染: 使用 v-ifv-elsev-else-if 动态控制元素的显示与隐藏。
  2. 循环渲染: 使用 v-for 遍历数组、对象或字符串,动态渲染多个元素。
  3. 数据插入: 使用文本插值 {{ }} 和 HTML 插值 v-html 将数据插入模板。
  4. 属性绑定: 使用 v-bind 动态绑定 HTML 属性。
  5. 事件绑定: 使用 v-on 绑定事件处理函数,支持事件修饰符。
  6. 双向数据绑定: 使用 v-model 创建表单输入的双向绑定。
  7. 插槽: 使用默认插槽、具名插槽和作用域插槽实现组件的内容分发。
  8. 性能提升相关指令: 使用 v-oncev-memo 提升应用性能。
  9. 自定义指令: 创建和使用自定义指令扩展 Vue 的功能,并通过生命周期钩子函数控制指令行为。
  10. 综合性案例: 通过两个综合案例(待办事项列表和用户卡片组件)展示如何在实际项目中综合运用 Vue3 指令。

通过本章的学习,读者应能够熟练掌握 Vue3 指令的使用方法,并在实际项目中灵活应用,构建高效、可维护的 Vue3 应用程序。


网站公告

今日签到

点亮在社区的每一天
去签到