创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<!-- 引入 vue -->
<script src="../js/vue.js"></script>
</head>
<body>
<div id="root">
<h1>年龄:{{age}} 姓名:{{name}} 时间戳 {{Date.now()}}</h1>
</div>
</body>
<script>
const app = new Vue({
el: "#root", // 指定当前 vue 实例为哪个容器服务
data: {
age: 18,
name: "scarf",
},
});
</script>
</html>

vue 实例与容器为一对一关系,一个容器只能被一个 vue 实例管理,一个 vue 实例只能管理一个容器。

可使用以下方式动态指定服务的容器:

1
2
3
4
5
6
7
8
const vm = new Vue({
data: {
age: 18,
name: "scarf",
},
});

vm.$mount('#root'); // 指定为哪个容器服务,可实现所服务容器的切换

data 也可函数式定义 (vue 组件中必须使用这种写法):

1
2
3
4
5
6
7
8
9
10
11
data: function () {
return {
name: "scarf"
};
},
// 简写
data() {
return {
name: "scarf"
};
},

基础语法

  1. 插值语法

    1
    <p>{{name}}</p>
  2. 指令语法

    1
    2
    3
    <a v-bind:href="url">点击跳转</a>
    <!-- 简写 -->
    <a :href="url">点击跳转</a>
  3. 数据绑定

    1
    2
    3
    4
    5
    6
    <!-- 单向数据绑定,输入框内容的变化不会影响到 data 中的 name -->
    <input type="text" :value="name" />
    <!-- 双向数据绑定,只能应用在表单类元素上 -->
    <input type="text" v-model:value="name" />
    <!-- 简写 -->
    <input type="text" v-model="name" />

不止 data 中的数据,Vue 实例本身具有的属性也可以进行绑定:

image-20240608201053592

1
2
<p>{{$el}}</p>
<input type="text" :value="$el" />

数据代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let person = {
name: "围巾",
};

// 为 person 对象添加 age 属性
Object.defineProperty(person, "age", {
value: 18,
// enumerable: true, // 控制属性是否可枚举(遍历),默认 false
// writable: true, // 控制属性是否可写(可改),默认 false
// configurable: true, // 控制属性是否可以被删除,默认 false
});
console.log(person);

// 输出 person 对象所有属性
console.log(Object.keys(person));

for (let key in person) {
console.log(key);
}

image-20240609015117405

通常情况下一段 js 代码执行后对象中的属性值就已经确定下来,不会随着另一个数据的改变而改变,如下:

1
2
3
let number = 10;
let age = number;
// 执行后 number 和 age 的值都是 10,之后对 number 的修改并不会影响到 age。

Object.defineProperty 提供了一种方式来让 agenumber 能够互相感知对方的变化,Vue 就使用了这一方式实现了数据的实时渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let number = 10;
let person = {
name: "围巾",
};

Object.defineProperty(person, "age", {
// 每次获取 age 值时,都会调用该方法
get() {
return number;
},
// 每次修改 age 时,调用该方法
set(value) {
number = value;
},
});

image-20240609021539926

writable 纵使没有设置为 true 也可以进行修改,因为修改的实际对象是 number 而不是 age

image-20240609143208751

简要实现:

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
<script>
// 构造函数
function Vue(data) {
this._data = data;

const keys = Object.keys(this._data);
keys.forEach((key) => {
// 此处 this 指 vue 对象,为 vue 对象生成 data 中数据的代理
Object.defineProperty(this, key, {
get() {
return this._data[key];
},
set(val) {
this._data[key] = val;
},
});
});
}

let data = {
name: "scarf",
age: 12,
};

// 创建 vm 实例
let vm = new Vue(data);
</script>

image-20240615171812109

问题:

1)对 data 的代理是在创建 vm 实例时就完成的,如果 vm 实例化后再对 data 中的属性(包括嵌套的属性)进行变更,这些属性将不会被 vue 所管理。

image-20240615173541923

使用 Vue.set(target,key,val) 方法解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="root">
<button @click="addProperty">添加属性</button>
</div>
</body>
<script>
let vm = new Vue({
el: "#root",
data: {
person: {
name: "scarf",
},
},
methods: {
addProperty() {
// this.$set(this.$data, "no", 66); // 不允许变更根数据对象中的属性
this.$set(this.$data.person, "age", 12);
},
},
});
</script>

综上,data 中的各属性最好在 vue 实例创建前就确定好,特别是根数据对象中的属性。

2)对于值是数组的属性,当通过索引对数组中元素值进行修改时,vue 并不会监测到数组的改变,也就是页面不会立刻重新渲染,虽然值已经改变。如果想让 vue 监视到数组的改变,需要使用对数组操作相关的函数,如 pushpopshift 等(vue 对这些方法进行了重写)。

