生命周期
异步组件
Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义;只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
1
2
3
4
5
6
7
8
9
10
11
12
|
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 Promise 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
});
|
插件系统
插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制:
- 添加全局方法或者属性;
- 添加全局资源:指令/过滤器/过渡等;
- 通过全局混入来添加一些组件选项;
- 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现;
- 提供自己的 API,同时提供上面提到的一个或多个功能。
举个例子:
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
|
Example.install = (Vue, options) => {
Vue.$shareConfig = configObj => {
const { sharetitle, sharedesc, shareimg, shareurl } = configObj;
const shareObj = {
title: sharetitle,
desc: sharedesc,
link: shareurl,
imgUrl: shareimg,
success: function () {},
cancel: function () {}
};
wx.config({
appId: this.$store.state.appId,
debug: false,
jsApiList: [
'onMenuShareTimeline',
'onMenuShareAppMessage',
'onMenuShareQQ',
'onMenuShareWeibo'
],
nonceStr: this.$store.state.nonceStr,
signature: this.$store.state.signature,
timestamp: this.$store.state.timestamp
});
wx.ready(function () {
wx.onMenuShareTimeline(shareObj);
wx.onMenuShareQQ(shareObj);
wx.onMenuShareAppMessage(shareObj);
wx.onMenuShareWeibo(shareObj);
});
};
};
// 使用插件并向插件传入可选选项
Vue.use(Example, {});
|
自定义指令
自定义指令适用于需要对普通 DOM 元素进行底层操作的情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 全局注册
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时
inserted: function (el) {
el.focus(); // 聚焦元素
}
});
// 局部注册
new Vue({
directives: {
focus: {
inserted(el) {
el.focus();
}
}
}
});
|
指令定义对象可以提供如下几个钩子函数:
- bind:只调用一次,指令第一次绑定到元素时调用;在这里可以进行一次性的初始化设置;
- inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中);
- update:所在组件的 VNode 更新时调用,可以通过比较更新前后的值来忽略不必要的模板更新;
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用;
- unbind:只调用一次,指令与元素解绑时调用。
钩子函数参数:
- el:指令所绑定的元素,可以用来直接操作 DOM;
- binding:一个对象,包含以下属性:
- name:指令名,不包括 v- 前缀;
- value:指令的绑定值,例如:v-my=“1 + 1"中,绑定值为 2;
- oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用;
- expression:字符串形式的指令表达式,例如 v-my=“1 + 1"中,表达式为 “1 + 1”;
- arg:传给指令的参数,可选;例如 v-my:foo 中,参数为 “foo”;
- modifiers:一个包含修饰符的对象;例如:v-my.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
- vnode:编译生成的虚拟节点;
- oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
举个例子:
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
|
<div id="hook-arguments" v-demo:foo.a.b="message"></div>
<script>
Vue.directive('demo', {
bind(el, binding, vnode) {
var s = JSON.stringify;
el.innerHTML = `
name: ${s(binding.name)},
value: ${s(binding.value)},
expression: ${s(binding.expression)},
argument: ${s(binding.arg)},
modifiers: ${s(binding.modifiers)},
vnode keys: ${Object.keys(vnode).join(', ')}
`;
}
});
new Vue({
el: '#hook-arguments',
data: {
message: 'hello'
}
});
/**
* name: "demo"
* value: "hello"
* expression: "message"
* argument: "foo"
* modifiers: {"a":true,"b":true}
* vnode keys: tag, data, children, text...
*/
</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
|
<div id="dynamic">
<h3>Scroll down inside this section...</h3>
<p v-pin:[direction]="200">
I am pinned onto the page at 200px to the left...
</p>
</div>
<script>
new Vue({
el: '#dynamic',
data() {
return {
direction: 'left'
};
},
directives: {
pin: {
bind(el, binding, vnode) {
el.style.position = 'fixed';
el.style[binding.arg] = `${binding.value}px`;
}
}
}
});
</script>
|
响应式原理
当把一个普通的 JS 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter;在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更;每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖;之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
Object.freeze 会阻止修改现有的属性,也意味着响应系统无法再追踪变化。
Vue 不能检测以下数组的变动(因为性能问题):
- 利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue;
- 修改数组的长度时,例如:vm.items.length = newLength。
举个例子:
1
2
3
4
5
6
7
8
9
10
|
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
});
vm.items[1] = 'x'; // 不是响应性的
vm.$set(vm.items, indexOfItem, newValue); // 解决问题
vm.items.splice(indexOfItem, 1, newValue); // 解决问题
vm.items.length = 2; // 不是响应性的
vm.items.splice(newLength); // 解决问题
|
Vue 不能检测对象属性的添加或删除:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var vm = new Vue({
data: {
a: 1,
userProfile: {
name: 'Anika'
}
}
});
// 对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性
vm.b = 2; // vm.b 不是响应式的
// 可以向嵌套对象添加响应式属性
vm.$set(vm.userProfile, 'age', 27);
vm.userProfile = Object.assign({}, vm.userProfile, {
favoriteColor: 'Green'
});
|
Vue 在更新 DOM 时是异步执行的;只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更;如果同一个 watcher 被多次触发,只会被推入到队列中一次;然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
可以在数据变化之后立即使用 Vue.nextTick(callback);回调函数将在 DOM 更新完成后被调用;例如:
1
2
3
4
5
6
7
8
9
10
11
|
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
});
vm.message = 'new message'; // 更改数据
vm.$el.textContent === 'new message'; // false
vw.$nextTick(function () {
vm.$el.textContent === 'new message'; // true
});
|
依赖注入
Vue 中也有 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
|
<google-map>
<google-map-region v-bind:shape="cityBoundaries">
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map-region>
</google-map>
<script>
Vue.component('google-map', {
data() {
return {
sigma: 123
};
},
provide() {
return {
sigma: this.sigma,
getMap: this.getMap
};
},
methods: {
getMap() {
// ...
}
}
});
Vue.component('google-map-markers', {
// 可以跨越多个层级拿到值
inject: ['sigma', 'getMap']
});
</script>
|
事件监听器
- 通过 $on(eventName, eventHandler) 侦听一个事件
- 通过 $once(eventName, eventHandler) 一次性侦听一个事件
- 通过 $off(eventName, eventHandler) 停止侦听一个事件
举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
new Vue({
mounted() {
this.picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
});
},
beforeDestroy() {
this.picker.destroy();
}
});
// 可以修改为
new Vue({
mounted() {
var picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
});
this.$once('hook:beforeDestroy', function () {
picker.destroy();
});
}
});
|
混入
混入提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能;一个混入对象可以包含任意组件选项;当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
混入原则:
- 数据对象在内部会进行递归合并,并在发生冲突时以『组件数据优先』;
- 同名钩子函数将合并为一个数组,因此都将被调用;『混入对象』的钩子将在『组件自身』钩子『之前』调用;
- 值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象;『组件对象』优先级较高。
Vue.extend 也使用同样的合并策略。
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
|
var myMixin = {
data() {
return {
message: 'hello',
foo: 'abc'
};
},
methods: {
hi() {
console.log('hi');
},
bye() {
console.log('bye');
}
},
mounted() {
console.log('mixin mounted');
}
};
var vm = new Vue({
mixins: [myMixin],
data() {
return {
message: "it's me",
bar: 'def'
};
},
methods: {
hi() {
console.log('hihihi');
},
do() {
console.log('dododo');
}
},
created() {
console.log(this.$data); // { message: "it's me", foo: "abc", bar: "def" }
},
mounted() {
console.log('component mounted'); // mixin mounted, component mounted
}
});
vm.hi(); // hihihi
vm.bye(); // bye
vm.do(); // dododo
// 全局混入,将影响每一个之后创建的 Vue 实例
Vue.mixin(myMixin);
|
computed vs watch vs methods
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
|
<template>
<div>
<div>{v1()}</div>
<div>{v2}</div>
<div>{v3}</div>
</div>
</template>
<script>
new Vue({
data() {
// data 选项必须是一个函数,每个实例可以维护一份被返回对象的独立的拷贝
return {
value: 123,
v3: 0
};
},
methods: {
// 每当触发重新渲染时,方法将总会再次执行;无论 vm.value 是否变化
v1() {
return `v1: ${this.value}`;
}
},
computed: {
// 计算属性是基于响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值
v2() {
return `v2: ${this.value}`;
}
},
watch: {
// 在相关数据变化时,执行异步或开销较大的副作用有点像 React.useEffect
v3() {
this.v3 = `v3: ${this.value}`;
}
}
});
</script>
|
v-model
v-model 本质上是语法糖;它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理;在内部为不同的输入元素使用不同的属性并抛出不同的事件:
1
2
3
4
5
6
|
<input v-model="searchText" />
等价于
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
/>
|
- text 和 textarea 使用 value 属性和 input 事件;
- checkbox 和 radio 使用 checked 属性和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
v-model 有配套的修饰符:
1
2
3
4
5
6
|
<!-- 使用 change 事件替换 input 事件将输入框的值与数据进行同步 -->
<input v-model.lazy="msg" />
<!-- 将用户的输入值转为数值类型 -->
<input v-model.number="age" type="number" />
<!-- 过滤用户输入的首尾空白字符 -->
<input v-model.trim="msg" />
|
一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件;但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
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>;
|
props
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
|
new Vue({
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
});
new Vue({
// 类型可以是:String、Number、Boolean、Array、Object、Date、Function、Symbol
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise
}
});
new Vue({
props: {
// 基础的类型检查(null 和 undefined 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default() {
return { message: 'hello' };
}
},
// 自定义验证函数
propF: {
validator(value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1;
}
}
}
});
|
key
举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
|
<!--
如果不设置独立的 key 属性,在切换 loginType 时,input 将被复用
用户已经输入的内容将不会被清除,很明显是不合理的
-->
<template v-if="loginType === 'username'">
<label>UserName</label>
<input placeholder="Enter your username" key="username" />
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email" key="email" />
</template>
|
is
is 用来将普通元素或组件指定渲染为指定的组件。
有些 HTML 元素,诸如 <ul>、<ol>、<table> 和 <select>,对于哪些元素可以出现在其内部是有严格限制的;而有些元素,诸如 <li>、<tr> 和 <option>,只能出现在其它某些特定的元素内部。
1
2
3
4
5
6
7
8
9
10
|
<!-- 这个自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并导致最终渲染结果出错 -->
<table>
<blog-post-row></blog-post-row>
</table>
<!-- 可以这样解决 -->
<table>
<tr is="blog-post-row"></tr>
</table>
<!-- 通过将 currentTabComponent 设定为不同的组件来切换渲染结果-->
<component v-bind:is="currentTabComponent"></component>
|
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
|
<!-- <base-layout> 组件 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot>后备用内容,name="default"</slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<!-- 这样使用 -->
<base-layout>
<template v-slot:header>
<h1>header 内容</h1>
</template>
<p>这两行会被插入到 main 标签</p>
<p>这两行会被插入到 main 标签</p>
<template v-slot:footer>
<p>footer 内容</p>
</template>
</base-layout>
<!-- 缩写 -->
<base-layout>
<template #header>
<h1>header 内容</h1>
</template>
<p>这两行会被插入到 main 标签</p>
<p>这两行会被插入到 main 标签</p>
<template #footer>
<p>footer 内容</p>
</template>
</base-layout>
|
作用域插槽(让插槽内容能够访问子组件中才有的数据):
1
2
3
4
5
6
7
8
9
10
|
<!-- <current-user> 组件 -->
<span>
<!-- user 存在于 current-user 组件内作用域中-->
<slot>{{ user.lastName }}</slot>
</span>
<!-- 在父组件中 -->
<current-user>
<!-- 取不到 current-user 组件内作用域中的 user-->
{{ user.firstName }}
</current-user>
|
为了让 user 在父级的插槽内容中可用,可以将 user 作为 <slot> 元素的一个属性绑定上去:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<!-- <current-user> 组件 -->
<span>
<slot v-bind:user="user">{{ user.lastName }}</slot>
</span>
<!-- 在父组件中 -->
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
<template v-slot:one="oneSlotProps">
slotProps is NOT available here
</template>
</template>
<template v-slot:two="twoSlotProps">slotProps is NOT available here</template>
</current-user>
<!-- 也可以酱 -->
<current-user v-slot="{ user }">{{ user.firstName }}</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
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
<script>
new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
],
scrollBehavior(to, from, savedPosition) {
// return 期望滚动到哪个的位置
if (savedPosition) {
return savedPosition;
} else {
return { x: 0, y: 0 };
}
}
});
</script>
|
路由组件传参
有一个 User 组件,对于所有 ID 不同的用户,都要使用这个组件来渲染;通常的做法是“动态路由匹配”或者“Query 参数”,在组件中使用 vm.$route 来获取参数;在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。
组件与 $route 的耦合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<script>
new Router({
routes: [
{
path: '/user/:id',
name: 'User',
component: User
}
]
});
</script>
<template>
<div class="user">
<div>userId:{{$route.params.id}}</div>
</div>
</template>
|
通过 props 与组件解耦:
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
|
<script>
// 布尔模式
new Router({
routes: [
{
path: '/user/:id',
name: 'User',
component: User,
props: true
}
]
});
// 对象模式
new Router({
routes: [
{
path: '/user',
name: 'User',
component: User,
props: { id: 1 }
}
]
});
// 函数模式
new Router({
routes: [
{
path: '/user',
name: 'User',
component: User,
// /user?id=123 会将 {query: '123'} 作为属性传递给 user 组件
props: route => ({ query: route.query.id })
}
]
});
</script>
<template>
<div class="user">
<div>userId:{{id}}</div>
</div>
</template>
<script>
export default {
props: ['id']
};
</script>
|
路由钩子
参数或查询的改变并不会触发进入/离开的导航守卫;可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。
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
|
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
// 路由独享守卫
beforeEnter: (to, from, next) => {
/**
* to:Route 即将要进入的目标路由
* from:Route 当前导航正要离开的路由
* next:Function 调用该方法来 resolve 这个钩子
* next()
* 进行管道中的下一个钩子
* next(false)
* 中断当前的导航
* next('/') 或者 next({ path: '/' })
* 当前的导航被中断,然后进行一个新的导航;可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: 'home' 之类的选项
* next(error)
* 传入 Error 实例,导航会被终止且该错误会被传递给 router.onError
*/
}
}
]
});
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 参数含义同上
});
// 全局解析守卫
router.beforeResolve((to, from, next) => {
// 参数含义同上,所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用
});
// 全局后置钩子
router.afterEach((to, from) => {
// 参数含义同上
});
new Vue({
// 用来在导航完成前获取数据
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 this
// 守卫执行前,组件实例还没被创建
next(vm => {
// 通过 vm 访问实例
});
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 组件实例会被复用;而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 this
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 this
let isBye = window.confirm();
if (isBye) {
next();
} else {
next(false);
}
}
});
|
完整的导航解析流程:
- 导航被触发;
- 在失活的组件里调用 beforeRouteLeave;
- 调用全局的 beforeEach;
- 在重用的组件里调用 beforeRouteUpdate;
- 在路由配置里调用 beforeEnter;
- 解析异步路由组件;
- 在被激活的组件里调用 beforeRouteEnter;
- 调用全局的 beforeResolve;
- 导航被确认;
- 调用全局的 afterEach;
- 触发 DOM 更新;
- 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
路由元信息
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
|
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
meta: { requiresAuth: true }
}
]
}
]
});
router.beforeEach((to, from, next) => {
if (to.matched.some(v => v.meta.requiresAuth)) {
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
});
} else {
next();
}
} else {
next();
}
});
|