插值

文本插值

  1. 格式{{expression}}
  2. 使用v-once指令执行一次插值,后续不再更新

html 插值

双大括号将数据解释为普通文本而不是html代码,如果需要渲染为html,则需要使用v-html指令

指令

带有v-前缀的特殊的attribute

参数

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

1
2
<a v-bind:href="url">跳转</a>
<a v-on:click="todo">点击</a>

动态参数

用方括号括起来的js表达式作为指令的参数

  1. 动态参数会转换为字符串,异常情况下为null,将会移除绑定
1
2
3
4
5
6
7
8
9
10
11
12
<a v-bind:[attributeName]="url">跳转</a>
<a v-on:[eventName]="todo">事件</a>
<script>
var app = new Vue({
el: '#app',
data: {
attributeName: 'href',
url: 'www.baidu.com',
eventName: 'click'
}
})
</script>

计算属性和侦听器

计算属性

对于复杂的逻辑,使用计算属性比在插值逻辑或函数中效率更高

计算属性默认只有getter,也可以在定义计算属性的同时指定setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}

侦听器

vue通过watch来响应数据的变化,当侦听的数据发生变化时,执行相应的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Vue({
el: '#app',
data: {
oldMessage: 'message',
message: 'message'
},
watch: {
// 侦听oldMessage,发生变化改变message
oldMessage: function(newValue, oldValue){
console.log(newValue + '' + oldValue);
this.message = newValue + oldValue;
}
},
mounted(){
setTimeout(() => this.oldMessage = 'suxi', 1000);
}
})

classstyle 绑定

绑定 class

  1. 传入对象,动态切换class

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!-- isActive为真值时active存在 -->
    <div v-bind:class="{ active: isActive }" class="static"></div>
    <script>
    new Vue({
    el: '#app',
    data: {
    isActive: true
    }
    })
    </script>

    <!-- 渲染为下方dom -->
    <div class="static active"></div>
  2. 传入数组,动态应用数组中的class

    1
    2
    3
    4
    <div v-bind:class="['static', 'active']"></div>

    <!-- 渲染为下方dom -->
    <div class="static active"></div>
  3. 自定义组件,在组件上绑定的类名,将会添加在组件的根元素上面,这个元素上存在的类名不会被覆盖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <my-component class="baz boo"></my-component>
    <script>
    Vue.component('my-component', {
    template: '<p class="foo bar">Hi</p>'
    })
    </script>

    <!-- 渲染为以下dom -->
    <p class="foo bar baz boo">Hi</p>

绑定 style

  1. 传入对象,类型css的对象

    1
    <div v-bind:style="{color: 'red', fontSize: '14px'}"></div>
  2. 传入对象数组,将多个样式对象应用于同一个元素上

  3. 一个样式声明允许提供多个值,这样只会渲染数组中最后一个被浏览器支持的值

    1
    <div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

条件渲染

v-if / v-else-if / v-else

v-if / v-else-if指令可以条件性的渲染一块内容,v-else当不满足条件时渲染某块

1
2
3
<div v-if="total > 100">111</div>
<div v-else-if="total > 80">222</div>
<div v-else>333</div>

vue会高效的渲染元素,通常会复用已有的元素而不是从头渲染,如果在一个业务中,两个元素逻辑上是相互独立的,共用一个元素显然是不合理的。vue采用添加唯一的key来避免复用

v-show

v-show不支持templatev-elsev-show渲染的元素会被保留在dom中,只切换元素css中的display属性,所以v-if有更高的切换开销,而v-show有更高的初始化渲染开销,切换频次少使用v-if,反之使用v-show

列表渲染

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ul id="example-1">
<li v-for="(item, index) in items" :key="item.message">
{{item.message}} -- {{index}}
</li>
</ul>

<script>
new Vue({
el: '#example-1',
data: {
items: [
{message: 'foo'},
{message: 'bar'}
]
}
})
</script>

对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ul id="v-for-object" class="demo">
<li v-for="(value, name, index) in object">
{{value}} -- {{name}} -- {{index}}
</li>
</ul>

<script>
new Vue({
el: '#v-for-object',
data: {
object: {
title: 'How to do lists in Vue',
author: 'Jane Doe'
}
}
})
</script>

维护状态

vue根据每项提供的唯一的key来追踪每一个节点的身份,使用v-for时,建议为每一项提供唯一的key,在组件中使用v-forkey是必须的

数组更新检测

  1. vue重写了数组的一些方法,调用以下方法将会触发视图更新
    • push
    • pop
    • shift
    • unshift
    • splice
    • sort
    • reverse
  2. 数组的一些方法不改变原数组而是返回一个新数组,用新数组替换原数组也会触发视图更新
    • filter
    • map
    • concat
    • slice

