# Vue基础核心

邂逅VueJS

认识Vue

  • Vue (读音 /vjuː/,类似于 view ) 是一套用于构建用户界面的渐进式 JavaScript 框架
    • 官网 : https://cn.vuejs.org/
    • 全称是 Vue.js 或者 Vuejs;
    • 它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型
    • 可以帮助你高效地开发用户界面,无论任务是简单还是复杂;
  • 什么是渐进式框架 :
    • 表示我们可以在项目中一点点来引入和使用 Vue,而不一定需要全部使用 Vue 来开发整个项目

1660797226523

vue的地位

  • 目前前端最流行的是三大框架:Vue 、 React 、 Angular

1660797277942

框架的选择

  • 谁是最好的前端框架

    • 首先,这是一个敏感的话题,在很多地方都争论不休,就像很多人喜欢争论谁才是世界上最好的语言一样;
    • 其次,争论这个话题是没有意义的,是一个争论不休的话题;
  • 我们从现实的角度,分析一下,学习哪一门语言更容易找到工作

    • 找后端的工作:优先推荐 Java、其次推荐 Go、再次推荐 Node(JavaScript),可能不推荐 PHP、C#;
    • 找前端的工作:优先推荐 JavaScript(TypeScript)、其次 Flutter、再次 Android(Java、Kotlin)、iOS(OC、Swift);
    • 也有很多的其他方向:游戏开发、人工智能、算法工程师等等;
  • 那么,就前端来说,学习了 HTML、CSS、JavaScript,哪一个框架更容易找到工作?

    • 如果去国外找工作,优先推荐 React、其次是 Vue 和 Angular;
    • 如果在国内找工作,优先推荐且必须学习 Vue,其次是 React,再其次是 Angular
  • 学好 Vue 你一定可以找到一份满意的前端工作,如果你没有掌握 Vue,很难找到一份满意的前端工作

Vue版本的选择

  • 在 2020 年的 9 月 19 日,万众期待的 Vue3 终于发布了正式版,命名为 “One Piece”。相对于 Vue2 , Vue3 具有以下优点 :

    • 更好的性能;
    • 更小的包体积;
    • 更好的 TypeScript 集成;
    • 更优秀的 API 设计。
  • 那么现在是否是学习 vue3 的时间呢

    • 答案是肯定的
    • Vue3 目前已经是稳定的版本,并且 Vue3 在 2022 年 2 月 7 日已经成为默认安装版本;
    • 目前社区也经过一定时间的沉淀,更加的完善了,包括 AntDesignVue、Element-Plus 都提供了对 Vue3 的支持,所以很多公司目前新的项目都已经在使用 Vue3 来进行开发了。
    • 并且在面试的时候,几乎都会问到各种各样 Vue3 相关的问题;

devtool插件的安装

VSCode代码片段

  • 有些代码片段是需要经常写的,我们在 VSCode 中我们可以生成一个代码片段,方便我们快速生成。

  • VSCode 中的代码片段有固定的格式,所以我们一般会借助于一个在线工具来完成。

  • 具体的步骤如下:

    • 第一步,复制自己需要生成代码片段的代码;
    • 第二步,访问 https://snippet-generator.app/ ,在该网站中生成代码片段;
    • 第三步,在 VSCode 中配置代码片段;
      • 设置 - 配置用户代码片段 - html.json/vue.json - 逗号分隔,粘贴网站中生成的代码片段

Vue的基本使用

Vue的引入方式

  • Vue 的本质,就是一个 JavaScript 的库:

    • 刚开始我们不需要把它想象的非常复杂;
    • 我们就把它理解成一个已经帮助我们封装好的库;
    • 在项目中可以引入并且使用它即可。
  • 那么安装和使用 Vue 这个 JavaScript 库有哪些方式呢

    • 方式一:在页面中通过 CDN 的方式来引入;
    • 方式二:下载 Vue 的 JavaScript 文件,并且自己手动通过 script 标签引入;
    • 方式三:通过 npm 包管理工具安装使用
      • 该方式也是后期项目开发时使用的方式
      • NPM 包管理工具的使用在 webpack 有讲述;
    • 方式四:直接通过 Vue CLI 创建项目,并且使用它;
      • 该方式也是后期项目开发时使用的方式

CDN引入

1
<script src="https://unpkg.com/vue@next"></script>
1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div id="app"></div>
<!-- CDN 引入 -->
<script src="https://unpkg.com/vue@next"></script>
<script>
// 01 创建 Vue 实例对象
const app = Vue.createApp({
template: `<h2>Hello World</h2><span>呵呵呵</span>`
})
// 02 挂载
app.mount("#app")
</script>
</body>

本地下载引入

  • 可以直接打开 CDN 的链接下载 Vue 的源码:
    • 打开链接,复制其中所有的代码;
    • 创建一个新的文件,比如 vue.js,将代码粘贴到其中;
    • 通过 script 标签,引入刚才的文件:
1
2
3
4
5
6
7
8
9
10
11
12
<body>
<div id="app"></div>
<script src="./lib/vue.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
template: `<h1>Hello Vue</h1>`
})
// 2.挂载app
app.mount("#app")
</script>
</body>

Vue 初体验

动态展示数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<div id="app"></div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 01 创建 vue 实例对象
const app = Vue.createApp({
template: `<h2>{{message}}</h2>`,
data: function() {
return {
message: "你好啊, Vue3"
}
}
})
// 02 挂载
app.mount("#app")
</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
<body>
<div id="app"></div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 01 创建 vue 实例对象
const app = Vue.createApp({
template: `
<h2>{{message}}</h2>
<ul>
<li v-for="item in movies" :key="item">{{item}}</li>
</ul>
`,
data: function () {
return {
message: "电影列表",
movies: ["大话西游", "星际穿越", "盗梦空间", "少年派", "飞驰人生"]
}
}
})
// 02 挂载
app.mount("#app")
</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
<body>
<div id="app"></div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 01 创建 vue 实例对象
const app = Vue.createApp({
// 模板
template: `
<h2>当前计数: {{counter}}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
`,
// 数据
data: function () {
return {
counter: 0
}
},
// 方法
methods: {
increment: function () {
this.counter++
},
decrement: function () {
this.counter--
}
}
})
// 02 挂载
app.mount("#app")
</script>
</body>
  • 计数器案例实现的方法二:
    • 若创建 vue 实例对象时传递的配置对象中没有 template 模板选项, 就会将 div#app 容器中的代码作为模板渲染
    • 若创建 vue 实例对象时传递的配置对象中有 template 模板选项,就会将 template 模板选项作为模板渲染, div#app 容器中的代码会被 template 模板覆盖
      • 在刷新页面时出现短暂的 mustache 语法, 出现的原因以及处理方法, 见 v-clock 指令章节
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
<body>
<div id="app">
<h2>当前计数: {{counter}}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 01 创建 vue 实例对象
const app = Vue.createApp({
// 数据
data: function () {
return {
counter: 0
}
},
// 方法
methods: {
increment: function () {
this.counter++
},
decrement: function () {
this.counter--
}
}
})
// 02 挂载
app.mount("#app")
</script>
</body>
  • 计算器案例的原生 JS 实现
    • HTML DOM 对象的 textContent 属性用于设置或返回指定节点及其所有后代的文本内容。此属性与 nodeValue 属性非常相似,但此属性返回所有子节点的文本。
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
<body>
<h2>当前计数: <span class="counter"></span></h2>
<button class="add">+1</button>
<button class="sub">-1</button>
<script>
// 1.获取dom
const counterEl = document.querySelector(".counter")
const addBtnEl = document.querySelector(".add")
const subBtnEl = document.querySelector(".sub")

// 2.定义一个变量记录数据
let counter = 100
counterEl.textContent = counter // 将数据放到页面中

// 2.监听按钮的点击更新数据
addBtnEl.onclick = function () {
counter++ // 更新数据
counterEl.textContent = counter // 将数据放到页面中
}
subBtnEl.onclick = function () {
counter-- // 更新数据
counterEl.textContent = counter // 将数据放到页面中
}
</script>
</body>

编程范式

  • 原生开发和 Vue 开发的模式和特点,我们会发现是完全不同的,这里其实涉及到两种不同的编程范式:
    • 命令式编程声明式编程
    • 命令式编程关注的是 “how to do” 自己完成整个 how 的过程;
    • 声明式编程关注的是 “what to do”,由框架(机器)完成 “how” 的过程;
  • 在原生的实现过程中,我们是如何操作的呢
    • 我们每完成一个操作,都需要通过 JavaScript 编写一条代码,来给浏览器一个指令;
    • 这样的编写代码的过程,我们称之为命令式编程
    • 在早期的原生 JavaScript 和 jQuery 开发的过程中,我们都是通过这种命令式的方式在编写代码的;
  • 在 Vue 的实现过程中,我们是如何操作的呢?
    • 我们会在 createApp 传入的对象中声明需要的内容,比如模板 template、数据 data、方法 methods 等等;
    • 这样的编写代码的过程,我们称之为是声明式编程
    • 目前 Vue、React、Angular、小程序的编程模式,我们称之为声明式编程;

MVVM模型

  • MVC 和 MVVM 都是一种软件的体系结构
    • MVC 是 Model – View –Controller 的简称,是在前期被使用非常广泛的架构模式,比如 iOS、前端
    • MVVM 是 Model-View-ViewModel 的简称,是目前非常流行的架构模式
  • 通常情况下,我们也经常称 Vue 是一个 MVVM 的框架
    • Vue 官方其实有说明,Vue 虽然并没有完全遵守 MVVM 的模型,但是整个设计是受到它的启发的。

1660802961303

data属性

  • data 属性是一个函数,并且该函数需要返回一个对象:

    • 在 Vue2.x 的时候,也可以是一个对象(但是官方推荐是一个函数)
    • 在 Vue3.x 的时候,必须是一个函数,否则就会直接在浏览器中报错;
  • data 中返回的对象会被 Vue 的响应式系统劫持,之后对该对象的修改或者访问都会在劫持中被处理

    • 所以我们在 template 或者 app 中通过 {{counter}} ,就可以从 data 对象中直接获取到数据
    • 所以我们修改 counter 的值时,data 中的 counter 也会发生改变, 进而引入页面的重新渲染, 即数据驱动页面的更新
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
<body>
<div id="app">
<h2>{{message}}</h2>
<button @click="changeMessage">改变message</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
// 数据
data: function () {
return {
message: "Hello Data"
}
},
// 方法
methods: {
changeMessage: function () {
this.message = "你好, 师姐!"
}
}
})
app.mount("#app")
</script>
</body>

methods属性

  • methods 属性是一个对象,通常我们会在这个对象中定义很多的方法:
    • 这些方法可以被绑定到模板中;
    • 在 methods 对象中定义的方法,我们可以使用 this 关键字来直接访问到 data 中返回的对象的属性;
  • 对于有经验的同学,在这里我提一个问题,官方文档有这么一段描述:
    • 问题一:为什么不能使用箭头函数(官方文档有给出解释)
      • 因为我们在 methods 中要使用 data 返回的对象中的数据:
        • 那么这个 this 是必须有值的,并且应该可以通过 this 获取到 data 返回的对象中的数据。
      • 那么我们这个 this 能不能是 window 呢
        • 不可以是 window,因为 window 中我们无法获取到 data 返回对象中的数据;
        • 但是如果我们使用箭头函数,那么这个 this 就会是 window 了
        • 事实上 Vue 的源码当中就是对 methods 中的所有函数进行了遍历,并且通过 bind 绑定了 this , (箭头函数都没有 this , 自然没有修改 this 指向的说法)
    • 问题二:不使用箭头函数的情况下,this 到底指向的是什么(可以作为一道面试题)
      • 官网的解释 : methods 将被混入到 Vue 实例中。可以直接通过 VM 实例访问这些方法,或者在指令表达式中使用。方法中的 this 自动绑定指向为 Vue 实例对象
      • 官网的解释 : 注意,不应该使用箭头函数来定义 method 函数 (例如 plus: () => this.a++)。理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例,this.a 将是 undefined。
      • 为什么是 window 呢
        • 这里涉及到箭头函数使用 this 的查找规则,它会在自己的上层作用域中来查找 this;
        • 最终刚好找到的是 script 作用域中的 this,所以就是 window;

1660821570206

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 01 创建 vue 实例对象
const app = Vue.createApp({
data: function () {
return {
counter: 0
}
},
methods: {
increment: function () {
console.log(this) // Proxy {increment: ƒ, …} , 当前的 app 实例对象
this.counter++
},
decrement: () => {
console.log(this) // window , 使用的是箭头函数,则 this 指向为 window
}
}
})

// 02 挂载
app.mount("#app")

模板语法

模板语法

  • React 的开发模式:
    • React 使用的 jsx,所以对应的代码都是编写的类似于 js 的一种语法;
    • 之后通过 Babel 将 jsx 编译成 React.createElement 函数调用;
  • Vue 也支持 jsx 的开发模式(后续有时间也会讲到):
    • 但是大多数情况下,使用基于 HTML 的模板语法;
    • 在模板中,允许开发者以声明式的方式将 DOM 和底层组件实例的数据绑定在一起;
    • 在底层的实现中,Vue 将模板编译成虚拟 DOM 渲染函数,这个我会在后续给大家讲到;

插值语法

  • 如果我们希望把数据显示到模板(template)中,使用最多的语法是 “Mustache” 语法 (双大括号) 的文本插值。
    • 并且我们前面提到过,data 返回的对象是有添加到 Vue 的响应式系统中的;
      • 当 data 中的数据发生改变时,模板会重新渲染, 对应的内容也会发生更新。
    • 当然,Mustache 中不仅仅可以是 data 中的属性,也可以是一个 JavaScript 的表达式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<!-- 1.基本使用 -->
<h2>{{ message }}</h2>
<h2>当前计数: {{ counter }} </h2>

<!-- 2.表达式 -->
<h2>计数双倍: {{ counter * 2 }}</h2>
<h2>展示的信息: {{ info.split(" ") }}</h2>

<!-- 3.三元运算符 -->
<h2>{{ age >= 18? "成年人": "未成年人" }}</h2>

<!-- 4.调用methods中函数,并将 data 中的数据作为参数传递 -->
<h2>{{ formatDate(time) }}</h2>

<!-- 5.注意: 这里不能定义语句,否则会报错 -->
<!-- <h2>{{ const name = "why" }}</h2> -->
</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
<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
// 数据
data: function() {
return {
message: "Hello Vue",
counter: 100,
info: "my name is why",
age: 22,
time: 123
}
},
// 方法
methods: {
// 事件回调并传递参数
formatDate: function(date) {
return "2022-10-10-" + date
}
}
})
// 2.挂载app
app.mount("#app")
</script>

指令语法

v-once

  • v-once 用于指定元素或者组件只渲染一次
    • 当数据发生变化时,元素或者组件以及其所有的子元素将视为静态内容并且跳过,不再重新渲染
    • 如果是子节点,也是只会渲染一次
    • 该指令可以用于性能优化
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
<body>
<div id="app">
<!-- 指令: v-once -->
<h2 v-once>
{{ message }}
<span>数字: {{counter}}</span>
</h2>
<h1>{{message}}</h1>
<button @click="changeMessage">改变message</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function() {
return {
message: "Hello Vue",
counter: 100
}
},
methods: {
changeMessage: function() {
this.message = "你好啊, 李银河"
this.counter += 100
console.log(this.message, this.counter)
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>

v-text

  • 用于更新元素的 textContent
  • 如果 v-text 所在的标签内部还有内容,则会被 v-text 绑定的值覆盖,并会报警告 : Template compilation error: v-text will override element children
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="app">
<h2> aa {{message}} bbb</h2>
<!-- v-text -->
<h2 v-text="message">aaa</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function() {
return {
message: "Hello Vue"
}
},
})
// 2.挂载app
app.mount("#app")
</script>
</body>

v-html

  • 默认情况下,如果我们展示的内容本身是 html 的,那么 vue 并不会对其进行特殊的解析。
    • 如果我们希望这个内容被 Vue 可以解析出来,那么可以使用 v-html 来展示;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<body>
<div id="app">
<h2>{{ content }}</h2>
<h2 v-html="content"></h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function() {
return {
content: `<span style="color: red; font-size: 30px;">哈哈哈</span>`
}
},
})
// 2.挂载app
app.mount("#app")
</script>
</body>

v-pre

  • v-pre 用于跳过元素和它的子元素的编译过程,显示原始的 Mustache 标签:
    • 跳过不需要编译的节点,加快编译的速度;
    • 该指令可以用于性能优化
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="app">
<div v-pre>
<h2>{{ message }}</h2>
<p>当前计数: {{ counter }}</p>
<p>{{}}</p>
</div>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function() {
return {
message: "Hello Vue",
counter: 0
}
},
})
// 2.挂载app
app.mount("#app")
</script>
</body>

v-cloak

  • 这个指令保持在元素上直到关联组件实例结束编译。
  • 存在的一种现象: 当模板编译未完成时,会在页面中显示 Mustache 标签
    • 造成的原因: CDN 引入的 Vue 加载延时,或者延时加载 Vue 等原因 ,导致模板的编译迟缓,进而导致在页面中出现 Mustache 标签
    • 解决方法 : 和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到组件实例准备完毕。
      • 当模板编译完毕时, Vue 内部就会自动去掉 v-cloak 属性
1
2
3
4
5
6
<style>
[v-cloak] {
display: none;
}
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="app">
<h2 v-cloak>{{message}}</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
setTimeout(() => {
// 1.创建app
const app = Vue.createApp({
data: function() {
return {
message: "Hello Vue"
}
},
})
// 2.挂载app
app.mount("#app")
}, 3000)
</script>
</body>

v-memo

  • Vue3.2 新增的指令
  • 只有 v-memo 指定的数据发生变化时,v-memo 指令所在的结构才会被重新渲染
  • 官网解释: 为了实现缓存,该指令需要传入一个固定长度的依赖值数组进行比较。如果数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过。
  • 该指令可以用于性能优化
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
<body>
<div id="app">
<!-- 只有指定的数据中任意一个发生变化时,v-memo 指令所在的结构才会重新渲染 -->
<div v-memo="[name, age]">
<h2>姓名: {{ name }}</h2>
<h2>年龄: {{ age }}</h2>
<h2>身高: {{ height }}</h2>
</div>
<p>我是 P 中的 : {{height}}</p>
<button @click="updateInfo">改变信息</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function () {
return {
name: "why",
age: 18,
height: 1.88
}
},
methods: {
updateInfo: function () {
// this.name = "kobe"
// this.age = 20
this.height = 2.1
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>

v-bind

v-bind的作用

  • 前面讲的一系列指令,主要是将值插入到模板内容中。

  • 但是,除了内容需要动态来决定外,某些属性我们也希望动态来绑定。

    • 比如动态绑定 a 元素的 href 属性;
    • 比如动态绑定 img 元素的 src 属性;
  • 绑定属性我们使用 v-bind:

  • 缩写::

  • 预期:any (with argument) | Object (without argument)

  • 参数:attrOrProp (optional)

  • 修饰符:

    • .camel : 将 kebab-case attribute 名转换为 camelCase。
  • 用法:动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。

绑定基本属性

  • v-bind 用于绑定一个或多个属性值,或者向另一个组件传递 props 值(这个学到组件时再介绍);

  • 在开发中,有哪些属性需要动态进行绑定呢

    • 还是有很多的,比如图片的链接 src、网站的链接 href、动态绑定一些类、样式等等
  • v-bind 有一个对应的语法糖,也就是简写方式。

  • 在开发中,我们通常会使用语法糖的形式,因为这样更加简洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<div>
<button @click="switchImage">切换图片</button>
</div>
<!-- 1.绑定img的src属性 -->
<img v-bind:src="showImgUrl" alt="">
<!-- 语法糖: v-bind : -->
<img :src="showImgUrl" alt="">

<!-- 2.绑定a的href属性 -->
<a :href="href">百度一下</a>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function() {
return {
imgUrl1: "http://p1.music.126.net/agGc1qkogHtJQzjjyS-kAA==/109951167643767467.jpg",
imgUrl2: "http://p1.music.126.net/_Q2zGH5wNR9xmY1aY7VmUw==/109951167643791745.jpg",

showImgUrl: "http://p1.music.126.net/_Q2zGH5wNR9xmY1aY7VmUw==/109951167643791745.jpg",
href: "http://www.baidu.com"
}
},
methods: {
switchImage: function() {
this.showImgUrl = this.showImgUrl === this.imgUrl1 ? this.imgUrl2: this.imgUrl1
}
}
})
// 2.挂载app
app.mount("#app")
</script>