如果数组元素是对象,通过索引操作可以被 vue 监视到。

1
2
3
4
5
hobby: [{ age: 1 }, { age: 2 }, { age: 3 }]
// 可以被监视到
this.hobby[0].age = 6;
// 不会被监视到
this.hobby[0] = { age: 7 };

当使用 obj = {a:1, b:2} 方式替换对象或者通过 shift 等方法向数组添加对象,并且对象中包含了原本不存在的属性,后续这些属性的变化是可以被监视到的。

事件处理

  1. 点击事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <body>
    <div id="root">
    <button v-on:click="showInfo1">展示信息</button>
    <!-- 简写 -->
    <button @click="showInfo2(66, $event)">展示信息</button>
    <!-- 阻止默认行为 -->
    <a href="http://www.baidu.com" @click.prevent="showInfo1">点击跳转</a>
    </div>
    </body>
    <script>
    const vm = new Vue({
    el: "#root",
    methods: {
    showInfo1(event) {
    alert("你好");
    },
    showInfo2(number, event) {
    alert(number);
    },
    },
    });
    </script>
  2. 事件修饰符

    • prevent:组织默认事件
    • stop:阻止事件冒泡
    • once:事件只触发一次
    • capture:使用事件的捕获模式
    • self:只有 event.target 是当前操作的元素才触发事件
    • passive:事件的默认行为立即执行,无需等待事件回调执行完毕,并非所有都生效
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <a href="http://www.baidu.com" @click.prevent="showInfo1">点击跳转</a>
    <!-- 阻止事件冒泡,点击按钮会阻止外层元素的点击事件 -->
    <div @click="showInfo1">
    <button @click.stop="showInfo1">提示信息</button>
    </div>
    <button @click.once="showInfo1">提示信息</button>
    <!-- 点击按钮会先执行内层事件再去执行外层事件,事件捕获可以将执行顺序改为由外向里 -->
    <div @click.capture="showInfo2(1)">
    <button @click="showInfo2(2)">提示信息</button>
    </div>
    <!-- 点击按钮不会触发 div 定义的事件,因为触发者不是自身 -->
    <div @click.self="showInfo1">
    <button @click="showInfo1">提示信息</button>
    </div>

    修饰符之间允许连写:

    1
    2
    3
    <div @click="showInfo">
    <a href="http://www.baidu.com" @click.prevent.stop="showInfo1">点击跳转</a>
    </div>
  3. 键盘事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <body>
    <div id="root">
    <!-- 按键点击触发事件(此处指定回车键) -->
    <input type="text" placeholder="按下回车提示" @keyup.enter="showInfo" />
    </div>
    </body>
    <script>
    const vm = new Vue({
    el: "#root",
    methods: {
    showInfo(event) {
    console.log(event.target.value);
    },
    },
    });
    </script>

    vue 默认提供的按键别名:enterdeleteescspacetabupdownleftright。对于 vue 未提供的别名可使用按键原始 keykeyCode (不推荐)进行绑定,具体可以通过 event.key/event.keyCode 进行查看:

    image-20240609162013927

    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 多个单词组成的 key 需要使用以下格式 -->
    <input type="text" placeholder="按下中英文切换键提示" @keyup.caps-lock="showInfo" />
    <!-- tab 键按下时光标会从当前元素切走,需使用 keydown -->
    <input type="text" placeholder="按下 tab 键提示" @keydown.tab="showInfo" />
    <!-- 系统修饰键 -->
    <input type="text" placeholder="按下 command 键提示" @keydown.Meta="showInfo" />
    <!-- 连用 -->
    <input type="text" placeholder="按下 Command+e 提示" @keydown.Meta.e="showInfo" />

可以直接将 @XXX="method" 中的方法替换成具体的语句,点击时会自动执行。但是需注意,语句中使用的属性或方法必须是被 vm 所管理的。