数组过滤 / 排序(使用计算属性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ul>
<li v-for="item in filter" :key="item">{{item}}</li>
</ul>

<script>
new Vue({
el: 'ul',
data: {
nums: [1, 2, 3, 4, 5, 6],
},
computed: {
filter: function(){
return this.nums.filter(item => item % 2 === 0)
}
}
})
</script>

v-forv-if 一起使用

不推荐在同一个元素中同时使用 v-forv-ifv-for 的优先级高于v-if,当只渲染部分节点时,可以同时使用~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<ul>
<li v-for="item in todos" v-if="item.isShow" :key="item.key">{{item.todo}}</li>
</ul>

<script>
new Vue({
el: 'ul',
data: {
todos: [
{isShow: true, todo: '起床', key: 'one'},
{isShow: false, todo: '早餐', key: 'two'},
{isShow: true, todo: '午饭', key: 'three'}
],
}
})
</script

事件绑定

使用 v-on 指令监听 dom事件,例如 v-on:click="function",可以使用简写 @,例如 @click="function"

事件处理方法

  1. v-on 指令中直接书写 js 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <div class="app">
    <button @click="sum += 2">点击</button>
    <p>sum: {{sum}}</p>
    </div>
    <script>
    new Vue({
    el: '.app',
    data: {
    sum: 0,
    },
    })
    </script>
  2. 事件处理流程写入方法中(默认将event对象传入)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <div class="app">
    <button @click="add">点击</button>
    <p>sum: {{sum}}</p>
    </div>
    <script>
    new Vue({
    el: '.app',
    data: {
    sum: 0,
    },
    methods: {
    add(event){
    console.log(event);
    this.sum += 2;
    }
    }
    })
    </script>

内联处理器中的方法

除了直接绑定方法,还可以在指令中直接调用方法,并将参数传入,如果需要在内联语句中访问dom事件,可以用特殊变量 $event 传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="app">
<button @click="add(2, $event)">点击</button>
<p>sum: {{sum}}</p>
</div>
<script>
new Vue({
el: '.app',
data: {
sum: 0,
},
methods: {
add(num, event){
console.log(event);
this.sum += num;
}
}
})
</script>

事件修饰符

在事件处理程序中调用 event.preventDefault()event.stopPropagation() 是非常常见的需求,vue 提供了事件修饰符来处理 dom 事件细节

  1. .stop
  2. .prevent
  3. .capture
  4. .self
  5. .once
  6. .passive

passive 这个修饰符会执行默认的方法,但明明是默认执行为什么要设置这个修饰符呢?因为浏览器只有等内核线程执行到事件监听器对应的 JavaScript 代码时,才能知道内部是否会调用 preventDefault 函数来阻止事件的默认行为,所以浏览器本身是没有办法对这种场景进行优化的。这种场景下,用户的手势事件无法快速产生,会导致页面无法快速执行滑动逻辑,从而让用户感觉到页面卡顿。通俗点说就是每次事件产生,浏览器都会去查询一下是否有 preventDefault 阻止该次事件的默认动作。我们加上 passive 就是为了告诉浏览器,不用查询了,我们没用 preventDefault 阻止默认动作, 这里一般用在滚动监听,@scoll,@touchmove。因为滚动监听过程中,移动每个像素都会产生一次事件,每次都使用内核线程查询 prevent 会使滑动卡顿。我们通过 passive 将内核线程查询跳过,可以大大提升滑动的流畅度

使用修饰符时,顺序很重要,相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击。passiveprevent 冲突,不能同时绑定在一个监听器上

按键修饰符

监听键盘事件时,v-on在监听键盘事件时添加按键修饰符

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

按键码

keyCode 的事件用法已经被废弃了并可能不会被最新的浏览器支持

1
2
<!-- 当event.keyCode = 13时触发 submit -->
<input v-on:keyup.13="submit">

vue 提供了绝大多数常用的按键码的别名,并且有一些按键 (.esc 以及所有的方向键) 在 IE9 中有不同的 key 值, 如果你想支持 IE9,这些内置的别名应该是首选

  1. .enter
  2. .tab
  3. .delete
  4. .esc
  5. .space
  6. .up
  7. .down
  8. .left
  9. .right
1
2
3
<!-- 还可以通过全局config.keyCodes对象自定义按键修饰符别名 -->
<!-- 使用v-on:keyup.f1 -->
Vue.config.keyCodes.f1 = 112;

系统修饰符

  1. 用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器
    • .ctrl
    • .alt
    • .shift
    • .meta
  2. .exact修饰符
    • .exact 修饰符允许控制由精确的系统修饰符组合触发的事件

请注意修饰键与常规按键不同,在和 keyup 事件一起用时,事件触发时修饰键必须处于按下状态。换句话说,只有在按住 ctrl 的情况下释放其它按键,才能触发 keyup.ctrl。而单单释放 ctrl 也不会触发事件。如果你想要这样的行为,请为 ctrl 换用 keyCode:keyup.17

1
2
3
4
5
6
7
8
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button v-on:click.ctrl="onClick">A</button>

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

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button v-on:click.exact="onClick">A</button>

鼠标按钮修饰符

  1. .left
  2. .right
  3. .middle

表单输入绑定

v-model 指令在表单 input、textarea、select 元素上创建双向数据绑定。根据控件类型自动选取正确的方法来更新元素。v-model 会忽略所有表单元素的 value、checked、selected 属性的初始值而总是将vue 实例的数据作为数据来源

  1. v-model在内部为不同的输入元素使用不同的property并抛出不同的事件
    • texttextarea元素使用value属性和input事件
    • checkboxradio使用checked属性和change事件
    • select元素将value作为prop并将change作为事件

修饰符

  1. .lazy,在默认情况下 v-model 在每次 input 事件触发后将输入框的值与数据进行同步,添加 .lazy 修饰符,从而转为在 change 事件之后进行同步
  2. .number,自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符,如果这个值无法被 parseFloat() 解析,则会返回原始的值
  3. .trim,自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符

组件

1
2
3
4
5
6
7
8
9
10
11
12
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

// 使用
<button-counter></button-counter>

data

在组件中,data必须是一个函数,因为每一个实例需要维护被返回对象的独立的拷贝

prop 传递数据

1
2
3
4
Vue.component('blog-post', {
props: ['title'],
template: '<h3>{{title}}</h3>'
})

监听子组件事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<div id="blog-posts-events-demo">
<div :style="{ fontSize: postFontSize + 'em' }">
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:post="post"
v-on:enlarge-text="postFontSize += 0.1"
></blog-post><!-- 绑定事件 -->
</div>
</div>
<script>
Vue.component('blog-post', {
props: ['post'],
// 触发事件 $emit
template: `
<div class="blog-post">
<h3>{{ post.title }}</h3>
<button v-on:click="$emit('enlarge-text')">
Enlarge text
</button>
<div v-html="post.content"></div>
</div>
`
})
new Vue({
el: '#blog-posts-events-demo',
data: {
posts: [/* ... */],
postFontSize: 1
}
})
</script>

使用事件抛出一个值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<button v-on:click="$emit('enlarge-text', 0.1)">
Enlarge text
</button>

<!-- 通过$event获取这个值 -->
<blog-post
v-on:enlarge-text="postFontSize += $event"
></blog-post>

<!-- 如果绑定的事件是一个函数 这个值通过参数获取 -->
<blog-post
v-on:enlarge-text="changeFontSize"
></blog-post>

<script>
methods: {
changeFontSize(size){
this.postFontSize += size;
}
}
</script>

组件中使用 v-model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<input v-model="searchText" />

<!-- 等价于 -->

<input :value="searchText" @input="searchText = $event.target.value" />

<!-- 当用到组件中等价于 -->

<my-component
v-model="searchText"
></my-component>

<my-conponent
:value="searchText"
@input="searchText = $event"
></my-component>

<!-- 如果需要组件正常工作,必须在 value 属性绑定在一个名字是value的prop上,并且input事件被触发时,将新的值通过自定义的ihput事件抛出 -->

<script>
Vue.component('my-component', {
props: ['value'],
template: `
<input
:value="value"
@input="$emit('input', $event.target.value)"
/>
`
})
</script>

动态组件

vuecomponent 元素加一个特殊的 is 属性来实现动态组件,is后面的值可以是一个组件的名字或者是一个组件的选项对象

组件名

使用 kebab-case

当使用 kebab-case 定义一个组件时,也必须在引用这个自定义元素时使用kebab-case

1
2
3
4
Vue.component('my-component-name', { /* ... */ })

// 使用
<my-component-name></my-component-name>

使用 PascalCase

当使用 PascalCase 定义一个组件时,引用这个自定义元素时两种命名法都可以使用

1
2
3
4
5
Vue.component('MyComponentName', { /* ... */ })

// 使用 两种都可以
<my-component-name></my-component-name>
<MyComponentName></MyComponentName>

解析 dom 模板时的注意事项

有些 HTML 元素,诸如 <ul><ol><table><select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li><tr><option>,只能出现在其它某些特定的元素内部

1
2
3
4
5
6
7
8
9
<table>
<item></item>
</table>

<script>
Vue.component('item', {
template: '<tr><td>1</td></tr><tr><td>2</td></tr>'
})
</script>

上例子中,自定义组件会被提升到外部,并导致最终渲染失败,可以通过 is 属性来避免

1
2
3
<table>
<tr is="item"></tr>
</table>

当然如果使用单文件组件和 x-template 以及模板字符串定义的模板,那么这条限制是不存在的

全局注册

通过 Vue.component 来创建的组件都是全局注册的组件,注册以后就可以用在任何新创建的 vue 根实例

1
2
3
4
5
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })

new Vue({ el: '#app' })

局部注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }

new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})

// 对于单文件组件
import ComponentA from './ComponentA.vue'

export default {
components: {
ComponentA
},
// ...
}

// 局部注册的组件在其子组件中不可用。例如,如果你希望 ComponentA 在 ComponentB 中可用,需要在组件B中声明A组件
var ComponentA = { /* ... */ }

var ComponentB = {
components: {
'component-a': ComponentA
},
// ...
}

局部注册的组件在其子组件中不可用

基础组件的自动化全局注册

使用 webpack 中的方法 require.context 自动化导入组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
require.context(directory: String, useSubdirectories: Boolean, regExp: RegExp): Object
// directory -> 文件路径
// useSubdirectories -> 是否查找子目录
// regExp -> 要匹配文件的正则

// test
// ├─components
// ├─ A.js
// ├─ B.js
// ├─ C.js
// └─ D.js
// └─ index.js

require.context('./components', true, /\.js$/);

// 返回
var map = {
"./A.js": "./src/components/test/components/A.js",
"./B.js": "./src/components/test/components/B.js",
"./C.js": "./src/components/test/components/C.js",
"./D.js": "./src/components/test/components/D.js"
};

function webpackContext(req) {
var id = webpackContextResolve(req);
return __webpack_require__(id); // 返回__webpack_require__(id) 相当于require或者import
}
function webpackContextResolve(req) {
// 接收一个参数,req 是test文件夹下面匹配文件的相对路径,返回这个文件相对整个工程的相对路径
var id = map[req];
if(!(id + 1)) { // check for number or string
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
}
return id; // 执行环境的id,返回的是一个字符串,用于热加载 module.hot.accept
}
webpackContext.keys = function webpackContextKeys() {
return Object.keys(map); // 获取模型map内部的键名
};
webpackContext.resolve = webpackContextResolve;
module.exports = webpackContext; // 返回一个函数 函数有两个方法 resolve 和 keys
webpackContext.id = "./src/components/test/components sync recursive \\.js$";


// 引入组件
const webpack = require.context('./components', true, /\.js$/);
const map = {};

for(const key of webpack.keys()){
map[key] = webpack(key);
}

console.log(map); // map中就是一个个module