绑定 class 介绍

  • 在开发中,有时候我们的元素 class 也是动态的,比如:
    • 当数据为某个状态时,字体显示红色。
    • 当数据另一个状态时,字体显示黑色。
  • 绑定 class 有两种方式:
    • 对象语法
    • 数组语法

绑定 class 对象写法

  • 对象语法:我们可以传给 :class (v-bind:class 的简写) 一个对象,以动态地切换 class。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<!-- 1.基本绑定class -->
<h2 :class="classes">Hello World</h2>

<!-- 2.动态class 三目运算符 -->
<button :class=" isActive ? 'active': '' " @click="btnClick">我是按钮</button>

<!-- 2.1.对象语法的基本使用(掌握) -->
<button :class="{ active: isActive }" @click="btnClick">我是按钮</button>

<!-- 2.2.对象语法的多个键值对 -->
<button :class="{ active: isActive, why: true, kobe: false }" @click="btnClick">我是按钮</button>

<!-- 2.3.动态绑定的class是可以和普通的class同时的使用,类名会进行合并 -->
<button class="abc cba" :class="{ active: isActive, why: true, kobe: false }" @click="btnClick">我是按钮</button>

<!-- 2.4.动态绑定的class是一个对象,并且该对象是函数调用后的返回值,类名会进行合并 -->
<button class="abc cba" :class="getDynamicClasses()" @click="btnClick">我是按钮</button>
</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
<script src="https://unpkg.com/vue@next"></script>  
<script>
// 1.创建app
const app = Vue.createApp({
data: function () {
return {
classes: "abc cba nba",
isActive: false,
className: "why"
}
},
methods: {
btnClick: function () {
this.isActive = !this.isActive
},
getDynamicClasses: function () {
return { active: this.isActive, why: true, kobe: false }
}
}
})
// 2.挂载app
app.mount("#app")
</script>

绑定 class 数组写法

  • 数组语法:我们可以把一个数组传给 :class,以应用一个 class 列表
    • 注意: 数组中的变量如果加了引号,将会成为一个普通的类名,不加引号作为变量使用,即可通过 Vue 实例管理
1
2
3
4
5
6
7
8
<div id="app">
<!-- 3.动态class可以写数组语法(了解) , 若加引号会成为字符串普通类名,不再是变量-->
<h2 :class="['abc', 'cba']">Hello Array</h2>
<h2 :class="['abc', className]">Hello Array</h2>
<h2 :class="['abc', className, isActive? 'active': '']">Hello Array</h2>
<h2 :class="['abc', className, { active: isActive }]">Hello Array</h2>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function () {
return {
isActive: false,
className: "why"
}
},
})
// 2.挂载app
app.mount("#app")
</script>

绑定 style 介绍

  • 我们可以利用 v-bind:style 来绑定一些 CSS 内联样式:
    • 这次因为某些样式我们需要根据数据动态来决定;
    • 比如某段文字的颜色,大小等等;
  • CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名;
  • 绑定 class 有两种方式:
    • 对象语法
    • 数组语法

绑定 style 对象写法

  • v-bind 绑定 style 的对象写法:
    • 绑定的对象中的 key 为 CSS 的样式名,某些样式名需要用驼峰命名
    • 绑定的对象中的 value 为 CSS 的样式值,若有单位,则需要注意拼接单位
1
2
3
4
5
6
7
8
9
10
11
<div id="app">
<!-- 1.普通的html写法 -->
<h2 style="color: red; font-size: 30px;">哈哈哈哈</h2>

<!-- 2.style中的某些值, 来自data中 -->
<!-- 2.1.动态绑定style, 在后面跟上 对象类型 (重要) -->
<h2 v-bind:style="{ color: fontColor, fontSize: fontSize + 'px' }">哈哈哈哈</h2>
<!-- 2.2.动态的绑定属性, 这个属性是一个对象 -->
<h2 :style="objStyle">呵呵呵呵</h2>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function() {
return {
fontColor: "blue",
fontSize: 30,
objStyle: {
fontSize: '50px',
color: "green"
}
}
},
})
// 2.挂载app
app.mount("#app")
</script>

绑定 style 数组写法

  • :style 的数组语法可以将多个样式对象应用到同一个元素上;
    • 数组中的元素是一个个对象,对象的 key 和 value 分别对应 CSS 的样式名和样式值,注意某些样式名要用驼峰命名
1
2
3
4
5
<div id="app">
<!-- 3. style 的数组语法 -->
<h2 :style="[objStyle, { backgroundColor: 'purple' }]">嘿嘿嘿嘿</h2>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function() {
return {
objStyle: {
fontSize: '50px',
color: "green"
}
}
},
})
// 2.挂载app
app.mount("#app")
</script>

绑定属性名

  • 在某些情况下,我们属性的名称可能也不是固定的:
    • 前面我们无论绑定 src、href、class、style,属性名称都是固定的;
    • 如果属性名称不是固定的,我们可以使用:[属性名]="值"的格式来定义;
    • 这种绑定的方式,我们称之为动态绑定属性
  • 动态绑定属性名, 该属性名来自 data 中
    • [ ] 中的变量不能使用驼峰命名,不能包含大写字母,多个字段可以通过下划线分隔,但是不能以下划线开头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body>
<div id="app">
<h2 :[name]="'aaaa'">Hello World</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function () {
return {
name: "class"
}
},
})
// 2.挂载app
app.mount("#app")
</script>
</body>

绑定对象作为属性

  • 如果我们希望将一个对象的所有属性,绑定到元素上的所有属性,应该怎么做呢?
    • 非常简单,我们可以直接使用 v-bind 绑定一个 对象;v-bind 也可以直接使用简写( : )形式
    • 绑定的 infos 对象的 key 和 value 会被拆解成标签的各个属性和对应的属性值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<body>
<div id="app">
<h2 :name="name" :age="age" :height="height">Hello World</h2>
<!-- v-bind绑定对象: 给组件传递参数,此处 v-bind 也可以直接使用简写( : )形式,-->
<h2 v-bind="infos">Hello Bind</h2>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function() {
return {
infos: { name: "why", age: 18, height: 1.88, address: "广州市" },
name: "why",
age: 18,
height: 1.88
}
},
})
// 2.挂载app
app.mount("#app")
</script>
</body>

v-on

v-on 的介绍

  • 前面我们绑定了元素的内容和属性,在前端开发中另外一个非常重要的特性就是交互
  • 在前端开发中,我们需要经常和用户进行各种各样的交互:
    • 这个时候,我们就必须监听用户发生的事件,比如点击、拖拽、键盘事件等等
    • 在 Vue 中如何监听事件呢?使用 v-on 指令进行事件绑定

v-on的基本使用

  • v-on 的基本使用
  • v-on 的语法糖写法
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
<body>
<div id="app">
<!-- 1.基本的写法 -->
<div class="box" v-on:click="divClick"></div>

<!-- 2.语法糖写法(重点掌握) -->
<div class="box" @click="divClick"></div>
</div>

<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function() {
return {
counter: 0
}
},
methods: {
divClick() {
console.log("divClick")
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>
  • 可以在 v-on 指令所在的标签中直接写事件回调处理程序
  • v-on 指令绑定其他事件类型
  • v-on 绑定多个事件类型及其回调
  • v-on 绑定多个事件类型以对象的形式存在
  • v-on 绑定多个事件类型以对象的形式存在时的简写
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
<body>
<div id="app">
<!-- 3.绑定的方法位置, 也可以写成一个表达式(不常用, 不推荐) -->
<h2>{{ counter }}</h2>
<button @click="counter++">+1</button>
<button @click="counter--">-1</button>

<!-- 4.绑定其他方法(掌握) -->
<div class="box" @mousemove="divMousemove"></div>

<!-- 5.1元素绑定多个事件(掌握) -->
<div class="box" @click="divClick" @mousemove="divMousemove"></div>
<!-- 5.2 v-on 绑定的事件及其回调以对象的形式存在,对象的 key 和 value ,分别对应事件类型和事件回调-->
<div class="box" v-on="{ click: divClick, mousemove: divMousemove }"></div>
<!-- 5.3 v-on 的简写: @ -->
<div class="box" @="{ click: divClick, mousemove: divMousemove }"></div>
</div>

<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function() {
return {
counter: 0
}
},
methods: {
divClick() {
console.log("divClick")
},
divMousemove() {
console.log("divMousemove")
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>

v-on 传递参数

  • 当通过 methods 中定义方法,以供 @click 调用时,需要注意参数问题:
  • 情况一:如果该方法不需要额外参数,那么方法后的()可以不添加, 即使不添加括号,也会默认将事件对象 event 传递到回调函数中。
    • 但是注意:如果回调方法本身中有一个形参,那么该形参会默认接收原生事件对象 event , 当然如果不需要使用事件对象 event ,可以不写形参
  • 情况二:如果需要同时传入某个参数,同时需要 event 时,可以通过 $event 传入事件回调。
    • 建议在模板中传递参数时, $event 放到所有实参的最前面,在回调函数中,事件对象 evnet 放到所有形参的最前面
    • 当然,不管事件对象放到那个位置,都会按实参与形参从左到右依次匹配传递
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="app">
<!-- 1.默认传递event对象 -->
<button @click="btn1Click">按钮1</button>

<!-- 2.只有自己的参数 -->
<button @click="btn2Click('why', age)">按钮2</button>

<!-- 3. event 对象和自己的参数, 在模板中想要明确的获取 event 对象: $event -->
<button @click="btn3Click( $event,'why', age)">按钮3</button>
</div>

<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data: function () {
return {
message: "Hello Vue",
age: 18
}
},
methods: {
// 1.默认参数: event对象, 如果在绑定事件的时候, 没有传递任何的参数, 那么 event 对象会被默认传递进来
btn1Click(event) {
console.log("btn1Click:", event)
},
// 2.明确参数:
btn2Click(name, age) {
console.log("btn2Click:", name, age)
},
// 3.event对象 和 明确参数
btn3Click(event, name, age) {
console.log("btn3Click:", event, name, age,)
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>

v-on 的事件修饰符

  • v-on 支持修饰符,修饰符相当于对事件进行了一些特殊的处理:
    • .stop 阻止事件冒泡,相当于调用 event.stopPropagation()。
    • .prevent 阻止事件的默认行为,相当于调用 event.preventDefault()。
    • .capture 添加事件侦听器时使用 capture 捕获模式。
    • .self 只当事件是从侦听器绑定的元素本身触发时才触发回调。
    • .{keyAlias} 仅当事件是从特定键触发时才触发回调。
    • .once 只触发一次回调。
    • .left 只当点击鼠标左键时触发。
    • .right 只当点击鼠标右键时触发。
    • .middle 只当点击鼠标中键时触发。
    • .passive { passive: true } 模式添加侦听器
1
2
3
4
5
6
7
<div id="app">
<div class="box" @click="divClick">
<!-- 添加 .stop 事件修饰符,阻止事件冒泡-->
<button @click.stop="btnClick">按钮</button>
</div>
</div>

v-if

条件渲染介绍

  • 在某些情况下,我们需要根据当前的条件决定某些元素或组件是否渲染,这个时候我们就需要进行条件判断了。
  • Vue 提供了下面的指令来进行条件判断:
    • v-if
    • v-else
    • v-else-if
    • v-show
  • 下面我们来对它们进行学习。

v-if/v-else

  • v-if、v-else、v-else-if 用于根据条件来渲染某一块的内容:

    • 这些内容只有在条件为 true 时,才会被渲染出来;
    • 这三个指令与 JavaScript 的条件语句 if、else、else if 类似;
  • v-if 的渲染原理:

    • v-if 是惰性的;
    • 当条件为 false 时,其判断的内容完全不会被渲染或者会被销毁;
    • 当条件为 true 时,才会真正渲染条件块中的内容;
  • v-if

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<!-- Object.keys(info).length : 由 info 对象的属性组成的数组的长度.
Object.keys(info):由 info 对象的属性组成的数组 -->
<div class="info" v-if="Object.keys(info).length">
<h2>个人信息</h2>
<ul>
<li>姓名: {{info.name}}</li>
<li>年龄: {{info.age}}</li>
</ul>
</div>
</div>

  • v-if / v-else
    • v-if 与 v-else 两个指令之间,不能有别的标签中断 v-if / v-else 逻辑结构, 否则会报警告,且两个结构都会渲染
1
2
3
4
5
6
7
<div id="app">
<ul v-if="names.length > 0">
<li v-for="item in names">{{item}}</li>
</ul>
<h2 v-else>当前names没有数据, 请求获取数据后展示</h2>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="app">
<!-- v-if="条件" -->
<div class="info" v-if="Object.keys(info).length">
<h2>个人信息</h2>
<ul>
<li>姓名: {{info.name}}</li>
<li>年龄: {{info.age}}</li>
</ul>
</div>
<!-- v-else, v-if 与 v-else 两个指令之间,不能有别的标签中断 v-if / v-else 逻辑结构,否则会报警告,且两个结构都会渲染-->
<div v-else>
<h2>没有输入个人信息</h2>
<p>请输入个人信息后, 再进行展示~</p>
</div>
</div>

  • v-if / v-else-if / v-else
    • 同样, v-if 逻辑结构不能被其他标签中断
1
2
3
4
5
6
7
<div id="app">
<h1 v-if="score > 90">优秀</h1>
<h2 v-else-if="score > 80">良好</h2>
<h3 v-else-if="score >= 60">及格</h3>
<h4 v-else>不及格</h4>
</div>

template元素

  • 因为 v-if 是一个指令,所以必须将其添加到一个元素上:
    • 但是如果我们希望切换的是多个元素呢?
    • 此时我们渲染 div,但是我们并不希望 div 这种元素被渲染,因为会渲染一个多余无用的 div 结构;
    • 这个时候,我们可以选择使用 template 标签;
  • template 元素可以当做不可见的包裹元素,并且在 v-if 上使用,但是最终 template 不会被渲染出来
    • 有点类似于小程序中的 block
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app">
<!-- v-if="条件" -->
<template v-if="Object.keys(info).length">
<h2>个人信息</h2>
<ul>
<li>姓名: {{info.name}}</li>
<li>年龄: {{info.age}}</li>
</ul>
</template>

<!-- v-else -->
<template v-else>
<h2>没有输入个人信息</h2>
<p>请输入个人信息后, 再进行展示~</p>
</template>
</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
<body>
<div id="app">
<div>
<!-- 02 给按钮添加点击事件 -->
<button @click="toggle">切换</button>
</div>
<!-- 04 变量,用于控制显示与隐藏-->
<template v-if="isShowCode">
<img src="https://game.gtimg.cn/images/yxzj/web201706/images/comm/floatwindow/wzry_qrcode.jpg" alt="">
</template>
</div>

<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() {
return {
// 01 准备一个变量,用于控制显示与隐藏
isShowCode: true
}
},
methods: {
// 03 按钮点击事件的回调,将变量取反
toggle() {
this.isShowCode = !this.isShowCode
}
}
})
app.mount("#app")
</script>
</body>

v-show

v-show的基本使用

  • v-show 和 v-if 的用法看起来是一致的,也是根据一个条件决定是否显示元素或者组件
1
2
3
4
  <!-- 通过 isShowCode 变量控制结构的显示与隐藏-->
<div v-show="isShowCode">
<img src="https://game.gtimg.cn/images/yxzj/web201706/images/comm/floatwindow/wzry_qrcode.jpg" alt="">
</div>

v-if与v-show的区别

  • 首先,在用法上的区别:
    • v-show 是不支持 template 标签, 因为 template 标签不会渲染到页面中,自然就不能给 template 标签添加行内样式 display:”block/none”;
    • v-show 不可以和 v-else 一起使用;
  • 其次,本质的区别:
    • v-show 元素无论是否需要显示到浏览器上,它的 DOM 实际都是有存在的,只是通过给标签添加行内样式 display:” block/none” 来进行切换;
    • v-if 当条件为 false 时,其对应的标签结构压根不会被渲染到 DOM 中;
  • 开发中如何进行选择呢?
    • 如果我们的元素结构需要在显示和隐藏之间频繁的切换,那么使用 v-show;
    • 如果不会频繁的发生切换,那么使用 v-if;

v-for

v-for的介绍

  • 在真实开发中,我们往往会从服务器拿到一组数据,并且需要对其进行渲染。
    • 这个时候我们可以使用 v-for 来完成;
  • v-for 类似于 JavaScript 的 for 循环,可以用于遍历一组数据;

1660907567054

v-for基本使用

  • v-for 的基本格式是 "item in 数组"
    • 数组通常是来自 data 或者 prop,也可以是其他方式;
    • item 是我们给每项元素起的一个别名,这个别名可以自定义;
  • 我们知道,在遍历一个数组的时候会经常需要拿到数组的索引:
    • 如果我们需要索引,可以使用格式: "(item, index) in 数组"
    • 注意上面的顺序:数组元素项 item 是在前面的,索引项 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<body>
<div id="app">
<!-- 1.电影列表进行渲染 movie 就是 movies 数组中的每一个元素 -->
<h2>电影列表</h2>
<ul>
<li v-for="movie in movies">{{ movie }}</li>
</ul>

<!-- 2.电影列表同时有索引, movie 和 index 就是 movies 数组中的每一个元素及其对应的下标索引值 -->
<ul>
<li v-for="(movie, index) in movies">{{index + 1}} - {{ movie }}</li>
</ul>

<!-- 3.遍历数组复杂数据, item 就是 products 数组中的每一个元素,该元素均为一个对象 ,开发常用 -->
<h2>商品列表</h2>
<div class="item" v-for="item in products">
<h3 class="title">商品: {{item.name}}</h3>
<span>价格: {{item.price}}</span>
<p>秒杀: {{item.desc}}</p>
</div>
</div>

<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() {
return {
// 1.movies
movies: ["星际穿越", "少年派", "大话西游", "哆啦A梦"],
// 2.数组: 存放的是对象
products: [
{ id: 110, name: "Macbook", price: 9.9, desc: "9.9秒杀, 快来抢购!" },
{ id: 111, name: "iPhone", price: 8.8, desc: "9.9秒杀, 快来抢购!" },
{ id: 112, name: "小米电脑", price: 9.9, desc: "9.9秒杀, 快来抢购!" },
]
}
},
})
app.mount("#app")
</script>
</body>

v-for 遍历其他类型

  • v-for 也支持遍历对象,并且支持 一个 或 二个 或 三个参数:
    • 一个参数: "value in object" , 参数 value 为对象的属性对应的属性值
    • 二个参数: "(value, key) in object" , 第一个参数为 object 对象的属性对应的属性值, 第二个参数是对象的属性
    • 三个参数: "(value, key, index) in object" , 三个参数依次分别为 : value值 key属性 index索引
  • v-for 同时也支持数字的遍历:
    • 每一个 item 都是一个数字, 数字从 1 开始递增 1
  • v-for 也可以遍历其他可迭代对象( Iterable )
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="app">
<!-- 2.遍历对象 , 同样参数也有先后循序的区别,三个参数分别为 : value值 key属性 index索引 -->
<ul>
<li v-for="(value, key, index) in info">{{key}}-{{value}}-{{index}}</li>
</ul>

<!-- 3.遍历字符串(iterable) ,每一项 item 都是一个字符,包括空格-->
<ul>
<li v-for="item in message">{{item}}</li>
</ul>

<!-- 4.遍历数字 每一项 item 都是数字,数字从 1 开始递增 1-->
<ul>
<li v-for="item in 100">{{item}}</li>
</ul>
</div>

<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data() {
return {
info: { name: "why", age: 18, height: 1.88 }
}
},
})
// 2.挂载app
app.mount("#app")
</script>