计算属性

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
<body>
<div id="root">
name: <input type="text" v-model="name" />
age: <input type="text" v-model="age" /><br />
<!-- 当 data 中数据变动,vue 会重新解析页面,也就是 info 方法会被再次调用 -->
info: {{getInfo()}} <br />
<!-- 计算属性 -->
info: {{info}} info2: {{info2}}
<input type="text" v-model="info" /><br />
</div>
</body>
<script>
const vm = new Vue({
el: "#root",
data: {
name: "scarf",
age: 18,
},
methods: {
getInfo() {
return this.name + "-" + this.age;
},
},
computed: {
info: {
get() {
return this.name + "-" + this.age;
},
// 非必须,如果 info 可能被修改
set(value) {
const arr = value.split("-");
this.name = arr[0];
this.age = arr[1];
},
},
// 如果不进行修改,可如下简写
info2() {
return this.name + "-" + this.age;
},
},
});
</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
39
40
41
42
43
44
<script>
const vm = new Vue({
el: "#root",
data: {
isHot: true,
numbers: {
a: 1,
b: 2,
},
},
watch: {
isHot: {
// immediate: true, // 立即执行一次,默认 false
handler(newVal, oldVal) { // 可以省略 oldVal
console.log(newVal, oldVal);
},
},
// 不需要配置 immediate、deep 时可进行简写
// isHot(newVal, oldVal) {},
// 监视对象中的某个属性
"numbers.a": {
handler(newVal, oldVal) {
console.log(newVal, oldVal);
},
},
// 深度监视整个对象,若不使用深度监视,只有整个对象被替换掉才会被监视到 eg: numbers={}
numbers: {
deep: true,
handler(newVal, oldVal) {
// 除非替换掉整个对象,否则这里的 newVal 和 oldVal 中的属性值都是最新的
// 因为它们所指向的对象并没有变
console.log(newVal, oldVal);
},
},
},
});

// 另一种写法
vm.$watch("isHot", {
handler(newVal, oldVal) {
console.log(newVal, oldVal);
},
});
</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
39
<style>...</style>
<body>
<!-- class 属性不能有两个 -->
<div class="basic bg1"></div>
<!-- 动态绑定,最终会合并成一个 -->
<div id="root">
<!-- 直接绑定 -->
<div class="basic" :class="bg"></div>
<!-- 数组绑定 -->
<div class="basic" :class="bgArr"></div>
<!-- 对象绑定 -->
<div class="basic" :class="bgObj"></div>
<!-- 绑定 style -->
<div class="basic" :style="{fontSize: size + 'px'}">hello</div>
<div class="basic" :style="styleObj">hello</div>
<div class="basic" :style="[styleObj, styleObj2]">hello</div>
</div>
</body>
<script>
const vm = new Vue({
el: "#root",
data: {
bg: "bg1",
bgArr: ["bg1", "bg2"],
bgObj: {
bg1: false,
bg2: true,
},
size: 40,
styleObj: {
fontSize: "20px",
color: "red",
},
styleObj2: {
backgroundColor: "orange",
},
},
});
</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
<body>
<div id="root">
<!-- 为 false 结构还在,只是隐藏 display: none -->
<p v-show="isShow">hello</p>
<!-- 为 false 整个结构被去除 -->
<p v-if="isShow">hello</p>

<p v-if="1 === 3">3</p>
<!-- 中间不能被打断 -->
<!-- <p></p> -->
<p v-else-if="1 === 3">2</p>
<p v-else>1</p>

<!--
template 不会影响层级结构,页面中不会存在该标签。跟 v-show 配合无效
-->
<template v-if="true">
<p>1</p>
<p>2</p>
<p>3</p>
</template>
</div>
</body>
<script>
new Vue({
el: "#root",
data: {
isShow: false,
},
});
</script>

列表渲染

1
2
3
4
5
6
7
8
9
10
<!-- 遍历数组 -->
<li v-for="person in persons" :key="person.id">{{person.name}}</li>
<!-- index 从零开始 -->
<li v-for="(person, index) in persons" :key="index">{{person.name}}</li>
<!-- 遍历对象 -->
<li v-for="(val,key) in persons[0]" :key="key">{{val}}</li>
<!-- 遍历字符串 -->
<li v-for="(char,index) in '123'" :key="index">{{char}}</li>
<!-- 遍历次数 -->
<li v-for="(num,index) in 5" :key="index">{{num}}</li>

最好不要使用 index 作为 key,如果后续向遍历对象非尾部新增一个数据,页面重新渲染后那些后移的元素 index 值将改变。而且由于对比算法,使用 index 效率低下。

表单收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<form>
<!-- 去掉前后空格 -->
账号:<input type="text" v-model.trim="account" /><br />
密码:<input type="password" v-model="password" /><br />
<!-- 让收集到的数据为数值而不是字符串 -->
年龄:<input type="number" v-model.number="age" /><br />
性别:
<input type="radio" name="sex" value="male" v-model="sex" />
<input type="radio" name="sex" value="female" v-model="sex" /><br />
爱好:
<!-- 如果不指定 value 默认收集 checked 属性 -->
吃饭<input type="checkbox" value="eat" v-model="hobby" />
睡觉<input type="checkbox" value="sleep" v-model="hobby" /><br />
城市:
<select v-model="city">
<option value="" selected disabled>选择地区</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
</select>
<br />
其他信息:
<!-- 失去焦点后再对数据进行收集 -->
<textarea v-model.lazy="other"></textarea><br />
</form>