require.context 执行后,返回一个方法 webpackContext,这个方法又返回一个 __webpack_require__ ,这个__webpack_require__ 就相当于 require 或者 import。同时webpackContext 还有二个静态方法 keysresolve,一个 id 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// vue基础组件全局注册

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
// 返回执行require或import的结果
const componentConfig = requireComponent(fileName)

// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 获取和目录深度无关的文件名
fileName
.split('/')
.pop()
.replace(/\.\w+$/, '')
)
)

// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 export default 导出的,
// 那么就会优先使用 .default,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})

全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生

prop

prop 的大小写

HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当使用 DOM 中的模板时,camelCaseprop 名需要使用其等价的 kebab-case 命名,如果使用字符串模板,那么这个限制就不存在了

1
2
3
4
5
6
7
8
9
Vue.component('blog-post', {
// 在 JavaScript 中是 camelCase 的
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})


// <!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>

prop 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 以数组形式列出props
props: ['title', 'likes', 'isPublished', 'commentIds', 'author'];

// 以对象形式列出props及其类型
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // 或者其他构造函数
}

// 传入一个静态值
<blog-post title="My journey with Vue"></blog-post>

// 传入一个动态值 需要使用v-bind
<blog-post v-bind:title="post.title"></blog-post>

// 传入一个数字 使用v-bind
<blog-post v-bind:likes="42"></blog-post>

// 传入一个布尔值

// 包含该 prop 没有值的情况在内,都意味着 true
<blog-post is-published></blog-post>

// 即便 false 是静态的,我们仍然需要 v-bind 来告诉 Vue
// 这是一个 JavaScript 表达式而不是一个字符串
<blog-post v-bind:is-published="false"></blog-post>

//用一个变量进行动态赋值
<blog-post v-bind:is-published="post.isPublished"></blog-post>

// 传入一个数组
// 即便数组是静态的,我们仍然需要 v-bind 来告诉 Vue
// 这是一个 JavaScript 表达式而不是一个字符串
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

// 用一个变量进行动态赋值
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>

// 传入一个对象

// 即便对象是静态的,我们仍然需要 v-bind 来告诉 Vue
// 这是一个 JavaScript 表达式而不是一个字符串
<blog-post
v-bind:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
></blog-post>

// 用一个变量进行动态赋值
<blog-post v-bind:author="post.author"></blog-post>

// 传入一个对象的所有property

// 如果你想要将一个对象的所有 property 都作为 prop 传入,你可以使用不带参数的 v-bind (取代 v-bind:prop-name)
post: {
id: 1,
title: 'My Journey with Vue'
}

<blog-post v-bind="post"></blog-post>
// 等价于
<blog-post
v-bind:id="post.id"
v-bind:title="post.title"
></blog-post>

单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data property 并将这个 prop 用作其初始值
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}

// 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性

props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}

// 注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态

prop 验证