v-for 与 template

  • 类似于 v-if,你可以使用 template 元素来循环渲染一段包含多个元素的内容
    • 如果 div 没有实际的意义, 那么可以使用 template 替换
    • 我们使用 template 来对多个元素进行包裹,而不是使用 div 来完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="app">
<template v-for="(value, key, index) in infos">
<span>{{value}}</span>
<strong>{{key}}</strong>
<i>{{index}}</i>
</template>
</div>

<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() {
return {
infos: { name: "why", age: 18, height: 1.88 }
}
},
})
app.mount("#app")
</script>
</body>

v-for 中 key 的作用

  • 在使用 v-for 进行列表渲染时,我们通常会给元素或者组件绑定一个 key 属性。
  • 这个 key 属性有什么作用呢?我们先来看一下官方的解释:
    • key 属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes
    • 如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法
    • 而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除/销毁 key 不存在的元素

认识VNode虚拟节点

  • 我们先来解释一下 VNode 的概念:
    • 因为目前我们还没有比较完整的学习组件的概念,所以目前我们先理解 HTML 元素创建出来的 VNode;
    • VNode 的全称是 Virtual Node,也就是虚拟节点
    • 事实上,无论是组件还是元素,它们最终在 Vue 中表示出来的都是一个个 VNode;
    • VNode 的本质是一个 JavaScript 的一般对象,对象中的各个属性用于描述要生成的真实 DOM 节点;

1660912490480

认识虚拟DOM

  • 如果我们不只是一个简单的 div,而是有一大堆的元素,那么它们应该会形成一个 VNode Tree,并能通过虚拟 DOM 渲染出不同平台的代码

1660912652676

列表插入数据案例

  • 我们先来看一个案例:这个案例是当我点击按钮时会在列表中间插入一个 f;

  • 我们可以确定的是,这次更新对于 ul 和 button 是不需要进行更新,需要更新的是我们 li 的列表:

    • 在 Vue 中,对于相同父元素的子元素节点并不会重新渲染整个列表;
    • 因为对于列表中 a、b、c、d 它们都是没有变化的;
    • 在操作真实 DOM 的时候,我们只需要在中间插入一个 f 的 li 即可;
  • 那么 Vue 中对于列表的更新究竟是如何操作的呢?

    • Vue 事实上会对于有 key 和没有 key 会调用两个不同的方法;
      • 有 key,那么就使用 patchKeyedChildren 方法;
      • 没有 key,那么就使用 patchUnkeyedChildren 方法;
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
<body>
<div id="app">
<button @click="insertF">插入f</button>
<ul>
<!-- key 要求是唯一: id -->
<li v-for="item in letters" :key="item">{{item}}</li>
</ul>
</div>

<script src="https://unpkg.com/vue@next"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data() {
return {
letters: ["a", "b", "c", "d", "e"]
}
},
methods: {
insertF() {
this.letters.splice(2, 0, "f")
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>
  • 没有 key 时的过程如下:
    • 对于 c 和 d 来说, 它们事实上并不需要有任何的改动;
    • 但是因为我们的 c 被 f 所使用了,所有后续所有的内容都要进行一次改动,并且最后还需要进行新增一个字母;

1660913107879

  • 有 key 时的 diff 算法如下:
  • 第一步的操作是从头开始进行遍历、比较:
    • a 和 b 是一致的会继续进行比较;
    • c 和 f 因为 key 不一致,所以就会 break 跳出循环;

1660913292071

  • 第二步的操作是从尾部开始进行遍历、比较 key 值是否相同:

1660913302742

  • 第三步是如果旧节点遍历完毕,但是依然有新的节点,那么就会新增节点:

1660913371627

  • 第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么就移除旧节点:

1660913378512

  • 第五步是最特色的情况,中间还有很多未知的或者乱序的节点:

1660913439809

  • 所以我们可以发现,Vue 在进行 diff 算法的时候,会尽量利用我们的 key 来进行优化操作:
    • 在没有 key 的时候我们的效率是非常低效的;
    • 在进行插入或者重置顺序的时候,保持相同新旧虚拟 DOM 的 key , 可以让 diff 算法更加的高效

数组的响应式方法

  • Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。
  • 这些被包裹过的方法包括:
    • push()
    • pop()
    • shift()
    • unshift()
    • splice()
    • sort()
    • reverse()
  • 替换数组的方法
    • 上面的方法会直接修改原来的数组;
    • 但是某些方法不会替换原来的数组,而是会生成新的数组,比如 filter()、concat() 和 slice();
  • 直接通过数组的索引值修改数组元素,在 Vue2 中不是响应式的, 但是在 vue 3 中是响应式的

数组的三个高阶方法

for…of 实现元素处理

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
// 定义一个数组
const nums = [10, 20, 111, 222, 444, 40, 50]

// 需求1: 取出所有小于 100 的数字
let newNums = []
for (let item of nums) {
if (item < 100) {
newNums.push(item)
}
}
console.log(newNums) // [ 10, 20, 40, 50 ]

// 需求2: 在需求1的结果的基础上,将数组的每一项都乘以 2
let newNums2 = []
for (let item of newNums) {
newNums2.push(item * 2)
}
console.log(newNums2) // [ 20, 40, 80, 100 ]

// 需求3: 在需求2的结果的基础上,将数组的每一项进行相加求和
let total = 0
for (let item of newNums2) {
total = total + item
}
console.log(total) //240

filter 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 需求1: 取出所有小于 100 的数字
// 需求2: 在需求1的结果的基础上,将数组的每一项都乘以 2
// 需求3: 在需求2的结果的基础上,将数组的每一项进行相加求和

// 定义一个数组
const nums = [10, 20, 111, 222, 444, 40, 50]

// 需求1: 取出所有小于 100 的数字
// filter 方法:
// filter 中的回调函数,必须返回一个 Boolean 值
// true : 当返回的是 true 时,函数内部会自动将这次回调的 item 加入到新的数组 nuwNums 中
// false : 当返回的是 false 时,函数内部会过滤掉这次的 item
// 对数组 nums 进行遍历,数组中有多少个元素,就会执行多少次回调方法,形参 item 为遍历的数组中的每一项
// filter 方法返回一个新的数组
let nuwNums = nums.filter(function (item) {
return item < 100
})
console.log(nuwNums) // [ 10, 20, 40, 50 ]

map 方法

1
2
3
4
5
6
7
8
9
// 需求2: 在需求1的结果的基础上,将数组的每一项都乘以 2
// map 方法:
// 对数组 nuwNums 进行遍历,数组中有多少个元素,就会执行多少次回调方法,形参 item 为遍历的数组中的每一项
// 新数组 newNum2 中的每一项,由 map 方法中的回调的返回值决定
// map 方法返回一个新的数组
let newNum2 = nuwNums.map(function (item) {
return item * 2
})
console.log(newNum2) //[ 20, 40, 80, 100 ]

reduce 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 需求3: 在需求2的结果的基础上,将数组的每一项进行相加求和
// reduce 方法:
// 参数一: 遍历时执行的回调
// 回调中的形参 preValue: 第一次遍历时,值为初始值 0 , 即 reduce 方法的参数二,
// 以后每次遍历时,该值等于每次遍历时回调的返回值
// 回调中的形参 item : 为每次遍历数组时的每一项数组元素
// 参数二: 初始值
// 遍历的数组由多少项,就会执行多少次回调函数
// refuce 方法的返回值,由遍历完后的最后一次返回值决定
let total1 = newNum2.reduce(function (preValue, item) {
return preValue + item
}, 0)
console.log(total1) // 240

对以上方法的综合写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义一个数组
const nums = [10, 20, 111, 222, 444, 40, 50]

// 对以上三个需求的综合写法
let total2 = nums
.filter(function (item) {
return item < 100
})
.map(function (item) {
return item * 2
})
.reduce(function (preValue, item) {
return preValue + item
}, 0)
console.log(total2) // 240

计算属性

复杂数据的处理

  • 我们知道,在模板中可以直接通过插值语法显示一些 data 中的数据。
  • 但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示;
    • 比如我们需要对多个 data 数据进行运算、三元运算符来决定结果、数据进行某种转化后显示;
    • 在模板中使用表达式,可以非常方便的实现,但是设计它们的初衷是用于简单的运算;
    • 在模板中放入太多的逻辑会让模板过重和难以维护;
    • 并且如果多个地方都使用到,那么会有大量重复的代码;
  • 我们有没有什么方法可以将逻辑抽离出去呢?
    • 可以,其中一种方式就是将逻辑抽取到一个 method 中,放到 methods 的 options 中;
    • 但是,这种做法有一个直观的弊端,就是所有的 data 使用过程都会变成了一个方法的调用;
    • 另外一种方式就是使用计算属性 computed;

认识计算属性

  • 什么是计算属性呢?
    • 官方并没有给出直接的概念解释;
    • 而是说:对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性;
    • 计算属性将被混入到组件实例中
      • 所有 getter 和 setter 的 this 上下文自动地绑定为组件实例
  • 计算属性的用法:
    • 选项:computed
    • 类型:{ [key: string]: Function | { get: Function, set: Function } }

三个案例引出计算属性

  • 我们来看三个案例:
  • 案例一:我们有两个变量:firstName 和 lastName,希望它们拼接之后在界面上显示;
  • 案例二:我们有一个分数:score
    • 当 score 大于 60 的时候,在界面上显示及格;
    • 当 score 小于 60 的时候,在界面上显示不及格;
  • 案例三:我们有一个变量 message,记录一段文字:比如 Hello World
    • 某些情况下我们是直接显示这段文字;
    • 某些情况下我们需要对这段文字进行反转;
  • 我们可以有三种实现思路:
    • 思路一:在模板语法中直接使用表达式;
    • 思路二:使用 method 对逻辑进行抽取;
    • 思路三:使用计算属性 computed;

案例_模板语法实现

  • 思路一的实现:模板语法
    • 缺点一:模板中存在大量的复杂逻辑,不便于维护(模板中表达式的初衷是用于简单的计算);
    • 缺点二:当有多次一样的逻辑时,存在重复的代码;
    • 缺点三:多次使用的时候,很多运算也需要多次执行,没有缓存;
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
<body>
<div id="app">
<!-- 1.拼接名字, 插值语法表达式直接进行拼接 -->
<h2>{{ firstName + " " + lastName }}</h2>
<h2>{{ firstName + " " + lastName }}</h2>
<h2>{{ firstName + " " + lastName }}</h2>

<!-- 2.显示分数等级 -->
<h2>{{ score >= 60 ? '及格': '不及格' }}</h2>

<!-- 3.反转单词显示文本 -->
<h2>{{ message.split(" ").reverse().join(" ") }}</h2>
</div>
<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
// data: option api
data() {
return {
// 1.姓名
firstName: "kobe",
lastName: "bryant",
// 2.分数: 及格/不及格
score: 80,
// 3.一串文本: 对文本中的单词进行反转显示
message: "my name is why"
}
},
})
app.mount("#app")
</script>
</body>

案例_用方法实现

  • 思路二的实现:method 实现
    • 缺点一:我们事实上先显示的是一个结果,但是都变成了一种方法的调用;
    • 缺点二:多次使用方法的时候,没有缓存,也需要多次计算;
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="app">
<!-- 插值语法表达式直接进行拼接 -->
<!-- 1.拼接名字 -->
<h2>{{ getFullname() }}</h2>
<h2>{{ getFullname() }}</h2>
<h2>{{ getFullname() }}</h2>

<!-- 2.显示分数等级 -->
<h2>{{ getScoreLevel() }}</h2>

<!-- 3.反转单词显示文本 -->
<h2>{{ reverseMessage() }}</h2>
</div>
<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
// 1.姓名
firstName: "kobe",
lastName: "bryant",
// 2.分数: 及格/不及格
score: 80,
// 3.一串文本: 对文本中的单词进行反转显示
message: "my name is why"
}
},
methods: {
getFullname() {
return this.firstName + " " + this.lastName
},
getScoreLevel() {
return this.score >= 60 ? "及格": "不及格"
},
reverseMessage() {
return this.message.split(" ").reverse().join(" ")
}
}
})
app.mount("#app")
</script>
</body>

案例_计算属性实现

  • 思路三的实现:computed 实现
    • 注意:计算属性看起来像是一个函数,但是我们在使用的时候不需要加 () ,这个后面讲 setter 和 getter 时会讲到;
    • 我们会发现无论是直观上,还是效果上计算属性都是更好的选择;
    • 并且计算属性是有缓存的;
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
<body>
<div id="app">
<!-- 插值语法表达式直接进行拼接 -->
<!-- 1.拼接名字 -->
<h2>{{ fullname }}</h2>
<h2>{{ fullname }}</h2>
<h2>{{ fullname }}</h2>

<!-- 2.显示分数等级 -->
<h2>{{ scoreLevel }}</h2>

<!-- 3.反转单词显示文本 -->
<h2>{{ reverseMessage }}</h2>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
// 1.姓名
firstName: "kobe",
lastName: "bryant",
// 2.分数: 及格/不及格
score: 80,
// 3.一串文本: 对文本中的单词进行反转显示
message: "my name is why"
}
},
computed: {
// 1.计算属性默认对应的是一个函数
fullname() {
return this.firstName + " " + this.lastName
},
scoreLevel() {
return this.score >= 60 ? "及格": "不及格"
},
reverseMessage() {
return this.message.split(" ").reverse().join(" ")
}
}
})
app.mount("#app")
</script>
</body>

计算属性和方法的对比

  • 在上面的实现思路中,我们会发现计算属性和 methods 的实现看起来是差别是不大的,而且我们多次提到计算属性有缓存的。
  • 接下来我们来看一下同一个计算多次使用,计算属性和 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
37
38
39
40
41
42
<body>
<div id="app">
<!-- 1.methods -->
<h2>{{ getFullname() }}</h2>
<h2>{{ getFullname() }}</h2>
<h2>{{ getFullname() }}</h2>

<!-- 2.computed -->
<h2>{{ fullname }}</h2>
<h2>{{ fullname }}</h2>
<h2>{{ fullname }}</h2>
<!-- 修改name值 -->
<button @click="changeLastname">修改lastname</button>
</div>
<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
firstName: "kobe",
lastName: "bryant"
}
},
methods: {
getFullname() {
console.log("getFullname-----")
return this.firstName + " " + this.lastName
},
changeLastname() {
this.lastName = "why"
}
},
computed: {
fullname() {
console.log("computed fullname-----")
return this.firstName + " " + this.lastName
}
}
})
app.mount("#app")
</script>
</body>
  • 为什么计算属性只执行一次,而方法使用多次次,就调用多少次
    • 这是因为计算属性会基于它们的依赖关系进行缓存
    • 在数据不发生变化时,计算属性是不需要重新计算的;
    • 但是如果依赖的数据发生变化,在使用时,计算属性依然会重新进行计算;

getter和setter

  • 计算属性在大多数情况下,只需要一个 getter 方法即可,所以我们会将计算属性直接写成一个函数。
  • 但是,如果我们确实想设置计算属性的值呢?
    • 这个时候我们也可以给计算属性设置一个 setter 的方法;
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="app">
<h2>{{ fullname }}</h2>
<button @click="setFullname">设置fullname</button>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
firstname: "coder",
lastname: "why"
}
},
computed: {
// 语法糖的写法,相当于调用 getter
// fullname() {
// return this.firstname + " " + this.lastname
// },
// 完整的写法:
fullname: {
// 读取计算属性 fullname 时调用 getter ,读取到的值以及模板中使用的的值就是 getter 的返回值
get: function () {
return this.firstname + " " + this.lastname
},
// 形参 value 为修改后的新值
set: function (value) {
const names = value.split(" ")
this.firstname = names[0]
this.lastname = names[1]
}
}
},
methods: {
setFullname() {
this.fullname = "kobe bryant"
}
}
})
app.mount("#app")
</script>
</body>
  • 你可能觉得很奇怪,Vue 内部是如何对我们传入的是一个 getter,还是说是一个包含 setter 和 getter 的对象进行处理的呢?
  • 事实上非常的简单,Vue 源码内部只是做了一个逻辑判断而已;

1660961806214

监听属性

认识侦听器

  • 什么是侦听器呢?

    • 开发中我们在 data 返回的对象中定义了数据,这个数据通过插值语法等方式绑定到 template 中;
    • 当数据变化时,template 会自动进行更新来显示最新的数据;
    • 但是在某些情况下,我们希望在代码逻辑中监听某个数据的变化,这个时候就需要用侦听器 watch 来完成了;
  • 侦听器的用法如下:

    • 选项:watch
    • 类型:{ [key: string]: string | Function | Object | Array}
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
<body>
<div id="app">
<h2>{{message}}</h2>
<button @click="changeMessage">修改message</button>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
message: "Hello Vue",
info: { name: "why", age: 18 }
}
},
methods: {
changeMessage() {
this.message = "你好啊, 李银河!"
this.info = { name: "kobe" }
}
},
watch: {
// 监听 message 默认有两个参数: newValue/oldValue
message(newValue, oldValue) {
console.log("message数据发生了变化:", newValue, oldValue)
},
// 监听 info 对象
info(newValue, oldValue) {
// 2.如果是对象类型, 那么拿到的是代理对象
console.log("info数据发生了变化:", newValue, oldValue)
console.log(newValue.name, oldValue.name)

// 3.将代理对象转为原生对象
console.log({ ...newValue }) // 此处生成的是一个新的对象
console.log(Vue.toRaw(newValue))
}
}
})
app.mount("#app")
</script>
</body>