过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body>
<div id="root">
<!-- 多个过滤器使用 | 隔开 -->
<p>{{ date | addMonth | addDay(1) }}</p>
<!-- 也可以应用于 v-bind -->
<input :value="date | addMonth | addDay(1)" />
</div>
</body>
<script>
new Vue({
el: "#root",
data: {
date: 2024,
},
filters: {
addMonth(date) {
return date + "-07";
},
addDay(date, day) {
return date + "-0" + day;
},
},
});
</script>

全局:

1
2
3
4
5
6
Vue.filter("addMonth", function (date) {
return date + "-07";
});
Vue.filter("addDay", function (date, day) {
return date + "-0" + day;
});

vue3 已不支持过滤器。

其他指令

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
<head>
<style>
/* 当 vue 未介入时隐藏 */
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="root">
<!-- 将数据当成文本 -->
<p v-text="name"></p>
<!-- 支持解析标签,存在安全性问题 -->
<p v-html="name"></p>
<!-- 只要 vue 一介入,v-cloak 属性就会被删除 -->
<p v-cloak>{{name}}</p>
<!-- 只渲染一次,后续视为静态内容,不再受 name 影响 -->
<p v-once>{{name}}</p>
<!-- 跳过对节点的解析,通常用于没有使用 vue 的节点,加快解析速度 -->
<p v-pre>111</p>
<p v-pre>{{name}}</p>
</div>
<!-- 若此处因网络导致 vue 引入延迟,上方未解析的模板将直接展示在页面中 -->
<script type="text/javascript" src="../js/vue.js"></script>
</body>

自定义指令

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
<body>
<div id="root">
<span v-hello="name"></span>
<input type="text" v-fbind="name" />
</div>
</body>
<script>
new Vue({
el: "#root",
data: {
name: "scarf",
},
directives: {
// 函数式
hello(element, binding) {
element.innerText = "hello " + binding.value;
},
// 对象式
fbind: {
// 指令与元素绑定时调用
bind(element, binding) {
element.value = binding.value;
},
// 元素被插入页面后调用
inserted(element, binding) {
// 获取焦点,此方法只有元素被插入页面后才有效
element.focus();
},
// 后续重新渲染时调用
update(element, binding) {
element.value = binding.value;
},
},
},
});
</script>

指令相关的回调函数中的 thiswindow 而不是 vm 实例。

生命周期

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
<script>
new Vue({
el: "#root",
// 还未对数据进行代理、监测,无法访问到 data、methods
beforeCreate() {
console.log("beforeCreate");
},
// 完成数据代理、数据监测,此时页面还未解析
created() {
console.log("created");
},
// 完成模板解析,但元素此时还是虚拟 dom (内存),未放入页面
beforeMount() {
console.log("beforeMount");
},
// 完成模板解析并将元素放入页面后调用
mounted() {
console.log("mounted");
},
// 此时数据是新的,但还未渲染到页面
beforeUpdate() {
console.log("beforeUpdate");
},
// 重新渲染后调用
updated() {
console.log("updated");
},
// vm 销毁前执行,这个阶段对数据的修改不会再触发更新
beforeDestroy() {
console.log("beforeDestroy");
},
destroyed() {
console.log("destroyed");
},
});
</script>

生命周期钩子中的 this 都是 vm 实例。

组件

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
<body>
<div id="root">
<nation></nation>
<!-- 引入多个 student 组件,它们各自是一个 vc (VueComponent) 实例,也就是 this 作用域都是自身 -->
<!-- 虽然 vc 与 vm 不同,但是 vc 可以通过原型链到达 vm 的原型 Vue,也就是 Vue 中的一些全局配置对 vc 来说是可见的 -->
<student></student>
<student />
</div>
</body>
<script>
// 全局组件
const nation = Vue.extend({
template: `
<div>{{name}}</div>
`,
data() {
// 组件可能会被用到多个地方,每个地方都需要维护一个新的 data 对象,而不是共用
return {
name: "China",
};
},
});
Vue.component("nation", nation);
// 局部组件
const student = Vue.extend({
name: "Scarf", // name 可以指定开发者工具组件名称显示
template: `
<div>{{name}}</div>
`,
data() {
return {
name: "scarf",
};
},
});

new Vue({
el: "#root",
components: {
student,
},
});
</script>

ref

获取 dom 元素:

1
<p ref="age">{{ age }}</p>
1
2
const el = this.$refs.age;
console.log(el.innerText);

获取 vc 实例:

1
2
<!-- id 属性实际上加在子组件的根标签上 -->
<Index ref="index" id="index" />
1
2
3
4
5
// 相当于子组件中的 this
const vc = this.$refs.index;
console.log(vc);
// 输出的是子组件中的根元素
console.log(document.getElementById("index"));

props

1
2
<!-- 可使用 v-bind 绑定,也可直接写死 -->
<Index :age="age" sex="男" />
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
// props: ["name", "age", "sex"],

// props: {
// name: String,
// age: Number,
// sex: String,
// },

props: {
name: {
type: String,
required: false,
default: "scarf",
},
age: {
type: Number,
required: true,
},
sex: {
type: String,
required: true,
},
obj: {
type: Object,
required: true,
}
}
data() {
return {
myName: this.name,
myObj: obj
};
},

子组件不能直接修改 props 中的数据,可在 data 中定义不同名的属性解决。如果接收的是一个对象,myObj 中的属性修改会连带父组件中的对象一起修改,除非以 myObj = {} 形式进行修改。

mixin

代码复用

1
2
3
4
5
6
7
export const mixin = {
methods: {
showName() {
alert(this.name);
},
},
};

image-20240616220635649

不仅能混入 methods,只要与 data 同级的都可以混入,包括生命周期钩子等,并且会自动进行合并。

如果混入冲突,以实际定义的为主。特别的,生命周期钩子混入冲突时,混入以及自定义的代码都会执行。

插件

1
2
3
4
5
6
7
8
export default {
install(Vue) {
// 定义过滤器
Vue.filter("sayHello", function (name) {
return "hello " + name;
});
},
};
1
2
3
import plugin from "./plugins/plugin";
// 应用插件,会自动调用 install 方法
Vue.use(plugin);
1
<p>{{ name | sayHello }}</p>

scoped

所有组件的 style 样式最终都会汇总到一起,使用 scoped 将作用域限制在自身。

不会影响到父组件,但子组件会被影响。

1
2
3
4
5
<style scoped>
.bg {
background-color: pink;
}
</style>

组件通信

函数传递

  1. 父组件定义函数

    1
    2
    3
    methods: {
    receive(data) {}
    }
  2. 将函数传给子组件

    1
    2
    <!-- 将 receive 方法传递给子组件 -->
    <Child :receive="receive" />
  3. 子组件接收函数

    1
    props: ["receive"]
  4. 子组件传递数据

    1
    this.receive(data);

自定义事件

  1. 父组件为子组件绑定自定义事件

    1
    <Student ref="student" @demo="printName" />
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 动态绑定
    mounted() {
    this.$refs.student.$on("demo2", this.printName);
    this.$refs.student.$on("demo3", function(name){
    // this 为子组件实例
    });
    this.$refs.student.$on("demo4", (name)=>{
    // this 为父组件实例
    });
    },
    methods: {
    printName(name) {
    // this 为父组件实例
    },
    },

    绑定后,自定义事件会存在于子组件实例 vc 上。

    对于自定义事件也可以使用事件修饰符。eg:@demo.once

  2. 子组件触发自定义事件

    1
    2
    this.$emit("demo", this.name);
    this.$emit("demo2", this.name);
  3. 子组件解绑自定义事件

    1
    2
    3
    4
    5
    this.$off("demo");
    // 同时解绑多个
    this.$off(["demo", "demo2"]);
    // 解绑所有自定义事件
    this.$off();

eg:

1
<Student @click="printName" />

默认为子组件绑定 click 自定义事件,而不是原生 dom 事件。如下方式解决:

1
<Student @click.native="printName" />

全局事件总线

实现原理就是在 Vue 的原型对象上添加一个 vm 实例。

1
2
3
4
5
6
new Vue({
render: (h) => h(App),
beforeCreate() {
+ Vue.prototype.$bus = this; // $bus 名称随意
},
}).$mount("#app");

之后所有组件都能都过 this.$bus 逐层向上找到所添加的 vm

由于 vm 本身具有 $on/$emit 通信基础,即各组件都能通过 this.$bus.$on/this.$bus.$emitvm 实例中绑定和触发自定义事件。

特别的,绑定的全局事件必须进行解绑:

1
2
3
beforeDestroy() {
this.$bus.$off("event");
},

消息订阅与发布

第三方库,任何前端框架都能使用。

  1. 安装

    1
    npm i pubsub-js
  2. 引入

    1
    import pubsub from "pubsub-js";
  3. 订阅消息

    1
    2
    3
    this.subId = pubsub.subscribe("demo", (msgName, data) => {
    console.log(data);
    });
  4. 发布消息

    1
    pubsub.publish("demo", "围巾");
  5. 取消订阅

    1
    pubsub.unsubscribe(this.subId);

浏览器存储

LocalStorage、SessionStorage

