前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Vue 基本指令用法详解与常见坑(四)

首页2018-08-26 14:10:32Front-End
Vue指令前端基础

刚上手 Vue 那阵,我最不适应的是「模板里怎么突然多了一堆 v- 开头的属性」。写惯了 jQuery 的人第一反应是把它们当成语法糖背下来,能跑就行。结果就是遇到列表更新错位、页面刷新瞬间闪出一串花括号、v-if 和 v-show 随手挑一个用,出了问题完全没方向。这篇把常用指令一个个过一遍,重点不是罗列语法,而是讲清楚每个指令在渲染流程里站在什么位置、什么场景该用它、哪些写法会埋雷。读完你至少能自己判断一件事:眼前这个需求,该用哪个指令。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 指令到底是什么,它和普通 HTML 属性的区别在哪
  • v-text 和 v-html 的差别,以及 v-html 的 XSS 风险
  • v-bind 和 v-on 的完整语法、缩写和传参方式
  • 事件修饰符的顺序为什么会影响结果
  • v-for 的四种遍历形式,以及 key 到底解决了什么问题
  • v-if 和 v-show 的取舍标准
  • v-cloak 怎么解决首屏闪烁,v-pre 和 v-once 怎么省渲染开销
  • 原文里的 v-ref / v-el 是 Vue 1 的写法,Vue 2 之后应该怎么写
  • 这套指令在 Vue 3 里哪些还在、哪些没了

Vue 常用指令一览

# 一、指令是什么

先说结论,指令就是「带副作用的模板属性」。

普通的 HTML 属性是静态的,浏览器解析到 class="btn" 就把这个类名放上去,结束。指令不一样,Vue 在编译模板的时候会把 v- 开头的属性挑出来,把属性值当成一段 JavaScript 表达式求值,然后在这个 DOM 节点上挂一个更新函数。以后只要表达式里用到的数据变了,这个更新函数就会被再跑一遍。

所以指令干的事情可以拆成两半,一半是「初次渲染时把数据塞进 DOM」,另一半是「数据变了之后把 DOM 改回来」。理解了这一点,后面为什么 v-once 能省性能、为什么 v-pre 能跳过编译,就都能自己推出来了。

指令的完整写法是 v-指令名:参数.修饰符="表达式"。比如 v-on:click.stop="handler" 里面,on 是指令名,click 是参数,stop 是修饰符,handler 是表达式。四个部分不是每个指令都有,但格式是统一的。

# 二、内容渲染 v-text 和 v-html

v-text 更新元素的 textContent:

<h1 v-text="msg"></h1>
@前端进阶之旅: 代码已经复制到剪贴板

v-html 更新元素的 innerHTML:

<h1 v-html="msg"></h1>
@前端进阶之旅: 代码已经复制到剪贴板

两个指令看着只差一个字,风险差得远。v-text 走的是 textContent,浏览器会把内容当纯文本,标签会被转义显示出来。v-html 走 innerHTML,内容会被当成 HTML 解析。

这里有个坑要注意。只要 v-html 的内容里混进了用户可控的字符串,就是一个现成的 XSS 入口。评论区、富文本详情、后端返回的 HTML 片段,这几类场景是重灾区。Vue 官方文档在这块的措辞很直接,绝不要对用户提供的内容使用 v-html。真要渲染富文本,先过一遍 DOMPurify 这类白名单过滤库。

另外 v-text 和插值语法的差别只在一处,插值可以做局部替换,比如一段文字里只有中间某几个字是动态的;v-text 是整体替换 textContent,元素里原来写的内容会被覆盖掉。

# 三、属性绑定 v-bind

v-bind 负责把表达式的值响应式地作用到 DOM 属性上:

<!-- 完整语法 -->
<a v-bind:href="url"></a>
<!-- 缩写 -->
<a :href="url"></a>
@前端进阶之旅: 代码已经复制到剪贴板