侦听器配置项

  • 我们先来看一个例子:
    • 当我们点击按钮的时候会修改 info.name 的值;
    • 这个时候我们使用 watch 来侦听 info,可以侦听到吗?答案是不可以。
  • 这是因为默认情况下,watch 只是在侦听对象 info 的引用变化,对于内部属性的变化是不会做出响应的
    • 这个时候我们可以使用一个选项 deep 进行更深层的侦听;
    • 注意前面我们说过 watch 里面侦听的属性对应的也可以是一个 Object;
  • 还有另外一个属性,是希望一开始的就会立即执行一次:
    • 这个时候我们使用 immediate 选项;
    • 这个时候无论后面数据是否有变化,侦听的函数都会优先执行一次;
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
<body>
<div id="app">
<h2>{{ info.name }}</h2>
<button @click="changeInfo">修改info</button>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
info: { name: "why", age: 18 }
}
},
methods: {
changeInfo() {
// 1.创建一个新对象, 赋值给info 直接修改了原对象,能监听到数据的变化
// this.info = { name: "kobe" }

// 2.直接修改原对象某一个属性 修改的是对象中的某个属性,默认watch监听不会进行深度监听,开启深度监听即可监听到
this.info.name = "kobe"
}
},
watch: {
// 默认watch监听不会进行深度监听
// info(newValue, oldValue) {
// console.log("侦听到info改变:", newValue, oldValue)
// }

// 进行深度监听
info: {
// 只要监听到数据的变化, handler 方法就会调用 , 两个参数分别为新值 和 旧值
handler(newValue, oldValue) {
console.log("侦听到info改变:", newValue, oldValue)
console.log(newValue === oldValue)
},
deep: true, // 开启深度监听
immediate: true, // 第一次渲染直接执行一次监听器
},
// 单独监听对象中的指定属性
"info.name": function (newValue, oldValue) {
console.log("name发生改变:", newValue, oldValue)
}
}
})
app.mount("#app")
</script>
</body>

$watch API

  • 还有另外一种方式就是使用 $watch 的API:
  • 我们可以在 created 的生命周期中,使用 this.$watchs 来侦听;
    • 第一个参数是要侦听的源;
    • 第二个参数是侦听的回调函数 callback;
    • 第三个参数是一个对象,额外的其他选项,比如 deep、immediate ;
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
<body>
<div id="app">
<h2>{{message}}</h2>
<button @click="changeMessage">修改message</button>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
message: "Hello Vue"
}
},
methods: {
changeMessage() {
this.message = "你好啊, 李银河!"
}
},
// created : 生命周期回调函数: 当前的组件被创建时自动执行,一般在该函数中, 会进行网络请求
created() {
this.$watch("message", (newValue, oldValue) => {
console.log("message数据变化:", newValue, oldValue)
}, { deep: true })
}
})
app.mount("#app")
</script>
</body>

案例_书籍购物车(空)

  • 案例说明:
    • 01 在界面上以表格的形式,显示一些书籍的数据;
    • 02 在底部显示书籍的总价格;
    • 03 点击 + 或者 - 可以增加或减少书籍数量(如果为1,那么不能继续-);
    • 04 点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空~);

1660967787189

1
2
//...

双向数据绑定

v-model的基本使用

  • 表单提交是开发中非常常见的功能,也是和用户交互的重要手段:
    • 比如用户在登录、注册时需要提交账号密码;
    • 比如用户在检索、创建、更新信息时,需要提交一些数据;
  • 这些都要求我们可以在代码逻辑中获取到用户提交的数据,我们通常会使用 v-model 指令来完成:
    • v-model 指令可以在表单 input、textarea 以及 select 元素上创建双向数据绑定;
    • 它会根据控件类型自动选取正确的方法来更新元素;
    • 尽管有些神奇,但 v-model 本质上不过是语法糖,它负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理;

1660977072972

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body>
<div id="app">
<!-- v-model 实现双向数据绑定 , 绑定 input 的 value 属性 -->
<input type="text" v-model="message">
<h2>{{message}}</h2>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
message: "Hello Model",
}
},
})
app.mount("#app")
</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
<body>
<div id="app">
<!-- 双向数据绑定,将 data 中的账号密码,与输入框中的账号密码双向数据绑定,绑定的是输入框的 value 值-->
<label for="account">
账号:<input id="account" type="text" v-model="account">
</label>
<label for="password">
密码:<input id="password" type="password" v-model="password">
</label>
<button @click="loginClick">登录</button>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() { // 数据
return {
account: "",
password: ""
}
},
methods: {
// 登录按钮的事件回调,获取到 data 中的账号密码,进行其他业务处理
loginClick() {
const account = this.account
const password = this.password
}
}
})
app.mount("#app")
</script>
</body>

v-model的原理

  • 官方有说到,v-model 的原理其实是背后有两个操作:
    • v-bind 绑定 value 属性的值
    • v-on 绑定 input 事件监听到函数中,函数会获取最新的值, 赋值到绑定的属性中
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
<body>
<div id="app">
<!-- 01 将 data 中的数据单向绑定到输入框的 value 值, 通过 input 事件触发回调 -->
<input type="text" :value="message" @input="inputChange">
<h2>{{message}}</h2>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
message: "Hello Model",
}
},
methods: {
// 02 在 input 的事件回调中,修改 data 中的值,data 中的值改变,又会重新渲染模板,页面数据更新
inputChange(event) {
this.message = event.target.value
}
}
})
app.mount("#app")
</script>
</body>

绑定 textarea

  • v-model 默认绑定的是输入框的 value 值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body>
<div id="app">
<textarea cols="30" rows="10" v-model="content"></textarea>
<p>输入的内容: {{content}}</p>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
content: ""
}
},
})
app.mount("#app")
</script>
</body>

绑定 checkbox

  • 我们来看一下 v-model 绑定 checkbox ,单个勾选框和多个勾选框
  • 单个勾选框:
    • v-model 即为布尔值。
    • 此时 input 的 value 属性并不影响 v-model 的值。
  • 多个复选框:
    • 当是多个复选框时,因为可以选中多个,所以对应的 data 中属性是一个数组。
    • 多选框当中, 必须明确的绑定一个 value 值 ,会将 value 绑定到 data 中
    • 当选中某一个复选框时,就会将 input 的 value 值添加到数组中。
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="app">
<!-- 01 checkbox单选框: 绑定到属性中的值是一个 Boolean -->
<label for="agree">
<input id="agree" type="checkbox" v-model="isAgree"> 同意协议
</label>
<h2>单选框: {{isAgree}}</h2>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
isAgree: false, // 默认值为 false ,则复选框不会被勾选
}
},
})
app.mount("#app")
</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
<body>
<div id="app">
<!-- 02 checkbox多选框: 绑定到属性中的值是一个Array数组 -->
<div class="hobbies">
<h2>请选择你的爱好:</h2>
<label for="sing">
<input id="sing" type="checkbox" v-model="hobbies" value="sing">
</label>
<label for="jump">
<input id="jump" type="checkbox" v-model="hobbies" value="jump">
</label>
<label for="rap">
<input id="rap" type="checkbox" v-model="hobbies" value="rap"> rap
</label>
<label for="basketball">
<input id="basketball" type="checkbox" v-model="hobbies" value="basketball"> 篮球
</label>
<h2>爱好: {{hobbies}}</h2>
</div>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
hobbies: []
}
},
})
app.mount("#app")
</script>
</body>

绑定 radio

  • v-model 会将单选框的 value 值绑定到 data 中
  • 单选框的 value 值与 data 中的值相同,就会被选中
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
<body>
<div id="app">
<div class="gender">
<label for="male">
<input id="male" type="radio" v-model="gender" value="male">
</label>
<label for="female">
<input id="female" type="radio" v-model="gender" value="female">
</label>
<h2>性别: {{gender}}</h2>
</div>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
gender: "female" // 默认值
}
},
})
app.mount("#app")
</script>
</body>

绑定 select

  • 和 checkbox 一样,select 也分单选和多选两种情况。
  • 单选:只能选中一个值
    • v-model 绑定的是一个值;
    • 当我们选中 option 中的一个时,会将它对应的 value 赋值到 data 中;
  • 多选:可以按住 Ctrl 和 Shift 选中多个值
    • v-model 绑定的是一个数组;
    • 当选中多个值时,就会将选中的 option 对应的 value 添加到 data 的数组 fruits 中;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<body>
<div id="app">
<!-- select的单选 v-model指令写在 select 标签中,每一个 option 选项都需要有一个 value 值,会将该 value 值绑定到 data 中-->
<select v-model="fruit">
<option value="apple">苹果</option>
<option value="orange">橘子</option>
<option value="banana">香蕉</option>
</select>
<h2>单选: {{fruit}}</h2>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
fruit: "orange"
}
},
})
app.mount("#app")
</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
<body>
<div id="app">
<!-- select的多选 v-model指令写在 select 标签中,每一个 option 选项都需要有一个 value 值,会将该 value 值绑定到 data 中-->
<select multiple size="3" v-model="fruits">
<option value="apple">苹果</option>
<option value="orange">橘子</option>
<option value="banana">香蕉</option>
</select>
<h2>多选: {{fruits}}</h2>
</div>

<script src="../lib/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
fruits: []
}
},
})
app.mount("#app")
</script>
</body>

v-model的值绑定

  • 目前我们在前面的案例中大部分的值都是在 template 中固定好的:
    • 比如 gender 的两个输入框值 male、female;
    • 比如 hobbies 的三个输入框值 basketball、football、tennis;
  • 在真实开发中,我们的数据可能是来自服务器的,那么我们就可以先将值请求下来,绑定到 data 返回的对象中,再通过 v-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
43
44
45
46
47
48
49
50
51
52
53
<body>
<div id="app">
<!-- 1.select的值绑定 -->
<select multiple size="3" v-model="fruits">
<option v-for="item in allFruits"
:key="item.value"
:value="item.value">
{{item.text}}
</option>
</select>
<h2>多选: {{fruits}}</h2>

<!-- 2.checkbox的值绑定 -->
<div class="hobbies">
<h2>请选择你的爱好:</h2>
<template v-for="item in allHobbies" :key="item.value">
<label :for="item.value">
<input :id="item.value" type="checkbox" v-model="hobbies" :value="item.value"> {{item.text}}
</label>
</template>
<h2>爱好: {{hobbies}}</h2>
</div>
</div>

<script src="../lib/vue.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data() {
return {
// 水果 allFruits 为服务器返回的数据 , fruits 为用户的选择结果
allFruits: [
{ value: "apple", text: "苹果" },
{ value: "orange", text: "橘子" },
{ value: "banana", text: "香蕉" },
],
fruits: [],

// 爱好 allHobbies 为服务器返回的数据 , hobbies 为用户的选择结果
allHobbies: [
{ value: "sing", text: "唱" },
{ value: "jump", text: "跳" },
{ value: "rap", text: "rap" },
{ value: "basketball", text: "篮球" }
],
hobbies: []
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>

v-model 的修饰符

.lazy

  • lazy 修饰符是什么作用呢?
    • 默认情况下,v-model 在进行双向绑定时,绑定的是 input 事件,那么会在每次内容输入后就将最新的值和绑定的属性进行同步;
    • 如果我们在 v-model 后跟上 lazy 修饰符,那么会将绑定的事件切换为 change 事件,只有在表单失去焦点时(比如回车)才会触发;
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="app">
<!-- 1.lazy: 绑定 change 事件,只有表单失去焦点时才会绑定数据到 data 中 -->
<input type="text" v-model.lazy="message">
<h2>message: {{message}}</h2>
</div>

<script src="../lib/vue.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data() {
return {
message: "Hello Vue"
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>

.number

  • v-model 绑定后的值是什么类型的:
    • 绑定的 message 总是 string 类型,即使在我们设置 type 为 number ,绑定到 data 中的值也是 string 类型;
    • 在我们进行逻辑判断时,如果是一个 string 类型,在可以转化的情况下会进行隐式转换的:
  • 如果我们希望转换为数字类型,那么可以使用 .number 修饰符,但只限输入的是数字, 如果输入其他非数字字符,依旧会转为字符串类型
  • 将 input 的类型指定为 number 类型,也可以将输入的内容转为 number 类型,并且 type="number", 只能输入数字
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
<body>
<div id="app">
<!-- 2.1 .number: 自动将内容转换成数字 -->
<input type="text" v-model.number="counter">
<h2>counter:{{counter}}-{{typeof counter}}</h2>

<!-- 2.2 将 input 的类型指定为 number 类型,也可以将输入的内容转为 number 类型-->
<input type="number" v-model="counter2">
<h2>counter2:{{counter2}}-{{typeof counter2}}</h2>
</div>

<script src="../lib/vue.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data() {
return {
counter: 0,
counter2: 0,
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>

.trim

  • 如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 .trim 修饰符:
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
<body>
<div id="app">
<!-- 3.trim: 去除收尾的空格 -->
<input type="text" v-model.trim="content">
<h2>content: {{content}}</h2>
</div>

<script src="../lib/vue.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data() {
return {
content: ""
}
},
watch: {
// 监听 content 属性
content(newValue) {
console.log("content:", newValue)
}
}
})
// 2.挂载app
app.mount("#app")
</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
<body>
<div id="app">
<!-- 4. 同时使用多个修饰符 -->
<input type="text" v-model.lazy.trim="content">
<h2>content: {{content}}</h2>
</div>

<script src="../lib/vue.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data() {
return {
content: ""
}
},
watch: {
content(newValue) {
console.log("content:", newValue)
}
}
})
// 2.挂载app
app.mount("#app")
</script>
</body>

组件化开发

复杂问题的解决

  • 人面对复杂问题的处理方式:
    • 任何一个人处理信息的逻辑能力都是有限的
    • 所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。
    • 但是,我们人有一种天生的能力,就是将问题进行拆解。
    • 如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。

1660985670801

认识组件化开发

  • 组件化也是类似的思想:
    • 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展;
    • 但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了;
    • 如果我们将一个个功能块拆分后,就可以像搭建积木一下来搭建我们的项目;

1660985825360

  • 现在可以说整个的大前端开发都是组件化的天下,

    • 无论从三大框架(Vue、React、Angular),还是跨平台方案的 Flutter,甚至是移动端都在转向组件化开发,包括小程序的开发也是采用组件化开发的思想。
  • 所以,学习组件化最重要的是它的思想,每个框架或者平台可能实现方法不同,但是思想都是一样的。

  • 我们需要通过组件化的思想来思考整个应用程序:

    • 我们将一个完整的页面分成很多个组件;
    • 每个组件都用于实现页面的一个功能块;
    • 而每一个组件又可以进行细分;
    • 而组件本身又可以在多个地方进行复用;

Vue 的组件化

  • 组件化是 Vue、React、Angular 的核心思想,也是我们后续课程的重点(包括以后实战项目):
    • 前面我们的 createApp 函数传入了一个对象 App,这个对象其实本质上就是一个组件,也是我们应用程序的根组件;
    • 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用;
    • 任何的应用都会被抽象成一颗组件树;

1660986197775

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="app">
<h2>{{message}}</h2>
</div>

<script src="../lib/vue.js"></script>
<script>
// App组件(根组件)
const App = {
data() {
return {
message: "Hello Vue"
}
}
}
// 1.创建app
const app = Vue.createApp(App)
// 2.挂载app
app.mount("#app")
</script>
</body>

Vue组件的注册

  • 如果我们现在有一部分内容(模板、逻辑等),我们希望将这部分内容抽取到一个独立的组件中去维护,这个时候如何注册一个组件呢?

  • 注册组件分成两种:

    • 全局组件:在任何其他的组件中都可以使用的组件;
    • 局部组件:只有在注册的组件中才能使用的组件;

注册全局组件

  • 我们先来学习一下全局组件的注册:

    • 全局组件需要使用我们全局创建的 app 来注册组件;
    • 通过 component 方法传入组件名称、组件对象即可注册一个全局组件了;
    • 之后,我们可以在 App 组件的 template 中直接使用这个全局组件:
  • 我们组件本身也可以有自己的代码逻辑:

    • 比如自己的 data、computed、methods 等等
  • 全局组件的特点: 一旦注册成功后, 可以在任意其他组件的 template 中使用

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
<body>
<div id="app">
<!-- 4. 使用组件: 组件可以多次使用 -->
<product-item></product-item>
<product-item></product-item>
</div>

<!-- 组件 product-item 的模板 -->
<template id="item">
<div class="product">
<h2>我是商品</h2>
<div>商品图片</div>
<div>商品价格: <span>¥9.9</span></div>
<p>商品描述信息, 9.9秒杀</p>
</div>
</template>

<script src="../lib/vue.js"></script>
<script>
// 1.组件: App组件(根组件)
const App = {}

// 2.创建app
const app = Vue.createApp(App)

// 3.注册一个全局组件 product-item 全局组件
// 参数一:为使用组件时的标签名 参数二: 为该全局组件的配置对象
app.component("product-item", {
template: "#item" // 模板
})

// 2.挂载app
app.mount("#app")
</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
37
38
39
40
41
42
43
44
45
46
47
48
49
<body>
<div id="app">
<!-- 4. 在容器中使用组件 -->
<product-item></product-item>
<product-item></product-item>
<product-item></product-item>
</div>

<!-- product-item 组件的模板 -->
<template id="product">
<div class="product">
<h2>{{title}}</h2>
<p>商品描述, 限时折扣, 赶紧抢购</p>
<p>价格: {{price}}</p>
<button @click="favarItem">收藏</button>
</div>
</template>

<script src="../lib/vue.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
data() {
return {

}
},
})

// 3. 注册全局组件,以及该组件的逻辑,业务处理等
app.component("product-item", {
template: "##product",
data() {
return {
title: "我是商品Item",
price: 9.9
}
},
methods: {
favarItem() {
console.log("收藏了当前的item")
}
}
})

// 2.挂载app
app.mount("#app")
</script>
</body>

注册局部组件

  • 全局组件往往是在应用程序一开始就会全局注册完成,那么就意味着如果某些组件我们并没有用到,也会一起被注册:
    • 比如我们注册了三个全局组件:ComponentA、ComponentB、ComponentC;
    • 在开发中我们只使用了 ComponentA、ComponentB,如果 ComponentC 没有用到但是我们依然在全局进行了注册,那么就意味着类似于 webpack 这种打包工具在打包我们的项目时,我们依然会对其进行打包;
    • 这样最终打包出的 JavaScript 包就会有关于 ComponentC 的内容,用户在下载对应的 JavaScript 时也会增加包的大小;
  • 所以在开发中我们通常使用组件的时候采用的都是局部注册:
    • 局部注册是在我们需要使用到的组件中,通过 components 属性选项来进行注册;
    • 比如之前的 App 组件中,我们有 data、computed、methods 等选项了,事实上还可以有一个 components 选项;
    • 该 components 选项对应的是一个对象,对象中的键值对是 组件的名称: 组件对象
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
<body>
<div id="app">
<!-- 3. 在 App 跟组件中使用自己的两个子组件 -->
<home-nav></home-nav>
<product-item></product-item>
<product-item></product-item>
<product-item></product-item>
</div>

<!-- ProductItem 组件的模板 -->
<template id="product">
<div class="product">
<h2>{{title}}</h2>
<p>商品描述, 限时折扣, 赶紧抢购</p>
<p>价格: {{price}}</p>
<button>收藏</button>
</div>
</template>