四个 API:getItemsetItemremoveItemclear

$nextTick

1
<input v-if="isShow" type="text" ref="input" />
1
2
3
4
handle(){
this.isShow = true;
this.$refs.input.focus();
}

方法执行后输入框并没有自动获取焦点,原因是 isShow 改变后,vue 并没有立刻重新解析模板。出于效率问题,当方法执行完后才会对模板重新进行解析。

解决:

1
2
3
4
5
6
7
handle(){
this.isShow = true;
// nextTick 指定的回调会在下次页面渲染后执行
this.$nextTick(function () {
this.$refs.input.focus();
});
}

动画效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<button @click="isShow = !isShow">显示/隐藏</button>
<!-- apper 表示动画立即生效 -->
<transition name="hello" appear>
<h2 v-show="isShow">hello</h2>
</transition>

<style scoped>
/* 自定义关键帧 */
@keyframes demo {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0px);
}
}
/* 如果 transition 标签没有指定 name 属性,默认 v-xxx */
.hello-enter-active {
animation: demo 1s;
}
.hello-leave-active {
animation: demo 1s reverse;
}
</style>

transition 标签会自动判断元素状态并将对应的样式绑定到元素上,v-show 非必须。

样式可用如下写法替换:

1
2
3
4
5
6
7
8
9
10
11
12
.hello-enter-active,
.hello-leave-active {
transition: 1s;
}
.hello-enter,
.hello-leave-to {
transform: translateX(-100%);
}
.hello-enter-to,
.hello-leave {
transform: translateX(0px);
}
  • hello-enter/leave-active:整个进入/离开过程一直生效
  • hello-enter:起点样式
  • hello-enter-to:终点样式

同时绑定多个元素:

1
2
3
4
5
<!-- transition-group 绑定多个元素,各元素需要设置 key -->
<transition-group name="hello2" appear>
<h2 v-show="isShow" key="1">hello2</h2>
<h2 v-show="isShow" key="2">hello2</h2>
</transition-group>

第三方库:

1
npm i animate.css
1
import "animate.css";
1
2
3
4
5
6
7
8
<transition
name="animate__animated animate__bounce"
enter-active-class="animate__zoomInDown"
leave-active-class="animate__zoomOutDown"
appear
>
<h2 v-show="isShow">hello3</h2>
</transition>

配置代理

由于浏览器同源策略,需要解决跨域问题。

原理:请求先转发给代理服务器(地址跟前端一致),代理服务器再去请求后端资源并把结果转发给前端。代理服务器与后端的通信不经过浏览器,所以不受同源策略影响。

注意:代理服务器只适用于开发环境,打包后就不存在了。

1
2
3
4
5
6
module.exports = defineConfig({
// 配置代理服务器
devServer: {
proxy: "http://localhost:8888", // 后端地址
},
});
1
npm i axios
1
import axios from "axios";
1
2
3
4
5
6
7
8
9
// 会先在 public 文件夹中找 /api/user/1 资源,如果没有才会通过代理请求后端
axios.get("http://localhost:8080/api/user/1").then(
(response) => {
console.log(response.data);
},
(error) => {
console.log(error.response.data.message);
}
);

改进:

1
2
3
4
5
6
7
8
9
10
11
devServer: {
proxy: {
// 若当前 uri 前缀匹配,直接走代理
"/api": {
target: "http://localhost:8888",
// pathRewrite: {
// "^/api": "", // 将 /api 替换掉
// },
},
},
},

vue-resource

1
npm i vue-resource
1
2
import vueResource from "vue-resource"
Vue.use(vueResource);

之后 vm/vc 实例上会多一个 $http,用法与 axios 类似。

插槽

默认插槽

  1. 子组件定义插槽

    1
    <slot>默认显示的结构</slot>
  2. 父组件传递结构

    1
    2
    3
    4
    5
    6
    <Category>
    <img src="https://xxx.jpg" alt="" />
    </Category>
    <Category>
    <p>hello</p>
    </Category>

具名插槽

1
2
3
<slot name="slot1">111</slot>
<slot name="slot2">222</slot>
<slot name="slot3">333</slot>
1
2
3
4
5
6
7
8
9
10
11
<Category>
<img slot="slot1" src="https://s3.ax1x.com/2021/01/16/srJlq0.jpg" />
<!-- 使用 template 避免每个元素都写 slot 属性且不改变原有层级 -->
<template slot="slot2">
<a href="#">跳转</a><a href="#">跳转</a>
</template>
<!-- 专属 template 的另一种写法 -->
<template v-slot:slot3>
<p>你好</p>
</template>
</Category>

作用域插槽

允许子组件通过插槽传递数据给父组件。

