2.Vue 介绍

一、 Vue 介绍

  • Vue.js 是一套构建用户界面的渐进式 JavaScript 框架
  • 可以轻松构建 SPA 应用程序
  • 通过指令扩展了 HTML,通过表达式绑定数据到 HTML
  • 最大程度上解放了传统 JavaScript 中频繁的 DOM 操作,让开发人员更加专注于业务操作
  • 通过组件方便模板重用以及增加可维护性,使得代码结构更加合理,维护成本更低
  • Vue 的特性:
    • MVVM
    • 双向数据绑定
    • 组件化
    • 渐进式

通过下面这个姓名展示的案例我们先来对比下操作 DOM 的繁琐 与使用 Vue 的简便:

  1. 通过操作 DOM 元素实现
  • HTML 页面和 JS 代码耦合
  • 对DOM元素 id 的依赖高
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="app">
        姓氏:<input type="text" id="surname" value="黄">
        <br>
        名字:<input type="text" id="givenname" value="慧">
        <br>
        <p id="fullname">黄慧</p>
    </div>
    <script>
        var surname = document.getElementById('surname')
        var givenname = document.getElementById('givenname')
        var fullname = document.getElementById('fullname')

        surname.addEventListener('input', handleTextInput)
        givenname.addEventListener('input', handleTextInput)

        function handleTextInput() {
            fullname.innerHTML = surname.value + givenname.value
            console.log(fullname.innerHTML)
        }
    </script>
</body>
</html>
  1. 使用 Vue
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="app">
        姓氏:<input type="text" v-model="surname">
        <br>
        名字:<input type="text" v-model="givenname">
        <br>
        <p>{{ surname + givenname }}</p>
    </div>
    <script src="node_modules/vue/dist/vue.js"></script>
    <script>
        new Vue({
            el:'#app',
            data:{
                surname:'黄',
                givenname:'慧'
            }
        })
    </script>
</body>
</html>

二、 Vue 实例

Vue 实例的选项

每个 Vue 应用都是通过 Vue 函数创建一个新的 Vue 实例开始的:

var vm = Vue({
  // 选项
})

当创建一个 Vue 实例时,你可以传入一个选项对象。这篇教程主要描述的就是如何使用这些选项来创建你想要的行为。作为参考,可以在 API 文档 中浏览完整的选项列表。

1. el 选项:

参考文档:https://cn.vuejs.org/v2/api/#el

  • el 提供一个在页面上已存在 DOM 元素作为 Vue 实例的挂载目标。可以是 CSS 选择器(new Vue(el: '#app' //其他选项)),也可以是一个 HTMLElement 实例(el: 'document.getElementById('app')')。
  • el 不要作用到 <body> 和 <html> 节点上
  • 也可以通过 实例.$mount() 手动挂载(new Vue({ // 选项 }).$mount('#app'))
2. data 选项:

参考文档:https://cn.vuejs.org/v2/api/#data

  • data中的数据是响应式数据,即为数据驱动视图,当数据改变,那么所有绑定该数据的 DOM 都会跟着改变
  • 可以通过 vm.$data 访问原始数据对象
  • Vue 实例也代理了 data 对象上的所有属性,因此访问 vm.a 等价于 vm.$data.a
  • 视图中绑定的数据必须显式的初始化到 data 中,data 外只能修改,不能动态添加
3. methods 选项:

参考文档:https://cn.vuejs.org/v2/api/#methods

  • methods 将被混入到 Vue 实例中。可以直接通过 VM 实例访问这些方法,或者在指令表达式中使用。方法中的 this 自动绑定为 Vue 实例。
  • 不应该使用箭头函数来定义 method 函数 (例如 plus: () => this.a++)。理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例,this.a 将是 undefined。
  • 在 data 中定义的方法和在 methods 中定义的箭头函数 this 都指向 window 实例,而不是 Vue 实例
  • methods 中的方法定义推荐使用 ES6 的对象属性函数简写方式,该语法没有任何特性,只是纯粹的简写。也就是说:handleClick: function () {}等价于handleClick () {}
4. computed 选项:

用于定义计算属性,详细见 四、计算属性

Vue 实例的生命周期

什么是生命周期?
从 Vue 实例创建、运行、到销毁期间,总是伴随着各种各样的事件,这些事件,统称为生命周期。
生命周期钩子
生命周期钩子 = 生命周期函数 = 生命周期事件
有哪些生命周期函数?

  • 创建期间的生命周期函数:
    • beforeCreate:实例刚在内存中被创建出来,此时,还没有初始化好 data 和 methods 属性
    • created:实例已经在内存中创建OK,此时 data 和 methods 已经创建OK,此时还没有开始编译模板
    • beforeMount:此时已经在内存中完成了模板的编译,但是还没有挂载到页面中
    • mounted:此时,已经将编译好的模板,挂载到了页面指定的容器中显示
  • 运行期间的生命周期函数:
    • beforeUpdate:状态更新之前执行此函数, 此时 data 中的状态值是最新的,但是界面上显示的数据还是旧的,因为此时还没有开始重新渲染DOM节点
    • updated:实例更新完毕之后调用此函数,此时 data 中的状态值 和 界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了
  • 销毁期间的生命周期函数:
    • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。
    • destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
lifecycle.png

三、 数据绑定

1. 文本插值

数据绑定最常见的形式就是使用 “Mustache” 语法(双大括号)的文本插值,Mustache 标签将会被替代为对应数据对象上的 msg 属性的值。无论何时,绑定的数据对象上 msg 属性发生了改变,插值处的内容都会更新:<span>Message: {{ msg }}</span>

2. 一次性绑定

通过 v-once 指令,执行一次性插值,当数据改变时,插值处的内容不会更新。但会影响到该节点上的其它数据绑定:<span v-once>这个将不会改变: {{ msg }}</span><span>这个将不会改变: {{ *msg }}</span>

3. 输出 HTML
  • 双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,可以使用 v-html 指令。例如:
<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>

或者写成:

<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span>{{{ rawHtml }}}</span></p>

结果:


v-html
  • 绑定 v-html 指令的 HTML 字符串中不能使用 Vue 的语法特性。
  • 你的站点上动态渲染任意的 HTML 可能会非常危险,因为它非常容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。
4. 属性绑定
  • Mastache 语法不能作用在 HTML 特性上,遇到这种情况应该使用 v-bind 指令:<div v-bind:id="dynamicId"></div>
  • v-bind 只能用于属性绑定,它的值(即双引号里面的值)是一个JavaScript 表达式,和 {{}}里面的语法是一致的(语法:加单引号就是字符串,不加是变量或者表达式)。唯一的区别就是{{}}用于标签文本绑定,v-bind用于标签属性绑定。
5. 使用 JavaScript 表达式

Vue.js 数据绑定提供了完全的 JavaScript 表达式支持

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div v-bind:id="'list-' + id"></div>
  • 但是绑定都只能包含单个表达式,不能是语句或者流控制,所以下面的例子都不会生效。
<!-- 这是语句,不是表达式 -->
{{ var a = 1 }}

<!-- 流控制也不会生效,请使用三元表达式 -->
{{ if (ok) { return message } }}
  • 模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如 Math 和 Date 。你不应该在模板表达式中试图访问用户定义的全局变量。
6. 分隔符

Vue.js 数据绑定的语法被设计为可配置的。我们可以在 Vue.config 中配置绑定的语法。Vue.config 是一个对象,包含了 Vue.js 所有的全局配置,可以在 Vue 实例化前修改其中的属性。分隔符在 Vue.config 的源码定义如下:

<!--源码目录 src/config.js-->
let delimiters = ['{{','}}']
let unsafeDelimiters = ['{{{','}}}']
  • delimiters
    Vue.config.delimiters = ['<%','%>'] 可以修改默认的文本插值的分隔符,则文本插值的语法由 {{ example }} 变成 <% example %>

  • unsafeDelimiters
    Vue.config.unsafeDelimiters = ['<$','$>'] 可以修改默认的 HTML 插值的分隔符,则 HTML 插值的语法由 {{{ example }}} 变成 <$ example $>

四、 指令

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

1. 常用指令

详细指令参见https://cn.vuejs.org/v2/api/#%E6%8C%87%E4%BB%A4

条件渲染指令:
  • v-if
    • v-if 会根据条件进行渲染和不渲染,true 则渲染 DOM,false 则不渲染 DOM
  • v-else
    • 使用 v-else 指令来表示 v-if 的“else 块”
  • v-else-if
    • 使用 v-else-if 指令来表示 v-if 的“else-if 块”
    • v-else 和 v-else-if 元素必须紧跟在带 v-if 或者 v-else-if 的元素的后面,否则它将不会被识别。
  • v-show
    • 带有 v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS 属性 display
  • <template> 元素上使用 v-if 条件渲染分组
    因为 v-if 是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template> 元素当做不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含 <template> 元素。
<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>
  • 用 key 管理可复用的元素
    这个例子在本来会复用的两个 input 元素上指定不同的 key 。从而告诉 vue 它们是完全独立的,不要复用它们。这样每次切换时,输入框都将被重新渲染。
<template v-if="loginType === 'username'">
  <label>Username</label>
  <input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Enter your email address" key="email-input">
</template>

v-ifv-show 对比,用哪个?
1. v-if 支持 template 语法,v-show 不支持。
2. 在切换 v-if 模块时, Vue.js 有一个局部编译/卸载的过程,因为 v-if 中的模板可能包括数据绑定或子组件。v-if 是真实的条件渲染,因为它会确保条件块在切换时合适点的销毁和重建条件块内部的事件监听器和子组件。v-if 是惰性的,如果初始渲染条件为假,则什么也不做,在条件第一次变为真时才开始局部编译(编译会被缓存起来)。相比之下 v-show 简单的多,元素始终被编译并保留,只是简单的基于 CSS 切换。
3. v-if 有更高的切换消耗,而 v-show 有更高的初始渲染消耗。如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

列表渲染指令:v-for

详细参见 五、列表渲染

注册事件指令:v-on

详细参加 五、事件处理

属性绑定指令: v-bind
只渲染一次指令:v-once

只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

绑定输出 HTML 指令:v-html
表单控件双向数据绑定指令:v-model

参考官网:https://cn.vuejs.org/v2/guide/forms.html

  • 只有 v-model 指令即可以实现数据驱动视图变化,又可以实现视图驱动数据变化的双向绑定。

  • v-model 指令只能作用于表单元素,可在input、select、text、checkbox、radio 等表单控件上创建双向数据绑定。v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

    • text 和 textarea 元素使用 value 属性和 input 事件;
    • checkbox 和 radio 使用 checked 属性和 change 事件;
    • select 字段将 value 作为 prop 并将 change 作为事件。
  • 修饰符:

    • v-model.number: 将用户输入值转为数值类型。
      <input v-model.number="age" type="number">
    • v-model.lazy:在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步,添加 lazy 修饰符,可以转变为使用 change 事件进行同步。
      <input v-model.lazy="msg" >
    • v-model.trim :输入首尾空格过滤
      <input v-model.trim="msg">
数据绑定指令:v-text
  • 和 {{}} 功能一样的
  • 当使用插值表达式时,由于浏览器不认识 vue 的插值代表式,只有在浏览器加载了vue.js后,执行 script 将插值表达式解析替换为指定的值后,才会显示出真正的数据,这样切换会导致页面闪烁。v-text指令可以解决插值表达式闪烁问题。
  • 数据渲染前样式指令:v-cloak
    • 这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如[v-cloak] { display: none }一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。

    • v-cloak 指令也可以解决插值表达式闪烁问题:

      • 使用方法:在头部加一个特殊的样式 [v-cloak] { display: none },然后在被 Vue 管理的模板入口节点上作用 v-cloak 指令
      • 原理:在浏览器在解析过程中,发现具有 v-cloak 的属性隐藏不显示,所以就看不见这个 {{}} 闪烁的问题了,当 Vue 解析替换完之后, Vue 会自动把 v-cloak 样式移除。
加快编译指令:v-pre

告诉 vue不要解析这个节点节内部的内容,浪费时间。跳过大量没有指令的节点会加快编译。例如含有大量的网页文本内容节点可以加这个指令。

2. 参数

一些指令能够接收一个“参数”,在指令名称之后以冒号表示,如:

<a v-bind:href="url">...</a>
<a v-on:click="doSomething">...</a>

3. 指令修饰符

修饰符 (modifier) 是以半角句号 . 指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。例如,.prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault():<form v-on:submit.prevent="onSubmit">...</form>

4. 指令缩写

Vue 为 v-bind 和 v-on 这两个最常用的指令,提供了特定简写:

  • v-bind 缩写:
<!-- 完整语法 -->
<a v-bind:href="url">...</a>

<!-- 缩写 -->
<a :href="url">...</a>
  • v-on 缩写:
<!-- 完整语法 -->
<a v-on:click="doSomething">...</a>

<!-- 缩写 -->
<a @click="doSomething">...</a>

五、列表渲染

v-for 指令的基本用法

  • v-for 指令基于源数据重复渲染元素。
  • v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名。v-for 还支持一个可选的第二个参数,即当前项的索引,使用(item, index) in items形式的特殊语法。
  • 在 v-for 块中,我们可以访问所有父作用域的属性。
<ul id="example-2">
  <li v-for="(item, index) in items">
    {{ parentMessage }} - {{ index }} - {{ item.message }}
  </li>
</ul>
var example2 = new Vue({
  el: '#example-2',
  data: {
    parentMessage: 'Parent',
    items: [
      { message: 'Foo' },
      { message: 'Bar' }
    ]
  }
})
结果
  • 也可以用 of 替代 in 作为分隔符,因为它更接近 JavaScript 迭代器的语法:<div v-for="item of items"></div>
  • 也可以用 v-for 来遍历一个对象的属性。第一个参数是对象的值,可以提供第二个的参数为 property 名称 (也就是键名),还可以用第三个参数作为索引。
<ul id="v-for-object" class="demo">
  <li v-for="(value, name, index) in object">
    {{ index }}. {{ name }}: {{ value }}
  </li>
</ul>
new Vue({
  el: '#v-for-object',
  data: {
    object: {
      title: 'How to do lists in Vue',
      author: 'Jane Doe',
      publishedAt: '2016-04-10'
    }
  }
})
结果
  • 为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
<div v-for="item in items" v-bind:key="item.id">
  <!-- 内容 -->
</div>

数组更新检测

变异方法(mutation method)

我们知道,JS Array 对象有以下变异方法(会改变调用了这些方法的原始数组),其 MDN 解释如下:

  • push() - 将一个或多个元素添加到数组的末尾,并返回该数组的新长度。
  • pop() - 从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。
  • shift() - 从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。
  • unshift() - 将一个或多个元素添加到数组的开头,并返回该数组的新长度。
  • splice() - 通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。
  • sort() - 用原地算法对数组的元素进行排序,并返回数组。排序算法现在是稳定的。默认排序顺序是根据字符串Unicode码点。
  • reverse() - 将数组中元素的位置颠倒,并返回该数组。该方法会改变原数组。

Vue.js 将被侦听数组的变异方法进行了包裹,所以在 v-for 循环数组进行列表渲染时候如果该数组调用了这些方法,会触发视图的重新渲染。可以打开控制台,然后对前面例子的 items 数组尝试调用变异方法。比如 app.items.push({ message: 'Baz' })

DEMO演示如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Vue-mutation method</title>
</head>
<body>
    <ul id="app">
      <li v-for="item in items">
        {{ item }}
      </li>
    </ul>
    <script src="node_modules/vue/dist/vue.js"></script>
    <script>
        var app = new Vue({
          el: '#app',
          data: {
            items: ['March', 'Jan', 'Feb', 'Dec']
          }
        })
    </script>
</body>
</html>
演示结果
替换数组

数组的非变异方法(non-mutating method)不会改变原始数组,而总是返回一个新数组,例如:

  • filter() - 创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。
  • slice() - 返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。
  • concat() - 用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
    当使用非变异方法时,可以用新数组替换旧数组:
app.items = app.items.filter(function (item) {
  return item.message.match(/Foo/)
})

你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

注意事项

由于 JavaScript 的限制,Vue 不能检测以下数组的变动:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

为了解决问题 1,可以用以下两种方式实现相同的效果,同时也将在响应式系统内触发状态更新:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

为了解决问题 2,可以这样:

vm.items.splice(newLength)

另外,数组更新检测除了 Vue.set() (实例方法又名为vm.$set)方法,还有 Vue.delete (实例方法又名为 vm.$delete)方法,用于从目标数组中查找并删除元素。

// Vue.delete
Vue.delete(vm.items, indexOfItem)
// vm.$delete
vm.$delete(vm.items, indexOfItem)
// 也等价于
vm.items.splice(indexOfItem, 1)
演示结果

对象更新检测

注意事项

还是由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除:

var vm = new Vue({
  data: {
    a: 1
  }
})
// `vm.a` 现在是响应式的

vm.b = 2
// `vm.b` 不是响应式的

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是可以使用Vue.set(object, propertyName, value) (或者 vm.$set(object, propertyName, value))方法向嵌套对象添加响应式属性。

有时你可能需要为已有对象赋值多个新属性,比如使用 Object.assign() 或 _.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:

Object.assign(vm.object, {
  favoriteColor: 'Vue Green'
})

因为表达式只会修改vm.userProfile,vm.userProfile仍然指向原引用地址(常用于 合并 对象)
应该这样做:

vm.object= Object.assign({}, vm.object, {
  favoriteColor: 'Vue Green'
})

因为 vm.userProfile 可能是父级组件传过来的prop,为了遵循单向数据流的设计理念,不直接修改该数据对象,而是生成一个新的数据对象。该表达式右侧会生成一个新的对象,vm.userProfile 会指向一个新的引用地址(常用于 浅拷贝 对象)

Demo :

    <ul id="v-for-object" class="demo">
      <li v-for="(value, name, index) in object">
        {{ index }}. {{ name }}: {{ value }}
      </li>
    </ul>
    <script>
        const vm = new Vue({
          data: {
            object: {
              title: 'How to do lists in Vue',
              author: 'Jane Doe',
              publishedAt: '2016-04-10'
            }
          }
        }).$mount('#v-for-object')
直接添加对象属性
通过vm.$set()添加对象属性
通过Object.assign()合并对象
通过Object.assign()浅拷贝对象
  • v-for 里使用值范围
<div>
  <span v-for="n in 10">{{ n }} </span>
</div>
执行结果
  • <template> 上使用 v-for
    类似于 v-if,你也可以利用带有 v-for 的 <template> 来循环渲染一段包含多个元素的内容。比如:
<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>
  • 在组件上使用 v-for
    • 当在组件上使用 v-for 时,key 现在是必须的。如果在组件中使用列表渲染而不提供 key attribute 则 Vue 会出现warning :component lists rendered with v-for should have explicit keys.

    • 任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。为了把迭代数据传递到组件里,我们要使用 prop:

<ul id="v-for-component" class="demo">
  <my-component
    v-for="(value, name, index) in object"
    :value="value"
    :name="name"
    :index="index"
    :key="index">   
  </my-component>
</ul>
<script>
  Vue.component('my-component',{
    props: ['value','name','index'],
    template: `
      <li>{{ index }}. {{ name }}: {{ value }}</li>         
    `
  })

  const vm = new Vue({
    data: {
      object: {
        title: 'How to do lists in Vue',
        author: 'Jane Doe',
        publishedAt: '2016-04-10'
      }
    }
  }).$mount('#v-for-component')

不自动将 item 注入到组件里的原因是,这会使得组件与 v-for 的运作紧密耦合。明确组件数据的来源能够使组件在其他场合重复使用。

注意,由于 <li> 只能出现在 <ul> 列表的内部,上述写法会导致我们使用这些有约束条件的元素时遇到一些问题潜在的浏览器解析错误,例如,组件的 template 中不含有 <li> 标签,则会解析报错。幸好特殊的 is 特性给了我们一个变通的办法。

<ul id="v-for-component" class="demo">
  <li is="my-component"
    v-for="(value, name, index) in object"
    :value="value"
    :name="name"
    :index="index"
    :key="index">   
  </li>
</ul>

六、事件处理

用 v-on 指令可以监听 DOM 事件,并在触发时运行一些 JavaScript 代码。很多事件事件处理逻辑会更为复杂,直接把 JavaScript 代码写在 v-on 指令中是不可行的,因此 v-on 还可以接收一个需要调用的方法名称,该方法定义在 methods 选项中。

v-on绑定方法的两种方式

  • v-on 直接绑定方法(不使用内联处理器)。
    不传参,该方法天生自带event 参数,即原生 DOM 代表事件的状态的 Event 对象。

    直接绑定一个方法

  • v-on 使用内联处理器绑定方法。
    在内联语句处理器中访问原始的 DOM 事件,需要将特殊变量 $event 把它传入方法才能使用原生 DOM 的 event 对象。

    内联语句处理器中访问 DOM

事件修饰符

在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。Vue 提供了以下事件修饰符:

  • .stop - 调用 event.stopPropagation(),阻止事件继续传播。
    例如:<a v-on:click.stop="doThis"></a> 可以阻止单击事件继续传播。
  • .prevent - 调用 event.preventDefault(),阻止事件的默认行为。
    例如:<form v-on:submit.prevent="onSubmit"></form> 提交事件不再重载页面。
  • .capture - 添加事件监听器时使用事件捕获模式。即元素自身触发的事件先在此处理,然后才交由内部元素进行处理 。
    例如:<div v-on:click.capture="doThis">...</div>
  • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
    例如:<div v-on:click.self="doThat">...</div>,只当在 event.target 是当前元素自身时触发处理函数,即事件不是从内部元素触发的。
  • .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
  • .native - 监听组件根元素的原生事件。
  • .once - 只触发一次回调。例如: <a v-on:click.once="doThis"></a>
  • .left - (2.2.0) 只当点击鼠标左键时触发。
  • .right - (2.2.0) 只当点击鼠标右键时触发。
  • .middle - (2.2.0) 只当点击鼠标中键时触发。
  • .passive - (2.3.0) 以 { passive: true } 模式添加侦听器

这些事件修饰符还可以串联,例如:<a v-on:click.stop.prevent="doThat"></a>

Demo演示:

var vm = new Vue({
  methods: {
    div1Handler() {
      console.log('这是触发了 inner div 的点击事件')
    },
    btnHandler() {
      console.log('这是触发了 btn 按钮 的点击事件')
    },
    linkClick() {
      console.log('触发了连接的点击事件')
    },
    div2Handler() {
      console.log('这是触发了 outer div 的点击事件')
    }
  }
}).$mount('#app')
  1. 单击事件冒泡
<div id="app">
  <div class="inner" @click="div1Handler">
    <input type="button" value="戳他" @click="btnHandler">
  </div>
</div>
单击事件冒泡
  1. 使用 .stop 阻止冒泡
<div id="app">
  <div class="inner" @click="div1Handler">
    <input type="button" value="戳他" @click.stop="btnHandler">
  </div>
</div>
使用 .stop 阻止冒泡
  1. 使用 .self 实现只有点击当前元素时候,才会触发事件处理函数
<div id="app">
  <div class="inner" @click.self="div1Handler">
    <input type="button" value="戳他" @click="btnHandler">
  </div>
</div>
使用 .self 实现只有点击当前元素时候,才会触发事件处理函数
  1. 演示: .stop 和 .self 的区别
<div id="app">
  <div class="outer" @click="div2Handler">
    <div class="inner" @click="div1Handler">
      <input type="button" value="戳他" @click.stop="btnHandler">
    </div>
  </div>
</div>
使用 .stop 能阻止当前元素的冒泡行为

.self 只会阻止自己身上冒泡行为的触发,并不会真正阻止 冒泡的行为:

<div id="app">
  <div class="outer" @click="div2Handler">
    <div class="inner" @click.self="div1Handler">
      <input type="button" value="戳他" @click="btnHandler">
    </div>
  </div>
</div>
使用 .self 实现只有点击当前元素时候,才会触发事件处理函数
  1. 使用 .capture 实现捕获触发事件的机制
<div id="app">
  <div class="inner" @click.capture="div1Handler">
    <input type="button" value="戳他" @click="btnHandler">
  </div>
</div>
使用 .capture 实现捕获触发事件的机制
  1. 单击触发事件默认行为
<div id="app">
  <a href="http://www.baidu.com" @click="linkClick">有问题,先去百度</a>
</div>
单击触发事件默认行为
  1. 使用 .prevent 阻止默认行为
<div id="app">
  <a href="http://www.baidu.com" @click.prevent="linkClick">有问题,先去百度</a>
</div>
使用 .prevent 阻止默认行为
  1. 使用 .once 只触发一次事件处理函数
<div id="app">
  <a href="http://www.baidu.com" @click.prevent.once="linkClick">有问题,先去百度</a>
</div>
使用 .once 只触发一次事件处理函数

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent只会阻止对元素自身的点击。

按键修饰符

在监听键盘事件时,我们经常需要检查详细的按键。Vue 允许为 v-on 在监听键盘事件时添加按键修饰符:

<!-- 只有在 $event.key 是 Enter 时调用 vm.submit() -->
<input v-on:keyup.enter="submit">

<!-- 只有在 $event.key 等于 PageDown 时调用 vm.onPageDown() -->
<input v-on:keyup.page-down="onPageDown">

使用 keyCode 特性也是允许的:
<input v-on:keyup.13="submit">

Vue 提供了绝大多数常用的按键码的别名:

  • .enter
  • .tab
  • .delete (捕获“删除”和“退格”键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

可以通过全局 config.keyCodes 对象自定义按键修饰符别名:

// 可以使用 `v-on:keyup.f1`
Vue.config.keyCodes.f1 = 112

系统修饰键

  • 可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
    • .ctrl
    • .alt
    • .shift
    • .meta:在 Mac 系统键盘上,meta 对应 command 键 (⌘)。在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞)。
<!-- Alt + C -->
<input @keyup.alt.67="clear">

<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>
  • .exact 修饰符
    .exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>
  • 鼠标按键修饰符
    以下修饰符会限制处理函数仅响应特定的鼠标按钮:
    • .left
    • .right
    • .middle

七、计算属性和侦听器

计算属性

在模板中放入太多的逻辑会让模板过重且难以维护,当你想要在模板中多次引用此处功能时,就会更加难以处理。例如:

<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

这时有以下两个方案来解决:

  1. 使用方法把这种复杂逻辑封装起来
  • 每使用一次就调用一次,重复使用效率不高
  1. 使用计算属性
  • 不要让模板逻辑太重
  • 解决重复调用性能问题
    所以,对于任何复杂逻辑,你都应当使用计算属性。

计算属性是 Vue 提供的一大特色,他定义在 computed 选项中。顾名思义,它一种带有行为的属性。
计算属性和方法的区别:

  • 计算属性当属性来使用;方法当方法来调用
  • 计算属性会缓存计算的结果,多次使用也只调用一次;方法不会缓存结果,每次使用都会重复调用。
  • 计算属性不能当做方法使用,所以不能用于事件处理函数,事件处理函数还是使用 methods 方法。
<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }
})

我们提供的函数将用作属性 vm.reversedMessage 的 getter 函数,也可以这样写:

computed: {
  // 计算属性的 getter
  reversedMessage:  {
    get() {
      return this.message.split('').reverse().join('')
    }
  }
}

Vue 知道 vm.reversedMessage 依赖于 vm.message,因此当 vm.message 发生改变时,所有依赖 vm.reversedMessage 的绑定也会重新计算并更新。

计算属性用于进行控件渲染的时候,通常控件那边用 v-model 进行双向绑定,而计算属性定义 get() 和 set() 函数,两者结合。

侦听属性 watch

Vue 为我们提供了用于监视数据变化的 watch 选项,你可以通过这个特定根据数据的变化做出对应的业务处理。当需要在数据变化执行异步或开销较大时,这个方法是最为有用的。
引用类型只能监视一层,无法监视内部子成员的改变。如果监视数据是数组或者对象需要配置深度 watcher:deep: true

八、Class与Style绑定

参考官网:https://cn.vuejs.org/v2/guide/class-and-style.html

操作元素的 class 列表和内联样式是数据绑定的一个常见需求。
例如:<h1 class="red thin">Hello World</h1>
因为它们都是属性,所以我们可以用 v-bind 处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind 用于 class 和 style 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

绑定 HTML Class

对象语法

我们可以传给 v-bind:class 一个对象,以动态地切换 class:
例如:

<h1 :class="{ 'red': true, 'thin': true, 'italic': false, 'active': false }">Hello World</h1>

或者:

<h1 :class="classObj">Hello World</h1>

在 data 选项中定义这个对象,对象的属性是类名,可带引号,也可不带引号:

data: {
  classObj: { red: true, thin: true, italic: false, active: false }
}

我们也可以绑定一个返回对象的计算属性。这样可以根据 data 选项中值来渲染对应的样式。例如:

<div v-bind:class="classObject"></div>
data: {
  isActive: true,
  error: null
},
computed: {
  classObject: function () {
    return {
      active: this.isActive && !this.error,
      'text-danger': this.error && this.error.type === 'fatal'
    }
  }
}
数组语法

我们可以把一个数组传给 v-bind:class,以应用一个 class 列表。
例如:<h1 :class="['thin', 'italic']">Hello World</h1>
如果你也想根据条件切换列表中的 class,可以用三元表达式。
例如: <h1 :class="['thin', 'italic', isActive?'active':'']">Hello World</h1>
在数组语法中也可以使用对象语法,像这样在数组中使用对象来代替三元表达式,可以提高代码的可读性。<h1 :class="['thin', 'italic', {'active':isActive} ]">Hello World</h1>

用在组件上

当在一个自定义组件上使用 class 属性时,这些类将被添加到该组件的根元素上面。这个元素上已经存在的类不会被覆盖。
例如,如果你声明了这个组件:

Vue.component('my-component', {
  template: '<p class="foo bar">Hi</p>'
})

然后在使用它的时候添加一些 class:
<my-component class="baz boo"></my-component>
HTML 将被渲染为:
<p class="foo bar baz boo">Hi</p>
isActive 为 true 时,HTML 将被渲染成为:
<p class="foo bar active">Hi</p>

绑定内联样式

对象语法

v-bind:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS 属性名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名:

<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
  activeColor: 'red',
  fontSize: 30
}

直接绑定到一个样式对象通常更好,这会让模板更清晰:

<div v-bind:style="styleObject"></div>
data: {
  styleObject: {
    color: 'red',
    fontSize: '13px'
  }
}

同样的,对象语法常常结合返回对象的计算属性使用。

数组语法

v-bind:style 的数组语法可以将多个样式对象应用到同一个元素上:

<h1 :style="[ styleObj1, styleObj2 ]">Hello World</h1>
data: {
  styleObj1: { color: 'red', 'font-weight': 200 },
  styleObj2: { 'font-style': 'italic' }
}
自动添加前缀

v-bind:style 使用需要添加浏览器引擎前缀的 CSS 属性时,如 transform,Vue.js 会自动侦测并添加相应的前缀。

九、自定义指令

网址:https://cn.vuejs.org/v2/guide/custom-directive.html

1. What?

除了使用 Vue 提供的内置指令(例如 v-modelv-show)之外,我们可以自定义一些自己的指令。

2. When?

当需要不可避免的操作 DOM 的时候,使用自定义指令来解决。

3. How?

(1)注册

  • 全局注册:在任何组件都可以使用全局注册自定义指令。如果需要在多 个不同的组件中使用该指令,就把它自定义为全局的。
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

参数说明:
第一个参数:自定义指令的名字
第二个参数:配置指令的生命钩子函数(当符合一定的时机调用的方法叫做钩子函数)

  • 局部注册:只能在当前组件使用该指令。非通用的,不需要多次使用的指令定义到局部。
directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

(2)使用

<!-- 当页面加载的时候时,该元素将获得焦点-->
<input v-focus>
  • 自定义指令命名规则:
    • 指令的名字随便起,但是使用的时候务必加上 v- 前缀
    • 如果是驼峰命名法的名称,则使用的时候需要把驼峰转为小写使用 - 连接起来
  • 全局注册的自定义指令可以在任何组件中使用
  • 组件内注册的自定义指令只能被该组件管理的模板中使用
4. 指令的生命钩子函数

(1)一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • unbind:只调用一次,指令与元素解绑时调用。可以用来做一些 DOM 被移除后的收尾工作,例如清除定时器。

bind 与 inserted 异同
相同之处是一上来就执行一次,以后再也不会执行。
不同之处是 bind 中拿不到父元素,inserted可以拿到父元素。
(如果需要操作指令的父元素,则需要写到 inserted 中)

update 和 componentUpdated 异同
相同之处是都是作用该指令的模板发生更新的时候会触发调用。
不同之处是update 拿到的是数据改变视图之前的 DOM 内容,componentUpdated 拿到的是数据改变视图之后的 DOM 内容。

(如果你需要获取数据改变视图之前的内容,则把代码写到 update 中; 如果需要获取数据改变视图之后的内容,则把代码写到componentUpdated 中)

(2)钩子函数的参数 (即 el、binding、vnode 和 oldVnode)。

  • el 参数:作用该指令的 DOM 元素
  • binding 参数:一个对象,包含以下属性:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="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 钩子中可用。

案例:模拟 v-show 实现,根据值的真假来显示或隐藏该指令的元素
只需要在指令第一次绑定到元素时(bind)和指令对应的值更新时(update)改变该 DOM 元素。

Vue.directive('myShow', {
    bind: function (el, binding) {
        if(binding.value) {
            el.style.display = 'block'
        } else {
            el.style.display = 'none'
        }
    },
    update: function (el,binding) {
        if(binding.value) {
            el.style.display = 'block'
        } else {
            el.style.display = 'none'
        }
    }
})

在很多时候,你可能想在 bind 和 update 时触发相同行为,而不关心其它的钩子。我们可以采取类似以下的简写形式来实现与上面相同的功能。

Vue.directive('myShow', function (el, binding) {
    if(binding.value) {
        el.style.display = 'block'
    } else {
        el.style.display = 'none'
    }
})

(3)对象字面量
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。例如:
HTML:

<div v-demo="{ color: 'white', text: 'hello!' }"></div>

JavaScript:

Vue.directive('demo', function (el, binding) {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text)  // => "hello!"
})

十、过滤器

Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。

过滤器的定义

全局定义

在创建 Vue 实例之前全局定义过滤器:

Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
  // ...
})
局部定义

可以在一个组件的选项中定义本地的过滤器:

filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

过滤器的使用

  • 过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:
<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize 过滤器函数将会收到 message 的值作为第一个参数。

  • 过滤器可以串联:
    例如:{{ message | filterA | filterB }}
    在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。

  • 过滤器是 JavaScript 函数,因此可以接收参数:
    例如:{{ message | filterA('arg1', arg2) }}
    这里,filterA 被定义为接收三个参数的过滤器函数。其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数。

  • 过滤器调用的时候采用的是就近原则,如果私有过滤器和全局过滤器名称一致,优先调用私有过滤器。

开发与调试

为了方便调试和观察 Vue 应用,我们可以使用 Vue 官方开发的一个浏览器插件:Vue Devtools 来辅助调试。Chrome 商店可以安装该插件。
使用:

  • 在 Web 服务器中打开你要调试的 Vue 应用
  • 打开开发人员工具,切换到 Vue 面板


    Vue Devtools 使用
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349