<!-- HomeNav 组件的模板 -->
<template id="nav">
<h1>我是home-nav的组件</h1>
<!-- 3. 在模板中使用自己的子组件 -->
<product-item></product-item>
</template>

<script src="../lib/vue.js"></script>
<script>

// 2 创建 ProductItem 组件
const ProductItem = {
template: "##product", // 组件模板
data() { // 组件的数据
return {
title: "我是product的title",
price: 9.9
}
}
}

// 1 创建 App 根组件
const app = Vue.createApp({
components: { // 注册组件
ProductItem, // 将 ProductItem 组件注册成 App 组件的子组件
HomeNav: { // 将 HomeNav 组件注册成 App 组件的子组件
template: "#nav",
components: {
ProductItem // 将 ProductItem 组件注册成 HomeNav 组件的子组件
}
}
},
data() {
return {
message: "Hello Vue"
}
}
})

// 2.挂载app
app.mount("#app")
</script>
</body>

组件名的命名

  • 在通过 app.component 注册一个组件的时候,第一个参数是组件的名称,定义组件名的方式有两种:
  • 方式一:使用 kebab-case(短横线分割符)
    • 当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case
      • 例如 <my-component-name>
  • 方式二:使用 PascalCase(驼峰标识符)
    • 当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。
    • 也就是说 <my-component-name> <MyComponentName> 都是可接受的;

单文件组件

Vue 的开发模式

  • 目前我们使用 vue 的过程都是在 html 文件中,通过 template 编写自己的模板、脚本逻辑、样式等。
  • 但是随着项目越来越复杂,我们会采用组件化的方式来进行开发:
    • 这就意味着每个组件都会有自己的模板、脚本逻辑、样式等;
    • 当然我们依然可以把它们抽离到单独的 js、css 文件中,但是它们还是会分离开来;
    • 也包括我们的 script 是在一个全局的作用域下,很容易出现命名冲突的问题;
    • 并且我们的代码为了适配一些浏览器,必须使用 ES5 的语法;
    • 在我们编写代码完成之后,依然需要通过构建工具对代码进行构建打包处理;
  • 所以在真实开发中,我们可以通过一个后缀名为.vue的 single-file components (单文件组件) 来解决,并且可以使用 webpack 或者 vite 或者 rollup 等构建工具来对其进行处理。

单文件组件的特点

  • 在这个组件中我们可以获得非常多的特性:
    • 代码的高亮;
    • ES6、CommonJS 的模块化能力;
    • 组件作用域的 CSS;
    • 可以使用预处理器来构建更加丰富的组件,比如 TypeScript、Babel、Less、Sass 等;

单文件组件的支持

  • 如果我们想要使用这一的 SFC 的.vue文件,比较常见的是两种方式:
    • 方式一:使用 Vue CLI 来创建项目,项目会默认帮助我们配置好所有的配置选项,可以在其中直接使用.vue文件;
    • 方式二:自己使用 webpack 或 rollup 或 vite 这类打包工具,对其进行打包处理;
  • 最终无论是后期我们做项目,还是在公司进行开发,通常都会采用 Vue CLI 的方式来完成。

VScode对SFC的支持

  • 在前面我们提到过,真实开发中多数情况下我们都是使用 SFC( single-file components (单文件组件) )。

  • 我们先说一下 VSCode 对 SFC 的支持:

    • 插件一:Vetur,从 Vue2 开发就一直在使用的 VSCode 支持 Vue 的插件;
    • 插件二:Volar,官方推荐的插件;

Vue脚手架

脚手架的介绍

  • 什么是 Vue 脚手架?
    • 我们前面学习了如何通过 webpack 配置 Vue 的开发环境,但是在真实开发中我们不可能每一个项目从头来完成所有的 webpack 配置,这样开发的效率会大大的降低;
    • 所以在真实开发中,我们通常会使用脚手架来创建一个项目,Vue 的项目我们使用的就是 Vue 的脚手架;
    • 脚手架其实是建筑工程中的一个概念,在我们软件工程中也会将一些帮助我们搭建项目的工具称之为脚手架;
  • Vue 的脚手架就是 Vue CLI:
    • CLI 是 Command-Line Interface, 翻译为命令行界面;
    • 我们可以通过 CLI 选择项目的配置和创建出我们的项目;
    • Vue CLI 已经内置了 webpack 相关的配置,我们不需要从零来配置;

脚手架的安装

  • 安装 Vue CLI

    • 我们是进行全局安装,这样在任何时候都可以通过 vue 的命令来创建项目;
    • 命令 : npm install @vue/cli -g
  • 升级脚手架:

    • 如果是比较旧的版本,可以通过下面的命令来升级
    • 命令: npm update @vue/cli -g

用脚手架创建项目

  • 通过 Vue 的命令来创建项目

    • 命令: vue create 项目的名称
  • 项目的创建过程:

1660998056066

项目的目录结构

1660998089776

  • jsconfig.json 的作用 : 给 VSCode 来进行读取, VSCode 在读取到其中的内容时, 给我们的代码更加友好的提示

jsconfig.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"compilerOptions": {
"target": "es5", // 最终转为 ES5 版本的代码
"module": "esnext", // 使用的模块化
"baseUrl": "./", // 基础路径
"moduleResolution": "node", // 按 node 的查找规则查找模块
"paths": { // 路径别名
"@/*": [
"src/*"
],
"utils/*": [
"src/utils/*"
]
},
"lib": [ // 项目中可能用到的库
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
  • 配置路径别名:

vue.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
resolve: {
// 配置路径别名 , @ 是已经配置好的路径别名: 对应的是src路径
alias: {
"utils": "@/utils"
}
}
}
})

关于vue的版本

  • 引入的 vue 的版本
    • 默认 vue 版本: runtime, vue-loader 完成 template 的编译过程
    • vue.esm-bundler : runtime + compile, 对 template 进行编译

单文件组件的使用

  • 01 新建 ./src/components/AppHeader.vue 单文件组件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div></div>
</template>

<script>
export default {
name:"AppHeader",
components: {},
data() {
return {};
},
methods: {},
};
</script>

<style scoped lang="less"></style>
  • 02 在 App.vue 根组件中引入并注册使用 AppHeader.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
<template>
<div class="app">
<h2 class="title">我是App.vue中的h2元素</h2>
<!-- 03 使用 AppHeader 组件 -->
<AppHeader></AppHeader>
</div>
</template>

<script>
// 01 导入 AppHeader.vue 组件
import AppHeader from "./components/AppHeader"

export default {
name: 'App',
components: {
// 02 注册 AppHeader.vue 组件
AppHeader
}
}
</script>

<style scoped>
.title {
color: red;
}
</style>

CSS的作用域

  • 在 单文件组件中 , 给 style 标签添加 scoped 即可让当前的 CSS 样式仅在当前组件中生效,不添加 scoped 关键字,则样式全局生效

用Vite创建Vue项目

  • 01 输入命令: npm init vue@latest
  • 02 安装依赖: npm install
  • 03 运行项目: npm run dev

脚手架的运行原理

1660998125458

组件间通信

组件的嵌套与拆分

  • 前面我们是将所有的逻辑放到一个 App.vue 中:

    • 在之前的案例中,我们只是创建了一个组件 App;
    • 如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护;
    • 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;
    • 再将这些组件组合嵌套在一起,最终形成我们的应用程序;
  • 我们来分析一下下面代码的嵌套逻辑,假如我们将所有的代码逻辑都放到一个 App.vue 组件中:

    • 我们会发现,将所有的代码逻辑全部放到一个组件中,代码是非常的臃肿和难以维护的。
    • 并且在真实开发中,我们会有更多的内容和代码逻辑,对于扩展性和可维护性来说都是非常差的。
    • 所以,在真实的开发中,我们会对组件进行拆分,拆分成一个个功能的小组件。
  • 我们可以按照如下的方式进行拆分:

1661005868890

  • 按照如上的拆分方式后,我们开发对应的逻辑只需要去对应的组件编写就可。

  • 01 在 ./src/components 文件夹中分别创建上述的五个组件, Header.vue , Main.vue , Footer.vue , Banner.vue , ProducList.vue

  • 02 在 App.vue 组件中,将 Header.vue , Main.vue , Footer.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
<template>
<div>
<!-- 03 使用组件 -->
<Header></Header>
<Main></Main>
<Footer></Footer>
</div>
</template>

<script>
// 01 导入组件
import Header from './components/Header.vue'
import Main from './components/Main.vue'
import Footer from './components/Footer.vue'

export default {
name:"App",
// 02 注册组件
components: {Header,Main,Footer},
};
</script>

<style scoped lang="less"></style>

  • 03 在 Main.vue: 组件中, 将 Banner.vue , ProducList.vue 两个组件注册为子组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<!-- 03 使用组件 -->
<Banner></Banner>
<ProducList></ProducList>
</div>
</template>

<script>
// 01 导入组件
import Banner from "./components/Banner.vue";
import ProducList from "./components/ProducList.vue";
export default {
name: "Main",
// 02 注册组件
components: { Banner, ProducList },
};
</script>

<style scoped lang="less"></style>

组件间通信介绍

  • 上面的嵌套逻辑如下,它们存在如下关系:

    • App 组件是 Header、Main、Footer 组件的父组件;
    • Main 组件是 Banner、ProductList 组件的父组件;
  • 在开发过程中,我们会经常遇到需要组件之间相互进行通信,即组件间相互传递数据:

    • 比如 App 可能使用了多个 Header,每个地方的 Header 展示的内容不同,那么我们就需要使用者传递给 Header 一些数据,让其进行展示;
    • 又比如我们在 Main 中一次性请求了 Banner 数据和 ProductList 数据,那么就需要传递给它们来进行展示;
    • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
  • 总之,在一个 Vue 项目中,组件之间的通信是非常重要的环节,所以接下来我们就具体学习一下组件之间是如何相互之间传递数据的;

组件间通信方式

  • 父子组件之间如何进行通信呢?
    • 父组件传递给子组件:子组件通过 props 属性接收由父组件传递过来的数据
    • 子组件传递给父组件:子组件通过自定义事件 $emit 触发事件并携带要传递的数据,父组件监听子组件传递的自定义事件;

1661007932753

父组件传向子组件

  • 在开发中很常见的就是父子组件之间通信,比如父组件有一些数据,需要子组件来进行展示:
    • 这个时候我们可以通过 props 来完成组件之间的通信;
  • 什么是 Props 呢?
    • Props 是你可以在组件上注册一些自定义的 attribute;
    • 父组件给这些 attribute 赋值,子组件通过 attribute 的名称获取到对应的值;
  • Props 有两种常见的用法:
    • 方式一:字符串数组,数组中的字符串就是 attribute 的名称;
      • 弊端:
        • 01 不能对类型进行验证
        • 02 没有默认值
    • 方式二:对象类型,对象类型我们可以在指定 attribute 名称的同时,指定它需要传递的类型、是否是必须的、默认值等等;
      • 如果默认值是 对象 或者 数组, 则要求 default 必须是一个函数, 返回要设置的默认值

数组类型接收

父组件 App.vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<!-- 03 使用子组件并传递数据 -->
<show-info name="why" :age="18" :height="1.88" address="广州市" abc="cba" class="active" />
</template>

<script>
// 01 导入子组件
import ShowInfo from './components/ShowInfo.vue'

export default {
name: 'App',
components: {
// 02 注册子组件
ShowInfo,
},
}
</script>

**子组件 ShowInfo.vue : **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="infos">
<!-- 05 使用父组件传递过来的数据 -->
<h2>姓名: {{ name }}</h2>
<h2>年龄: {{ age }}</h2>
<h2>身高: {{ height }}</h2>
</div>

<div class="others" v-bind="$attrs"></div>
</template>

<script>
export default {
// 04 接收父组件传递过来的数据 props 数组语法 弊端: 01 不能对类型进行验证 02 没有默认值
props: ["name", "age", "height"]
};
</script>

  • 数据由父组件传向子组件过程图解:

1661078657538

对象类型接收

  1. 数组用法中我们只能说明传入的 attribute 的名称,并不能对其进行任何形式的限制,接下来我们来看一下对象的写法是如何让我们的 props 变得更加完善的。

  2. 当使用对象语法的时候,我们可以对传入的内容限制更多:

    • 比如指定传入的 attribute 的类型;
    • 比如指定传入的 attribute 是否是必传的;
    • 比如指定没有传入时,attribute 的默认值;
  3. type 的类型都可以是以下类型: String Number Boolean Array Object Date Function Symbol

  4. Prop 的大小写命名(camelCase vs kebab-case):

    • HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符;
    • 这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名)命名;
  5. 在向子组件传递数据时,在属性前面加 : (v-bind),则值会成为 number 类型

    1. 只限于等号后面是数字才可以这么操作
    2. 如果等号后面的是字符串,而又加了:,则会把等号后面的字符串作为变量处理, 会报警告

父组件 App.vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<!-- 03 使用子组件并传递数据 在属性前面加 `:` ,则值会成为 number 类型,仅限数值型 -->
<show-info name="why" :age="18" :height="1.88" address="广州市" abc="cba" class="active" />
</template>

<script>
// 01 导入子组件
import ShowInfo from './components/ShowInfo.vue'

export default {
name: 'App',
components: {
// 02 注册子组件
ShowInfo,
},
}
</script>

子组件 ShowInfo.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
34
35
36
37
38
39
40
41
42
<template>
<div class="infos">
<h2>姓名: {{ name }}</h2>
<h2>年龄: {{ age }}</h2>
<h2>身高: {{ height }}</h2>
</div>

<div class="others" v-bind="$attrs"></div>
</template>

<script>
export default {
// 2.props对象语法(必须掌握)
props: {
name: {
type: String,
default: "我是默认name",
},
age: {
type: Number,
required: true, // 要求必须传递
default: 0,
},
height: {
type: Number,
default: 2,
},
// 对象类型写默认值时, 要求 default 必须是一个函数, 返回默认值
friend: {
type: Object,
default() {
return { name: "james" };
},
},
// 数组类型写默认值时, 要求 default 必须是一个函数, 返回默认值
hobbies: {
type: Array,
default: () => ["篮球", "rap", "唱跳"],
}
},
};
</script>

关于 $attrs

  • 什么是非 Prop 的 Attribute 呢?

    • 当我们传递给一个组件某个属性,但是该属性并没有定义对应的 props 或者 emits 时,就称之为 非 Prop 的 Attribute;
    • 常见的包括 class、style、id 属性等;
  • Attribute 继承

    • 当组件有单个根节点时,非 Prop 的 Attribute 将自动添加到根节点的 Attribute 中:
  • 如果我们不希望组件的根元素继承 attribute,可以在组件中设置 inheritAttrs: false

    • 禁用 attribute 继承的常见情况是需要将 attribute 应用于根元素之外的其他元素;
    • 我们可以通过 $attrs 来访问所有的 非 props 的 attribute;
  • 多个根节点的 attribute

    • 多个根节点的 attribute 如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上:
  • 对以上内容的简单解释 :

    • 组件在使用并往组件中传递属性(数据)时 , 如果在子组件中没有通过 props 接收,则会保存到一个叫 $attrs 的对象中
    • 并且会默认将 $attrs 对象中的属性和属性值绑定子组件的根标签上, 属性相同则会合并,不会覆盖原有的标签属性
    • 可以通过在子组件中添加 inheritAttrs: false,,取消绑定到根标签
    • 如果子组件中有多个根标签,若没有指定 $attrs 对象绑定到那个根标签,则会报警告
    • $attrs 对象是一个由父组件传递数据给子组件,但是子组件没有通过 props 接收的参数组成的普通对象
    • $attrs 对象的格式形如 : { “address”: “广州市”, “abc”: “cba”, “class”: “active” } , 通过 props 接收了的数据,不会出现在 $attrs 对象中

父组件 App.vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<!-- 03 使用子组件并传递数据 -->
<show-info name="why" :age="18" :height="1.88" address="广州市" abc="cba" class="active" />
</template>

<script>
// 01 导入子组件
import ShowInfo from './components/ShowInfo.vue'

export default {
name: 'App',
components: {
// 02 注册子组件
ShowInfo,
},
}
</script>

子组件 ShowInfo.vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="infos">
<!-- 05 使用父组件传递过来的数据 -->
<h2>姓名: {{ name }}</h2>
<h2>年龄: {{ age }}</h2>
<h2>身高: {{ height }}</h2>
</div>

<!-- 若有多个根标签,不指定 $attrs 对象绑定到那个根标签,就会报警告 -->
<!-- <div class="others" v-bind="$attrs"></div> -->
</template>

<script>
export default {
inheritAttrs: false, //取消默认绑定 $attrs 对象到根标签
// 04 接收父组件传递过来的数据 props 数组语法 弊端: 01 不能对类型进行验证 02 没有默认值的
props: ["name", "age", "height"]
};
</script>

子组件传向父组件

传递的过程介绍

  • 什么情况下子组件需要传递内容到父组件呢?
    • 当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容;
    • 子组件有一些内容想要传递给父组件的时候;
  • 我们如何完成上面的操作呢?
    • 首先,我们需要在子组件中定义好在某些情况下触发的事件名称;
    • 其次,在父组件中以 v-on 的方式传入要监听的事件名称,并且绑定到对应的方法中;
    • 最后,在子组件中发生某个事件的时候,根据事件名称触发对应的事件;

数据传递详细过程

子组件 SubCounter :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="sub">
<button @click="btnClick(1)">-1</button>
<button @click="btnClick(5)">-5</button>
<button @click="btnClick(10)">-10</button>
</div>
</template>

<script>
export default {
name:"SubCounter",
methods: {
// 01 按钮的事件回调
btnClick(count) {
// 02 发射自定义事件 sub , 并同时携带参数 count
this.$emit("sub", count);
},
},
};
</script>

子组件 AddCounter.vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="add">
<button @click="btnClick(1)">+1</button>
<button @click="btnClick(5)">+5</button>
<button @click="btnClick(10)">+10</button>
</div>
</template>

<script>
export default {
name:"AddCounter",
methods: {
// 01 按钮的事件回调
btnClick(count) {
// 02 子组件发出去一个自定义事件, 第一个参数自定义的事件名称,第二个参数是传递给父组件的参数
this.$emit("add", count);
},
},
};
</script>

父组件 App.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
34
35
36
<template>
<div class="app">
<h2>当前计数: {{ counter }}</h2>
<!-- 3. 自定义add-counter, 并且监听内部的add事件,并触发事件回调 -->
<add-counter @add="addBtnClick"></add-counter>
<!-- 3. 自定义sub-counter, 并且监听内部的sub事件,并触发事件回调 -->
<sub-counter @sub="subBtnClick"></sub-counter>
</div>
</template>

<script>
import AddCounter from "./components/AddCounter.vue";
import SubCounter from "./components/SubCounter.vue";
export default {
name:"App",
components: {
AddCounter,
SubCounter,
},
data() {
return {
counter: 0,
};
},
methods: {
// 4. 监听的子组件发射的自定义事件对应的回调,携带来的数据由count接收
addBtnClick(count) {
this.counter += count;
},
subBtnClick(count) {
this.counter -= count;
},
},
};
</script>

  • 子组件传递数据给父组件详细过程图解: (仅展示一个子组件与一个父组件之间的数据传递)

1661088280352