1
<slot name="slot4" :foods="foods">444</slot>
1
2
3
4
5
6
7
8
<Category>
<!-- 必须使用 tempalte 标签包裹 -->
<template slot="slot4" slot-scope="data">
<ul>
<li v-for="(food, index) in data.foods" :key="index">{{ food }}</li>
</ul>
</template>
</Category>

Vuex

基本使用

统一管理多个组件共享的数据。适用于任意组件间通信。

image-20240630002614615

1
2
# vue3 使用 vuex@4
npm i vuex@3
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
import Vue from "vue";
import Vuex from "vuex";

const state = {
num: 0,
};

const mutations = {
INCR(state, value) {
state.num += value;
},
};

const actions = {
incrment(context, value) {
context.commit("INCR", value);
},
};

const getters = {
doubleNum(state) {
return state.num * 2;
},
};

Vue.use(Vuex); // 必须在创建 Store 前使用插件
export default new Vuex.Store({
state,
mutations,
actions,
getters,
});

actions 中的方法参数 context 内容如下,可对 store 中定义的各层级进行操作。

image-20240629132911472

1
2
3
4
5
6
+import store from "./store/index.js";

new Vue({
render: (h) => h(App),
+ store,
}).$mount("#app");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<!-- 当 state 中的值改变会自动重新渲染 -->
<h2>求和结果: {{ $store.state.num }}</h2>
<h2>结果翻倍: {{ $store.getters.doubleNum }}</h2>
<button @click="incr">自增</button>
</div>
</template>

<script>
export default {
name: "Vuex",
methods: {
incr() {
this.$store.dispatch("incrment", 1);
},
},
};
</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
<template>
<div>
<h2>求和结果: {{ num }}</h2>
<h2>结果翻倍: {{ doubleNum }}</h2>
<button @click="incrment(1)">自增</button>
</div>
</template>

<script>
import { mapState, mapGetters, mapActions, mapMutations } from "vuex";
export default {
name: "Vuex",
computed: {
// 相当于 num(){ return this.$store.state.num; }
// ...mapState({ num: "num" }), // 写法一, 方法名可自定义
...mapState(["num"]), // 写法二, 名称一致

...mapGetters(["doubleNum"]),
},
methods: {
// 相当于 incrment(value){ return this.$store.dispatch("incrment", value); }
...mapActions(["incrment"]),

// mapMutations 略
},
};
</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
import Vue from "vue";
import Vuex from "vuex";

const a = {
namespaced: true, // 默认 false
state: {},
mutations: {},
actions: {},
getters: {},
};

const b = {
namespaced: true,
state: {},
mutations: {},
actions: {},
getters: {},
};

Vue.use(Vuex);
export default new Vuex.Store({
modules: {
a,
b,
},
});
1
2
3
4
5
6
7
// 对应的改变
this.$store.state.a.num
this.$store.getters["a/doubleNum"]
this.$store.dispatch("a/incrment", value)
...mapState(["a", "b"]) ==> {{ a.num }}
...mapState("a", ["num"]) ==> {{ num }} // 当 namespaced: true 时此写法才可用
// 其余同理

路由

基本使用

1
2
# vue3 使用 vue-router@4
npm i vue-router@3
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
import VueRouter from "vue-router";
import About from "@/pages/About";
import Home from "@/pages/Home";
import Message from "@/pages/Message";

export default new VueRouter({
routes: [
{
name: "about", // 给路由命名
path: "/about",
component: About,
},
{
name: "home",
path: "/home/:age?/:title?", // params 传参,? 表示可传可不传
component: Home,
children: [
{
path: "message", // 子路由无需加 /
component: Message,
},
],
},
],
});
1
2
3
4
5
6
7
8
9
10
import Vue from "vue";
+import VueRouter from "vue-router";
+import router from "./router/index.js";

Vue.use(VueRouter);

new Vue({
render: (h) => h(App),
+ router,
}).$mount("#app");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="route">
<ul>
<li>
<!-- 类似 a 标签 -->
<router-link to="/about">About</router-link>
</li>
<li>
<router-link to="/home">Home</router-link>
</li>
</ul>
<div>
<!-- 展示区 -->
<router-view></router-view>
</div>
</div>
  • 当展示区中的组件被替换,默认会销毁原来的组件。
  • 组件实例身上会多出 $router$route 属性。

路由传参

query:

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-link to="/about?title=关于">About</router-link>
<router-link :to="`/about?title=${title}`">About</router-link>

<!-- 对象写法 -->
<router-link
:to="{
path: '/about',
query: {
title,
},
}"
>
About
</router-link>

<router-link
:to="{
name: 'about',
query: {
title,
},
}"
>
About
</router-link>
1
this.$route.query.title

params:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<router-link to="/home/66/主页">Home</router-link>
<router-link :to="`/home/${age}/${title}`">Home</router-link>

<router-link
:to="{
name: 'home', // 只能使用 name,不能使用 path
params: {
age,
title,
},
}"
>
Home
</router-link>
1
this.$route.params.xxx

props:

1
2
3
4
5
6
7
{
name: "about",
path: "/about",
component: About,
// 以 props 形式传递给 About 组件,传递的是死数据
props: { age: 66, name: "scarf" },
},
1
2
3
4
5
6
7
8
9
10
11
{
name: "about",
path: "/about",
component: About,
// 函数写法
props($route) {
return {
title: $route.query.title,
};
},
},
1
2
3
4
5
6
{
name: "home",
path: "/home/:age?/:title?",
component: Home,
props: true, // 若为 true,将 params 参数以 props 的形式传递给 Home 组件
},

meta:

1
2
3
4
5
6
{
name: "about",
path: "/about",
component: About,
meta: { sex: "male" }, // $route.meta.sex
},

历史记录

模式:

  • push (默认):使用栈存储地址的变化记录
  • replace:新地址会覆盖栈中上一条地址记录
1
<router-link replace to="/about"></router-link>

编程式路由

示例:

1
2
3
4
5
6
this.$router.push({
path: "/about",
});

this.$router.replace("/home");
// 对象写法规则与上述一致

$router 其他方法略。

缓存组件

组件被替换时,不进行销毁,而是缓存,从而保留原先数据。

1
2
3
4
5
6
7
8
9
<!-- include 指定需要缓存的组件,值为组件名。如果不写 include 属性,默认缓存所有组件 -->
<keep-alive include="Message">
<router-view></router-view>
</keep-alive>

<!-- 指定多个 -->
<keep-alive :include="['News', 'Message']">
<router-view></router-view>
</keep-alive>

生命周期钩子

1
2
3
4
5
6
7
8
// 当组件被路由时执行
activated() {
console.log("激活");
},
// 当组件被替换时执行
deactivated() {
console.log("被替换");
},

路由守卫

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
import VueRouter from "vue-router";
import About from "@/pages/About";

const router = new VueRouter({
routes: [
{
name: "about",
path: "/about",
component: About,
// 路由独享守卫
beforeEnter: (to, from, next) => {
next();
},
},
],
});

// 全局前置守卫
router.beforeEach((to, from, next) => {
next(); // 放行
});

// 全局后置守卫
router.afterEach((to, from) => {});

export default router;
1
2
3
4
5
6
7
8
9
// 组件前置守卫
beforeRouteEnter(to, from, next) {
next();
},

// 组件失活守卫
beforeRouteLeave(to, from, next) {
next();
},

执行顺序:

  1. 全局前置守卫
  2. 路由独享守卫
  3. 组件前置守卫
  4. 全局后置守卫
  5. 组件失活守卫

工作模式

  • hash (默认):地址中 # 即之后的路径不会作为网络请求的一部分。
  • history:地址中不会出现 #,但是可能因为路径冲突导致直接访问后端而不是路由跳转。
1
2
3
4
const router = new VueRouter({
mode: "history",
routes: [],
});

测试:

  1. 新建文件夹并将之作为后端项目

    1
    2
    npm init
    npm i express
  2. 编写 server.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const express = require("express");

    const app = express();
    app.use(express.static(__dirname + "/static"));

    app.listen(5001, (err) => {
    if (!err) {
    console.log("服务器启动成功");
    }
    });

    app.get("/about", (req, res) => {
    res.send({
    name: "scarf",
    age: 66,
    });
    });
  3. 打包前端代码

    1
    npm run build
  4. dist 文件夹下的文件拷贝到后端服务的 static 文件夹下

  5. 启动后端服务

    1
    node server
  6. 访问 http://localhost:5001/about 将直接返回后端服务响应数据,而不是具体的页面。

通常前后端代码都是分开部署,不用担心上述问题。

ElementUI

1
npm i element-ui
1
2
3
4
import ElementUI from "element-ui"; // 引入组件库
import "element-ui/lib/theme-chalk/index.css"; // 引入样式

Vue.use(ElementUI)

按需引入:

1
npm i babel-plugin-component -D

babel.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
presets: [
"@vue/cli-plugin-babel/preset",
+ ["@babel/preset-env", { modules: false }],
],
+ plugins: [
+ [
+ "component",
+ {
+ libraryName: "element-ui",
+ styleLibraryName: "theme-chalk",
+ },
+ ],
+ ],
};
1
2
3
4
import { Button, Row } from "element-ui";

Vue.component("el-button", Button);
Vue.component("el-row", Row);