可以为组件的 prop 指定验证要求,如果有一个需求没有被满足,则 Vue 会在浏览器控制台中发出警告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Vue.component('my-component', {
props: {
// 基础的类型检查 (null 和 undefined 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})

注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data、computed 等) 在 defaultvalidator 函数中是不可用的

  1. type 可以是下面原生的构造函数或其他自定义构造函数,并且通过 instanceof 来进行检查确认
    • String
    • Number
    • Boolean
    • Array
    • Object
    • Date
    • Function
    • Symbol
1
2
3
4
5
6
7
8
9
10
function Person (firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

Vue.component('blog-post', {
props: {
author: Person,
}
})

propAttribute

一个非 propattribute 是指传向一个组件,但是该组件并没有相应 prop 定义的 attribute,因为显式定义的 prop 适用于向一个子组件传入信息,然而组件库的作者并不总能预见组件会被用于怎样的场景。这也是为什么组件可以接受任意的 attribute,而这些 attribute 会被添加到这个组件的根元素上,例如组件的 class 属性会被添加到组件的根元素上

替换 / 合并已有的 Attribute

1
2
3
4
5
6
7
8
Vue.component('myInput', {
template: `<input type="date" class="form-control" />`
})

<my-input type="text" class="abc"/>

// 渲染为
<input type="text" class="form-control abc" />

对于绝大多数 attribute 来说,从外部提供给组件的值会替换掉组件内部设置好的值,所以如果传入 type="text" 就会替换掉 type="date" 并把它破坏!庆幸的是,class 和 style attribute 会稍微智能一些,即两边的值会被合并起来

禁止 Attribute 继承

如果不希望组件的根元素继承 attribute,在组件选项中设置 inheritAttrs: false

1
2
3
Vue.component('my-component', {
inheritAttrs: false
})

禁止组件根元素继承属性以后,依然可以使用 $attrs 手动决定这些属性将会赋予哪个元素,$attrs 会将没有声明的属性合并为一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 将其他属性启用到input元素上
Vue.component('base-input', {
props: ['label', 'value'],
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
`
})

// 仅仅将placeholder属性应用

Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
template: `
<label>
{{ label }}
<input
v-bind:placeholder="$attrs.placeholder"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
`
})

<base-input label="姓名" value="苏西" type="text" placeholder="请输入姓名~"></base-input>

inheritAttrs: false 不会影响 styleclass 的绑定

自定义事件

事件名

1
2
3
4
5
<my-component v-on:my-event="doSomething"></my-component>

// 监听my-event

this.$emit('myEvent'); // 不会触发事件 没有任何效果

不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或 property 名,所以就没有理由使用 camelCasePascalCase 了。并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到,因此,我们推荐始终使用 kebab-case 的事件名

自定义组件的 v-model

一个组件上的 v-model 默认会利用名为 valueprop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的,model 选项可以用来避免这样的冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// model中填入 prop 和 event,表示组件的 v-model 利用 checked 的 prop 和 change 事件
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
// 最好在 props 中声明这个 prop 不声明这个 prop 会报警告:Property or method "checked" is not defined on the instance but referenced during render
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})

// 使用

<base-checkbox v-model="lovingVue"></base-checkbox>
// lovingVue作为checked的prop传入到组件,当触发change事件后,改变checked的值

将原生事件绑定到组件

如果需要在组件的根元素上直接监听一个原生事件,可以使用 v-on.native 修饰符

1
<base-input v-on:focus.native="onFocus"></base-input>

如果尝试监听一个类似 <input> 的非常特定的元素时,这可能不是一个好主意,比如上方的 base-input 组件中如果 <input>不是它的根元素,那么组件中使用 v-on:focus.native 监听器将静默失败,虽然不会产生任何报错,但是也不会触发 onFocus 的事件,为了解决这个问题,vue 提供了 $listeners 属性,它是一个 Object,里面包含了作用在这个组件的所有监听器,$listeners 属性,可以配合 v-on="$listeners" 将所有事件监听器值向这个组件的某个特定的子元素,当然也可以绑定特定的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// $listeners的格式
{
focus: function(event){/* ... */},
input: function(value){/* ... */}
}

Vue.component('base-input', {
props: ['label', 'value'],
template: `
<label>
{{label}}
<input
type="text"
v-bind:value="value"
v-on="$listeners"
>
</label>
`
})

new Vue({
el: '#app',
methods: {
onChange(e){
console.log('change');
console.log(e);
},
onFocus(e){
console.log('focus');
console.log(e);
}
}
})

// 使用 注意 当使用 $listeners 时,组件上就不可以使用 .native修饰符了,否则 $listeners 将是一个 空对象(不是 null 而是 {})
<base-input @change="onChange" @focus="onFocus"></base-input>

对于 input 类型的如果需要配合 v-model 工作的组件来说,为这些监听器创建一个计算属性通常是有用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this
// Object.assign 将所有的对象合并为一个新对象
return Object.assign({},
// 我们从父级添加所有的监听器
this.$listeners,
// 然后我们添加自定义监听器,
// 或覆写一些监听器的行为
{
// 这里确保组件配合 v-model 的工作,如果组件绑定了 v-model 事件
input: function (event) {
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
})

.sync 修饰符

在有些情况下,我们可能需要对一个 prop 进行双向绑定,不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件两侧都没有明显的变更来源vue 推荐以 update:myPropName 的模式触发事件取而代之

1
this.$emit('update:title', newTitle)
父组件监听这个事件,并根据需要变更本地的而数据属性
1
2
3
4
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
vue 为了方便将这种模式提供了一个缩写,即 .sync 修饰符
1
<text-document v-bind:title.sync="doc.title"></text-document>

带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync="doc.title + '!'" 是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似 v-model,所以将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync="{ title: doc.title }",是无法正常工作的

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用

1
2
3
4
5
6
7
8
data(){
return {
doc: {
id: 'xx',
title: 'yy'
}
}
}
1
2
3
4
5
6
7
8
9
10
11
<text-document v-bind.sync="doc"></text-document>

<!-- 这样会把 doc 对象中的每一个 property (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。 -->
<text-document v-bind:title.sync="doc.title" v-bind:id.sync="doc.id"></text-document>

<text-document
v-bind:title="doc.title"
v-bind:id="doc.id"
v-on:update:title="doc.title = $event"
v-on:update:id="doc.id = $event"
></text-document>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- .sync 意味这监听了一个 update:title 事件名的事件,当title改变时触发这个事件,实现双向绑定 -->
<div class="app">
<span>{{title}}</span>
<base-input v-bind:title.sync="title"></base-input>
</div>

<script>
Vue.component('base-input', {
inheritAttrs: false,
props: ['title'],
template: `
<div class="text" @click="$emit('update:title', '居庙堂之高则忧其民,处江湖之远而忧其君')">
{{title}}
</div>
`
})
new Vue({
el: '.app',
data: {
title: '不以物喜,不以己悲'
}
})
</script>

插槽

插槽内容

vue 实现了一套内容分发的 API,将 <slot> 元素作为承载分发内容的出口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 组件navigation-link -->
<navigation-link url="/profile">
Your Profile
</navigation-link>

<!-- 组件navigation-link的模板 -->
<a
v-bind:href="url"
class="nav-link"
>
<slot></slot>
</a>

<!-- 组件渲染时,会将 <slot></slot> 将会被替换为Your Profile -->

<!-- 插槽中可以包含任意的内容,包括html,甚至是一个组件 -->

<!-- 如果组件中的模板中没有 slot 元素,则该组件起始标签和结束标签之间的内容都会被抛弃 -->

插槽作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

<!-- 插槽跟模板的其它地方一样可以访问相同的实例 property (也就是相同的“作用域”),而不能访问 <navigation-link> 的作用域,例如url是无法访问的 -->
<div class="app">
<navigation-link url="/profile">
<!-- 无法获取 url 的内容 -->
Your Profile {{title}} {{url}}
</navigation-link>
</div>

<script>
Vue.component('navigation-link', {
template: `
<div>
<span>插槽</span>
<slot></slot>
</div>
`
})
new Vue({
el: '.app',
data: {
title: '不以物喜,不以己悲'
}
})
</script>

父级模板里的所有内容都是在父级作用域中编译的,子模板里的所有内容都是在子作用域中编译的

后备内容

有时为一个插槽设置具体的后备内容是很有用的,它只会在没有提供内容的时候被渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- submit-button组件的模板 -->
<button type="submit">
<slot></slot>
</button>

<!-- 如果希望按钮在大多数情况下都渲染文本 Submit 可以在slot中放入希望渲染的后备内容 -->
<button type="submit">
<slot>Submit</slot>
</button>

<!-- 使用时不使用插槽就会渲染默认内容,使用插槽就渲染插槽内容 -->

<submit-button></submit-button>

<submit-button>save</submit-button>

<submit-button><div style="width: 10px; height: 10px; background: red; border-radius: 50%;"></div></submit-button>

具名插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 当一个模板需要多个插槽时,需要使用到具名插槽 -->
<!-- base-layout组件模板 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>

slot 的一个特殊的属性 name,这个属性就是用来定义额外的插槽的,其中一个不带 name 属性的 slot 带有隐含的名字 default,在向具名插槽提供内容时,在 template 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>

<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>

<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>

v-slot 一般只能添加在 <template>

作用域插槽

有时候需要插槽中的内容能够访问到子组件的数据,那么就需要作用域插槽,将子组件 slot 元素中的数据通过 v-bind 暴露给插槽,插槽通过 v-slot:[param]="slotProps" 获取到一个包含子组件的 slotv-bind 参数的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<div class="app">
<div>
<current-user>
<!-- 插槽不起作用,无法访问到 user -->
<template>
{{user.firstName}}
</template>
</current-user>
</div>

<div>
<current-user>
<!-- 通过v-slot获取到 v-bind的参数 -->
<template v-slot:default="slotProps">
{{slotProps.user.firstName}}
</template>
</current-user>
</div>
</div>

<script>
Vue.component('current-user', {
data(){
return {
user: {
firstName: '苏西',
lastName: '佩奇'
}
}
},
template: `
<span>
<slot v-bind:user="user">
{{user.lastName}}
</slot>
</span>
`
})
new Vue({
el: '.app'
})
</script>

独占默认插槽的缩写语法

在上面情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用,这样我们就可以把 v-slot 直接用在组件上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</current-user>

<!-- 当然还可以简单,不带参数的 v-slot 假定为默认插槽 -->

<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>

<!-- 注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确 -->

<!-- 无效,会导致警告 -->
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
<template v-slot:other="otherSlotProps">
slotProps is NOT available here
</template>
</current-user>

<!-- 只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法 -->
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>

<template v-slot:other="otherSlotProps">
</template>
</current-user>

<!-- 就像下面一样,当有多个插槽时必须要使用完整的template语法 -->

<div class="app">
<div>
<current-user>
<!-- 通过v-slot获取到 v-bind的参数 -->
<template v-slot:default="slotProps">
{{slotProps.user.firstOne}}
</template>
<template v-slot:slotone="slotProps">
{{slotProps.brother.firstOne}}
</template>
</current-user>
</div>
</div>

<script>
Vue.component('current-user', {
data(){
return {
user: {
firstOne: '苏西',
lastTwo: '佩奇'
},
brother: {
firstOne: '乔治1',
lastTwo: '乔治2'
}
}
},
template: `
<span>
<slot v-bind:user="user">
{{user.lastTwo}}
</slot>

<slot v-bind:brother="brother" name="slotone">
{{brother.lastTwo}}
</slot>
</span>
`
})
new Vue({
el: '.app'
})
</script>

解构插槽 Prop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<current-user>
<!-- 通过v-slot获取到 v-bind的参数 -->
<template v-slot:default="{user}">
{{user.firstName}}
</template>
</current-user>

<!-- 或者解构赋值 重命名 -->
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>

<!-- 解构赋值默认值 -->
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>

动态插槽名

1
2
3
4
<base-layout>
<template v-slot:[dynamicSlotName]>
</template>
</base-layout>

具名插槽的缩写

v-onv-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot) 替换为字符 #。例如 v-slot:header 可以被重写为 #header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<base-layout>
<template #header>
<h1>Here might be a page title</h1>
</template>

<p>A paragraph for the main content.</p>
<p>And another one.</p>

<template #footer>
<p>Here's some contact info</p>
</template>
</base-layout>

<!-- 相当于 -->
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>

<p>A paragraph for the main content.</p>
<p>And another one.</p>

<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>

和其它指令一样,该缩写只在其有参数的时候才可用

1
2
3
4
5
6
7
8
9
<!-- 这样会触发一个警告 -->
<current-user #="{ user }">
{{ user.firstName }}
</current-user>

<!-- 明确插槽名才可以使用 -->
<current-user #default="{ user }">
{{ user.firstName }}
</current-user>

作用域插槽示例

当需要根据子组件的某些条件来确认是否需要渲染某些内容时,作用域插槽是可用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<div class="app">
<div>
<todo-list>
<template v-slot:todo="{todo}">
<span v-if="todo.isComplete"></span>
{{todo.text}}
</template>
</todo-list>
</div>
</div>
<script>
Vue.component('todo-list', {
data(){
return {
filteredTodos: [
{id: '1', text: '111', isComplete: true},
{id: '2', text: '222', isComplete: false},
{id: '3', text: '333', isComplete: true}
]
}
},
template: `
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
<slot name="todo" v-bind:todo="todo">
<!-- 后备内容 -->
{{ todo.text }}
</slot>
</li>
</ul>
`
})
new Vue({
el: '.app'
})
</script>

插槽废弃的语法

v-slot 指令自 Vue 2.6.0 起被引入,提供更好的支持 slotslot-scope attributeAPI 替代方案。虽然在所有的 2.x 版本中 slotslot-scope attribute 仍会被支持,但已经被官方废弃且不会出现在 Vue 3

带有 slot 属性的具名插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!-- base-layout模板 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<!-- 使用 -->
<!-- 新版是 v-slot:或# 旧版是 slot -->
<base-layout>
<template slot="header">
<h1>Here might be a page title</h1>
</template>

<p>A paragraph for the main content.</p>
<p>And another one.</p>

<template slot="footer">
<p>Here's some contact info</p>
</template>
</base-layout>

<!-- 或者直接将slot作用于一个普通元素上 -->
<base-layout>
<h1 slot="header">Here might be a page title</h1>

<!-- 未命名的插槽(默认插槽)会捕获未被匹配的内容 -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>

<p slot="footer">Here's some contact info</p>
</base-layout>

<!-- 以上两个示例会被渲染为 -->

<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>

带有 slot-scope 属性的具名插槽

v-slot:default="slotProp" 一样可以接收传递给插槽的 slotProp,这个 slotProp 声明了被接收的 prop 对象会作为 slotProps 变量存在于 template 作用域中,slotProp 可以随意命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<slot-example>
<template slot="default" slot-scope="slotProps">
{{ slotProps.msg }}
</template>
</slot-example>

<!-- 如果slot="default" 可以省略 -->
<slot-example>
<template slot-scope="slotProps">
{{ slotProps.msg }}
</template>
</slot-example>

<!-- slot-scope 可以直接用于非 template元素,包括组件 -->
<slot-example>
<span slot-scope="slotProps">
{{ slotProps.msg }}
</span>
</slot-example>

<!-- slot-scope 的值可以接收任何有效的可以出现在函数定义的参数位置上的 JavaScript 表达式。这意味着在支持的环境下 (单文件组件或现代浏览器),可以在表达式中使用 ES2015 解构 -->

<slot-example>
<span slot-scope="{ msg }">
{{ msg }}
</span>
</slot-example>

<!-- 之前的todo-list组件也可以这样使用 -->

<todo-list>
<template slot="todo" slot-scope="{ todo }">
<span v-if="todo.isComplete"></span>
{{ todo.text }}
</template>
</todo-list>

动态组件 & 异步组件

在动态组件上使用 keep-alive

1
<component v-bind:is="currentTabComponent"></component>

当在这些组件之间切换的时候,有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题

可以看到,选择一篇文章,切换到 archive 之后再切回 posts,不会展示之前显示的文章,这是因为每次切换标签的时候,vue 都创建了一个新的currentTabComponent 实例

重新创建动态组件的行为通常是非常有用的,但是有时更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,可以用一个 <keep-alive> 元素将其动态组件包裹起来

1
2
3
4
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>

注意这个 <keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册

异步组件

在大型应用中,可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义组件,这个工厂函数会异步解析组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vue.component('async-example', function (resolve, reject) {
// 定时器模拟异步
setTimeout(function () {
// 向 resolve 回调传递组件定义 也可以reject(reason)来表示加载失败
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})

// 也可以在工厂函数中返回一个 promise

Vue.component('async-webpack-example', () => import('./my-async-componet'));

// 局部注册的时候,可以直接提供一个返回Promise的函数
new Vue({
components: {
'my-component': () => import('./my-async-component')
}
})

处理加载状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})

// 注意如果你希望在 Vue Router 的路由组件中使用上述语法的话,你必须使用 Vue Router 2.4.0+ 版本

处理边界情况

访问元素 & 组件

访问根实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 在每个 new Vue 实例的子组件中,其根实例可以通过 $root property 进行访问
// Vue 根实例
new Vue({
data: {
foo: 1
},
computed: {
bar: function () { /* ... */ }
},
methods: {
baz: function () { /* ... */ }
}
})

// 所有的子组件都可以将这个实例作为一个全局 store 来访问或使用
// 获取根组件的数据
this.$root.foo

// 写入根组件的数据
this.$root.foo = 2

// 访问根组件的计算属性
this.$root.bar

// 调用根组件的方法
this.$root.baz()


Vue.component('todo-list', {
data(){
return {
filteredTodos: [
{id: '1', text: '111', isComplete: true},
{id: '2', text: '222', isComplete: false},
{id: '3', text: '333', isComplete: true}
]
}
},
mounted(){
// 子组件中获取根组件的数据
console.log(this.$root.foo);
},
template: `
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
<slot name="todo" v-bind:todo="todo">
<!-- 后备内容 -->
{{ todo.text }}
</slot>
</li>
</ul>
`
})
new Vue({
el: '.app',
data: {
foo: 1
}
})

访问父级组件实例

$root 类似,$parent 属性可以用来从一个子组件访问父组件的实例,它提供了一种机会,可以在后期随时触达父级组件,来替代将数据以 prop 的方式传入子组件的方式

在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Vue.component('todo-list', {
data(){
return {
filteredTodos: [
{id: '1', text: '111', isComplete: true},
{id: '2', text: '222', isComplete: false},
{id: '3', text: '333', isComplete: true}
]
}
},
mounted(){
// 子组件中获取根组件的数据
console.log(this.$parent.foo); // 1
},
template: `
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
<slot name="todo" v-bind:todo="todo">
<!-- 后备内容 -->
{{ todo.text }}
</slot>
</li>
</ul>
`
})
new Vue({
el: '.app',
data: {
foo: 1
}
})

访问子组件或子组件实例和方法

尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript直接访问一个子组件。为了达到这个目的,可以通过 ref 这个属性为子组件赋予一个 ID 引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<div class="app">
<div>
<todo-list ref="todolist">
<template v-slot:todo="{todo}">
<span v-if="todo.isComplete"></span>
{{todo.text}}
</template>
</todo-list>
</div>
</div>

<script>
Vue.component('todo-list', {
data(){
return {
filteredTodos: [
{id: '1', text: '111', isComplete: true},
{id: '2', text: '222', isComplete: false},
{id: '3', text: '333', isComplete: true}
]
}
},
mounted(){
console.log(this.$parent.foo);
},
methods: {
test(){
console.log(this.filteredTodos);
}
},
template: `
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
<slot name="todo" v-bind:todo="todo">
<!-- 后备内容 -->
{{ todo.text }}
</slot>
</li>
</ul>
`
})
new Vue({
el: '.app',
data: {
foo: 1
},
mounted(){
this.$refs.todolist.test() // 访问子组件方法
}
})
</script>

refv-for 一起使用的时候,得到的 ref 将会是一个包含了对应数据源的这些子组件的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<div class="app">
<div>
<todo-list ref="todolist" v-for="item in list" :key="item.id">
<template v-slot:todo="{todo}">
<span v-if="todo.isComplete"></span>
{{todo.text}}
</template>
</todo-list>
</div>
</div>
<script>
Vue.component('todo-list', {
data(){
return {
filteredTodos: [
{id: '1', text: '111', isComplete: true},
{id: '2', text: '222', isComplete: false},
{id: '3', text: '333', isComplete: true}
]
}
},
mounted(){
console.log(this.$parent.foo);
},
methods: {
test(){
console.log(this.filteredTodos);
}
},
template: `
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
<slot name="todo" v-bind:todo="todo">
<!-- 后备内容 -->
{{ todo.text }}
</slot>
</li>
</ul>
`
})
new Vue({
el: '.app',
data: {
foo: 1,
list: [
{id: 1},
{id: 2}
]
},
mounted(){
console.log(this.$refs.todolist) // 数组
// this.$refs.todolist: VueComponet[]
}
})
</script>

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的逃生舱——应该避免在模板或计算属性中访问 $refs

依赖注入

使用两个新的实例选项:provideinjectprovide 选项允许提供给任意后代组件数据和方法,在后代组件中使用 inject 选项来接收传给后代组件的数据和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Vue.component('todo-list', {
data(){
return {
filteredTodos: [
{id: '1', text: '111', isComplete: true},
{id: '2', text: '222', isComplete: false},
{id: '3', text: '333', isComplete: true}
]
}
},
inject: ['one', 'foo'], // 引入one foo
mounted(){
this.one();
console.log(this.foo);
}
template: `
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
<slot name="todo" v-bind:todo="todo">
<!-- 后备内容 -->
{{ todo.text }}
</slot>
</li>
</ul>
`
})
new Vue({
el: '.app',
data: {
foo: 1
},
provide(){
return {
one: this.one,
foo: this.foo
}
},
methods: {
one(){
console.log('one' + this.foo);
}
}
})

实际上,可以把依赖注入看作一部分大范围有效的 prop除了祖先组件不需要哪些后代组件使用了它提供的 property,后代组件不需要知道被注入的 property 来自哪里

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的 property 是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root 做这件事都是不够好的

程序化的事件侦听器

  1. 通过 $on(eventName, eventHandler) 侦听一个事件
  2. 通过 $once(eventName, eventHandler) 一次性侦听一个事件
  3. 通过 $off(eventName, eventHandler) 停止侦听一个事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一次性将这个日期选择器附加到一个输入框上
// 它会被挂载到 DOM 上。
mounted: function () {
// Pikaday 是一个第三方日期选择器的库
this.picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
},
// 在组件被销毁之前,
// 也销毁这个日期选择器。
beforeDestroy: function () {
this.picker.destroy()
}

这里存在了两个问题

  • 它需要在这个组件实例中保存这个 picker,如果可以的话最好只有生命周期钩子可以访问到它,这并不算严重的问题,但是它可以被视为杂物
  • 建立代码独立于我们的清理代码,这使得我们比较难于难于程序化地清理我们建立的所有东西
1
2
3
4
5
6
7
8
9
10
11
mounted: function () {
var picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})

// 组件内的生命周期函数在执行结束后会 $emit 一个hook + 生命周期名字的自定义事件
this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}

使用这个逻辑,甚至可以让多个输入框同时使用不同的 pikaday每个实例都程序化的在后期清理它们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mounted: function () {
this.attachDatepicker('startDateInput')
this.attachDatepicker('endDateInput')
},
methods: {
attachDatepicker: function (refName) {
var picker = new Pikaday({
field: this.$refs[refName],
format: 'YYYY-MM-DD'
})

this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}
}

注意 vue 的事件系统不同于浏览器的 EventTarget API,尽管它们工作起来是相似的,但是 $emit$on, 和 $off 并不是 dispatchEventaddEventListenerremoveEventListener 的别名

循环引用

递归组件

组件是可以在它们自己的模板中调用自身的,不过它们只能通过 name 选项来做这件事。当全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项

1
2
3
4
5
Vue.component('unique-name-of-my-component', {
// ...
})

// name: 'unique-name-of-my-component'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div class="app">
<list :list="list"></list>
</div>

<script>
Vue.component('list', {
props: ['list'],
// 注意一定要有停止条件,否则栈溢出
template: `
<ul>
<li v-for="item in list" :key="item.id"><div>{{item.content}}</div> <list :list="item.children" v-if="item.children !== null" ></list></li>
</ul>
`
})

new Vue({
el: '.app',
data: {
list: [
{id: '1', content: 'list1', children: [{id: '1-1', content: 'list1-1'}, {id: '1-2', content: 'list1-2'}]},
{id: '2', content: 'list2'},
{id: '3', content: 'list3'}
]
}
})
</script>

组件之间的循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!-- 组件tree-folder模板 -->
<p>
<span>{{ folder.name }}</span>
<tree-folder-contents :children="folder.children"/>
</p>
<!-- 组件tree-folder-contents模板 -->

<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child"/>
<span v-else>{{ child.name }}</span>
</li>
</ul>

<!-- 当你仔细观察的时候,你会发现这些组件在渲染树中互为对方的后代和祖先——一个悖论!当通过 Vue.component 全局注册组件的时候,这个悖论会被自动解开 -->

<!-- 如果你使用一个模块系统依赖/导入组件,例如通过 webpack 或 Browserify,你会遇到一个错误: Failed to mount component: template or render function not defined.

模块系统发现它需要 A,但是首先 A 依赖 B,但是 B 又依赖 A,但是 A 又依赖 B,如此往复。这变成了一个循环,不知道如何不经过其中一个组件而完全解析出另一个组件。为了解决这个问题,我们需要给模块系统一个点,在那里“A 反正是需要 B 的,但是我们不需要先解析 B

在我们的例子中,把 <tree-folder> 组件设为了那个点。我们知道那个产生悖论的子组件是 <tree-folder-contents> 组件,所以我们会等到生命周期钩子 beforeCreate 时去注册它
-->

<script>
// browserify
beforeCreate: function () {
this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}
// webpack import
components: {
TreeFolderContents: () => import('./tree-folder-contents.vue')
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<div class="app">
<tree-folder :folder="folder"></tree-folder>
</div>

<script>
Vue.component('tree-folder', {
props: ['folder'],
template: `
<p>
<span>{{folder.name}}</span>
<tree-folder-contents :children="folder.children"/>
</p>
`
})

Vue.component('tree-folder-contents', {
props: ['children'],
template: `
<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child" />
<span v-else>{{child.name}}</span>
</li>
</ul>
`
})

new Vue({
el: '.app',
data: {
folder: {name: '我的电脑', children: [
{name: '用户', id: 'user', children: [{name: '默认', id: 'defalut'}, {name: '公用', id: 'public'}]},
{name: 'System32', id: 'system32'},
{name: 'System64', id: 'system64'}
]}
}
})
</script>

模板定义的替代品

内联模板

inline-template 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板而不是将其作为被分发的内容,这使得模板的撰写工作更加灵活

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- inline-template中的内容直接作为模板渲染出来 -->
<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>

<script>
Vue.component('my-component', {
// 即使写模板也不会生效
template: '<span>111</span>'
})
</script>

inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板

X-Template

另一个定义模板的方式是在一个 <script> 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去,但这些模板一般用于极小的应用,其他情况下避免使用,这会将模板和组件的其他定义分开来

1
2
3
4
5
6
7
8
9
10
11
<hello-world></hello-world>

<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>

<script>
Vue.component('hello-world', {
template: '#hello-world-template'
})
</script>

控制更新

强制更新

通过 $forceUpdate 来强制更新,对于数组和对象的变更检测,视图是无法及时更新的,需要强制更新( $forceUpdate )

通过 v-once 创建低开销的静态组件

渲染普通的 HTML 元素在 vue 中是非常快速的,但有的时候可能有一个组件,这个组件包含了大量静态内容,在这种情况下,可以在根元素上添加 v-once 属性以确保这些内容只计算一次然后缓存起来

1
2
3
4
5
6
7
8
Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})

进入 / 离开 & 列表过渡

vue 在插入、更新或移除 dom 时,提供多种不同方式的应用过渡效果

  1. css 过渡和动画中自动应用 class
  2. 可以配合使用第三方 css 动画库,如 animate.css
  3. 在过渡钩子函数中使用 javascript 直接操作 dom
  4. 可以配合使用第三方 javascript 动画库,如 velocity.js

单元素 / 组件的过渡

vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素或组件添加进入 / 离开过渡

  1. 条件渲染(v-ifv-show)
  2. 动态组件(<component :is="xx"></component>)
  3. 组件根节点

当插入或删除包含在 transition 组件中的元素时,vue 将会做出以下处理

  1. 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名
  2. 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用
  3. 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行

过渡的类名

在进入 / 离开的过渡中,会有 6class 切换

  1. v-enter:定义进入过渡的开始状态,在元素被插入之前生效,在元素被插入之后的下一帧移除
  2. v-enter-active:定义进入过渡生效时的状态,在整个进入过渡阶段中应用,在元素被插入之前生效,在过渡 / 动画完成之后移除,这个类被用来定义进入过渡的过程时间,延迟和曲线函数
  3. v-enter-to:定义进入过渡的结束状态,在元素被插入之后下一帧生效(与此同时 v-enter 被移除),在过渡 / 动画完成之后移除
  4. v-leave:定义离开过渡的开始状态,在离开过渡被触发时立刻生效,下一帧被移除
  5. v-leave-active:定义离开过渡生效时的状态,在整个离开过渡阶段中应用,在离开过渡被触发时立刻生效,在过渡 / 动画完成之后移除,这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数
  6. v-leave-to:定义离开过渡的结束状态,在离开过渡被触发之后下一帧生效(与此同时 v-leave 被删除),在过渡 / 动画完成之后移除
过渡过程
过渡过程
过渡过程
过渡过程

对于这些在过渡中切换的类名来说,如果使用了一个没有名字的 <transition>,则 v- 是这些类名的默认前缀,如果使用了 name 属性 <transition name="my-transition">,则 v-enter 会替换为 my-transition。其中 v-enter-activev-leave-active 可以控制进入 / 离开过渡的不同的缓和曲线

CSS 过渡

1
2
3
4
5
6
7
8
<div id="example-1">
<button @click="show = !show">
Toggle render
</button>
<transition name="slide-fade">
<p v-if="show">hello</p>
</transition>
</div>
1
2
3
4
5
6
new Vue({
el: '#example-1',
data: {
show: true
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
/* 可以设置不同的进入和离开动画 */
/* 设置持续时间和动画函数 */
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active for below version 2.1.8 */ {
transform: translateX(10px);
opacity: 0;
}

CSS 动画

css 动画用法同 css 过渡,区别是在动画中 v-enter 类名在节点插入 dom 后不会立即删除,而是在 animationend 事件触发时删除

自定义过渡的类名

  1. 可以通过以下的属性来自定义过渡类名
    • enter-class
    • enter-active-class
    • enter-to-class
    • leave-class
    • leave-active-class
    • leave-to-class
  2. 它们的优先级高于普通的类名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">

<div id="app">
<button @click="show = !show">切换</button>
<transition
name="custom-classes-transition"
enter-active-class="animated tada"
leave-active-class="animated bounceOutRight"
>
<p v-if="show">hello</p>
</transition>
</div>

<script>
new Vue({
el: '#app',
data: {
show: true
}
})
</script>

同时使用过渡和动画

vue 为了知道过渡的完成,必须设置相应的事件监听器,它可以是 transitionendanimationend,这取决于给元素应用的 css 规则,如果使用其中任何一种,vue 能自动识别类型并设置监听,但是,假设需要给同一个元素设置两种过渡动效,如果 animation 很快的被触发完成了,而 transition 效果还没结束,在这种情况下,需要使用 type 属性并设置 animationtransition 来明确声明需要 vue 监听的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">

<div id="app">
<button @click="show = !show">切换</button>
<transition
name="fade"
enter-active-class="animated tada duration-1s"
leave-active-class="animated bounceOutRight duration-1s"
type="transition"
>
<p v-if="show">hello</p>
</transition>
</div>

<script>
new Vue({
el: '#app',
data: {
show: true
}
})
</script>

显性的过渡持续时间

在很多情况下,vue 可以自动得出过渡效果的完成时机。默认情况下,vue 会等待其在过渡效果的根元素的第一个 transitionendanimationend 事件。然而也可以不这样设定——比如,我们可以拥有一个精心编排的一系列过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素有延迟的或更长的过渡效果,在这种情况下可以用 <transition> 组件上的 duration prop定制一个显性的过渡持续时间

1
2
3
4
5
<!-- 设置显性的过渡时间 单位为毫秒 -->
<transition :duration="1000">...</transition>

<!-- 定制进入和移除的过渡时间 -->
<transition :duration="{ enter: 500, leave: 800 }">...</transition>

javascript 钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter"
v-on:enter-cancelled="enterCancelled"

v-on:before-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
v-on:leave-cancelled="leaveCancelled"
>
<!-- ... -->
</transition>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// ...
methods: {
// --------
// 进入中
// --------

beforeEnter: function (el) {
// ...
},
// 当与 CSS 结合使用时
// 回调函数 done 是可选的
enter: function (el, done) {
// ...
done()
},
afterEnter: function (el) {
// ...
},
enterCancelled: function (el) {
// ...
},

// --------
// 离开时
// --------

beforeLeave: function (el) {
// ...
},
// 当与 CSS 结合使用时
// 回调函数 done 是可选的
leave: function (el, done) {
// ...
done()
},
afterLeave: function (el) {
// ...
},
// leaveCancelled 只用于 v-show 中
// 离开过渡被打断时
leaveCancelled: function (el) {
// ...
}
}
  1. 可以在属性中声明 javascript 钩子
    • before-enter:进入过渡运行前
    • enter:进入过渡运行时
    • after-enter:进入过渡运行后
    • enter-cancelled:进入过渡被打断时
    • before-leave:离开过渡运行前
    • leave:离开过渡运行时
    • after-leave:离开过渡运行后
    • leave-cancelled:离开过渡被打断时
  2. 这些钩子函数可以结合 transitions / animations 使用,也可以单独使用
  3. 当只有 javascript 过渡的时候,在 enterleave 中必须使用 done 进行回调,否则,它们将被同步调用,过渡会立即完成
  4. 推荐对于使用 javascript 过渡的元素添加 v-bind:css="false"vue 会跳过 css 的检测,也可以避免过渡过程中 css 的影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<div id="app">
<button @click="show = !show">
切换
</button>
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:leave="leave"
v-bind:css="false"
>
<p v-if="show">
Hello
</p>
</transition>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<script>
new Vue({
el: '#example-4',
data: {
show: false
},
methods: {
beforeEnter: function (el) {
el.style.opacity = 0
el.style.transformOrigin = 'left'
},
enter: function (el, done) {
Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 })
Velocity(el, { fontSize: '1em' }, { complete: done })
},
leave: function (el, done) {
Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 })
Velocity(el, { rotateZ: '100deg' }, { loop: 2 })
Velocity(el, {
rotateZ: '45deg',
translateY: '30px',
translateX: '30px',
opacity: 0
}, { complete: done })
}
}
})
</script>

初始渲染的过渡

通过 appear 属性来设置节点在初始渲染的过渡

1
2
3
<transition appear>
<!-- ... -->
</transition>

自定义 css 类名

1
2
3
4
5
6
7
8
<transition
appear
appear-class="custom-appear-class"
appear-to-class="custom-appear-to-class"
appear-active-class="custom-appear-active-class"
>
<!-- ... -->
</transition>

自定义 javascript 钩子

1
2
3
4
5
6
7
8
9
<transition
appear
v-on:before-appear="customBeforeAppearHook"
v-on:appear="customAppearHook"
v-on:after-appear="customAfterAppearHook"
v-on:appear-cancelled="customAppearCancelledHook"
>
<!-- ... -->
</transition>

多个元素的过渡

一个 transition 标签中含有多个过渡元素,但是当有多个相同标签名的元素切换时,需要通过 key 属性设置唯一的值来标记以让 vue 区分它们,否则 vue 为了效率只会替换相同标签内部的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<transition>
<button v-if="isEditing" key="save">
Save
</button>
<button v-else key="edit">
Edit
</button>
</transition>

<!-- 或者 -->
<transition>
<button v-bind:key="isEditing">
{{ isEditing ? 'Save' : 'Edit' }}
</button>
</transition>

过渡模式

点击下面的按钮,在 onetwo 按钮的过渡中,两个按钮都被重绘了,一个离开过渡的时候另一个开始进入过渡,这是 <transition>默认行为。接下来点击切换定位为绝对定位在彼此的上面运行正常,然后滑动进度条运动变为滑动过渡

同时生效的进入和离开的过渡不能满足所有要求,所以 vue 提供了过渡模式

  1. in-out:新元素先过渡,完成之后当前元素过渡离开
  2. out-in:当前元素先进行过渡,完成之后新元素过渡进入(点击切换模式查看效果)

多个组件的过渡

多个组件的过渡需要使用动态组件

1
2
3
<transition fade mode="out-in">
<component v-bind:is="view"></component>
</transition>
1
2
3
4
5
6
.fade-enter-active, .fade-leave-active {
transition: opacity .3s ease;
}
.fade-enter, .fade-leave-to{
opacity: 0;
}

列表过渡

对于列表过渡,我们采用 transition-group 组件,该组件的特点如下:

  1. 不同于 transition,它会以一个真实元素呈现:默认为一个 span,可以通过 tag 属性更换为其他元素
  2. 过渡模式不可用,因为不需要相互切换特有的元素
  3. 内部元素总是需要提供唯一的 key
  4. css 过渡的类将会应用在内部的元素中,而不是这个组 / 容器本身

列表的进入 / 离开过渡

上面的过渡并不平滑,当添加或移除元素时,周围的元素会瞬间移动到它们的新布局的位置

列表的排序过渡

transition-group 组件还有一个特殊之处,不仅可以进入和离开动画,还可以改变定位,要使用这个功能需要 v-move 这个类,它会在元素的改变定位的过程中应用,像之前的类名一样,可以通过 name 属性来自定义前缀,也可以通过 move-class 属性手动设置,v-move 对于设置过渡的切换时机和过渡曲线非常有用

1
2
3
4
5
6
7
8
9
<div id="app" class="demo">
<button v-on:click="shuffle">Shuffle</button>
<transition-group name="flip-list" tag="ul">
<li v-for="item in items" v-bind:key="item">
{{ item }}
</li>
</transition-group>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>
1
2
3
4
5
6
7
8
9
10
11
12
new Vue({
el: '#app',
data: {
items: [1,2,3,4,5,6,7,8,9]
},
methods: {
shuffle: function () {
// _.shuffle() 返回一个打乱的集合
this.items = _.shuffle(this.items)
}
}
})
1
2
3
.flip-list-move {
transition: transform 1s;
}

使用 v-move时,vue 内部使用了一个 FLIP 简单的动画队列,使用 transforms 将元素从之前的位置平滑过渡到新的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app" class="demo">
<button v-on:click="shuffle">Shuffle</button>
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list-complete" tag="p">
<span
v-for="item in items"
v-bind:key="item"
class="list-complete-item"
>
{{ item }}
</span>
</transition-group>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
new Vue({
el: '#app',
data: {
items: [1,2,3,4,5,6,7,8,9],
nextNum: 10
},
methods: {
randomIndex: function () {
return Math.floor(Math.random() * this.items.length)
},
add: function () {
this.items.splice(this.randomIndex(), 0, this.nextNum++)
},
remove: function () {
this.items.splice(this.randomIndex(), 1)
},
shuffle: function () {
this.items = _.shuffle(this.items)
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.list-complete-item {
/* 也可以在move-class和list-complete-enter/leave-active中分别加transition,注意一定要是all才可以,可能过渡效果不止一种,这里在item中加transition相当于在move和active中分别加transition */
transition: all 1s;
display: inline-block;
margin-right: 10px;
}
.list-complete-enter, .list-complete-leave-to{
opacity: 0;
transform: translateY(30px);
}
.list-complete-leave-active {
/* 即将删除的dom需要脱离文档流后面的元素才可以过渡过来 */
position: absolute;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 另一种css写法 */
.list-complete-item {
display: inline-block;
margin-right: 10px;
}
.list-complete-enter, .list-complete-leave-to{
opacity: 0;
transform: translateY(30px);
}

.list-complete-enter-active,.list-complete-leave-active{
transition: all 1s;
}

.list-complete-move{
transition: all 1s;
}

.list-complete-leave-active {
/* 即将删除的dom需要脱离文档流后面的元素才可以过渡过来 */
position: absolute;
}

需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline 。作为替代方案,可以设置为 display: inline-block 或者放置于 flex

flip 动画不仅可以实现单列过渡,多维网格也可以过渡

列表的交错过渡

传统的交错过渡需要使用到大量的 cssvue 提供了钩子函数配合 setTimeout来实现交错过渡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* 使用 css 来实现交错过渡 */
/* 可以发现 需要写大量的css */
.list-enter-active, .list-leave-active {
transition: all 1s;
}

.list-enter, .list-leave-to{
opacity: 0;
transform: translateY(100%);
}

.list-css-enter-active, .list-css-leave-active {
transition: all 1s;
}

.list-css-enter, .list-css-leave-to{
opacity: 0;
transform: translateY(100%);
}

.list-css-enter-active:nth-child(5n+2){
transition-delay: .3s;
}

.list-css-enter-active:nth-child(5n+3){
transition-delay: .6s;
}

.list-css-enter-active:nth-child(5n+4){
transition-delay: .9s;
}

.list-css-enter-active:nth-child(5n+5){
transition-delay: 1.2s;
}

.list-css-leave-active:nth-child(5n+1){
transition-delay: 1.2s;
}

.list-css-leave-active:nth-child(5n+2){
transition-delay: .9s;
}

.list-css-leave-active:nth-child(5n+3){
transition-delay: .6s;
}

.list-css-leave-active:nth-child(5n+4){
transition-delay: .3s;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 通过钩子函数进行过渡
Vue.component('tab-c', {
props: ['num'],
template: `
<transition-group
:css="false"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
>
<item v-for="item in num" :key="item" :data-enter="item * 300" :data-leave="(num - item) * 300"></item>
</transition-group>
`,
methods: {
beforeEnter(el){
// 为过渡元素添加类
el.classList.add('list-enter', 'list-enter-active');
},
enter(el, done){
let delay = el.dataset.enter;
setTimeout(() => {
el.classList.remove('list-enter');
el.classList.add('list-enter-to');
// 监听 transitionend 事件完成后调用 done 执行after-enter钩子
el.addEventListener('transitionend', function onEnd(){
el.removeEventListener('transitionend', onEnd);
done(); // 告诉vue 动画已经完成,触发afterEnter钩子
})
}, delay)
},
afterEnter(el){
el.classList.remove('list-enter-to', 'list-enter-active');
},
beforeLeave(el){
el.classList.add('list-leave', 'list-leave-active');
},
leave(el, done){
let delay = el.dataset.leave;
setTimeout(() => {
el.classList.remove('list-leave');
el.classList.add('list-leave-to');
// 监听transitionend事件
el.addEventListener('transitionend', function onEnd(){
el.removeEventListener('transitionend', onEnd);
done();
})
}, delay)
},
afterLeave(el){
el.classList.remove('list-leave-active', 'list-leave-to');
}
}
})

可复用的过渡

过渡可以通过 vue 的组件系统来实现复用,创建一个可复用的组件,需要将 transitiontransition-group 作为根组件,然后将任何子组件放置在其中就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vue.component('my-special-transition', {
template: '\
<transition\
name="very-special-transition"\
mode="out-in"\
v-on:before-enter="beforeEnter"\
v-on:after-enter="afterEnter"\
>\
<slot></slot>\
</transition>\
',
methods: {
beforeEnter: function (el) {
// ...
},
afterEnter: function (el) {
// ...
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 函数式组件
Vue.component('my-special-transition', {
functional: true,
render: function (createElement, context) {
var data = {
props: {
name: 'very-special-transition',
mode: 'out-in'
},
on: {
beforeEnter: function (el) {
// ...
},
afterEnter: function (el) {
// ...
}
}
}
return createElement('transition', data, context.children)
}
})

动态过渡

1
2
3
4
<!-- 动态绑定过渡类名 -->
<transition v-bind:name="transitionName">
<!-- ... -->
</transition>

状态过渡

状态动画和侦听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js"></script>

<div id="animated-number-demo">
<input v-model.number="number" type="number" step="20">
<p>{{ animatedNumber }}</p>
</div>

<script>
new Vue({
el: '#animated-number-demo',
data: {
number: 0,
tweenedNumber: 0
},
computed: {
animatedNumber: function() {
return this.tweenedNumber.toFixed(0);
}
},
watch: {
number: function(newValue) {
gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue });
}
}
})
</script>

把过渡放在组件里

1
2
3
4
5
6
7
8
9
10
11
12
<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>

<div id="example-8">
<input v-model.number="firstNumber" type="number" step="20"> +
<input v-model.number="secondNumber" type="number" step="20"> =
{{ result }}
<p>
<animated-integer v-bind:value="firstNumber"></animated-integer> +
<animated-integer v-bind:value="secondNumber"></animated-integer> =
<animated-integer v-bind:value="result"></animated-integer>
</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 这种复杂的补间动画逻辑可以被复用
// 任何整数都可以执行动画
// 组件化使我们的界面十分清晰
// 可以支持更多更复杂的动态过渡
// 策略。
Vue.component('animated-integer', {
template: '<span>{{ tweeningValue }}</span>',
props: {
value: {
type: Number,
required: true
}
},
data: function () {
return {
tweeningValue: 0
}
},
watch: {
value: function (newValue, oldValue) {
this.tween(oldValue, newValue)
}
},
mounted: function () {
this.tween(0, this.value)
},
methods: {
tween: function (startValue, endValue) {
var vm = this
function animate () {
if (TWEEN.update()) {
requestAnimationFrame(animate)
}
}

new TWEEN.Tween({ tweeningValue: startValue })
.to({ tweeningValue: endValue }, 500)
.onUpdate(function () {
vm.tweeningValue = this.tweeningValue.toFixed(0)
})
.start()

animate()
}
}
})

// 所有的复杂度都已经从 Vue 的主实例中移除!
new Vue({
el: '#example-8',
data: {
firstNumber: 20,
secondNumber: 40
},
computed: {
result: function () {
return this.firstNumber + this.secondNumber
}
}
})