携带参数和验证

  • 在 vue3 当中,我们可以对传递的参数进行验证,以及对自定义事件的注册:
    • (验证数据和注册自定义事件为可选操作,不写也能正常传递数据)
  • 01 emits 数组语法,注册自定义事件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
export default {
// 1. emits 数组语法,注册自定义事件(常用,推荐,非必须操作,但能添加代码提示)
emits: ["add"],
methods: {
// 01 按钮的事件回调
btnClick(count) {
console.log("btnClick:", count);
// 子组件发出去一个自定义事件, 第一个参数自定义的事件名称,第二个参数是传递给父组件的参数
this.$emit("add", 100);
},
},
};
</script>

  • 02 emits 对象语法,并进行数据验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
export default {
// 2. emits 对象语法,并进行数据验证(不常用,非必须操作)
emits: {
add: function(count) {
if (count <= 10) {
return true // 验证通过
}
return false // 验证不通过 , 即使验证不通过,还是可以正常传递数据的,但是会报警告
}
},
methods: {
// 01 按钮的事件回调
btnClick(count) {
console.log("btnClick:", count);
// 子组件发出去一个自定义事件, 第一个参数自定义的事件名称,第二个参数是传递给父组件的参数
this.$emit("add", 100);
},
},
};
</script>

组件间通信案例

1661135610224

父组件 App.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
<template>
<div class="app">
<!-- 03 将要展示的数据传递给子组件 -->
<!-- 07 监听子组件的自定义事件及回调 -->
<TabControl :titles="pageContents" @tabItemClick="tabItemClick"></TabControl>
<h2>{{ pageContents[currentIndex] }}</h2>
</div>
</template>
<script>
// 01 导入注册并使用子组件
import TabControl from "./components/TabControl.vue";
export default {
name: "App",
components: {
TabControl,
},
data() {
return {
// 02 需要展示的数据
pageContents: ["衣服列表", "鞋子列表", "裤子列表"],
currentIndex: 0, // 用于控制显示那个内容
};
},
methods: {
// 08 自定义事件回调及业务处理
tabItemClick(index) {
this.currentIndex = index; // 用于控制显示那个内容
},
},
};
</script>

**子组件 TabControl.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<template>
<div class="tab-control">
<!-- 05 遍历数据生成结构 -->
<template v-for="(item, index) in titles" :key="item">
<div class="tab-control-item" :class="{ active: currentIndex === index }" @click="itemClick(index)"><span>{{ item }}</span></div>
</template>
</div>
</template>
<script>
export default {
name: "TabControl",
components: {},
// 04 子组件接收来自父组件的数据
props: {
titles: {
type: Array,
default: () => [],
},
},
data() {
return {
currentIndex: 0, // 用于控制 active 类名的切换
};
},
methods: {
// 06 事件回调,切换 active 类名,并将当前索引传递给父组件
itemClick(index) {
this.currentIndex = index; // 控制 active 类名的切换
this.$emit("tabItemClick", index); // 自定义事件,传数据给父组件
},
},
};
</script>

<style scoped>
.tab-control {
display: flex;
height: 44px;
line-height: 44px;
text-align: center;
}

.tab-control-item {
flex: 1;
}

.tab-control-item.active {
color: red;
font-weight: 700;
}

.tab-control-item.active span {
border-bottom: 3px solid red;
padding: 8px;
}
</style>

  • 案例业务逻辑大致流程图解:

1661136533354

插槽Slot

认识插槽Slot

  • 在开发中,我们会经常封装一个个可复用的组件:
    • 前面我们会通过 props 传递给组件一些数据,让组件来进行展示;
    • 但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的 div、span 等等这些元素;
    • 比如某种情况下我们使用组件,希望组件显示的是一个按钮,某种情况下我们使用组件希望显示的是一张图片;
    • 我们应该让使用者可以决定某一块区域到底存放什么内容和元素;
  • 举个栗子:假如我们定制一个通用的导航组件 - NavBar
    • 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定;
    • 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示;
    • 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等;
    • 右边可能是一个文字,也可能是一个图标,也可能什么都不显示;

1661136905929

使用插槽slot

  • 这个时候我们就可以来定义插槽 slot:
    • 插槽的使用过程其实是抽取共性、预留不同;
    • 我们会将共同的元素、内容依然在组件内进行封装;
    • 同时会将不同的元素使用 slot 作为占位,让外部决定到底显示什么样的元素;
  • 如何使用 slot 呢?
    • Vue 中将 slot 元素作为承载分发内容的出口;
    • 在封装组件中,使用特殊的元素 slot 就可以为封装组件开启一个插槽;
    • 该插槽插入什么内容取决于父组件如何使用;
image-20230312102310666

使用slot与默认内容

  • 有时候我们希望在使用插槽时,如果没有插入对应的内容,那么我们需要显示一个默认的内容:
    • 当然这个默认的内容只会在没有提供插入的内容时,才会显示;

**父组件 App.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
<template>
<div class="app">
<!-- 内容是一张图片 -->
<show-message>
<img src="@/img/04.webp" alt="" />
</show-message>

<!-- 内容是button -->
<show-message title="哈哈哈">
<button>我是按钮元素</button>
</show-message>

<!-- 内容是超链接 -->
<show-message>
<a href="#">百度一下</a>
</show-message>

<!-- 内容没有传递,就会使用插槽的默认内容 -->
<show-message></show-message>
</div>
</template>


<script>
import ShowMessage from "./components/ShowMessage.vue";
export default {
name: "App",
components: {
ShowMessage,
},
};
</script>

**子组件 ShowMessage.vue : **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<h2>{{ title }}</h2>
<div class="content">
<slot>
<!-- 插槽的默认内容 -->
<p>我是默认内容, 哈哈哈</p>
</slot>
</div>
</template>

<script>
export default {
name:"ShowMessage",
props: {
title: {
type: String,
default: "我是title默认值",
},
},
};
</script>

  • 插槽基本使用图解:

1661138040895

多个插槽的默认效果

  • 我们先测试一个知识点:如果一个组件中含有多个插槽,我们插入多个内容时是什么效果?
    • 我们会发现默认情况下每个插槽都会获取到我们插入的内容来显示;(组件标签中的所有内容会作为一个整体,插入到插槽 slot 中)

父组件 App.vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div class="app">
<!-- 内容是一张图片 -->
<show-message>
<div>
<span>hai~我是图片</span>
<img src="@/img/04.webp" alt="" />
<a href="##">百度一下</a>
</div>
</show-message>
</div>
</template>

**子组件 ShowMessage.vue : **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="content">
<slot>
<p>我是默认内容, 哈哈哈</p>
</slot>
<slot>
<p>我是默认内容, 哈哈哈</p>
</slot>
<slot>
<p>我是默认内容, 哈哈哈</p>
</slot>
</div>
</template>

具名插槽

  • 事实上,我们希望达到的效果是插槽对应的显示,这个时候我们就可以使用 具名插槽:
    • 具名插槽顾名思义就是给插槽起一个名字,slot 元素有一个特殊的 name 属性
    • 一个不带 name 的 slot,会带有隐含的默认名字 default
    • 具名插槽要配合 template 标签使用
    • 具名插槽会按照对应的 name,插入到指定的 slot 插槽中
  • 具名插槽使用的时候缩写:
    • 跟 v-on 和 v-bind 一样,v-slot 也有缩写;
    • 即把参数之前的所有内容 (v-slot:) 替换为字符 #

父组件 App.vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<nav-bar>
<!-- 02 使用具名插槽: 通过 v-slot:xxx 指示将内容插入到那一个插槽中 ,#left 是 v-slot:left 的简写 ,具名插槽要配合 template 标签使用-->
<template #left>
<button>返回</button>
</template>

<template #center>
<span>内容</span>
</template>

<template v-slot:right>
<a href="##">登录</a>
</template>
</nav-bar>
</template>

**子组件 ShowMessage.vue : **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="nav-bar">
<div class="left">
<!-- 01 具名插槽 : 用 slot 的 name 属性标识-->
<slot name="left">left</slot>
</div>
<div class="center">
<slot name="center">center</slot>
</div>
<div class="right">
<slot name="right">right</slot>
</div>
</div>

<div class="other">
<slot name="default"></slot>
</div>
</template>

  • 图解:

动态插槽名

  • 什么是动态插槽名呢?
    • 目前我们使用的插槽名称都是固定的;
    • 比如 v-slot:left、v-slot:center 等等;
    • 我们可以通过 v-slot:[dynamicSlotName] 方式动态绑定一个名称;

父组件 App.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
<template>
<!-- nav-bar只给一个插槽传入数据 position 作为一个变量,值来自于 data-->
<nav-bar>
<template v-slot:[position]>
<a href="#">注册</a>
</template>
</nav-bar>

<!-- 通过方法修改 position 的值 -->
<button @click=" position = 'left' ">左边</button>
<button @click=" position = 'center' ">中间</button>
<button @click=" position = 'right' ">右边</button>
</template>
<script>
import NavBar from './components/ShowMessage.vue'
export default {
components: {
NavBar
},
data() {
return {
position: "center"
}
}
}
</script>

**子组件 ShowMessage.vue : **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="nav-bar">
<div class="left">
<slot name="left">left</slot>
</div>
<div class="center">
<slot name="center">center</slot>
</div>
<div class="right">
<slot name="right">right</slot>
</div>
</div>

<div class="other">
<!-- 插槽 slot 不指定 name 值, name 会有一个默认值 default -->
<slot name="default"></slot>
</div>
</template>

<script>
export default {};
</script>

  • 图解:

1661147524832

渲染作用域

  • 在 Vue 中有渲染作用域的概念:
    • 父级模板里的所有内容都是在父级作用域中编译的;
    • 子模板里的所有内容都是在子作用域中编译的;
  • 如何理解这句话呢?我们来看一个案例:
    • 在我们的案例中 ChildCpn.vue 自然是可以使用自己作用域中的 title 内容的;
    • 但是在 App.vue 中,是访问不了 ChildCpn.vue 中的内容的,因为它们是跨作用域的访问;

1661147855415

作用域插槽

  • 但是有时候我们希望插槽可以访问到子组件中的内容是非常重要的:

    • 当一个组件被用来渲染一个数组元素时,我们使用插槽,并且希望插槽中没有显示每项的内容;
    • 这个 Vue 给我们提供了作用域插槽;
  • 我们来看下面的一个案例:

    • 1.在 App.vue 中定义好数据
    • 2.传递给 ShowNames 组件中
    • 3.ShowNames 组件中遍历 names 数据
    • 4.定义插槽的 prop
    • 5.通过 v-slot:default 的方式获取到 slot 的 props
    • 6.使用 slotProps 中的 item 和 index
  • 案例图解:

1661148108771

默认插槽的缩写

  • 如果我们的插槽是默认插槽 default,那么在使用的时候, v-slot:default="slotProps" 可以简写为 v-slot="slotProps"

1661148242579

  • 并且如果我们的插槽只有默认插槽时,组件的标签可以被当做插槽的模板来使用,这样,我们就可以将 v-slot 直接用在组件上:

1661148249289

具名与默认插槽混合

  • 但是,如果我们有默认插槽和具名插槽,那么按照完整的 template 来编写。

1661148370289

  • 只要出现多个插槽,请始终为所有的插槽使用完整的基于 template 的语法:

1661148378114

全局事件总线

全局事件总线的使用

  • Vue3 从实例中移除了 $on$off$once 方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库:
    • Vue3官方有推荐一些库,例如 mitt 或 tiny-emitter
    • 全局事件总线传递数据,跟组件之间的嵌套关系无关,任意组件都可以触发自定义事件提供数据,其他任意组件都可以监听自定义事件和接收数据
    • 父子组件间的数据传递,不推荐使用全局事件总线, 推荐使用父子组件间通信的 props 和 $emit 方法实现
  • 这里我们主要讲解一下 hy-event-store 的使用;
    • 首先,我们需要先安装这个库: npm install hy-event-bus
    • 其次,我们可以封装一个工具 **./src/utils/eventbus.js **:
1
2
3
4
5
6
7
// 导入第三方库
import { HYEventBus } from 'hy-event-store'
// 创建实例对象
const eventBus = new HYEventBus()
// 导出实例对象
export default eventBus

  • App.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
<template>
<div class="app">
<h2>App Message: {{ message }}</h2>
<home></home>
</div>
</template>

<script>
// 01 导入创建的第三方库的实例对象
import eventBus from './utils/event-bus'
import Home from './Home.vue'
export default {
components: {
Home,
Category
},
data() {
return {
message: "Hello App",
}
},
created() {
// 03 在需要数据的组件中监听自定义事件,数据由回调方法形参接收
// 参数1: 触发的自定义事件名 参数2: 自定义事件的回调
eventBus.on("whyEvent", (name, age, height) => {
console.log("whyEvent事件在app中监听", name, age, height)
this.message = `name:${name}, age:${age}, height:${height}`
})
}
}
</script>

