一、简介
Vue3+TypeScript从入门到进阶(一)——Vue3简介及介绍——附沿途学习案例及项目实战代码
二、Vue2和Vue3区别
Vue3+TypeScript从入门到进阶(二)——Vue2和Vue3的区别——附沿途学习案例及项目实战代码
三、Vue知识点学习
Vue3+TypeScript从入门到进阶(三)——Vue3基础知识点(上)——附沿途学习案例及项目实战代码
五、Vue3的表单和开发模式
1、v-model
v-model的基本使用
表单提交是开发中非常常见的功能,也是和用户交互的重要手段:
-
比如用户在登录、注册时需要提交账号密码;
-
比如用户在检索、创建、更新信息时,需要提交一些数据;
这些都要求我们可以在代码逻辑中获取到用户提交的数据,我们通常会使用v-model指令来完成:
-
v-model指令可以在表单 input、textarea以及select元素上创建双向数据绑定;
-
它会根据控件类型自动选取正确的方法来更新元素;
-
尽管有些神奇,但 v-model 本质上不过是语法糖,它负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理;
<body>
<div id="app"></div>
<template id="my-app">
<!-- 1.v-bind value的绑定 2.监听input事件, 更新message的值 -->
<!-- <input type="text" :value="message" @input="inputChange"> -->
<input type="text" v-model="message">
<h2>{{message}}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
const App = {
template: '#my-app',
data() {
return {
message: "Hello World"
}
},
methods: {
inputChange(event) {
this.message = event.target.value;
}
}
}
Vue.createApp(App).mount('#app');
</script>
</body>
v-model的原理
官方有说到,v-model的原理其实是背后有两个操作:
-
v-bind绑定value属性的值;
-
v-on绑定input事件监听到函数中,函数会获取最新的值赋值到绑定的属性中;
v-model绑定textarea
我们再来绑定一下其他的表单类型:textarea、checkbox、radio、select
我们来看一下绑定textarea:
<label for="intro">
自我介绍
<textarea name="intro" id="intro" cols="30" rows="10" v-model="intro"></textarea>
</label>
<h2>intro: {{intro}}</h2>
v-model绑定checkbox
我们来看一下v-model绑定checkbox:单个勾选框和多个勾选框
单个勾选框:
-
v-model即为布尔值。
-
此时input的value并不影响v-model的值。
多个复选框:
-
当是多个复选框时,因为可以选中多个,所以对应的data中属性是一个数组。
-
当选中某一个时,就会将input的value添加到数组中。
<!-- 2.checkbox -->
<!-- 2.1.单选框 -->
<label for="agree">
<input id="agree" type="checkbox" v-model="isAgree"> 同意协议
</label>
<h2>isAgree: {{isAgree}}</h2>
<!-- 2.2.多选框 -->
<span>你的爱好: </span>
<label for="basketball">
<input id="basketball" type="checkbox" v-model="hobbies" value="basketball"> 篮球
</label>
<label for="football">
<input id="football" type="checkbox" v-model="hobbies" value="football"> 足球
</label>
<label for="tennis">
<input id="tennis" type="checkbox" v-model="hobbies" value="tennis"> 网球
</label>
<h2>hobbies: {{hobbies}}</h2>
v-model绑定radio
v-model绑定radio,用于选择其中一项;
<!-- 3.radio -->
<span>你的爱好: </span>
<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: {{gender}}</h2>
v-model绑定select
和checkbox一样,select也分单选和多选两种情况。
单选:只能选中一个值
-
v-model绑定的是一个值;
-
当我们选中option中的一个时,会将它对应的value赋值到fruit中;
多选:可以选中多个值
-
v-model绑定的是一个数组;
-
当选中多个值时,就会将选中的option对应的value添加到数组fruit中;
<!-- 4.select -->
<span>喜欢的水果: </span>
<select v-model="fruit" multiple size="2">
<option value="apple">苹果</option>
<option value="orange">橘子</option>
<option value="banana">香蕉</option>
</select>
<h2>fruit: {{fruit}}</h2>
v-model的值绑定
目前我们在前面的案例中大部分的值都是在template中固定好的:
-
比如gender的两个输入框值male、female;
-
比如hobbies的三个输入框值basketball、football、tennis;
在真实开发中,我们的数据可能是来自服务器的,那么我们就可以先将值请求下来,绑定到data返回的对象中,
再通过v-bind来进行值的绑定,这个过程就是值绑定。
- 这里不再给出具体的做法,因为还是v-bind的使用过程。
v-model修饰符 - lazy
lazy修饰符是什么作用呢?
-
默认情况下,v-model在进行双向绑定时,绑定的是input事件,那么会在每次内容输入后就将最新的值和绑定的属性进行同步;
-
如果我们在v-model后跟上lazy修饰符,那么会将绑定的事件切换为 change 事件,只有在提交时(比如回车)才会触发;
<!-- 1.lazy修饰符 -->
<input type="text" v-model.lazy="message">
v-model修饰符 - number
我们先来看一下v-model绑定后的值是什么类型的:
- message总是string类型,即使在我们设置type为number也是string类型;
<template id="my-app">
<input type="text" v-model="message">
<input type="number" v-model="message">
<h2>{{message}}</h2>
</template>
如果我们希望转换为数字类型,那么可以使用 .number 修饰符:
<input type="text" v-model.number="message">
<h2>{{message}}</h2>
<button @click="showType">查看类型</button>
另外,在我们进行逻辑判断时,如果是一个string类型,在可以转化的情况下会进行隐式转换的:
- 下面的score在进行判断的过程中会进行隐式转化的;
const score = "100"
if (score > 90) {
console.log("优秀")
}
console.log(typeof score)
v-model修饰符 - trim
如果要自动过滤用户输入的首尾空白字符,可以给v-model添加 trim 修饰符:
<!-- 3.trim修饰符 -->
<input type="text" v-model.trim="message">
<button @click="showResult">查看结果</button>
2、组件化开发
人处理问题的方式
人面对复杂问题的处理方式:
-
任何一个人处理信息的逻辑能力都是有限的
-
所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。
-
但是,我们人有一种天生的能力,就是将问题进行拆解。
-
如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。
认识组件化开发
组件化也是类似的思想:
-
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展;
-
但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面 的管理和维护就变得非常容易了;
-
如果我们将一个个功能块拆分后,就可以像搭建积木一下来搭建我们的项目;
组件化开发
现在可以说整个的大前端开发都是组件化的天下,无论从三大框架(Vue、React、Angular),还是跨平台方案的Flutter,甚至是移动端都在转向组件化开发,包括小程序的开发也是采用组件化开发的思想。
所以,学习组件化最重要的是它的思想,每个框架或者平台可能实现方法不同,但是思想都是一样的。
我们需要通过组件化的思想来思考整个应用程序:
-
我们将一个完整的页面分成很多个组件;
-
每个组件都用于实现页面的一个功能块;
-
而每一个组件又可以进行细分;
-
而组件本身又可以在多个地方进行复用;
Vue的组件化
组件化是Vue、React、Angular的核心思想,也是我们后续课程的重点(包括以后实战项目):
-
前面我们的createApp函数传入了一个对象App,这个对象其实本质上就是一个组件,也是我们应用程序的根组件;
-
组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用;
-
任何的应用都会被抽象成一颗组件树;
接下来,我们来学习一下在Vue中如何注册一个组件,以及之后如何使用这个注册后的组件。
注册组件的方式
如果我们现在有一部分内容(模板、逻辑等),我们希望将这部分内容抽取到一个独立的组件中去维护,这个时候
如何注册一个组件呢?
我们先从简单的开始谈起,比如下面的模板希望抽离到一个单独的组件:
<h2>{{message}}</h2>
<h2>{{title}}</h2>
<p>{{desc}}</p>
注册组件分成两种:
-
全局组件:在任何其他的组件中都可以使用的组件;
-
局部组件:只有在注册的组件中才能使用的组件;
注册全局组件
我们先来学习一下全局组件的注册:
-
全局组件需要使用我们全局创建的app来注册组件;
-
通过component方法传入组件名称、组件对象即可注册一个全局组件了;
-
之后,我们可以在App组件的template中直接使用这个全局组件:
全局组件的逻辑
当然,我们组件本身也可以有自己的代码逻辑:
- 比如自己的data、computed、methods等等
// 使用app注册一个全局组件app.component()
// 全局组件: 意味着注册的这个组件可以在任何的组件模板中使用
app.component("component-a", {
template: "#component-a",
data() {
return {
title: "我是标题",
desc: "我是内容, 哈哈哈哈哈",
};
},
methods: {
btnClick() {
console.log("按钮的点击");
},
},
});
组件的名称
在通过app.component注册一个组件的时候,第一个参数是组件的名称,定义组件名的方式有两种:
方式一:使用kebab-case(短横线分割符)
- 当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,
例如 <my-component-name>
;
方式二:使用PascalCase(驼峰标识符)
- 当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说
<my-component-name>
和<MyComponentName>
都是可接受的;
注册局部组件
全局组件往往是在应用程序一开始就会全局组件完成,那么就意味着如果某些组件我们并没有用到,也会一起被注册:
-
比如我们注册了三个全局组件:ComponentA、ComponentB、ComponentC;
-
在开发中我们只使用了ComponentA、ComponentB,如果ComponentC没有用到但是我们依然在全局进行了注册,那么就意味着类似于webpack这种打包工具在打包我们的项目时,我们依然会对其进行打包;
-
这样最终打包出的JavaScript包就会有关于ComponentC的内容,用户在下载对应的JavaScript时也会增加包的大小;
所以在开发中我们通常使用组件的时候采用的都是局部注册:
-
局部注册是在我们需要使用到的组件中,通过components属性选项来进行注册;
-
比如之前的App组件中,我们有data、computed、methods等选项了,事实上还可以有一个components选项;
-
该components选项对应的是一个对象,对象中的键值对是 组件的名称: 组件对象;
布局组件注册代码
Vue的开发模式
目前我们使用vue的过程都是在html文件中,通过template编写自己的模板、脚本逻辑、样式等。
但是随着项目越来越复杂,我们会采用组件化的方式来进行开发:
-
这就意味着每个组件都会有自己的模板、脚本逻辑、样式等;
-
当然我们依然可以把它们抽离到单独的js、css文件中,但是它们还是会分离开来;
-
也包括我们的script是在一个全局的作用域下,很容易出现命名冲突的问题;
-
并且我们的代码为了适配一些浏览器,必须使用ES5的语法;
-
在我们编写代码完成之后,依然需要通过工具对代码进行构建、代码;
所以在真实开发中,我们可以通过一个后缀名为 .vue 的single-file components (单文件组件) 来解决,并且可以使用webpack或者vite或者rollup等构建工具来对其进行处理。
单文件的特点
在这个组件中我们可以获得非常多的特性:
-
代码的高亮;
-
ES6、CommonJS的模块化能力;
-
组件作用域的CSS;
-
可以使用预处理器来构建更加丰富的组件,比如TypeScript、Babel、Less、Sass等;
<template>
<div>
<h2>{{ title }}</h2>
<p>{{ desc }}</p>
<button @click="btnClick">按钮点击</button>
</div>
</template>
<script>
export default {
data() {
return {
title: "我是标题",
desc: "我是内容, 哈哈哈哈哈",
};
},
methods: {
btnClick() {
console.log("按钮的点击");
},
},
};
</script>
<style scoped></style>
如何支持SFC
如果我们想要使用这一的SFC的.vue文件,比较常见的是两种方式:
-
方式一:使用Vue CLI来创建项目,项目会默认帮助我们配置好所有的配置选项,可以在其中直接使用.vue文件;
-
方式二:自己使用webpack或rollup或vite这类打包工具,对其进行打包处理;
我们最终,无论是后期我们做项目,还是在公司进行开发,通常都会采用Vue CLI的方式来完成。
六、Webpack和VueCLI学习
1、Webpack学习
基于篇幅的影响,这里不对webpack做过多的赘述
大家可以直接去其他知识学习处,跳转到Webpack入门学习
2、Webpack中的Vue
Vue源码的打包
我们课程主要是学习Vue的,那么我们应该包含Vue相关的代码:
const app = createApp({
template: "#my-app",
components: {
},
data() {
return {
title: "Hello World",
message: "哈哈哈"
}
}
});
界面上是没有效果的:
- 并且我们查看运行的控制台,会发现如下的警告信息;
Vue打包后不同版本解析
vue(.runtime).global(.prod).js:
-
通过浏览器中的
<script src="...">
直接使用; -
我们之前通过CDN引入和下载的Vue版本就是这个版本;
-
会暴露一个全局的Vue来使用;
vue(.runtime).esm-browser(.prod).js:
- 用于通过原生 ES 模块导入使用 (在浏览器中通过
<script type="module">
来使用)。
vue(.runtime).esm-bundler.js:
-
用于 webpack,rollup 和 parcel 等构建工具;
-
构建工具中默认是vue.runtime.esm-bundler.js;
-
如果我们需要解析模板template,那么需要手动指定vue.esm-bundler.js;
vue.cjs(.prod).js:
-
服务器端渲染使用;
-
通过require()在Node.js中使用;
运行时+编译器 vs 仅运行时
在Vue的开发过程中我们有三种方式来编写DOM元素:
-
方式一:template模板的方式(之前经常使用的方式);
-
方式二:render函数的方式,使用h函数来编写渲染的内容;
-
方式三:通过.vue文件中的template来编写模板;
它们的模板分别是如何处理的呢?
-
方式二中的h函数可以直接返回一个虚拟节点,也就是Vnode节点;
-
方式一和方式三的template都需要有特定的代码来对其进行解析:
方式三.vue文件中的template可以通过在vue-loader对其进行编译和处理;
方式一种的template我们必须要通过源码中一部分代码来进行编译;
所以,Vue在让我们选择版本的时候分为 运行时+编译器 vs 仅运行时
-
运行时+编译器包含了对template模板的编译代码,更加完整,但是也更大一些;
-
仅运行时没有包含对template版本的编译代码,相对更小一些;
全局标识的配置
我们会发现控制台还有另外的一个警告:
在GitHub上的文档中我们可以找到说明:
-
这是两个特性的标识,一个是使用Vue的Options,一个是Production模式下是否支持devtools工具;
-
虽然他们都有默认值,但是强烈建议我们手动对他们进行配置;
VSCode对SFC文件的支持
在前面我们提到过,真实开发中多数情况下我们都是使用SFC( single-file components (单文件组件) )。
我们先说一下VSCode对SFC的支持:
-
插件一:Vetur,从Vue2开发就一直在使用的VSCode支持Vue的插件;
-
插件二:Volar,官方推荐的插件(后续会基于Volar开发官方的VSCode插件)
编写App.vue代码
接下来我们编写自己的App.vue代码:
<template>
<h2>我是Vue渲染出来的</h2>
<h2>{{title}}</h2>
<hello-world></hello-world>
</template>
<script>
import HelloWorld from './HelloWorld.vue';
export default {
components: {
HelloWorld
},
data() {
return {
title: "Hello World",
message: "哈哈哈"
}
},
methods: {
}
}
</script>
<style scoped>
h2 {
color: red;
}
</style>
import { createApp } from "vue/dist/vue.esm-bundler"
import App from './vue/App.vue'
createApp(App).mount("#app")
App.vue的打包过程
我们对代码打包会报错:我们需要合适的Loader来处理文件。
这个时候我们需要使用vue-loader:
npm install vue-loader -D
在webpack的模板规则中进行配置:
{
test: /\.vue$/,
loader: "vue-loader"
}
@vue/compiler-sfc
打包依然会报错,这是因为我们必须添加@vue/compiler-sfc来对template进行解析:
npm install @vue/compiler-sfc -D
另外我们需要配置对应的Vue插件:
const { VueLoaderPlugin } = require('vue-loader/dist/index');
module.exports = {
....
plugins: [
.....
new VueLoaderPlugin()
]
}
重新打包即可支持App.vue的写法
3、VueCLI
什么是Vue脚手架?
-
我们前面学习了如何通过webpack配置Vue的开发环境,但是在真实开发中我们不可能每一个项目从头来完成所有的webpack配置,这样显示开发的效率会大大的降低;
-
所以在真实开发中,我们通常会使用脚手架来创建一个项目,Vue的项目我们使用的就是Vue的脚手架;
-
脚手架其实是建筑工程中的一个概念,在我们软件工程中也会将一些帮助我们搭建项目的工具称之为脚手架;
Vue的脚手架就是Vue CLI:
-
CLI是Command-Line Interface, 翻译为命令行界面;
-
我们可以通过CLI选择项目的配置和创建出我们的项目;
-
Vue CLI已经内置了webpack相关的配置,我们不需要从零来配置;
Vue CLI 安装和使用
安装Vue CLI(目前最新的版本是v4.5.13)
- 我们是进行全局安装,这样在任何时候都可以通过vue的命令来创建项目;
npm install @vue/cli -g
升级Vue CLI:
- 如果是比较旧的版本,可以通过下面的命令来升级
npm update @vue/cli -g
通过Vue的命令来创建项目
Vue create 项目的名称
vue create 项目的过程
项目的目录结构
Vue CLI的运行原理
4、Vite学习
认识Vite
Webpack是目前整个前端使用最多的构建工具,但是除了webpack之后也有其他的一些构建工具:
- 比如rollup、parcel、gulp、vite等等
什么是vite呢? 官方的定位:下一代前端开发与构建工具;
如何定义下一代开发和构建工具呢?
-
我们知道在实际开发中,我们编写的代码往往是不能被浏览器直接识别的,比如ES6、TypeScript、Vue文件等等;
-
所以我们必须通过构建工具来对代码进行转换、编译,类似的工具有webpack、rollup、parcel;
-
但是随着项目越来越大,需要处理的JavaScript呈指数级增长,模块越来越多;
-
构建工具需要很长的时间才能开启服务器,HMR也需要几秒钟才能在浏览器反应出来;
-
所以也有这样的说法:天下苦webpack久矣;
Vite (法语意为 “快速的”,发音 /vit/) 是一种新型前端构建工具,能够显著提升前端开发体验。
Vite的构造
它主要由两部分组成:
-
一个开发服务器,它基于原生ES模块提供了丰富的内建功能,HMR的速度非常快速;
-
一套构建指令,它使用rollup打开我们的代码,并且它是预配置的,可以输出生成环境的优化过的静态资源;
目前是否要大力学习vite?vite的未来是怎么样的?
-
我个人非常看好vite的未来,也希望它可以有更好的发展;
-
但是,目前vite虽然已经更新到2.0,依然并不算非常的稳定,并且比较少大型项目(或框架)使用vite来进行构建;
-
vite的整个社区插件等支持也还不够完善;
-
包括vue脚手架本身,目前也还没有打算迁移到vite,而依然使用webpack(虽然后期一定是有这个打算的);
-
所以vite看起来非常的火热,在面试也可能会问到,但是实际项目中应用的还比较少;
浏览器原生支持模块化
但是如果我们不借助于其他工具,直接使用ES Module来开发有什么问题呢?
-
首先,我们会发现在使用loadash时,加载了上百个模块的js代码,对于浏览器发送请求是巨大的消耗;
-
其次,我们的代码中如果有TypeScript、less、vue等代码时,浏览器并不能直接识别;
事实上,vite就帮助我们解决了上面的所有问题。
Vite的安装和使用
注意:Vite本身也是依赖Node的,所以也需要安装好Node环境
- 并且Vite要求Node版本是大于12版本的;
首先,我们安装一下vite工具:
npm install vite –g # 全局安装
npm install vite –D # 局部安装
通过vite来启动项目:
npx vite
Vite对css的支持
vite可以直接支持css的处理
- 直接导入css即可;
vite可以直接支持css预处理器,比如less
-
直接导入less;
-
之后安装less编译器;
npm install less -D
vite直接支持postcss的转换:
- 只需要安装postcss,并且配置 postcss.config.js 的配置文件即可;
npm install postcss postcss-preset-env -D
module.exports = {
plugins: [
require('postcss-preset-env')
]
}
Vite对TypeScript的支持
vite对TypeScript是原生支持的,它会直接使用ESBuild来完成编译:
- 只需要直接导入即可;
如果我们查看浏览器中的请求,会发现请求的依然是ts的代码:
-
这是因为vite中的服务器Connect会对我们的请求进行转发;
-
获取ts编译后的代码,给浏览器返回,浏览器可以直接进行解析;
注意:在vite2中,已经不再使用Koa了,而是使用Connect来搭建的服务器
Vite对vue的支持
vite对vue提供第一优先级支持:
-
Vue 3 单文件组件支持:@vitejs/plugin-vue
-
Vue 3 JSX 支持:@vitejs/plugin-vue-jsx
-
Vue 2 支持:underfin/vite-plugin-vue2
安装支持vue的插件:
npm install @vitejs/plugin-vue -D
在vite.config.js中配置插件:
import vue from '@vitejs/plugin-vue'
module.exports = {
plugins: [
vue()
]
}
Vite打包项目
我们可以直接通过vite build来完成对当前项目的打包工具:
npx vite build
我们可以通过preview的方式,开启一个本地服务来预览打包后的效果:
npx vite preview
ESBuild解析
ESBuild的特点:
-
超快的构建速度,并且不需要缓存;
-
支持ES6和CommonJS的模块化;
-
支持ES6的Tree Shaking;
-
支持Go、JavaScript的API;
-
支持TypeScript、JSX等语法编译;
-
支持SourceMap;
-
支持代码压缩;
-
支持扩展其他插件;
ESBuild的构建速度
ESBuild的构建速度和其他构建工具速度对比:
ESBuild为什么这么快呢?
-
使用Go语言编写的,可以直接转换成机器代码,而无需经过字节码;
-
ESBuild可以充分利用CPU的多内核,尽可能让它们饱和运行;
-
ESBuild的所有内容都是从零开始编写的,而不是使用第三方,所以从一开始就可以考虑各种性能问题;
-
等等…
Vite脚手架工具
在开发中,我们不可能所有的项目都使用vite从零去搭建,比如一个react项目、Vue项目;
- 这个时候vite还给我们提供了对应的脚手架工具;
所以Vite实际上是有两个工具的:
-
vite:相当于是一个构件工具,类似于webpack、rollup;
-
@vitejs/create-app:类似vue-cli、create-react-app;
如果使用脚手架工具呢?
npm init @vitejs/app
上面的做法相当于省略了安装脚手架的过程:
npm install @vitejs/create-app -g
create-app
七、Vue3组件化开发
1、组件的嵌套和拆分
前面我们是将所有的逻辑放到一个App.vue中:
-
在之前的案例中,我们只是创建了一个组件App;
-
如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护;
-
所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;
-
再将这些组件组合嵌套在一起,最终形成我们的应用程序;
我们来分析一下下面代码的嵌套逻辑,假如我们将所有的代码逻辑都放到一个App.vue组件中:
-
我们会发现,将所有的代码逻辑全部放到一个组件中,代码是非常的臃肿和难以维护的。
-
并且在真实开发中,我们会有更多的内容和代码逻辑,对于扩展性和可维护性来说都是非常差的。
-
所以,在真实的开发中,我们会对组件进行拆分,拆分成一个个功能的小组件。
组件的拆分
我们可以按照如下的方式进行拆分:
按照如上的拆分方式后,我们开发对应的逻辑只需要去对应的组件编写就可。
2、父子组件的通信
上面的嵌套逻辑如下,它们存在如下关系:
-
App组件是Header、Main、Footer组件的父组件;
-
Main组件是Banner、ProductList组件的父组件;
在开发过程中,我们会经常遇到需要组件之间相互进行通信:
-
比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示;
-
又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给它们来进行展示;
-
也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
总之,在一个Vue项目中,组件之间的通信是非常重要的环节,所以接下来我们就具体学习一下组件之间是如何相互之间传递数据的;
父子组件之间通信的方式
父子组件之间如何进行通信呢?
-
父组件传递给子组件:通过props属性;
-
子组件传递给父组件:通过$emit触发事件;
父组件传递给子组件
在开发中很常见的就是父子组件之间通信,比如父组件有一些数据,需要子组件来进行展示:
- 这个时候我们可以通过props来完成组件之间的通信;
什么是Props呢?
-
Props是你可以在组件上注册一些自定义的attribute;
-
父组件给这些attribute赋值,子组件通过attribute的名称获取到对应的值;
Props有两种常见的用法:
-
方式一:字符串数组,数组中的字符串就是attribute的名称;
-
方式二:对象类型,对象类型我们可以在指定attribute名称的同时,指定它需要传递的类型、是否是必须的、默认值等等;
Props的数组用法
Props的对象用法
数组用法中我们只能说明传入的attribute的名称,并不能对其进行任何形式的限制,接下来我们来看一下对象的写法是如何让我们的props变得更加完善的。
当使用对象语法的时候,我们可以对传入的内容限制更多:
-
比如指定传入的attribute的类型;
-
比如指定传入的attribute是否是必传的;
-
比如指定没有传入时,attribute的默认值;
props: {
title: String,
content: {
type: String,
required: true,
default: "123"
},
counter: {
type: Number
},
info: {
type: Object,
default() {
return {name: "why"}
}
},
messageInfo: {
type: String
}
}
细节一:那么type的类型都可以是哪些呢?
那么type的类型都可以是哪些呢?
-
String
-
Number
-
Boolean
-
Array
-
Object
-
Date
-
Function
-
Symbol
细节二:对象类型的其他写法
细节三:Prop 的大小写命名
Prop 的大小写命名(camelCase vs kebab-case)
-
HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符;
-
这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名;
<div>
<show-message messageInfo="哈哈"></show-message>
<show-message message-info="哈哈"></show-message>
</div>
非Prop的Attribute
什么是非Prop的Attribute呢?
-
当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为 非Prop的Attribute;
-
常见的包括class、style、id属性等;
Attribute继承
- 当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中:
禁用Attribute继承和多根节点
如果我们不希望组件的根元素继承attribute,可以在组件中设置 inheritAttrs: false:
-
禁用attribute继承的常见情况是需要将attribute应用于根元素之外的其他元素;
-
我们可以通过 $attrs来访问所有的 非props的attribute;
<div>
我是NotPropAttribute组件
<h2 :class="$attrs.class"></h2>
</div>
多个根节点的attribute
- 多个根节点的attribute如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性
<template>
<h2>MultiRootElement</h2>
<h2>MultiRootElement</h2>
<h2 :id="$attrs.id">MultiRootElement</h2>
</template>
子组件传递给父组件
什么情况下子组件需要传递内容到父组件呢?
-
当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容;
-
子组件有一些内容想要传递给父组件的时候;
我们如何完成上面的操作呢?
-
首先,我们需要在子组件中定义好在某些情况下触发的事件名称;
-
其次,在父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中;
-
最后,在子组件中发生某个事件的时候,根据事件名称触发对应的事件;
自定义事件的流程
我们封装一个CounterOperation.vue的组件:
- 内部其实是监听两个按钮的点击,点击之后通过 this.$emit的方式发出去事件;
自定义事件的参数和验证
自定义事件的时候,我们也可以传递一些参数给父组件:
incrementN() {
this.$emit('addN', this.num, "why", 18);
}
在vue3当中,我们可以对传递的参数进行验证:
// 对象写法的目的是为了进行参数的验证
emits: {
add: null,
sub: null,
addN: (num, name, age) => {
console.log(num, name, age);
if (num > 10) {
return true
}
return false;
}
},
组件间通信案例练习
我们来做一个相对综合的练习:
// App.vue
<template>
<div>
<tab-control :titles="titles" @titleClick="titleClick"></tab-control>
<h2>{{contents[currentIndex]}}</h2>
</div>
</template>
<script>
import TabControl from './TabControl.vue';
export default {
components: {
TabControl
},
data() {
return {
titles: ["衣服", "鞋子", "裤子"],
contents: ["衣服页面", "鞋子页面", "裤子页面"],
currentIndex: 0
}
},
methods: {
titleClick(index) {
this.currentIndex = index;
}
}
}
</script>
<style scoped>
</style>
// TabControl.vue
<template>
<div class="tab-control">
<div class="tab-control-item"
:class="{active: currentIndex === index}"
v-for="(title, index) in titles"
:key="title"
@click="itemClick(index)">
<span>{{title}}</span>
</div>
</div>
</template>
<script>
export default {
emits: ["titleClick"],
props: {
titles: {
type: Array,
default() {
return []
}
}
},
data() {
return {
currentIndex: 0
}
},
methods: {
itemClick(index) {
this.currentIndex = index;
this.$emit("titleClick", index);
}
}
}
</script>
<style scoped>
.tab-control {
display: flex;
}
.tab-control-item {
flex: 1;
text-align: center;
}
.tab-control-item.active {
color: red;
}
.tab-control-item.active span {
border-bottom: 3px solid red;
padding: 5px 10px;
}
</style>
3、非父子组件的通信
在开发中,我们构建了组件树之后,除了父子组件之间的通信之外,还会有非父子组件之间的通信。
这里我们主要讲两种方式:
-
Provide/Inject;
-
Mitt全局事件总线;
Provide和Inject
Provide/Inject用于非父子组件之间共享数据:
-
比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容;
-
在这种情况下,如果我们仍然将props沿着组件链逐级传递下去,就会非常的麻烦;
对于这种情况下,我们可以使用 Provide 和 Inject :
-
无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者;
-
父组件有一个 provide 选项来提供数据;
-
子组件有一个 inject 选项来开始使用这些数据;
实际上,你可以将依赖注入看作是“long range props”,除了:
-
父组件不需要知道哪些子组件使用它 provide 的 property
-
子组件不需要知道 inject 的 property 来自哪里
Provide和Inject基本使用
我们开发一个这样的结构:
Provide和Inject函数的写法
如果Provide中提供的一些数据是来自data,那么我们可能会想要通过this来获取:
这个时候会报错:
- 这里给大家留一个思考题,我们的this使用的是哪里的this?
处理响应式数据
我们先来验证一个结果:如果我们修改了this.names的内容,那么使用length的子组件会不会是响应式的?
我们会发现对应的子组件中是没有反应的:
- 这是因为当我们修改了names之后,之前在provide中引入的 this.names.length 本身并不是响应式的;
那么怎么样可以让我们的数据变成响应式的呢?
-
非常的简单,我们可以使用响应式的一些API来完成这些功能,比如说computed函数;
-
当然,这个computed是vue3的新特性,在后面我会专门讲解,这里大家可以先直接使用一下;
注意:我们在使用length的时候需要获取其中的value
- 这是因为computed返回的是一个ref对象,需要取出其中的value来使用;
全局事件总线mitt库
Vue3从实例中移除了 o n 、 on、 on、off 和 $once 方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库:
-
Vue3官方有推荐一些库,例如 mitt 或 tiny-emitter;
-
这里我们主要讲解一下mitt库的使用;
首先,我们需要先安装这个库:
npm install mitt
其次,我们可以封装一个工具eventbus.js:
import mitt from 'mitt';
const emitter = mitt();
export default emitter;
使用事件总线工具
在项目中可以使用它们:
-
我们在Home.vue中监听事件;
-
我们在App.vue中触发事件;
<script>
import emitter from './utils/eventbus';
export default {
created() {
emitter.on("why", (info) => {
console.log("why:", info);
});
emitter.on("kobe", (info) => {
console.log("kobe:", info);
});
emitter.on("*", (type, info) => {
console.log("* listener:", type, info);
})
}
}
</script>
<script>
import emitter from './utils/eventbus';
export default {
methods: {
btnClick() {
console.log("about按钮的点击");
emitter.emit("why", {name: "why", age: 18});
// emitter.emit("kobe", {name: "kobe", age: 30});
}
}
}
</script>
Mitt的事件取消
在某些情况下我们可能希望取消掉之前注册的函数监听:
// 取消emitter中的所有监听
emitter.all.clear()
// 定义一个函数
function onFoo() {}
emitter.on('foo', onFoo) // 监听
emitter.off('foo', onFoo) // 取消监听
4、插槽Slot
认识插槽Slot
在开发中,我们会经常封装一个个可复用的组件:
-
前面我们会通过props传递给组件一些数据,让组件来进行展示;
-
但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素;
-
比如某种情况下我们使用组件,希望组件显示的是一个按钮,某种情况下我们使用组件希望显示的是一张图片;
-
我们应该让使用者可以决定某一块区域到底存放什么内容和元素;
举个栗子:假如我们定制一个通用的导航组件 - NavBar
-
这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定;
-
左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示;
-
中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等;
-
右边可能是一个文字,也可能是一个图标,也可能什么都不显示;
如何使用插槽slot?
这个时候我们就可以来定义插槽slot:
-
插槽的使用过程其实是抽取共性、预留不同;
-
我们会将共同的元素、内容依然在组件内进行封装;
-
同时会将不同的元素使用slot作为占位,让外部决定到底显示什么样的元素;
如何使用slot呢?
-
Vue中将
<slot>
元素作为承载分发内容的出口; -
在封装组件中,使用特殊的元素
<slot>
就可以为封装组件开启一个插槽; -
该插槽插入什么内容取决于父组件如何使用;
插槽的基本使用
我们一个组件MySlotCpn.vue:该组件中有一个插槽,我们可以在插槽中放入需要显示的内容;
我们在App.vue中使用它们:我们可以插入普通的内容、html元素、组件元素,都可以是可以的;
有时候我们希望在使用插槽时,如果没有插入对应的内容,那么我们需要显示一个默认的内容:
- 当然这个默认的内容只会在没有提供插入的内容时,才会显示;
多个插槽的效果
我们先测试一个知识点:如果一个组件中含有多个插槽,我们插入多个内容时是什么效果?
- 我们会发现默认情况下每个插槽都会获取到我们插入的内容来显示;
具名插槽的使用
事实上,我们希望达到的效果是插槽对应的显示,这个时候我们就可以使用 具名插槽:
-
具名插槽顾名思义就是给插槽起一个名字,
<slot>
元素有一个特殊的 attribute:name; -
一个不带 name 的slot,会带有隐含的名字 default;
动态插槽名
什么是动态插槽名呢?
-
目前我们使用的插槽名称都是固定的;
-
比如 v-slot:left、v-slot:center等等;
-
我们可以通过 v-slot:[dynamicSlotName]方式动态绑定一个名称;
具名插槽使用的时候缩写
具名插槽使用的时候缩写:
-
跟 v-on 和 v-bind 一样,v-slot 也有缩写;
-
即把参数之前的所有内容 (v-slot:) 替换为字符 #;
<template>
<div>
<nav-bar :name="name">
<template #left>
<button>左边的按钮</button>
</template>
<template #center>
<h2>我是标题</h2>
</template>
<template #right>
<i>右边的i元素</i>
</template>
<template #[name]>
<i>why内容</i>
</template>
</nav-bar>
</div>
</template>
渲染作用域
在Vue中有渲染作用域的概念:
-
父级模板里的所有内容都是在父级作用域中编译的;
-
子模板里的所有内容都是在子作用域中编译的;
如何理解这句话呢?我们来看一个案例:
-
在我们的案例中ChildCpn自然是可以让问自己作用域中的title内容的;
-
但是在App中,是访问不了ChildCpn中的内容的,因为它们是跨作用域的访问;
渲染作用域案例
认识作用域插槽
但是有时候我们希望插槽可以访问到子组件中的内容是非常重要的:
-
当一个组件被用来渲染一个数组元素时,我们使用插槽,并且希望插槽中没有显示每项的内容;
-
这个Vue给我们提供了作用域插槽;
我们来看下面的一个案例:
-
1.在App.vue中定义好数据
-
2.传递给ShowNames组件中
-
3.ShowNames组件中遍历names数据
-
4.定义插槽的prop
-
5.通过v-slot:default的方式获取到slot的props
-
6.使用slotProps中的item和index
作用域插槽的案例
// App.vue
<template>
<div>
<!-- 编译作用域 -->
<!-- <child-cpn>
<button>{{title}}</button>
</child-cpn> -->
<show-names :names="names">
<template v-slot="coderwhy">
<button>{{coderwhy.item}}-{{coderwhy.index}}</button>
</template>
</show-names>
<show-names :names="names" v-slot="coderwhy">
<button>{{coderwhy.item}}-{{coderwhy.index}}</button>
</show-names>
<!-- 注意: 如果还有其他的具名插槽, 那么默认插槽也必须使用template来编写 -->
<show-names :names="names">
<template v-slot="coderwhy">
<button>{{coderwhy.item}}-{{coderwhy.index}}</button>
</template>
<template v-slot:why>
<h2>我是why的插入内容</h2>
</template>
</show-names>
<show-names :names="names">
<template v-slot="slotProps">
<strong>{{slotProps.item}}-{{slotProps.index}}</strong>
</template>
</show-names>
</div>
</template>
<script>
import ChildCpn from './ChildCpn.vue';
import ShowNames from './ShowNames.vue';
export default {
components: {
ChildCpn,
ShowNames
},
data() {
return {
names: ["why", "kobe", "james", "curry"]
}
}
}
</script>
<style scoped>
</style>
// ChildCpn.vue
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
data() {
return {
title: "我是title"
}
}
}
</script>
<style scoped>
</style>
// ShowNames.vue
<template>
<div>
<template v-for="(item, index) in names" :key="item">
<slot :item="item" :index="index"></slot>
<slot name="why"></slot>
</template>
</div>
</template>
<script>
export default {
props: {
names: {
type: Array,
default: () => []
}
}
}
</script>
<style scoped>
</style>
独占默认插槽的缩写
如果我们的插槽是默认插槽default,那么在使用的时候 v-slot:default="slotProps"可以简写为v-slot=“slotProps”:
<show-names :names="names">
<template v-slot="slotProps">
<strong>{{slotProps.item}}-{{slotProps.index}}</strong>
</template>
</show-names>
并且如果我们的插槽只有默认插槽时,组件的标签可以被当做插槽的模板来使用,这样,我们就可以将 v-slot 直 接用在组件上:
<show-names :names="names" v-slot="slotProps">
<strong>{{slotProps.item}}-{{slotProps.index}}</strong>
</show-names>
默认插槽和具名插槽混合
但是,如果我们有默认插槽和具名插槽,那么按照完整的template来编写。
只要出现多个插槽,请始终为所有的插槽使用完整的基于 的语法:
5、切换组件案例
比如我们现在想要实现了一个功能:
- 点击一个tab-bar,切换不同的组件显示;
这个案例我们可以通过两种不同的实现思路来实现:
-
方式一:通过v-if来判断,显示不同的组件;
-
方式二:动态组件的方式;
v-if显示不同的组件
我们可以先通过v-if来判断显示不同的组件,这个可以使用我们之前讲过的知识来实现:
<template v-if="currentTab === 'home'">
<home></home>
</template>
<template v-else-if="currentTab === 'about'">
<about></about>
</template>
<template v-else>
<category></category>
</template>
动态组件的实现
动态组件是使用 component 组件,通过一个特殊的attribute is 来实现:
<button v-for="item in tabs" :key="item"
@click="itemClick(item)"
:class="{active: currentTab === item}">
{{item}}
</button>
<!-- 2.动态组件 -->
<keep-alive include="home,about">
<component :is="currentTab"
name="coderwhy"
:age="18"
@pageClick="pageClick">
</component>
</keep-alive>
这个currentTab的值需要是什么内容呢?
-
可以是通过component函数注册的组件;
-
在一个组件对象的components对象中注册的组件;
动态组件的传值
如果是动态组件我们可以给它们传值和监听事件吗?
-
也是一样的;
-
只是我们需要将属性和监听事件放到component上来使用;
<component :is="currentTab"
name="coderwhy"
:age="18"
@pageClick="pageClick">
认识keep-alive
我们先对之前的案例中About组件进行改造:
- 在其中增加了一个按钮,点击可以递增的功能;
比如我们将counter点到10,那么在切换到home再切换回来about时,状态是否可以保持呢?
-
答案是否定的;
-
这是因为默认情况下,我们在切换组件后,about组件会被销毁掉,再次回来时会重新创建组件;
但是,在开发中某些情况我们希望继续保持组件的状态,而不是销毁掉,这个时候我们就可以使用一个内置组件:keep-alive。
keep-alive属性
keep-alive有一些属性:
-
include - string | RegExp | Array。只有名称匹配的组件会被缓存;
-
exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存;
-
max - number | string。最多可以缓存多少组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁;
include 和 exclude prop 允许组件有条件地缓存:
-
二者都可以用逗号分隔字符串、正则表达式或一个数组来表示;
-
匹配首先检查组件自身的 name 选项;
缓存组件的生命周期
对于缓存的组件来说,再次进入时,我们是不会执行created或者mounted等生命周期函数的:
-
但是有时候我们确实希望监听到何时重新进入到了组件,何时离开了组件;
-
这个时候我们可以使用activated 和 deactivated 这两个生命周期钩子函数来监听;
activated() {
console.log("about activated");
},
deactivated() {
console.log("about deactivated");
}
6、Vue中实现异步组件
Webpack的代码分包
默认的打包过程:
-
默认情况下,在构建整个组件树的过程中,因为组件和组件之间是通过模块化直接依赖的,那么webpack在打包时就会将组件模块打包到一起(比如一个app.js文件中);
-
这个时候随着项目的不断庞大,app.js文件的内容过大,会造成首屏的渲染速度变慢;
打包时,代码的分包:
-
所以,对于一些不需要立即使用的组件,我们可以单独对它们进行拆分,拆分成一些小的代码块chunk.js;
-
这些chunk.js会在需要时从服务器加载下来,并且运行代码,显示对应的内容;
那么webpack中如何可以对代码进行分包呢?
Vue中实现异步组件
如果我们的项目过大了,对于某些组件我们希望通过异步的方式来进行加载(目的是可以对其进行分包处理),那么Vue中给我们提供了一个函数:defineAsyncComponent。
defineAsyncComponent接受两种类型的参数:
-
类型一:工厂函数,该工厂函数需要返回一个Promise对象;
-
类型二:接受一个对象类型,对异步函数进行配置;
工厂函数类型一的写法:
import { defineAsyncComponent } from 'vue';
const AsyncCategory = defineAsyncComponent(() => import("./AsyncCategory.vue"))
export default {
components: {
AsyncCategory,
}
}
异步组件的写法二
const AsyncCategory = defineAsyncComponent({
// 工厂函数
loader: () => import("./AsyncCategory.vue"),
// 加载过程中显示的组件
loadingComponent: Loading,
// 加载失败时显示的组件
// errorComponent: Error,
// 在显示loadingComponent组件之前, 等待多长时间
delay: 2000,
/**
* err: 错误信息,
* retry: 函数, 调用retry尝试重新加载
* attempts: 记录尝试的次数
*/
onError: function(err, retry, attempts) {
}
// 如果提供了timeout,并且加载组件的时间超过了设定值,将显示错误组件
// 默认值: Infinity(即永不超时,单位ms)
// timeout: 0,
// 定义组件是否课挂起 | 默认值: true
suspensible: true
})
异步组件和Suspense
注意:目前(2022-03-09)Suspense显示的是一个实验性的特性,API随时可能会修改。
试验性
Suspense 是一个试验性的新特性,其 API 可能随时会发生变动。特此声明,以便社区能够为当前的实现提供反馈。
生产环境请勿使用。
Suspense是一个内置的全局组件,该组件有两个插槽:
-
default:如果default可以显示,那么显示default的内容;
-
fallback:如果default无法显示,那么会显示fallback插槽的内容;
<suspense>
<template #default>
<async-category></async-category>
</template>
<template #fallback>
<loading></loading>
</template>
</suspense>
7、引用元素和组件
$refs的使用
某些情况下,我们在组件中想要直接获取到元素对象或者子组件实例:
-
在Vue开发中我们是不推荐进行DOM操作的;
-
这个时候,我们可以给元素或者组件绑定一个ref的attribute属性;
组件实例有一个$refs属性:
- 它一个对象Object,持有注册过 ref attribute 的所有 DOM 元素和组件实例。
btnClick() {
// 访问元素
console.log(this.$refs.title);
// 访问组件实例
console.log(this.$refs.navBar.message);
this.$refs.navBar.sayHello();
// $el
console.log(this.$refs.navBar.$el);
}
p a r e n t 和 parent和 parent和root
我们可以通过$parent来访问父元素。
NavBar.vue的实现:
- 这里我们也可以通过$root来实现,因为App是我们的根组件;
<template>
<div>
<h2>NavBar</h2>
<button @click="getParentAndRoot">获取父组件和根组件</button>
</div>
</template>
<script>
export default {
data() {
return {
message: "我是NavBar中的message"
}
},
methods: {
sayHello() {
console.log("Hello NavBar");
},
getParentAndRoot() {
console.log(this.$parent);
console.log(this.$root);
}
}
}
</script>
<style scoped>
</style>
注意:在Vue3中已经移除了$children的属性,所以不可以使用了
8、生命周期
什么是生命周期呢?
-
每个组件都可能会经历从创建、挂载、更新、卸载等一系列的过程;
-
在这个过程中的某一个阶段,用于可能会想要添加一些属于自己的代码逻辑(比如组件创建完后就请求一些服务器数据);
-
但是我们如何可以知道目前组件正在哪一个过程呢?Vue给我们提供了组件的生命周期函数;
生命周期函数:
-
生命周期函数是一些钩子函数,在某个时间会被Vue源码内部进行回调;
-
通过对生命周期函数的回调,我们可以知道目前组件正在经历什么阶段;
-
那么我们就可以在该生命周期中编写属于自己的逻辑代码了;
生命周期的流程
9、v-model
前面我们在input中可以使用v-model来完成双向绑定:
-
这个时候往往会非常方便,因为v-model默认帮助我们完成了两件事;
-
v-bind:value的数据绑定和@input的事件监听;
如果我们现在封装了一个组件,其他地方在使用这个组件时,是否也可以使用v-model来同时完成这两个功能呢?
- 也是可以的,vue也支持在组件上使用v-model;
当我们在组件上使用的时候,等价于如下的操作:
- 我们会发现和input元素不同的只是属性的名称和事件触发的名称而已;
<hy-input v-model="message"></hy-input>
<hy-input :modelValue="message" @update:model-value="message = $event"></hy-input>
组件v-model的实现
那么,为了我们的MyInput组件可以正常的工作,这个组件内的 必须:
-
将其 value attribute 绑定到一个名叫 modelValue 的 prop 上;
-
在其 input 事件被触发时,将新的值通过自定义的 update:modelValue 事件抛出;
MyInput.vue的组件代码如下:
<template>
<div>
<input v-model="value">
<input v-model="why">
</div>
</template>
<script>
export default {
props: {
modelValue: String,
title: String
},
emits: ["update:modelValue", "update:title"],
computed: {
value: {
set(value) {
this.$emit("update:modelValue", value);
},
get() {
return this.modelValue;
}
},
why: {
set(why) {
this.$emit("update:title", why);
},
get() {
return this.title;
}
}
}
}
</script>
<style scoped>
</style>
<hy-input v-model="message" v-model:title="title"></hy-input>
computed实现
我们依然希望在组件内部按照双向绑定的做法去完成,应该如何操作呢?我们可以使用计算属性的setter和getter来完成。
绑定多个属性
我们现在通过v-model是直接绑定了一个属性,如果我们希望绑定多个属性呢?
-
也就是我们希望在一个组件上使用多个v-model是否可以实现呢?
-
我们知道,默认情况下的v-model其实是绑定了 modelValue 属性和 @update:modelValue的事件;
-
如果我们希望绑定更多,可以给v-model传入一个参数,那么这个参数的名称就是我们绑定属性的名称;
注意:这里我是绑定了多个属性的
<hy-input v-model="message" v-model:title="title" :title2="title2"></hy-input>
// 这几个绑定都是同样的
v-model:title相当于做了两件事:
-
绑定了title属性;
-
监听了 @update:title的事件;
<script>
export default {
props: {
modelValue: String,
title: String
},
emits: ["update:modelValue", "update:title"],
computed: {
value: {
set(value) {
this.$emit("update:modelValue", value);
},
get() {
return this.modelValue;
}
},
why: {
set(why) {
this.$emit("update:title", why);
},
get() {
return this.title;
}
}
}
}
</script>
八、Vue3过渡&动画实现
1、transition和animation动画
认识动画
在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验:
-
React框架本身并没有提供任何动画相关的API,所以在React中使用过渡动画我们需要使用一个第三方库react-transition-group;
-
Vue中为我们提供一些内置组件和对应的API来完成动画,利用它们我们可以方便的实现过渡动画效果;
我们来看一个案例:
-
Hello World的显示和隐藏;
-
通过下面的代码实现,是不会有任何动画效果的;
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<h2 v-if="isShow">Hello World</h2>
</div>
</template>
<script>
export default {
data() {
return {
isShow: true
}
}
}
</script>
没有动画的情况下,整个内容的显示和隐藏会非常的生硬:
- 如果我们希望给单元素或者组件实现过渡动画,可以使用 transition 内置组件来完成动画;
Vue的transition动画
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:
-
条件渲染 (使用 v-if)条件展示 (使用 v-show)
-
动态组件
-
组件根节点
<transition name="why">
<h2 v-if="isShow">Hello World</h2>
</transition>
<style scoped>
.why-enter-from,
.why-leave-to {
opacity: 0;
}
.why-enter-to,
.why-leave-from {
opacity: 1;
}
.why-enter-active,
.why-leave-active {
transition: opacity 2s ease;
}
</style>
Transition组件的原理
我们会发现,Vue自动给h2元素添加了动画,这是什么原因呢?
当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:
-
1.自动嗅探目标元素是否应用了CSS过渡或者动画,如果有,那么在恰当的时机添加/删除 CSS类名;
-
2.如果 transition 组件提供了JavaScript钩子函数,这些钩子函数将在恰当的时机被调用;
-
3.如果没有找到JavaScript钩子并且也没有检测到CSS过渡/动画,DOM插入、删除操作将会立即执行;
那么都会添加或者删除哪些class呢?
过渡动画class
我们会发现上面提到了很多个class,事实上Vue就是帮助我们在这些class之间来回切换完成的动画:
v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被删除),在过渡/动画完成之后移除。
class添加的时机和命名规则
class的name命名规则如下:
-
如果我们使用的是一个没有name的transition,那么所有的class是以 v- 作为默认前缀;
-
如果我们添加了一个name属性,比如
<transtion name="why">
,那么所有的class会以 why- 开头;
过渡css动画
前面我们是通过transition来实现的动画效果,另外我们也可以通过animation来实现。
<template>
<div class="app">
<div><button @click="isShow = !isShow">显示/隐藏</button></div>
<transition name="why">
<h2 class="title" v-if="isShow">Hello World</h2>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
isShow: true
}
}
}
</script>
<style scoped>
.app {
width: 200px;
margin: 0 auto;
}
.title {
display: inline-block;
}
.why-enter-active {
animation: bounce 1s ease;
}
.why-leave-active {
animation: bounce 1s ease reverse;
}
@keyframes bounce {
0% {
transform: scale(0)
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>
同时设置过渡和动画
Vue为了知道过渡的完成,内部是在监听 transitionend 或 animationend,到底使用哪一个取决于元素应用的CSS规则:
- 如果我们只是使用了其中的一个,那么Vue能自动识别类型并设置监听;
但是如果我们同时使用了过渡和动画呢?
-
并且在这个情况下可能某一个动画执行结束时,另外一个动画还没有结束;
-
在这种情况下,我们可以设置 type 属性为 animation 或者 transition 来明确的告知Vue监听的类型;
<transition name="why" type="transition" :duration="{enter: 800, leave: 1000}">
<h2 class="title" v-if="isShow">Hello World</h2>
</transition>
显示的指定动画时间
我们也可以显示的来指定过渡的时间,通过 duration 属性。
duration可以设置两种类型的值:
-
number类型:同时设置进入和离开的过渡时间;
-
object类型:分别设置进入和离开的过渡时间;
过渡的模式mode
我们来看当前的动画在两个元素之间切换的时候存在的问题:
我们会发现 Hello World 和 你好啊,李银河是同时存在的:
-
这是因为默认情况下进入和离开动画是同时发生的;
-
如果确实我们希望达到这个的效果,那么是没有问题;
但是如果我们不希望同时执行进入和离开动画,那么我们需要设置transition的过渡模式:
-
in-out: 新元素先进行过渡,完成之后当前元素过渡离开;
-
out-in: 当前元素先进行过渡,完成之后新元素过渡进入;
动态组件的切换
上面的示例同样适用于我们的动态组件:
<template>
<div class="app">
<div><button @click="isShow = !isShow">显示/隐藏</button></div>
<transition name="why" mode="out-in">
<h2 class="title" v-if="isShow">Hello World</h2>
<h2 class="title" v-else>你好啊,李银河</h2>
</transition>
</div>
</template>
appear初次渲染
默认情况下,首次渲染的时候是没有动画的,如果我们希望给他添加上去动画,那么就可以增加另外一个属性appear:
<template>
<div class="app">
<div><button @click="isShow = !isShow">显示/隐藏</button></div>
<transition name="why" appear>
<h2 class="title" v-if="isShow">Hello World</h2>
</transition>
</div>
</template>
2、animate.css和gsap
如果我们手动一个个来编写这些动画,那么效率是比较低的,所以在开发中我们可能会引用一些第三方库的动画库,
比如animate.css。
什么是animate.css呢?
-
Animate.css is a library of ready-to-use, cross-browser animations for use in your web projects. Great for emphasis, home pages, sliders, and attention-guiding hints.
-
Animate.css是一个已经准备好的、跨平台的动画库为我们的web项目,对于强调、主页、滑动、注意力引导非常有用;
如何使用Animate库呢?
-
第一步:需要安装animate.css库;
-
第二步:导入animate.css库的样式;
-
第三步:使用animation动画或者animate提供的类;
自定义过渡class
我们可以通过以下 attribute 来自定义过渡类名:
-
enter-from-class
-
enter-active-class
-
enter-to-class
-
leave-from-class
-
leave-active-class
-
leave-to-class
他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css. 结合使用十分有用。
animate.css库的使用
安装animate.css:
npm install animate.css
在main.js中导入animate.css:
import "animate.css";
接下来在使用的时候我们有两种用法:
-
用法一:直接使用animate库中定义的 keyframes 动画;
-
用法二:直接使用animate库提供给我们的类;
.why-enter-active {
animation: bounceInUp 1s ease-in;
}
.why-leave-active {
animation: bounceInUp 1s ease-in reverse;
}
<transition enter-active-class="animate__animated animate__fadeInDown"
leave-active-class="animate__animated animate__flipInY">
<h2 class="title" v-if="isShow">Hello World</h2>
</transition>
认识gsap库
某些情况下我们希望通过JavaScript来实现一些动画的效果,这个时候我们可以选择使用gsap库来完成。
什么是gsap呢?
-
GSAP是The GreenSock Animation Platform(GreenSock动画平台)的缩写;
-
它可以通过JavaScript为CSS属性、SVG、Canvas等设置动画,并且是浏览器兼容的;
这个库应该如何使用呢?
-
第一步:需要安装gsap库;
-
第二步:导入gsap库;
-
第三步:使用对应的api即可;
我们可以先安装一下gsap库:
npm install gsap
JavaScript钩子
在使用动画之前,我们先来看一下transition组件给我们提供的JavaScript钩子,这些钩子可以帮助我们监听动画执行到什么阶段了
当我们使用JavaScript来执行过渡动画时,需要进行 done 回调,否则它们将会被同步调用,过渡会立即完成。
添加 :css=“false”,也会让 Vue 会跳过 CSS 的检测,除了性能略高之外,这可以避免过渡过程中 CSS 规则的影响。
gsap库的使用
那么接下来我们就可以结合gsap库来完成动画效果:
<template>
<div class="app">
<div><button @click="isShow = !isShow">显示/隐藏</button></div>
<transition @enter="enter"
@leave="leave"
:css="false">
<h2 class="title" v-if="isShow">Hello World</h2>
</transition>
</div>
</template>
<script>
import gsap from 'gsap';
export default {
data() {
return {
isShow: true,
}
},
methods: {
enter(el, done) {
console.log("enter");
gsap.from(el, {
scale: 0,
x: 200,
onComplete: done
})
},
leave(el, done) {
console.log("leave");
gsap.to(el, {
scale: 0,
x: 200,
onComplete: done
})
}
}
}
</script>
<style scoped>
.title {
display: inline-block;
}
</style>
gsap实现数字变化
在一些项目中,我们会见到数字快速变化的动画效果,这个动画可以很容易通过gsap来实现:
<template>
<div class="app">
<input type="number" step="100" v-model="counter">
<!-- <h2>当前计数: {{showCounter}}</h2> -->
<h2>当前计数: {{showNumber.toFixed(0)}}</h2>
</div>
</template>
<script>
import gsap from 'gsap';
export default {
data() {
return {
counter: 0,
showNumber: 0
}
},
// computed: {
// showCounter() {
// return this.showNumber.toFixed(0);
// }
// },
watch: {
counter(newValue) {
gsap.to(this, {duration: 1, showNumber: newValue})
}
}
}
</script>
<style scoped>
</style>
3、列表过渡
认识列表的过渡
目前为止,过渡动画我们只要是针对单个元素或者组件的:
-
要么是单个节点;
-
要么是同一时间渲染多个节点中的一个;
那么如果希望渲染的是一个列表,并且该列表中添加删除数据也希望有动画执行呢?
- 这个时候我们要使用
<transition-group>
组件来完成;
使用<transition-group>
有如下的特点:
-
默认情况下,它不会渲染一个元素的包裹器,但是你可以指定一个元素并以 tag attribute 进行渲染;
-
过渡模式不可用,因为我们不再相互切换特有的元素;
-
内部元素总是需要提供唯一的 key attribute 值;
-
CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身;
列表过渡的基本使用
我们来做一个案例:
-
案例是一列数字,可以继续添加或者删除数字;
-
在添加和删除数字的过程中,对添加的或者移除的数字添加动画;
<template>
<div>
<button @click="addNum">添加数字</button>
<button @click="removeNum">删除数字</button>
<button @click="shuffleNum">数字洗牌</button>
<transition-group tag="p" name="why">
<span v-for="item in numbers" :key="item" class="item">
{{item}}
</span>
</transition-group>
</div>
</template>
<script>
import _ from 'lodash';
export default {
data() {
return {
numbers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
numCounter: 10
}
},
methods: {
addNum() {
// this.numbers.push(this.numCounter++)
this.numbers.splice(this.randomIndex(), 0, this.numCounter++)
},
removeNum() {
this.numbers.splice(this.randomIndex(), 1)
},
shuffleNum() {
this.numbers = _.shuffle(this.numbers);
},
randomIndex() {
return Math.floor(Math.random() * this.numbers.length)
}
},
}
</script>
<style scoped>
.item {
margin-right: 10px;
display: inline-block;
}
.why-enter-from,
.why-leave-to {
opacity: 0;
transform: translateY(30px);
}
.why-enter-active,
.why-leave-active {
transition: all 1s ease;
}
.why-leave-active {
position: absolute;
}
.why-move {
transition: transform 1s ease;
}
</style>
列表过渡的移动动画
在上面的案例中虽然新增的或者删除的节点是有动画的,但是对于哪些其他需要移动的节点是没有动画的:
-
我们可以通过使用一个新增的 v-move 的class来完成动画;
-
它会在元素改变位置的过程中应用;
-
像之前的名字一样,我们可以通过name来自定义前缀;
列表的交错过渡案例
- 我们来通过gsap的延迟delay属性,做一个交替消失的动画:
<template>
<div>
<input v-model="keyword">
<transition-group tag="ul" name="why" :css="false"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave">
<li v-for="(item, index) in showNames" :key="item" :data-index="index">
{{item}}
</li>
</transition-group>
</div>
</template>
<script>
import gsap from 'gsap';
export default {
data() {
return {
names: ["abc", "cba", "nba", "why", "lilei", "hmm", "kobe", "james"],
keyword: ""
}
},
computed: {
showNames() {
return this.names.filter(item => item.indexOf(this.keyword) !== -1)
}
},
methods: {
beforeEnter(el) {
el.style.opacity = 0;
el.style.height = 0;
},
enter(el, done) {
gsap.to(el, {
opacity: 1,
height: "1.5em",
delay: el.dataset.index * 0.5,
onComplete: done
})
},
leave(el, done) {
gsap.to(el, {
opacity: 0,
height: 0,
delay: el.dataset.index * 0.5,
onComplete: done
})
}
}
}
</script>
<style scoped>
/* .why-enter-from,
.why-leave-to {
opacity: 0;
}
.why-enter-active,
.why-leave-active {
transition: opacity 1s ease;
} */
</style>
Vue3+TypeScript从入门到进阶(五)——Vue3基础知识点(下)——附沿途学习案例及项目实战代码
四、TypeScript知识点
Vue3+TypeScript从入门到进阶(六)——TypeScript知识点——附沿途学习案例及项目实战代码
五、项目实战
Vue3+TypeScript从入门到进阶(七)——项目实战——附沿途学习案例及项目实战代码
六、项目打包和自动化部署
Vue3+TypeScript从入门到进阶(八)——项目打包和自动化部署——附沿途学习案例及项目实战代码
七、沿途学习代码地址及案例地址
1、沿途学习代码地址
https://gitee.com/wu_yuxin/vue3-learning.git
2、项目案例地址
https://gitee.com/wu_yuxin/vue3-ts-cms.git
八、知识拓展
1、ES6数组与对象的解构赋值详解
数组的解构赋值
基本用法
ES6允许按照一定的模式,从数组和对象中提取值,对变量进行赋值,这被称之为解构(Destructuring)
// 以前为变量赋值,只能直接指定值
var a = 1;
var b = 2;
var c = 3;
// ES6允许写成这样
var [a,b,c] = [1,2,3];
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
下面是一些使用嵌套数组进行解构的例子:
let [foo,[[bar],baz]] = [1,[[2],3]];
foo // 1
bar // 2
baz // 3
let [,,third] = ["foo","bar","baz"];
third // "baz"
let [head,...tail] = [1,2,3,4];
head // 1
tail // [2,3,4]
let [x,y,...z] = ['a'];
x // "a"
y // undefined
z // []
默认值
解构赋值允许制定默认值
var [foo = true] = [];
foo // true
[x,y='b'] = ['a'];
// x='a', y='b'
注意,ES6内部使用严格相等运算符(===),判断一个位置是否有值。
所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。
var [x=1] = [undefined];
x //1
var [x=1] = [null];
x // null
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值:
function f(){
console.log('aaa');
}
let [x=f()] = [1];
上面的代码中,因为x能取到值,所以函数f()根本不会执行。上面的代码其实等价于下面的代码:
let x;
if([1][0] === undefined){
x = f();
}else{
x = [1][0];
}
默认值可以引用解构赋值的其他变量,但该变量必须已经声明:
let [x=1,y=x] = [];
// x=1; y=1
let [x=1,y=x] = [2];
// x=2; y=2
let [x=1,y=x] = [1,2];
// x=1; y=2
let [x=y,y=1] = []; // ReferenceError
上面最后一个表达式,因为x用到默认值是y时,y还没有声明。
对象的解构赋值
1、最简单的案例
看下面的案例
let person = {
name: 'yhb',
age: 20
}
/*
注意:下面虽然看起来是创建了一个对象,对象中有两个属性 name 和 age
但是:其实是声明了两个变量
name:等于对象person 中的name属性的值
age:等于对象person 中的 age属性的值
*/
let { name, age } = person
console.log(name,age)
如上面注释中所说,声明了变量 name和age,然后分别从对象person中寻找与变量同名的属性,并将属性的值赋值给变量
所以,这里的关键,就是首先要知道对象中都有哪些属性,然后再使用字面量的方式声明与其同名的变量
2、属性不存在怎么办
如果不小心声明了一个对象中不存在的属性怎么办?
或者,实际情况下,可能是我们就是想再声明一个变量,但是这个变量也不需要从对象中获取值,这个时候,此变量的值就是 undefined
let person = {
name: 'yhb',
age: 20
}
let { name, age,address } = person
console.log(name,age,address)
此时,可以给变量加入一个默认值
let { name, age,address='北京' } = person
3、属性太受欢迎怎么办
当前声明了 name 和 age 变量,其值就是person对象中name和age属性的值,如果还有其他变量也想获取这两个属性的值怎么办?
let { name, age, address = '北京' } = person
console.log(name, age, address)
let { name, age } = person
console.log(name, age)
上面的方法肯定不行,会提示定义了重复的变量 name 和 age
那怎么办呢?
难道只能放弃结构赋值,使用老旧的方式吗?
let l_name=person.name
let l_age=person.age
console.log(l_name,l_age)
其实不然!
let {name:l_name,age:l_age}=person
console.log(l_name,l_age)
说明:
声明变量 l_name 并从对象person中获取name属性的值赋予此变量
声明变量 l_age, 并从对象person中获取age属性的值赋予此变量
这里的重点是下面这行代码
let {name:l_name,age:l_age}=person
按照创建对象字面量的逻辑,name 为键,l_name 为值。但注意,这里是声明变量,并不是创建对象字面量,所以争取的解读应该是
声明变量 l_name,并从person 对象中找到与 name 同名的属性,然后将此属性的值赋值给变量 l_name
所以,我们最后输出的是变量 l_name和l_age
console.log(l_name,l_age)
当然这种状态下,也是可以给变量赋予默认值的
let { name:l_name, age:l_age, address:l_address='北京' }=person
4、嵌套对象如何解构赋值
let person = {
name: 'yhb',
age: 20,
address: {
province: '河北省',
city: '保定'
}
}
// 从对象 person 中找到 address 属性,并将值赋给变量 address
let {address}=person
// 从对象 address 中找到 province 属性,并将值赋给变量 province
let {province}=address
console.log(province)
上面代码一层层的进行结构赋值,也可以简写为如下形式
let {address:{province}}=person
从peson 对象中找到 address 属性,取出其值赋值给冒号前面的变量 address,然后再将 变量address 的值赋值给 冒号 后面的变量 {province},相当于下面的写法
let {province}=address
字符串的解构赋值
1、字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
2、JavaScript的 …(展开运算符)
三个连续的点具有两个含义:展开运算符(spread operator)和剩余运算符(rest operator)。
展开运算符
展开运算符允许迭代器在接收器内部分别展开或扩展。迭代器和接收器可以是任何可以循环的对象,例如数组、对象、集合、映射等。你可以把一个容器的每个部分分别放入另一个容器。
const newArray = ['first', ...anotherArray];
剩余参数
剩余参数语法允许我们将无限数量的参数表示为数组。命名参数的位置可以在剩余参数之前。
const func = (first, second, ...rest) => {};
用例
定义是非常有用的,但是很难仅从定义中理解概念。我认为用日常用例会加强对定义的理解。
复制数组
当我们需要修改一个数组,但又不想改变原始数组(其他人可能会使用它)时,就必须复制它。
const fruits = ['apple', 'orange', 'banana'];
const fruitsCopied = [...fruits]; // ['apple', 'orange', 'banana']
console.log(fruits === fruitsCopied); // false
// 老方法
fruits.map(fruit => fruit);
它正在选择数组中的每个元素,并将每个元素放在新的数组结构中。我们也可以使用 map 操作符实现数组的复制并进行身份映射。
唯一数组
如果我们想从数组中筛选出重复的元素,那么最简单的解决方案是什么?
Set 对象仅存储唯一的元素,并且可以用数组填充。它也是可迭代的,因此我们可以将其展开到新的数组中,并且得到的数组中的值是唯一的。
const fruits = ['apple', 'orange', 'banana', 'banana'];
const uniqueFruits = [...new Set(fruits)]; // ['apple', 'orange', 'banana']
// old way
fruits.filter((fruit, index, arr) => arr.indexOf(fruit) === index);
串联数组
可以用 concat 方法连接两个独立的数组,但是为什么不再次使用展开运算符呢?
const fruits = ['apple', 'orange', 'banana'];
const vegetables = ['carrot'];
const fruitsAndVegetables = [...fruits, ...vegetables]; // ['apple', 'orange', 'banana', 'carrot']
const fruitsAndVegetables = ['carrot', ...fruits]; // ['carrot', 'apple', 'orange', 'banana']
// 老方法
const fruitsAndVegetables = fruits.concat(vegetables);
fruits.unshift('carrot');
将参数作为数组进行传递
当传递参数时,展开运算符能够使我们的代码更具可读性。在 ES6 之前,我们必须将该函数应用于 arguments。现在我们可以将参数展开到函数中,从而使代码更简洁。
const mixer = (x, y, z) => console.log(x, y, z);
const fruits = ['apple', 'orange', 'banana'];
mixer(...fruits); // 'apple', 'orange', 'banana'
// 老方法
mixer.apply(null, fruits);
数组切片
使用 slice 方法切片更加直接,但是如果需要的话,展开运算符也可以做到。但是必须一个个地去命名其余的元素,所以从大数组中进行切片的话,这不是个好方法。
const fruits = ['apple', 'orange', 'banana'];
const [apple, ...remainingFruits] = fruits; // ['orange', 'banana']
// 老方法
const remainingFruits = fruits.slice(1);
将参数转换为数组
Javascript 中的参数是类似数组的对象。你可以用索引来访问它,但是不能调用像 map、filter 这样的数组方法。参数是一个可迭代的对象,那么我们做些什么呢?在它们前面放三个点,然后作为数组去访问!
const mixer = (...args) => console.log(args);
mixer('apple'); // ['apple']
将 NodeList 转换为数组
参数就像从 querySelectorAll 函数返回的 NodeList 一样。它们的行为也有点像数组,只是没有对应的方法。
[...document.querySelectorAll('div')];
// 老方法
Array.prototype.slice.call(document.querySelectorAll('div'));
复制对象
最后,我们介绍对象操作。复制的工作方式与数组相同。在以前它可以通过 Object.assign 和一个空的对象常量来实现。
const todo = { name: 'Clean the dishes' };
const todoCopied = { ...todo }; // { name: 'Clean the dishes' }
console.log(todo === todoCopied); // false
// 老方法
Object.assign({}, todo);
合并对象
合并的唯一区别是具有相同键的属性将被覆盖。最右边的属性具有最高优先级。
const todo = { name: 'Clean the dishes' };
const state = { completed: false };
const nextTodo = { name: 'Ironing' };
const merged = { ...todo, ...state, ...nextTodo }; // { name: 'Ironing', completed: false }
// 老方法
Object.assign({}, todo, state, nextTodo);
需要注意的是,合并仅在层次结构的第一级上创建副本。层次结构中的更深层次将是相同的引用。
将字符串拆分为字符
最后是字符串。你可以用展开运算符把字符串拆分为字符。当然,如果你用空字符串调用 split 方法也是一样的。
const country = 'USA';
console.log([...country]); // ['U', 'S', 'A']
// 老方法
country.split('');
3、export ‘defineEmit’ (imported as ‘defineEmit’) was not found in ‘vue’
在学习vue3的顶层编写方式时的父子组件通信的时候,我们会看到一些比较老(2020、2021年初)的博客里面会有使用defineEmit的,但是如果我们用比较新版本的Vue3的话,就会报错。原因是,新版本的Vue3将defineEmit改成了defineEmits了
九、其他知识学习
1、Webpack学习
2、数据可视化-echarts
数据可视化-echarts入门、常见图表案例、超详细配置解析及项目案例