对应的实例:

<script>
    // 创建 Vue 的实例对象
    var vm = new Vue({
      // el 用来指定 vue 挂载到页面中的元素,值是选择器
      // 理解:用来指定 vue 管理的 HTML 区域
      el: '#app',
      // 数据对象,用来给视图中提供数据的
      data: {
        url: 'http://www.baidu.com'
      }
    })
</script>
@前端进阶之旅: 代码已经复制到剪贴板

加不加 v-bind 是两件完全不同的事。写 href="url" 传的是字符串字面量 "url",写 :href="url" 传的是变量 url 的值。这条规则在后面讲 v-for 的 :value 和讲组件 props 的时候还会反复出现,它是 Vue 模板里最容易出错也最容易解释清楚的一条。

v-bind 还有个特殊之处,绑到 class 和 style 上时,Vue 用的是合并而不是覆盖。也就是说静态的 class="btn" 和动态的 :class="{ active: isOn }" 可以共存,最后渲染出来两个类名都在。别的属性没有这个待遇,同名的会被动态值顶掉。

# 四、事件绑定 v-on

v-on 用来绑事件,绑定的处理函数从 methods 里取:

<!-- 完整语法 -->
<a v-on:click="doSomething"></a>
<!-- 缩写 -->
<a @click="doSomething"></a>
<!-- 方法传参 -->
<a @click="doSomething('123')"></a>
@前端进阶之旅: 代码已经复制到剪贴板
<script>
    var vm = new Vue({
      el: '#app',
      // methods 属性用来给 vue 实例提供方法(事件)
      methods: {
        doSomething: function(str) {
          // 接受参数,并输出
          console.log(str);
        }
      }
    })
</script>
@前端进阶之旅: 代码已经复制到剪贴板

原文这行传参示例里混进了全角括号和中文引号,直接粘到项目里会编译报错,这里换回了半角写法。老博客从网页复制代码时很容易带上这类字符,遇到「模板编译失败但看不出哪里错」,先检查一遍标点是不是全角的。

不带括号写 @click="doSomething",Vue 会把原生事件对象作为第一个参数传进去。带括号写 @click="doSomething('123')",事件对象就丢了,想拿到得手动写 $event,也就是 @click="doSomething('123', $event)"。这个细节在下一篇讲事件的文章里会展开。

# 4.1 事件修饰符

事件修饰符是 Vue 用得最舒服的设计之一,它把「处理业务逻辑」和「处理 DOM 事件细节」拆开了:

  • .stop 阻止冒泡,等价于在处理函数里调用 event.stopPropagation()
  • .prevent 阻止默认事件,等价于 event.preventDefault()
  • .capture 添加事件侦听器时使用捕获模式
  • .self 只当事件在该元素本身(不是子元素)触发时才执行回调
  • .once 事件只触发一次

有了它们,方法里就不用再写那两行 event.xxx(),方法可以保持纯粹,只关心业务。

修饰符可以串联,但顺序会影响结果。@click.prevent.self 是先阻止所有点击的默认行为,再判断是不是自身触发;@click.self.prevent 是先判断是不是自身触发,只有自身触发时才阻止默认行为。两者行为不同,写的时候按「从左到右依次生效」去理解就不会错。

# 五、双向绑定 v-model

v-model 在表单元素上创建双向数据绑定,监听用户输入事件来更新数据:

<input v-model="message" placeholder="edit me">
@前端进阶之旅: 代码已经复制到剪贴板

它不是什么特殊通道,就是「绑一个属性 + 听一个事件」的缩写。文本框绑 value 听 input,勾选框绑 checked 听 change。表单控件的完整绑定规则、三个修饰符、多选数组这些细节,我在 Vue 表单控件与 v-model 那篇里单独拆过。

# 六、列表渲染 v-for

v-for 基于源数据多次渲染元素或模板块,它支持四种遍历形式:

<!-- 1 基础用法 -->
<div v-for="item in items">
  {{ item.text }}
</div>
<!-- item 为当前项,index 为索引 -->
<p v-for="(item, index) in list">{{item}} -- {{index}}</p>
<!-- 遍历对象,item 为值,key 为键,index 为索引 -->
<p v-for="(item, key, index) in obj">{{item}} -- {{key}}</p>
<!-- 遍历整数,item 从 1 开始 -->
<p v-for="item in 10">{{item}}</p>
@前端进阶之旅: 代码已经复制到剪贴板

遍历整数那个写法要留意,它是从 1 开始而不是 0,做分页页码的时候正好省一次加一。

# 6.1 key 到底解决什么问题

v-for 用起来简单,出问题基本都出在没写 key 上。

Vue 更新已渲染的元素列表时,默认用的是「就地复用」策略。如果数据项的顺序变了,Vue 不会去移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素,确保它在特定索引下显示已被渲染过的每个元素。这套策略类似 Vue 1.x 里的 track-by="$index"。

<div v-for="item in items" :key="item.id">
  <!-- 内容 -->
</div>
@前端进阶之旅: 代码已经复制到剪贴板

那不写 key 到底会怎样?就地复用在纯展示的静态列表上完全没问题,性能还更好。翻车的是带内部状态的列表。你想想看,一个待办列表每项都有个复选框,你勾上了第三项,然后在头部插入一条新数据。没有 key 的话,Vue 认为第三个位置的 DOM 还是那个 DOM,只是文本内容变了,于是复用它,而复选框的勾选状态是 DOM 自身的状态,不会被重置。结果就是你勾的那条内容变了,勾还留在原位。

给了 key,Vue 就能按 key 去匹配新旧节点,该移动的移动,该销毁的销毁,状态自然跟着数据走。带表单元素、带过渡动画、带组件实例的列表,key 是必须的。

还有一条,别拿数组下标当 key。用 index 相当于没给,因为下标本身就是位置,插入删除之后所有下标都变了,等于告诉 Vue「每一项都变了」。用数据里稳定唯一的 id。

现在的 ESLint 规则 vue/require-v-for-key 会直接把漏写 key 报成错误,2018 年那会儿它还只是推荐项。

# 七、样式处理,class 和 style

class 和 style 都是 HTML 元素的属性,用 v-bind 绑定时,只需要通过表达式计算出结果即可。表达式的类型可以是字符串、数组、对象:

<!-- 对象语法:值为真的键名会被加上 -->
<div v-bind:class="{ active: true }"></div>
<!-- 渲染为 <div class="active"></div> -->

<!-- 数组语法 -->
<div :class="['active', 'text-danger']"></div>
<!-- 渲染为 <div class="active text-danger"></div> -->

<!-- 数组里套对象 -->
<div v-bind:class="[{ active: true }, errorClass]"></div>
<!-- 渲染为 <div class="active text-danger"></div> -->
@前端进阶之旅: 代码已经复制到剪贴板

style 同理,注意 CSS 属性名要写成驼峰形式,或者用引号包住中划线形式:

fe
  • 一、指令是什么
  • 二、内容渲染 v-text 和 v-html
  • 三、属性绑定 v-bind
  • 四、事件绑定 v-on
    • 4.1 事件修饰符
  • 五、双向绑定 v-model
  • 六、列表渲染 v-for
    • 6.1 key 到底解决什么问题
  • 七、样式处理,class 和 style
  • 八、条件渲染 v-if 和 v-show
  • 九、提升用户体验,v-cloak
  • 十、提升性能,v-pre 和 v-once
  • 十一、拿 DOM 元素,v-ref 和 v-el
  • 十二、Vue 3 里这套指令变了什么
  • 总结
  • 参考

← Vue 生命周期八个钩子完整拆解(五)Vue之class与style绑定 对象与数组两种语法(三) →