HomeBanner.vue: 触发自定义事件,并提供数据 ( HomeBanner.vue 组件时 App.vue 组件的孙子组件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="banner">
<button @click="bannerBtnClick">banner按钮</button>
</div>
</template>

<script>
import eventBus from './utils/event-bus'

export default {
methods: {
bannerBtnClick() {
// 02 在某个组件中触发自定义事件并携带要传递的数据
console.log("bannerBtnClick")
eventBus.emit("whyEvent", "why", 18, 1.88)
}
}
}
</script>

全局事件总线的取消

  • 在某些情况下我们可能希望取消掉之前注册的函数监听:
    • 通过全局事件总线的实例对象的 on 方法,监听事件总线的自定义方法,并接收数据
    • 通过全局事件总线的实例对象的 off 方法 , 取消全局事件总线的事件监听
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
import eventBus from './utils/event-bus'
export default {
methods: {
// 将方法抽离出来,是为了能在多个生命周期函数中都能调用,方便取消全局事件总线的事件监听
whyEventHandler() {
console.log("whyEvent在category中监听")
}
},
created() {
// 通过全局事件总线的实例对象的 on 方法,监听事件总线的自定义方法,并接收数据
eventBus.on("whyEvent", this.whyEventHandler)
},
unmounted() {
// 通过全局事件总线的实例对象的 off 方法取消全局事件总线的事件监听
console.log("category unmounted")
eventBus.off("whyEvent", this.whyEventHandler)
}
}
</script>

Provide 和 Inject

Provide 和 Inject

  • Provide/Inject 用于非父子组件之间共享数据:
    • 比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容;
    • 在这种情况下,如果我们仍然将 props 沿着组件链逐级传递下去,就会非常的麻烦;
  • 对于这种情况下,我们可以使用 Provide 和 Inject :
    • 无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者;
    • 父组件有一个 provide 选项来提供数据;
    • 子组件有一个 inject 选项来开始使用这些数据;
  • 实际上,你可以将依赖注入看作是 “long range props”,除了:
    • 父组件不需要知道哪些子组件使用它 provide 的 property
    • 子组件不需要知道 inject 的 property 来自哪里

1661151798350

基本使用

  • 我们开发一个这样的结构:

1661151907250

1661151928722

provide的函数写法

  • 如果 Provide 中提供的一些数据是来自 data,那么我们可能会想要通过 this 来获取:
    • 这个时候会报错:
    • 这里给大家留一个思考题,我们的 this 使用的是哪里的 this? 指向 vue 实例对象 Proxy {…}
  • 即使写成了函数写法,也只是可以获取到 data 中的数据,但是此时的数据不是响应式的

父组件 App.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
<template>
<ShowMessage> </ShowMessage>
<button @click="changeHeight">修改height</button>
</template>
<script>
import ShowMessage from "./components/ShowMessage.vue";
export default {
components: {
ShowMessage,
},
provide() {
console.log(this); // Vue实例对象
return {
name: "why",
age: "18",
height: this.height,
};
},
data() {
return {
height: 1.88,
};
},
methods: {
changeHeight() {
this.height = 2.23;
console.log(this.height);
},
},
};
</script>

子组件 ShowMessage.vue :

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<h2>{{ name }} - {{ age }} - {{ height }}</h2>
</div>
</template>

<script>
export default {
inject: ["name", "age", "height"],
};
</script>

处理响应式数据

  • 我们先来验证一个结果:如果我们修改了 this.names 的内容,那么使用 length 的子组件会不会是响应式的?
  • 我们会发现对应的子组件中是没有反应的:
    • 这是因为当我们修改了 names 之后,之前在 provide 中引入的 this.names.length 本身并不是响应式的;
  • 那么怎么样可以让我们的数据变成响应式的呢?
    • 非常的简单,我们可以使用响应式的一些 API 来完成这些功能,比如说 computed 函数;
    • 当然,这个 computed 是 vue3 的新特性,在后面我会专门讲解,这里大家可以先直接使用一下;
  • 注意:我们在使用 length 的时候需要获取其中的 value
    • 这是因为 computed 返回的是一个 ref 对象,需要取出其中的 value 来使用;
    • 首先需要导入 computd 方法: import { computed } from "vue";

image-20230312102715528

组件生命周期

认识生命周期

  • 什么是生命周期呢?
    • 生物学上,生物生命周期指得是一个生物体在生命开始到结束周而复始所历经的一系列变化过程;
    • 每个组件都可能会经历从创建、挂载、更新、卸载等一系列的过程;
    • 在这个过程中的某一个阶段,我们可能会想要添加一些属于自己的代码逻辑(比如组件创建完后就请求一些服务器数据);
    • 但是我们如何知道目前组件正在哪一个过程呢?Vue 给我们提供了组件的生命周期函数;
  • 生命周期函数:
    • 生命周期函数是一些钩子函数(回调函数),在某个时间会被 Vue 源码内部进行回调;
    • 通过对生命周期函数的回调,我们可以知道目前组件正在经历什么阶段;
    • 那么我们就可以在该生命周期中编写属于自己的逻辑代码了;

生命周期流程

vue2的组件生命周期

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<script>
export default {
// 1.组件被创建之前
beforeCreate() {
console.log("beforeCreate")
},

// 2.组件被创建完成
created() {
console.log("created")
console.log("1.发送网络请求, 请求数据")
console.log("2.监听eventbus事件")
console.log("3.监听watch数据")
},

// 3.组件template准备被挂载
beforeMount() {
console.log("beforeMount")
},

// 4.组件template被挂载: 虚拟DOM -> 真实DOM
mounted() {
console.log("mounted")
console.log("1.获取DOM")
console.log("2.使用DOM")
},

// 5.数据发生改变
// 5.1. 准备更新DOM
beforeUpdate() {
console.log("beforeUpdate")
},
// 5.2. 更新DOM
updated() {
console.log("updated")
},

// 6.卸载VNode -> DOM元素
// 6.1.卸载之前
beforeUnmount() {
console.log("beforeUnmount")
},
// 6.2.DOM元素被卸载完成
unmounted() {
console.log("unmounted")
}
}
</script>

组件的其他相关

获取组件中的DOM结构

$refs的使用

  • 某些情况下,我们在组件中想要直接获取到元素对象或者子组件实例:
    • 在 Vue 开发中我们是不推荐进行 DOM 操作的;
    • 这个时候,我们可以给元素或者组件绑定一个 ref 的 attribute 属性;
  • 组件实例有一个 $refs 属性:
    • 它一个对象 Object,持有注册过 ref attribute 的所有 DOM 元素和组件实例。
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
<template>
<div class="app">
<h2 ref="title" class="title" :style="{ color: titleColor }">{{ message }}</h2>
<button ref="btn" @click="changeTitle">修改title</button>
<banner ref="banner"/>
</div>
</template>

<script>
import Banner from "./Banner.vue"

export default {
name:"App",
components: {
Banner
},
data() {
return {
message: "Hello World",
titleColor: "red"
}
},
methods: {
changeTitle() {
// 1.不要主动的去获取DOM, 并且修改DOM内容
// this.message = "你好啊, 李银河!"
// this.titleColor = "blue"

console.log("this.$refs: ", this.$refs) // Proxy {title: h2.title, btn: button, banner: Proxy}

// 2.获取h2/button元素
console.log(this.$refs.title)
console.log(this.$refs.btn)

// 3.获取banner组件: 组件实例
console.log(this.$refs.banner)

// 3.1.在父组件中可以主动的调用子组件的对象方法
this.$refs.banner.bannerClick()

// 3.2.获取banner组件实例, 获取banner中的元素
console.log(this.$refs.banner.$el)

// 3.3.如果banner template是多个根, 拿到的是第一个node节点
// 注意: 开发中不推荐一个组件的template中有多个根元素
// console.log(this.$refs.banner.$el.nextElementSibling)

// 4.组件实例还有两个属性(了解):
console.log(this.$parent) // 获取父组件 // Proxy {changeTitle: ƒ, …} 若没有父组件,则为 null
console.log(this.$root) // 获取根组件 // Proxy {changeTitle: ƒ, …}
}
}
}
</script>

$parent

$root

  • 我们可以通过$parent来访问父元素。
  • HelloWorld.vue 的实现:
    • 这里我们也可以通过$root来实现,因为 App 是我们的根组件;
  • 注意:在 Vue3 中已经移除了 $children 的属性,所以不可以使用了。
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
export default {
name:"App",
methods: {
changeTitle() {
// 4.组件实例还有两个属性(了解):
console.log(this.$parent) // 获取父组件 // Proxy {changeTitle: ƒ, …} 若没有父组件,则为 null
console.log(this.$root) // 获取根组件 // Proxy {changeTitle: ƒ, …}
}
}
}
</script>

切换组件案例

  • 比如我们现在想要实现了一个功能:
    • 点击一个tab-bar,切换不同的组件显示;
image-20230312102853995
  • 这个案例我们可以通过两种不同的实现思路来实现:
    • 方式一:通过 v-if 来判断,显示不同的组件;
    • 方式二:动态组件的方式;

通过 v-if 实现

  • 00 准备 data 数据
  • 01 导入注册并使用组件
  • 02 通过已有的 data 数据,遍历生成按钮
  • 03 给按钮添加点击事件,修改 currentIndex 的值 ,根据 currentIndex 与索引值 index 比较决定 active 类名的添加
  • 04 根据 currentIndex 的值,决定组件的显示与隐藏
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
<template>
<div class="app">
<!-- 02 通过已有的data数据,遍历生成按钮 -->
<template v-for="(item, index) in tabs" :key="item">
<!-- 03 给按钮添加点击事件,修改 currentIndex 的值 ,根据currentIndex与索引值index比较决定active类名的添加-->
<button
@click="itemClick(index)"
:class="{ active: currentIndex === index }"
>
{{ item }}
</button>
</template>
<!-- 04 根据currentIndex的值,v-if进行判断逻辑,决定组件的显示与隐藏 -->
<div class="view">
<template v-if="currentIndex === 0">
<Home></Home>
</template>
<template v-else-if="currentIndex === 1">
<About></About>
</template>
<template v-else-if="currentIndex === 2">
<Category></Category>
</template>
</div>
</div>
</template>

<script>
// 01 导入注册并使用组件
import Home from "./views/Home.vue";
import About from "./views/About.vue";
import Category from "./views/Category.vue";
export default {
name: "App",
components: { Home, About, Category },
data() {
return {
tabs: ["home", "about", "category"],
currentIndex: 0, // 用于控制类名的切换和组件的显示隐藏
};
},
methods: {
itemClick(index) {
this.currentIndex = index;
},
},
};
</script>

<style scoped>
.active {
color: red;
}
</style>

动态组件实现

  • 动态组件是使用 component 组件,通过一个特殊的 attribute is 来实现:
  • 这个 currentTab 的值需要是什么内容呢?
    • 全局注册:可以是通过 component 函数注册的组件;
    • 局部注册:在一个组件对象的 components 对象中注册的组件;
  • 如果是动态组件我们可以给它们传值和监听事件吗?
    • 也是一样的;
    • 只是我们需要将属性和监听事件放到 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
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
<template>
<div class="app">
<!-- 02 通过已有的data数据,遍历生成按钮 -->
<template v-for="item in tabs" :key="item">
<!-- 03 给按钮添加点击事件,修改 currentIndex 的值 ,根据currentIndex与索引值index比较决定active类名的添加-->
<button @click="itemClick(item)" :class="{ active: currentTab === item }">
{{ item }}
</button>
</template>
<!-- 04 使用动态组件,决定组件的显示与隐藏
:is 中的组件需要来自两个地方: 1.全局注册的组件 2.局部注册的组件 -->
<div class="view">
<!-- 动态组件也可以给子组件传递数据,也可以监听子组件中的自定义事件 -->
<!-- <component :is="tabs[currentIndex]" name="why" :age="18" @homeClick="homeClick"></component> -->
<component name="why" :age="18" @homeClick="homeClick" :is="currentTab"></component>
</div>
</div>
</template>

<script>
// 01 导入注册并使用组件
import Home from './views/Home.vue'
import About from './views/About.vue'
import Category from './views/Category.vue'
export default {
name: 'App',
components: { Home, About, Category },
data() {
return {
tabs: ['home', 'about', 'category'],
currentIndex: 0, // 用于控制类名的切换和组件的显示隐藏
currentTab: 'home',
}
},
methods: {
itemClick(tab) {
this.currentTab = tab
},
homeClick(payload) {
// 形参 payload : 子组件传递来的数据
console.log('homeClick:', payload)
},
},
}
</script>

<style scoped>
.active {
color: red;
}
</style>

keep-alive

认识 keep-alive

  • 我们先对之前的案例中 About 组件进行改造:
    • 在其中增加了一个按钮,点击可以递增的功能;
  • 比如我们将 counter 点到 10,那么在切换到 home 再切换回来 about 时,状态是否可以保持呢?
    • 答案是不能保持;
    • 这是因为默认情况下,我们在切换组件后,about 组件会被销毁掉,再次回来时会重新创建组件;
  • 但是,在开发中某些情况我们希望继续保持组件的状态,而不是销毁掉,这个时候我们就可以使用一个内置组件:keep-alive。

image-20230312103004666

keep-alive属性

  • keep-alive 有一些属性:
    • include - string | RegExp | Array。只有名称匹配的组件会被缓存;
    • exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存;
    • max - number | string。最多可以缓存多少组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁;
  • include 和 exclude prop 允许组件有条件地缓存:
    • 二者都可以用逗号分隔字符串、正则表达式或一个数组来表示;
    • 匹配首先检查被缓存组件自身的 name 选项;

image-20230312103026894

缓存组件的生命周期

  • 对于缓存的组件来说,再次进入时,我们是不会执行 created 或者 mounted 等生命周期函数的:
    • 但是有时候我们确实希望监听到何时重新进入到了组件,何时离开了组件;
    • 这个时候我们可以使用 activated 和 deactivated 这两个生命周期钩子函数来监听

1661221951353

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>Home</div>
<h2>{{ count }}</h2>
<button @click="btnClick">+1</button>
</template>

<script>
export default {
name: 'home',
components: {},
data() {
return { count: 0 }
},
methods: {
btnClick() {
this.count++
},
},
activated() {
console.log('进来了')
},
deactivated() {
console.log('走了')
},
}
</script>

webpack的代码分包

  • 默认的打包过程:
    • 默认情况下,在构建整个组件树的过程中,因为组件和组件之间是通过模块化直接依赖的,那么 webpack 在打包时就会将组件模块打包到一起(比如一个 app.js 文件中);
    • 这个时候随着项目的不断庞大,app.js 文件的内容过大,会造成首屏的渲染速度变慢;
  • 打包时,代码的分包:
    • 所以,对于一些不需要立即使用的组件,我们可以单独对它们进行拆分,拆分成一些小的代码块 chunk.js;
    • 这些 chunk.js 会在需要时从服务器加载下来,并且运行代码,显示对应的内容;
  • 那么 webpack 中如何可以对代码进行分包呢?

1661222294885

Vue中实现异步组件

  • 如果我们的项目过大了,对于某些组件我们希望通过异步的方式来进行加载(目的是可以对其进行分包处理),那么Vue 中给我们提供了一个函数:defineAsyncComponent
  • defineAsyncComponent 接受两种类型的参数:
    • 类型一:工厂函数,该工厂函数需要返回一个 Promise 对象;
    • 类型二:接受一个对象类型,对异步函数进行配置;
  • 工厂函数类型一的写法:

1661222415410

  • 类型二写法:

1661222456274

组件的v-model

组件v-model的介绍

  • 前面我们在 input 中可以使用 v-model 来完成双向绑定:
    • 这个时候往往会非常方便,因为 v-model 默认帮助我们完成了两件事;
    • v-bind:value 的数据绑定和 @input 的事件监听;
  • 如果我们现在封装了一个组件,其他地方在使用这个组件时,是否也可以使用 v-model 来同时完成这两个功能呢?
    • 也是可以的,vue 也支持在组件上使用 v-model;
  • 当我们在组件上使用的时候,等价于如下的操作:
    • 我们会发现和 input 元素不同的只是属性的名称和事件触发的名称而已;

image-20230312103219463

组件v-model的实现

  • 那么,为了我们的 MyInput 组件可以正常的工作,这个组件内的 input 必须:
    • 将其 value attribute 绑定到一个名叫 modelValue 的 prop 上;
    • 在其 input 事件被触发时,将新的值通过自定义的 update:modelValue 事件抛出;
  • MyInput.vue的组件代码如下:

1661224599221

image-20230312103252276

父组件 App.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
<template>
<div class="app">
<!-- 1.input v-model -->
<input v-model="message">
<input :value="message" @input="message = $event.target.value">

<!-- 2.组件的 v-model: 默认 modelValue -->
<counter v-model="appCounter"></counter>
<counter :modelValue="appCounter" @update:modelValue="appCounter = $event"></counter>

</div>
</template>
<script>
import Counter from './Counter.vue'
export default {
components: {
Counter,
},
data() {
return {
message: "Hello 123456",
appCounter: 99,
}
}
}
</script>
<style scoped>
</style>

子组件 Counter.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
<template>
<div>
<h2>我是子组件的 Counter: {{ modelValue }}</h2>
<button @click="changeCounter">修改counter</button>
</div>
</template>

<script>
export default {
props: {
modelValue: {
type: Number,
default: 0
}
},
emits: ["update:modelValue"],
methods: {
changeCounter() {
this.$emit("update:modelValue", 999)
}
}
}
</script>

  • 绑定过程图解:

1661226108782

绑定多个属性

  • 我们现在通过 v-model 是直接绑定了一个属性,如果我们希望绑定多个属性呢?
    • 也就是我们希望在一个组件上使用多个 v-model 是否可以实现呢?
    • 我们知道,默认情况下的 v-model 其实是绑定了 modelValue 属性和 @update:modelValue 的事件;
    • 如果我们希望绑定更多,可以给 v-model 传入一个参数,那么这个参数的名称就是我们绑定属性的名称;
  • 注意:这里我是绑定了两个属性的

image-20230312103435976

  • v-model:title 相当于做了两件事:
    • 绑定了 title 属性;
    • 监听了 @update:title 的事件;

父组件 App.vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="app">
<!-- 3.组件的v-model: 自定义名称 counter -->
<counter2 v-model:counter="appCounter" v-model:why="appWhy"></counter2>
</div>
</template>
<script>
import Counter2 from './Counter2.vue'
export default {
components: {
Counter2
},
data() {
return {
appCounter: 99,
appWhy: "coderwhy"
}
}
}
</script>
<style scoped>
</style>

子组件 counter2.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
34
35
<template>
<div>
<h2>Counter: {{ counter }}</h2>
<button @click="changeCounter">修改counter</button>

<!-- why绑定 -->
<h2>why: {{ why }}</h2>
<button @click="changeWhy">修改why的值</button>
</div>
</template>

<script>
export default {
props: {
counter: {
type: Number,
default: 0
},
why: {
type: String,
default: ""
}
},
emits: ["update:counter", "update:why"],
methods: {
changeCounter() {
this.$emit("update:counter", 999)
},
changeWhy() {
this.$emit("update:why", "kobe")
}
}
}
</script>

  • 绑定过程图解:

1661226463980

Mixin 混合

认识Mixin

  • 目前我们是使用组件化的方式在开发整个 Vue 的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。
  • 在 Vue2 和 Vue3 中都支持的一种方式就是使用 Mixin 来完成:
    • Mixin 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能;
    • 一个 Mixin 对象可以包含任何组件选项;
    • 当组件使用 Mixin 对象时,所有 Mixin 对象的选项将被 混合 进入该组件本身的选项中;

Mixin的基本使用

1661226726990

Mixin的合并规则

  • 如果 Mixin 对象中的选项和组件对象中的选项发生了冲突,那么 Vue 会如何操作呢?
    • 这里分成不同的情况来进行处理;
  • 情况一:如果是 data 函数的返回值对象
    • 返回值对象默认情况下会进行合并;
    • 如果 data 返回值对象的属性发生了冲突,那么会保留组件自身的数据;
  • 情况二:如何生命周期钩子函数
    • 生命周期的钩子函数会被合并到数组中,都会被调用;
  • 情况三:值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。
    • 比如都有 methods 选项,并且都定义了方法,那么它们都会生效;
    • 但是如果对象的 key 相同,那么会取组件对象的键值对;

Mixin的全局混入

  • 如果组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的 mixin:
    • 全局的 Mixin 可以使用 app 的方法 mixin 来完成注册;
    • 一旦注册,那么全局混入的选项将会影响每一个组件;

1661226945330

Vue3_CompositionAPI

Option API的弊端

  • 在 Vue2 中,我们编写组件的方式是 Options API :
    • Options API 的一大特点就是在对应的属性中编写对应的功能模块;
    • 比如 data 定义数据、methods 中定义方法、computed 中定义计算属性、watch 中监听属性改变,也包括生命周期钩子;
  • 但是这种代码有一个很大的弊端:
    • 当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中;
    • 当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散;
    • 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人);
  • 下面我们来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分:
    • 这种碎片化的代码使用理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题;
    • 并且当我们处理单个逻辑关注点时,需要不断的跳到相应的代码块中;

1661229894005

  • 如果我们能将同一个逻辑关注点相关的代码收集在一起会更好。
  • 这就是 Composition API 想要做的事情,以及可以帮助我们完成的事情。
  • 也有人把 Vue Composition API 简称为 VCA 。

认识Composition API

  • 那么既然知道 Composition API 想要帮助我们做什么事情,接下来看一下到底是怎么做呢?
    • 为了开始使用 Composition API,我们需要有一个可以实际使用它(编写代码)的地方;
    • 在 Vue 组件中,这个位置就是 setup 函数;
  • setup 其实就是组件的另外一个选项:
    • 只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项;
    • 比如 methods、computed、watch、data、生命周期等等;

Setup函数

Setup函数的参数

  • 我们先来研究一个 setup 函数的参数,它主要有两个参数:
    • 第一个参数:props
    • 第二个参数:context
  • props 非常好理解,它其实就是父组件传递过来的属性会被放到 props 对象中,我们在 setup 中如果需要使用,那么就可以直接通过 props 参数获取:
    • 对于定义 props 的类型,我们还是和之前的规则是一样的,在 props 选项中定义;
    • 并且在 template 中依然是可以正常去使用 props 中的属性,比如 message;
    • 如果我们在 setup 函数中想要使用 props,那么不可以通过 this 去获取(后面我会讲到为什么);
    • 因为 props 有直接作为参数传递到 setup 函数中,所以我们可以直接通过参数来使用即可;
  • 另外一个参数是 context,我们也称之为是一个 SetupContext,它里面包含三个属性:
    • attrs:所有的非 prop 的 attribute;
    • slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用,后面会讲到);
    • emit:当我们组件内部需要发出事件时会用到 emit(因为我们不能访问 this,所以不可以通过 this.$emit 发出事件);

Setup函数的返回值

  • setup 既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?
    • setup 的返回值可以在模板 template 中被使用;
    • 也就是说我们可以通过 setup 的返回值来替代 data 选项;
  • 甚至是我们可以返回一个执行函数来代替在 methods 中定义的方法:

image-20230312103653872

  • 但是,如果我们将 counter 在 increment 或者 decrement 进行操作时,是否可以实现界面的响应式呢?
    • 答案是不可以;
    • 这是因为对于一个定义的变量来说,默认情况下,Vue 并不会跟踪它的变化,来引起界面的响应式操作;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<h2>message: {{ message }}</h2>
<button @click="changeMessage">修改message</button>
</div>
</template>

<script>
export default {
setup() {
// 1. 定义普通的数据: 可以正常的被使用 缺点: 数据不是响应式的
let message = "Hello World";
function changeMessage() {
message = "你好啊,李银河!";
console.log(message);
}
return { message, changeMessage };
},
};
</script>

Setup函数没有this

  • 官方关于 this 有这样一段描述(这段描述是我给官方提交了PR之后的一段描述):

    • 表达的含义是 this 并没有指向当前组件实例;
    • 并且在 setup 被调用之前,data、computed、methods 等都没有被解析;
    • 所以无法在 setup 中获取 this;
  • 其实在之前的这段描述是和源码有出入的(我向官方提交了PR,做出了描述的修改):

    • 之前的描述大概含义是不可以使用 this 是因为组件实例还没有被创建出来;
    • 后来我的PR也有被合并到官方文档中;组件的 instance 肯定是在执行 setup 函数之前就创建出来的。

Setup函数的简写

  • <script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖,当同时使用 SFC 与组合式 API 时则推荐该语法。
    • 更少的样板内容,更简洁的代码;
    • 能够使用纯 Typescript 声明 prop 和抛出事件;
    • 更好的运行时性能 ;
    • 更好的 IDE 类型推断性能 ;
  • 使用这个语法,需要将 setup attribute 添加到 <script>代码块上:

1661422699893

  • 里面的代码会被编译成组件 setup() 函数的内容:

    • 这意味着与普通的 script 只在组件被首次引入的时候执行一次不同;
    • <script setup> 中的代码会在每次组件实例被创建的时候执行。
  • 当使用 <script setup> 的时候,任何在<script setup>声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容)都能在模板中直接使用:

1661422889344

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>AppContent: {{ message }}</div>
<button @click="changeMessage">修改message</button>
</template>

<script setup>
// 1.所有编写在顶层中的代码, 都是默认暴露给template可以使用
import { ref } from "vue";

// 2.定义响应式数据
const message = ref("Hello World");

// 3.定义绑定的函数
function changeMessage() {
message.value = "你好啊, 李银河!";
}
</script>

  • <script setup> 范围里的值也能被直接作为自定义组件的标签名使用:

1661422961578

defineProps()

  • 为了在声明 props 选项时获得完整的类型推断支持,我们可以使用 defineProps,它们将自动地在 <script setup> 中可用:

1661428066021

defineEmits()

  • 为了在声明 props 和 emits 选项时获得完整的类型推断支持,我们可以使用 definePropsdefineEmits API,它们将自动地在 <script setup> 中可用:

1661428359309

defineExpose()

  • 使用 <script setup> 的组件是默认关闭的:
    • 通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定;
  • 通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的 property:

1661428652464

Reactive

认识reactive

  • 如果想为在 setup 中定义的数据提供响应式的特性,那么我们可以使用 reactive 的函数:

1661231420434

  • 那么这是什么原因呢?为什么就可以变成响应式的呢?
    • 这是因为当我们使用 reactive 函数处理我们的数据之后,数据再次被使用时就会进行依赖收集;
    • 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);
    • 事实上,我们编写的 data 选项,也是在内部交给了 reactive 函数将其编程响应式对象的;
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
<template>
<div>
<h2>账号: {{ account.username }}</h2>
<h2>密码: {{ account.password }}</h2>
<button @click="changeAccount">修改账号</button>
</div>
</template>

<script>
import { reactive } from "vue"; // 1. 导入 reactive 方法
export default {
setup() {
// 2.定义响应式数据
// 2.1. reactive函数: 定义复杂类型的数据
const account = reactive({
username: "coderwhy",
password: "123456",
});
function changeAccount() {
account.username = "kobe";
}
return { account, changeAccount };
},
};
</script>

reactive 的应用

  • 01 reactive 应用于本地的数据
  • 02 多个数据之间是有关系/联系(聚合的数据, 组织在一起会有特定的作用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<form>
账号: <input type="text" v-model="account.username" /> 密码:
<input type="password" v-model="account.password" />
</form>
</div>
</template>

<script>
import { reactive } from "vue";
export default {
setup() {
// 1.reactive的应用场景
const account = reactive({
username: "coderwhy",
password: "1234567",
});
return { account };
},
};
</script>

Ref

认识 Ref

  • reactive API 对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型:
    • 如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告;
  • 这个时候 Vue3 给我们提供了另外一个 API:ref API
    • ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是 ref 名称的来源;
    • 它内部的值是在 ref 的 value 属性中被维护的;

image-20230312103923552

  • 这里有两个注意事项:
    • 在模板中引入 ref 的值时,Vue 会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用;
    • 通过 ref 定义的数据,在模板中直接修改不用加 .value
  • 但是在 setup 函数内部,它依然是一个 ref 引用, 所以对其进行操作时,我们依然需要使用 .value 的方式;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<!-- 默认情况下在template中使用ref时, vue会自动对其进行解包(取出其中value)
通过 ref 定义的数据,在模板中直接修改不用加 .value-->
<h2>当前计数: {{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="counter++">+1</button>
</div>
</template>

<script>
import { ref } from "vue"; // 1. 导入 ref 方法
export default {
setup() {
// 2. ref函数: 定义简单类型的数据(也可以定义复杂类型的数据)
const counter = ref(0);
function increment() {
counter.value++;
}
return { counter, increment };
},
};
</script>

Ref 自动解包

  • 模板中的解包是浅层的解包,如果我们的代码是下面的方式:
  • 如果我们将 ref 放到一个 reactive 的属性当中,那么在模板中使用时,它会自动解包:
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
<template>
<div>
<!-- 使用的时候不需要写.value -->
<h2>当前计数: {{ info.counter }}</h2>
<!-- 修改的时候需要写.value -->
<button @click="info.counter.value++">+1</button>
</div>
</template>

<script>
import { ref } from "vue"; // 1. 导入 ref 方法
export default {
setup() {
// 2. ref函数: 定义简单类型的数据(也可以定义复杂类型的数据)
// counter定义响应式数据
const counter = ref(0);
// 3. ref是浅层解包
const info = {
counter,
};
return { info };
},
};
</script>

Ref应用场景

  • 01 定义本地的一些简单数据
  • 02 定义从网络中获取的数据也是使用 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
<template>
<div>
<template v-for="item in musics" :key="item">
<h2>{{ item }}</h2>
</template>
</div>
</template>

<script>
import { onMounted, ref } from "vue";
export default {
setup() {
// 2. ref 的应用场景: 其他的场景基本都用 ref(computed)
// 2.1.定义本地的一些简单数据
const message = ref("Hello World");
const counter = ref(0);
const name = ref("why");
const age = ref(18);

// 2.定义从网络中获取的数据也是使用 ref
const musics = ref([]);
onMounted(() => {
const serverMusics = ["海阔天空", "小苹果", "野狼"];
musics.value = serverMusics;
});
return { musics };
},
};
</script>

readonly

认识readonly

  • 我们通过 reactive 或者 ref 可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改,这个时候如何防止这种情况的出现呢?
    • Vue3 为我们提供了 readonly 的方法;
    • readonly 会返回原始对象的只读代理(也就是它依然是一个 Proxy,这是一个 proxy 的 set 方法被劫持,并且不能对其进行修改)
  • 在开发中常见的 readonly 方法会传入三个类型的参数:
    • 类型一:普通对象;
    • 类型二:reactive 返回的对象;
    • 类型三:ref 的对象;

readonly的使用

  • 在 readonly 的使用过程中,有如下规则:
    • readonly 返回的对象都是不允许修改的;
    • 但是经过 readonly 处理的原来的对象是允许被修改的;
      • 比如 const info = readonly(obj),info 对象是不允许被修改的;
      • 当 obj 被修改时,readonly 返回的 info 对象也会被修改;
      • 但是我们不能去修改 readonly 返回的对象 info;
  • 其实本质上就是 readonly 返回的对象的 setter 方法被劫持了而已;

单向数据流

  • 数据保存在父组件中,由父组件传递给子组件展示,而修改数据也是由子组件通过自定义事件通知父组件修改数据,不在子组件中修改数据

父组件 App.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
<template>
<div>
<ShowInfo :info="info" @changeInfoName="changeInfoName"></ShowInfo>
</div>
</template>

<script>
import { reactive } from "vue";
import ShowInfo from "./Showinfo.vue";
export default {
components: {
ShowInfo,
},
setup() {
// 本地定义多个数据, 都需要传递给子组件
// name/age/height
const info = reactive({
name: "why",
age: 18,
height: 1.88,
});
function changeInfoName(payload) {
info.name = payload;
}
return { info, changeInfoName };
},
};
</script>

子组件 Showinfo.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
<template>
<div>{{ info }}</div>
<!-- 代码没有错误, 但是违背规范(单项数据流) -->
<button @click="info.name = 'kobe'">ShowInfo按钮</button>
<!-- 正确的做法: 符合单项数据流-->
<button @click="showInfobtnClick">ShowInfo按钮</button>
</template>

<script>
export default {
props: {
// reactive数据
info: {
type: Object,
default: () => ({}),
},
},
emits: ["changeInfoName"],
setup(props, context) {
function showInfobtnClick() {
// 触发自定义事件,将新值传递给父组件
context.emit("changeInfoName", "kobe");
}
return { showInfobtnClick };
},
};
</script>

readonly的应用

  • 那么这个 readonly 有什么用呢?
    • 在我们传递给其他组件数据时,往往希望其他组件使用我们传递的内容,但是不允许它们修改时,就可以使用 readonly 了;
    • 如果需要修改数据,则由子组件通过自定义事件通知父组件修改经过 readonly 处理的原对象
    • readonly 返回的对象不能修改,但是可以修改经过 readonly 处理过的原对象

父组件 App.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
<template>
<div>
<h2>App: {{ info }}</h2>
<ShowInfo :roInfo="roInfo" @changeRoInfoName="changeRoInfoName"> </ShowInfo>
</div>
</template>

<script>
import { reactive, readonly } from "vue";
import ShowInfo from "./Showinfo.vue";
export default {
components: {
ShowInfo,
},
setup() {
// 本地定义多个数据, 都需要传递给子组件
const info = reactive({
name: "why",
age: 18,
height: 1.88,
});

// 使用readOnly包裹info
const roInfo = readonly(info);
function changeRoInfoName(payload) {
info.name = payload;
}
return { info, roInfo, changeRoInfoName };
},
};
</script>

子组件 Showinfo.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
<template>
<div>{{ roInfo }}</div>
<!-- 使用readonly的数据 -->
<h2>ShowInfo: {{ roInfo }}</h2>
<!-- 代码会无效(会报警告) -->
<button @click="roInfo.name = 'james'">ShowInfo按钮</button>
<!-- 正确的做法 -->
<button @click="roInfoBtnClick">roInfo按钮</button>
</template>

<script>
export default {
props: {
// readonly数据
roInfo: {
type: Object,
default: () => ({}),
},
},
emits: ["changeRoInfoName"],
setup(props, context) {
function roInfoBtnClick() {
context.emit("changeRoInfoName", "james");
}
return { roInfoBtnClick };
},
};
</script>

reactive其他API

  • isProxy
    • 检查对象是否是由 reactive 或 readonly 创建的 proxy。
  • isReactive
    • 检查对象是否是由 reactive 创建的响应式代理:
    • 如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true;
  • isReadonly
    • 检查对象是否是由 readonly 创建的只读代理。
  • toRaw
    • 返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。
  • shallowReactive
    • 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
  • shallowReadonly
    • 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。

toRefs与toRef

  • 如果我们使用 ES6 的解构语法,对 reactive 返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改 reactive 返回的 state 对象,数据都不再是响应式的:

1661407552860

  • 那么有没有办法让我们解构出来的属性是响应式的呢?
    • Vue 为我们提供了一个 toRefs 的函数,可以将 reactive 返回的对象中的属性都转成 ref;
    • 那么我们再次进行结构出来的 name 和 age 本身都是 ref 的;
  • 如果我们只希望转换一个 reactive 对象中的属性为 ref, 那么可以使用 toRef 的方法:
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
<template>
<div>
<h2>info: {{ name }} - {{ age }} - {{ height }}</h2>
<button @click="age++">修改age</button>
<button @click="height = 1.89">修改height</button>
</div>
</template>

<script>
import { reactive, toRefs, toRef } from "vue";
export default {
setup() {
const info = reactive({
name: "why",
age: 18,
height: 1.88,
});

// const { name, age } = info // reactive被解构后会变成普通的值, 失去响应式
const { name, age } = toRefs(info); // 使用 toRefs 包裹对象解构
const height = toRef(info, "height"); // 调用 toRef

return {
name,
age,
height,
};
},
};
</script>

  • 这种做法相当于已经在 state.name 和 ref.value 之间建立了 链接,任何一个修改都会引起另外一个的变化

ref的其他API

  • unref
    • 如果我们想要获取一个 ref 引用中的 value,那么也可以通过 unref 方法:
      • 如果参数是一个 ref,则返回内部值,否则返回参数本身;
      • 这是 val = isRef(val) ? val.value : val 的语法糖函数;
  • isRef
    • 判断值是否是一个 ref 对象。
  • shallowRef
    • 创建一个浅层的 ref 对象;
  • triggerRef
    • 手动触发和 shallowRef 相关联的副作用:

1661407961251

computed

  • 在前面我们讲解过计算属性 computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理
    • 在前面的 Options API 中,我们是使用 computed 选项来完成的;
    • 在 Composition API 中,我们可以在 setup 函数中使用 computed 方法来编写一个计算属性;
  • 如何使用 computed 呢?
    • 方式一:接收一个 getter 函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;
    • 方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)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
<template>
<div>
<h2>{{ fullname }}</h2>
<button @click="setFullname">设置fullname</button>
</div>
</template>

<script>
import { reactive, computed, ref } from "vue";
export default {
setup() {
// 1.定义数据
const names = reactive({
firstName: "kobe",
lastName: "bryant",
});

// // 2.计算属性的简写,只读
// const fullname = computed(() => {
// return names.firstName + " " + names.lastName;
// });

// 2. 计算属性的完整写法
const fullname = computed({
set: function (newValue) { // 修改计算属性
const tempNames = newValue.split(" ");
names.firstName = tempNames[0];
names.lastName = tempNames[1];
},
get: function () { // 读取计算属性
return names.firstName + " " + names.lastName;
},
});

// 3.修改计算属性
function setFullname() {
fullname.value = "coder why";
console.log(names);
}
return { fullname, setFullname };
},
};
</script>

Setup中使用ref

  • 在 setup 中如何使用 ref 获取元素或者组件?
    • 其实非常简单,我们只需要定义一个 ref 对象,绑定到元素或者组件的 ref 属性上即可;

父组件 App.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
34
35
<template>
<div>
<!-- 1.获取元素 -->
<h2 ref="titleRef">我是标题</h2>
<button ref="btnRef">按钮</button>

<!-- 2.获取组件实例 -->
<ShowInfo ref="showInfoRef"></ShowInfo>
</div>
</template>

<script>
import { ref, onMounted } from "vue";
import ShowInfo from "./ShowInfo.vue";
export default {
components: {
ShowInfo,
},
setup() {
const titleRef = ref();
const btnRef = ref();
const showInfoRef = ref();

// mounted的生命周期函数
onMounted(() => {
console.log(titleRef.value); // 获取标题
console.log(btnRef.value); // 获取按钮
console.log(showInfoRef.value); // 获取子组件
showInfoRef.value.showInfoFoo(); // 调用子组件中的方法
});
return { titleRef, btnRef, showInfoRef };
},
};
</script>

子组件 ShowInfo.vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>ShowInfo</div>
</template>

<script>
export default {
// methods: {
// showInfoFoo() {
// console.log("showInfo foo function")
// }
// }
setup() {
function showInfoFoo() {
console.log("showInfo foo function");
}
return {
showInfoFoo,
};
},
};
</script>

  • 图解:

vue3中使用生命周期

  • 我们前面说过 setup 可以用来替代 data 、 methods 、 computed 等等这些选项,也可以替代 生命周期钩子。
  • 那么 setup 中如何使用生命周期函数呢?
    • 可以使用直接导入的 onX 函数注册生命周期钩子;
    • Vue3 中的生命周期函数有多少个,什么时候调用,参考 Vue 生命周期章节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 01 先导入需要的生命周期函数
import { onMounted } from 'vue'

export default {
setup() {
// 02 在执行 setup 函数的过程中, 执行生命周期函数,传递进去一个回调函数,该回调会在生命周期函数指定的时刻调用
onMounted(() => {
console.log("onmounted")
})
}
}
</script>

1661414575072

Provide与inject

  • 事实上我们之前还学习过 Provide 和 Inject,Composition API 也可以替代之前的 Provide 和 Inject 的选项。

  • 我们可以通过 provide 来提供数据:

    • 可以通过 provide 方法来定义每个 Property;
  • provide 可以传入两个参数:

    • name:提供的属性名称;
    • value:提供的属性值;
  • 在 后代组件 中可以通过 inject 来注入需要的属性和对应的值:

    • 可以通过 inject 来注入需要的内容;
  • inject 可以传入两个参数:

    • 要 inject 的 property 的 name;
    • 默认值;
  • 为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 提供值时使用 ref 和 reactive。

父组件 App.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
<template>
<div>AppContent: {{ name }}</div>
<button @click="name = 'kobe'">app btn</button>
<ShowInfo></ShowInfo>
</template>

<script>
import { provide, ref } from "vue";
import ShowInfo from "./ShowInfo.vue";

export default {
components: {
ShowInfo,
},
setup() {
// 01 提供数据
const name = ref("why"); // name 是 ref 返回的,是响应式的
provide("name", name); // 参数分别为: 变量名 值
provide("age", 18); // 此时 age 不是响应式的,除非像 name 一样用 ref 包裹

return {
name,
};
},
};
</script>

子组件 ShowInfo.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
<template>
<div>ShowInfo: {{ name }}-{{ age }} - {{ height }}</div>
</template>

<script>
import { inject } from "vue";

export default {
// inject 的 options api注入, 那么依然需要手动来解包(模板中使用要加 .value)
// inject: ["name", "age"], // 如果不是响应式的数据,模板中使用时不需要 .value

setup() {
// 02 接收使用数据,通过 inject 函数接收的收据,模板中使用时不用加 .value
const name = inject("name");
const age = inject("age");
const height = inject("height", 1.99); // 给变量添加默认值,不提供该数据就使用默认值
return {
name,
age,
height,
};
},
};
</script>

watch

watch的使用

  • watch 的 API 完全等同于组件 watch 选项的 Property:
    • watch 需要侦听特定的数据源,并且执行其回调函数;
    • 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调;
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
<template>
<div>
<button @click="message = '你好啊,李银河!'">修改message</button>
<button @click="info.friend.name = 'james'">修改info</button>
</div>
</template>

<script>
import { reactive, ref, watch } from "vue";
export default {
setup() {
// 1.定义数据
const message = ref("Hello World");
const info = reactive({
name: "why",
age: 18,
friend: {
name: "kobe",
},
});

// 2.侦听数据的变化, 默认为深度监听
watch(message, (newValue, oldValue) => {
console.log(newValue, oldValue); // 你好啊,李银河! Hello World
});
watch(
info,
(newValue, oldValue) => {
console.log(newValue, oldValue);
console.log(newValue === oldValue); // 第一次为 false ,第二次为 true
},
{
immediate: true, // 上来就执行一次
}
);

// 3.监听reactive数据变化后, 获取普通对象 ,此时不是深度监听
watch(
() => ({ ...info }),
(newValue, oldValue) => {
console.log(newValue, oldValue);
},
{
immediate: true,
deep: true, // 开启深度监听
}
);
return { message, info };
},
};
</script>

侦听多个数据源

  • 侦听器还可以使用数组同时侦听多个源:

1661417796496

watchEffect

watchEffect的使用

  • 当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect。
  • 我们来看一个案例:
    • 首先,watchEffect 传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;
    • 其次,只有收集的依赖发生变化时,watchEffect 传入的函数才会再次执行;
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
<template>
<div>
<h2>当前计数: {{ counter }}</h2>
<button @click="counter++">+1</button>
<button @click="name = 'kobe'">修改name</button>
</div>
</template>

<script>
import { watchEffect, ref } from "vue";

export default {
setup() {
const counter = ref(0);
const name = ref("why");

// 1.watchEffect传入的函数默认会直接被执行
// 2.在执行的过程中, 会自动的收集依赖(依赖哪些响应式的数据)
const stopWatch = watchEffect(() => {
console.log("-------", counter.value, name.value);
// 判断counter.value > 10
if (counter.value >= 10) {
stopWatch();
}
});
return {
counter,
name,
};
},
};
</script>

停止监听

  • 如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取 watchEffect 的返回值,该返回值是一个函数,调用该函数即可停止侦听。

1661418452638

Hooks

useCounter

./hooks/useCounter :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { ref, onMounted } from 'vue'

export default function useCounter() {
const counter = ref(0)
function increment() {
counter.value++
}
function decrement() {
counter.value--
}

// onMounted(() => {
// setTimeout(() => {
// counter.value = 989
// }, 1000);
// })

return {
counter,
increment,
decrement
}
}

App.vue :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<h2>{{ counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>

<script>
import useCounter from "./hooks/useCounter";
export default {
setup() {
const { counter, increment, decrement } = useCounter();
return {
counter,
increment,
decrement,
};
},
};
</script>

  • 图解:

1661420282818

useTitle

1661421255800

1661421552135

1661421842971

案例实战练习(空)