vue3

vue3的优势

  • 更容易维护:组合式API;更好的TypeScript支持
  • 更小的体积;良好的TreeShaking;按需引入
  • 更快的速度;重写diff算法;模版编译优化;更高效的组件初始化
  • 更优的数据响应式;Proxy
  • 渐进式框架:表示在项目中可以一点点引入来使用ue,不一定需要全部使用
    (目前前端最流行的三大框架:Vue,React,=,Angular)
    vue的本质:js库

Vue初体验:

动态展示数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id ="app"> </div>
<script src="./vue.js"></script>
<script>
const app = Vue.createApp({
template: `<h3>{{message}}</h3>`,
data: function(){
return{
title:"vue3初体验"
message:"早上好,小聂同学"
}
}
})
app.mount("#app")
<script>

开发中从服务器把数据请求到data中,再通过插值语法动态地把数据放进去
声明式编程和响应式编程
vue受到MVVM模型启发,帮我们绑定数据,绑定dom;view视图和Model数据方法通过vuemodel这个桥梁来完成

OptionsAPI

data属性选项

在vue3中,data属性值必须是函数,用return返回对象的形式
(vue2中官方推荐是一个函数,传入对象也可以,但在vue3中不可以)

数据劫持

data中返回的对象会被Vue的响应式系统劫持,之后对该对象的修改或访问都会在劫持中被处理,一旦数据发生变化就会重新加载。
vue2和vue3的数据劫持方式不同:

1
2
3
4
5
6
7
8
9
//vue2采用
Object.defineProperty(info,"属性名",{
set:
get:
})
//vue3采用
new Proxy(indo,{set:
get:
})

options api–methods

methods只是个对象类型,没有作用域的说法
这些方法可以被绑定到模板中;
在该方法中,我们可以使用this关键字来直接访问到data中返回的对象的属性

1
2
3
4
5
6
methos:{
//完整写法
myfunc: function(){.....}
//简写
myfunc(){....}
}

methods里使用的this是vue帮你绑定好的this
注意:不应该使用箭头函数来定义method函数(例如 plus:()=>this.a++)
因为箭头函数绑定了父级作用域的上下文,this的指向是window,而不是组件实例,结果会是undefined

options API - computed计算属性

对于任何响应式数据的复杂逻辑操作,使用计算属性,比如需要对数据进行一些转化再现实,或对数据进行一些转化
计算属性将被混入到组件实例中,所有getter和seter的this上下文自动被绑定为组件实例
在计算属性中可以使用this

为什么不用methods?
methods会把所有data的使用过程都变成一个方法的调用

使用方法

核心步骤:1.导入computed函数2.执行函数在回调函数中return基于响应式数据做计算的值,用变量接收
应用如:基于原数据,动态计算新数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
// 导入
import {ref, computed } from 'vue'
// 原始数据
const count = ref(0)
// 计算属性
const doubleCount = computed(()=>count.value * 2)

// 原始数据
const list = ref([1,2,3,4,5,6,7,8])
console.log(list) //得到的是list对象
console.log(list.value) //得到的是数组[1,2,3,4,5,6,7,8]
// 计算属性list
const filterList = computed(item=>item > 2)
</script>
  1. 计算属性默认对应的是一个函数
    1
    2
    3
    4
    5
    computed:{
    计算属性名(){
    return .....
    }
    }
    使用时{{计算属性名}}直接调用
  2. 和methods区别
    计算属性是有缓存的

模板语法

要有一个意识:绑定可以分为绑定内容,绑定属性,绑定事件

Mustache模板语法

当data中的数据发生改变时,对应的内容也会发生更新
Mustache模板语法

基本指令(用得少)

  1. v-once
    v-once指令可以让我们渲染的内容只渲染一次,后续的修改不会被渲染
    (当数据发生变化时,元素或者组件以及其所有的子元素将视为静态内容并且跳过)
    该指令可以用于性能优化
  2. v-pre
    跳过不需要编译的节点,显示原始的Mustache标签,加快编译的速度
  3. v-text
    v-text指令,将内容插入到模板中,不会被编译成dom节点,而是直接被渲染成文本,如果有文本会被替代
  4. v-html
    v-html指令,将内容插入到模板中,会被编译成dom节点,而不是文本
  5. v-cloak
    这个指令保持在元素上直到关联组件实例结束编译
    和CSS 规则如 [v-cloak]{display:none}一起用时
    这个指令可以隐藏未编译的 Mustache 标签直到组件实例准备完毕
    v-cloak

v-memo指令(性能优化)

先用一个例子理解:

1
2
3
4
5
<div id= "app">
<div v-memo="[name]">
<h3>下午好{{name}}</h3>
<h3>今天{{day}}</h3>
</div> </div>

那么这个被v-memo包裹的div里,只有name属性对应的值改变时才会重新渲染,day值改变将不会更新渲染
v-memo

v-bind(绑定属性)

前面的指令主要是将值插入到模板内容中
想动态绑定一些属性,使用v-bind,缩写为:

v-bind绑定基本属性

使用场景:如动态绑定img的src属性(两张图来回切换:三元运算符实现),a元素的href属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="app">
<img v-bind:src="imgUrl">
<button @click="switchImg">切换图片</button>
<a v-bind:href="href">点击跳转链接</a >
</div>
<script>
const app = Vue.createApp({
data: function(){
return{
imgUrl01:".........."
imgUrl02:".........."
imgShow:"......"
href:".........."
}
},
methods:{
switchImg: function(){
this.imgShow=this.imgShow===this.imgUrl01 ? this.imgUrl02 : this.imgUrl01
}
}
})
app.mount("#app")
</script>

v-bind绑定class属性

比如当数据处于某个状态时为一种样式
特殊:动态class可以写成对象语法
绑定class有对象语法和数组语法
动态绑定的class可以和普通class并存,互不影响

动态绑定calss属性–对象语法(常用)

下面的对象语法中只有一个键值对,里面可以写多个键值对,值应该是boolean语法

  1. 对象语法绑定一个键值对
    这个例子,这种有和无切换的写法很常用,要想到
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <div id="app">
    <button :class="{active: isActive}"@click="btnClick">点我切换样式</button>
    </div>
    <script>
    const app = Vue.createApp({
    data: function(){
    return{
    isActive:true
    }
    },
    methods:{
    btnClick: function(){
    this.isActive = ! this.isActive
    }
    }
    })
    app.mount("#app")
    </script>

    在写methods中时,我的this.isActive总是忘记写this,为什么这里要加this呢,不加可以吗?
    首先不可以不加this,不然会找不到属性名报未声明的错误,其次伟大的AI老师这样说:

  2. 对象语法绑定多个键值对
    如果用对象语法绑定很多个键值对,都写在标签中可能不好看,那么这时候可以把这堆对象语法封装成一个函数放到methods属性中,在标签中只写这封装的函数名即可

动态绑定class属性–数组语法(用得少)

比如<h3 :class="['Mon','Tue','Wed','Thu','Fri','Sat','Sun']"></h3>
注意不可以写成双引号包双引号哦,比如这个:class=:"["Mon","Tue"]"错误写法
数组语法也能使用变量,比如’Mon’改成data里的属性名放进去
数组语法中也能使用对象,可以但开发中几乎不用

v-bind绑定style属性(常用)

用来绑定一些css内敛样式

同样有对象语法和数组语法,后者类似前面的,很少用,下面只举例对象语法
动态绑定style,后面跟对象类型,一旦用对象类型,里面的值就要有限制
<h3 v-bind:style="{color: myColor, 'font-size': '25px'}"
<h3 v-bind:style="{color: myColor, 'fontSize': size+'px'}"
短横线的单词和数值记得用单引号包裹起来,短横线也可以用驼峰fontSize
如果size是数字的话,要在拼单位'px'
如果里面东西很多,都写在标签中可能不好看,这时候也可以把这堆对象语法封装成一个对象放到data属性中,在标签中只写这封装的对象名即可(少用)

v-bind–动态绑定属性名称(少用了解)

比如不确定是color,还是font-size,用 :[属性名]="值" 的格式来定义
比如<div :[name]="value"></div>,name的值在data里面取

v-bind–绑定一个对象(重要)

在给组件传参的时候用很多
如果在标签上挨个写属性属性名很麻烦,直接绑定一个对象,去data里往这个对象里慢慢放东西,会自动挨个遍历对象里的东西绑定上去

1
2
3
4
5
<h3 v-bind="infos"></h3>
---
data: function(){
infos:{ name:"小聂",age:23}
}

v-on绑定事件

v-on: 简写@,参数event

v-on绑定事件基本使用

一个标签上可以绑定多个事件,如果觉得绑定的事件太多,可以写成对象的形式
<div v-on:click="dicClick" @mousemove="divMousemove"></div>
<div v-on="{click: divClick, mousemove: divMousemove}"></div>

两种写法都可,但是上面的可读性更强,下面的认识就好

v-on绑定事件参数传递

  1. 默认传递event对象
    如果在绑定事件时没有传递任何参数,那么会默认绑定event对象作为参数,被传递过来
  2. 明确参数
    有明确参数会把event替代
    参数没加引号会从data里面找属性对应的值,如果传递的参数是值,记得要加单引号
    1
    2
    3
    4
    5
    6
    7
    8
    <button @click="btnClick('nie', 23)"></button>
    <script>
    methods:{
    btnClick(name, age){
    console.log(name, age)
    }
    }
    </script>
  3. 明确参数+event对象
    想要传参的时候也把event传过去,要$event这样写
    1
    2
    3
    4
    5
    6
    7
    8
    <button @click="btnClick('nie',23,$event)"></button>
    <script>
    methods:{
    btnClick(name, age, event){
    console.log(name, age, event)
    }
    }
    </script>

v-on的修饰符

修饰符相当于自动帮你调方法对事件进行了处理
v-on的修饰符

vue条件渲染

(v-if,v-else-if,v-else,v-show之类的,和vue2一样)

  • v-show不支持template
  • v-show不可以和v-else一起使用
  • v-show元素无论是否需要显示到浏览器上,它的DOM实际都是有存在的,只是通过CSS的display属性来进行切换
  • v-if当条件为false时,其对应的原生不会被渲染到DOM中;
  • 如果我们的原生需要在显示和隐藏之间频繁的切换,那么使用v-show
  • 如果不会频繁的发生切换,那么使用v-if;

templat元素的使用

template元素可以当做不可见的包裹元素,配合v-if使用,但是最终template不会被渲染出来

1
2
<template v-if="isShowCode"><img src="......."">
</template>

遍历渲染

  • v-for可以遍历数组,对象,数字(从1开始数)
  • v-for可以和template结合
  • v-for …in…和v-for …of…的效果是一样的
  • v-for记得要有key

支持有一二三个参数:
一个参数:"value in object"
二个参数:"(value,key)in object"
三个参数:"(value, key, index) in object"

1
2
3
<u1>
<li v-for="(value, key,index)in info">{{value}}-{{key}}-{{index}}</li>
</ul>

vue数组侦听监测

Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。
这些被包裹过的方法包括:
push();pop();shift();unshift();splice();sort();reverse()
不修改原数组的方法是不能侦听的

双向绑定– v-model

v-model基本使用

它负责监听用户的输入事件来更新数据
常用在表单,选项元素伤,创建双向数据绑定
简单原理类似:v-bind+v-on的使用,但是真实开发中都是使用v-model来实现
给表单输入框设置默认内容–v-bind
<input type="text" v-bind:balye="message">
要让表单和vue的内容互相影响–v-model
<label><input type="text" v-model="count"></label>
count在data里面

v-model的特殊绑定(重要)

  1. v-model绑定checkbox单选框
    单选框:v-model默认绑定的是布尔值,单选框value属性不会影响v-model的值

    1
    2
    3
    4
    5
    6
    <label for="agree">
    <input id="agree" type="checkbox" v-model="isAgree"> 同意协议
    </label>
    data(){
    return{inAgree: false}
    }
  2. v-model绑定checkbox多选框
    多选框:绑定到属性中的值是一个数组,除了v-model,必须要绑定一个value值,当选中时会自动把值添加到属性数组里

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <div class="hobbies">
    <h3>你有什么爱好呀</h3>
    <label for="play">
    <input id="play" type="checkbox" v-model="hobbies" value="play"> 玩游戏
    </label>
    <label for="music">
    <input id="music" type="checkbox" v-model="hobbies" value="music"> 听音乐
    </label>
    </div>
    data(){
    return{ hobbies: []
    }
    }
  3. v-model绑定radio

  4. 使用v-model绑定同一个值会有互斥的效果,不再需要name属性进行互斥;也必须要写value属性,v-model会把选中的按钮的值赋值给data中接收的数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <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>
    </div>
    data(){
    return{
    gender: "female"
    }}
  5. v-model绑定select
    select添加mutiple属性是多选,不添加这个属性是单选;select多选时只可以使用size属性
    v-model要添加到select标签上,和前面不一样!
    单选时,传递的是选中的值;多选时,传递的是数组(类似checkbox多选框)

v-model的值绑定

上面写的例子,这些东西都是自己写死的,但是真是开发中这些值是服务器给的,前端进行值绑定
比如说,服务器传递来的数据是对象数组,把数组放多选框中,如下图select的值绑定
v-model的值绑定
再比如的chexobox的值绑定,如下图
v-model的值绑定

v-model修饰符

可以同时使用多个修饰符

  1. .lazy
    将v-model绑定的事件变为change事件,只有在提交时候(比如回车)才会触发
  2. .number
    自动将内容转换成数字
    如果让用户输入数字,只要用户输入了就会变成字符串类型,这时用v-model.number会把用户输入的数字转化成数字类型
  3. .trim
    去除收尾空格
    组件(这部分笔记看之前的把,会用就好)

vue3项目创建

create-vue创建项目

vue3推荐使用:create-vue是Vue官方新的脚手架工具,底层切换到了 vite(下一代构建工具),为开发提供极速响应

  1. 要已安装16.0以上的node.js
  2. npm init vue@latest 这一指令将会安装并执行 create-vue
    是这样的(我有一步和不一样所以项目有点不一样)alt text
    视频是这样的alt text
    然后进入项目安装依赖运行
    是这样的alt text
    进入页面是这样的alt text

项目目录和关键文件

关键文件:

1.vite.config.js-项目的配置文件 基于vite的配置
2.package.json-项目包文件 核心依赖项变成了 Vue
3.x和 vite3.main.js-入口文件 createApp函数创建应用实例
createApp(app).mount(…)设置挂载点,前半段在创建实例,后半段在把实例往上挂载
4.app.vue-根组件 SFC单文件组件 script-template-style
变化一:脚本script和模板template顺序调整,结构和样式放在一起更利于维护
变化二:模板template不再要求唯一根元素
变化三:脚本script添加setup标识支持组合式API
(加上setup允许在script中直接编写组合式API)
5.index.html-单页入口 提供id为app的挂载点


(但是我的打开.js文件的地方是.ts文件,文件里的代码和视频是一样的,检查发现是在创建项目的时候是否使用JSX语法的地方应该是NO,我选了YES) 语言类型:.ts 文件是 TypeScript 文件,而 .js 文件是 JavaScript 文件。TypeScript 是 JavaScript 的超集,它为 JavaScript 添加了静态类型和其他一些语言特性。

类型检查:TypeScript 具有静态类型检查功能,可以在编译时捕获一些常见的错误,并提供更好的代码提示和自动补全。JavaScript 不具备这种类型检查功能,它是一种动态类型的脚本语言。

语法扩展:TypeScript 扩展了 JavaScript 的语法,使得可以使用类、接口、泛型等特性,以及其他一些新的语法和功能。JavaScript 本身不支持这些语法扩展。

编译过程:TypeScript 需要先编译成 JavaScript,然后才能在浏览器或 Node.js 环境中运行。而 JavaScript 文件可以直接在浏览器或 Node.js 中执行,无需额外的编译步骤。


可能会弹提示因为你在跑vue3的项目但是插件用的还是vue2,去扩展禁用“vetur”安装“vue-Official”

vite

新一代前端构建工具
优势:
开发过程中无需打包,可快速冷启动
轻量快速的热重载
真正的按需编译,不再等待整个应用编译完成
vite

用vite创建vue3工程

看官方文档,更新版本命令可能会变动
初始化工程:根据官方命名创建的工程(最开始的样子,自己没有改动)
构造函数和工厂函数:

  • 构造函数一般首字母大写,用new构造
  • 工厂函数一般首字母小写,不用构造,直接调用就好

别名配置

别名配置
别名配置
但是做完这一步虽然别名能用,但是没有提示
做下面的配置可以让编译器有提示
别名配置

组合式API

组合式API-setup选项

  1. 组合式API:一系列函数
    写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script>
    export default {
    setup(){

    },
    beforeCreate(){

    }
    }
    </script>

    执行时机:在beforeCreate钩子之前执行alt text

  2. setup中写代码的特点
    setup函数中,获取不到this(this是undefined)
    在setup函数中写的数据和方法需要在末尾以对象的方式return,才能给模版使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
export default {
setup(){
const message = 'this is message'
const logMessage = ()=>{
console.log(message)
}
// 必须return才可以
return {
message,
logMessage
}
}
}
</script>
  1. <script setup>语法糖
    script标签添加setup标记,不需要再写导出语句,默认会添加导出语句,帮助我们return,在template里面直接用就好了
1
2
3
4
5
6
<script setup>
const message = 'this is message'
const logMessage = ()=>{
console.log(message)
}
</script>

组合式API - reactive和ref函数

  1. reactive:接受对象类型数据的参数传入,返回一个响应式的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
// 导入
import { reactive } from 'vue'
// 执行函数 传入参数 变量接收
const state = reactive({
msg:'this is msg'
})
const setSate = ()=>{
// 修改数据更新视图
state.msg = 'this is new msg'
}
</script>

<template>
{{ state.msg }}
<button @click="setState">change msg</button>
</template>
  1. ref:接收简单类型或者对象类型的数据传入并返回一个响应式的对象
    本质:如果接收的类型是简单类型,那么会在原有的类型外层包一个对象,包成了复杂类型,再接入reactive实现响应式
    注意:访问数据需要通过.value,因为它包了一层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
// 导入
import { ref } from 'vue'
// 执行函数 传入参数 变量接收
const count = ref(0)
const setCount = ()=>{
// 修改数据更新视图必须加上.value
count.value++
}
</script>

<template>
<button @click="setCount">{{count}}</button>
</template>
  1. reactive 对比 ref

  2. 都是用来生成响应式数据

  3. 不同点

    1. reactive不能处理简单类型的数据
    2. ref参数类型支持更好,但是必须通过.value做访问修改
    3. ref函数内部的实现依赖于reactive函数
  4. 在实际工作中的推荐

    1. 推荐使用ref函数,减少记忆负担,小兔鲜项目都使用ref

组件(这部分笔记看之前的把,会用就好)

开发中一定要进行好组件拆分

组件使用

注册全局组件(用得少)

1
2
3
4
app.component("组件名",{
template:...,
data(){return{}
}})

每个组件里面可以有自己的逻辑(data,methods等)

注册局部组件(常用)

放在app中,和data同级

1
2
3
components:{
组件名: {组件对象值}
}

组件名称规则

定义组件名有两种方式

  1. 短横线分隔符my-cat
    使用时```
  2. 大驼峰标识MyCat
    使用时<my-cat></my-cat>
    <MyCat></MyCat>

在项目中使用组件(快速回忆)

main.js中

1
2
3
import { createApp } from 'vue'
import App from './App.vue'
create(App).mount('#app')

App.vue中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="app">
<app-header></app-header>
</div>
</template>
<script>
//导入
import AppHeader from './components/AppHeader.vue'
//注册
export default{
components:{AppHeader}
}
</script>
<style scoped>
</style>

组件放在components文件夹
compenments/AppHeader.vue

1
2
3
4
5
6
7
<template>
<div>我是头部组件</div>
</template>
<script>
</script>
<style scoped>
</style>

组件也可以导入其他组件

组件通信

父子组件通信

父传子props

数据在父组件上,父用子组件展示数据,要把数据传递给子组件展示

  1. 父用子组件时,通过属性的形式传递东西

    1
    2
    3
    4
    <template>
    <show-info name="why" age="18" height="1.88" />
    <show-info name="kobe" age="30" height="1.87"/>
    </template>

    子组件拿到父传递的数据,放到对应的位置上
    子组件要通过props来接收父组件传递过来的属性
    父传子props
    简而言之,就是父组件通过属性的形式给这些名称赋值,子组件通过props拿到这些名称,使用这些名称展示数据

  2. props 的数组写法
    子组件使用props数组的形式接收数据(同1中写法)
    export default{ pops:["name","age","height"] }

  3. props的对象形式语法(常用)
    这种写法可以对父传递的数据进行验证
    默认值和必传一般二选一

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import default{
    props:{
    name:{
    type: String
    default: "我是默认的name哦"
    },
    age:{
    type: Number,
    default: 0
    },
    height:{
    type: Number,
    required: true, //必传
    }
    }
    }
  4. type常见的类型(图片转内容)

  5. 特殊类型--对象
    如果type使用的是Object类型
    那么默认值必须写函数(箭头不箭头都可以),函数返回一个对象
    比如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    friend{
    type: Object,
    default:()=>({name : "nie"})
    },
    message:{
    type:Object,
    default(){}
    return {message:"hello,nie"}
    }
  6. 特殊类型 – 数组
    如果type的类型是数组,那么也需要用函数

    1
    2
    3
    4
    hobbies:{
    typr: Array,
    default: ()=>["虞寻歌","枫糖","雾刃"]
    }
  7. 规则命名
    如果是小驼峰,那么组件用小驼峰或者-小写(和之前类似的规则一样,vue中组件命名都遵循这样的写法)

  8. 父传子子没接受
    如果父传子了一个属性,但是子没有接受,就称之为非prop的Attributue,该属性会默认添加到子组件的根元素上(根节点上)
    如果不喜欢没添加的属性被传递到根元素上,可以给子组件的export default{}里面添加inheritAttrs: false
    这是可以用$attrs.xxx获取,如图
    父传子props

子传父$emit

在子组件中触发一些事件时进行子传父
子组件发生的事件要改变父组件的内容
要让子组件发出去一个自定义事件
第一个参数是自定义的事件名称,第二个参数是参数
父传子props
子组件触发自定义事件

1
2
3
4
5
6
<button @click="btnClick(10)">-1</button>
export default{
methods:{
btnClick(count){
this.$emit("sub",count)
}}}

父组件修改数据

1
2
3
4
5
6
<sub-counter @sub="subClick"></sub-counter>
export default{
methods:{
subClick(count){
this.counter = count
}}}

小结:子组件监听对应事件发生,用this.$emit(“ye”)发送事件,父组件在子组件上用@ye=”yemethods”接收事件,触发父组件的方法methods:{yemethods(){} }
父传子props

emits数组

  1. emits数组语法(用的多)
    在子组件中用emits定义所有自定义事件名称,注册说明一下,这样父组件会有提示
    export default {
    emits:[“自定义事件1”,”自定义事件2”],
    methods:{}
    }
  2. emits对象语法(用得少)
    用对象写法的好处是可以对参数进行验证
    比如验证发送的数组必须要小于10
    1
    2
    3
    4
    5
    emits:{
    add: function(count){
    if(count<=10){
    return true}
    return false}}

插槽Slot

认识插槽

插槽
在组件中预留插槽,插槽中具体放什么东西由使用者决定,而不是组件写死

插槽的使用

  1. 插槽基本使用
    抽取共性做成插槽来占位,预留不同,让外部(组件使用者,如父组件)决定到底显示什么元素
    把要显示的东西放到子组件标签中,这部分内容会放到子组件的插槽中
    插槽用一个div包裹住,这样好控制
    插槽
    (图片)
  2. 插槽默认值
    如果父组件使用时没有传递东西,就会显示子组件插槽中的内容;如父组件使用时有传递东西,那么子组件插槽中的内容会被父组件传递的内容替换掉

具名插槽(多个插槽)

具名插槽的缩写v-slot: 可以换成#
告诉是给哪个插槽传入什么内容
插槽如果没有起名字,默认名default
插槽
插槽

动态插槽名

插槽

渲染作用域

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

作用域插槽

看图,有点绕(特殊场景用,平时用得少)
插槽
插槽

非父子组件通信–provide和inject

provide和inject (用的较少)

依赖注入,父组件通过provide提供数据,子组件通过inject使用数据
特点:父组件不知道子组件在哪里使用熟悉,子组件也不知道inject来自哪里(其实也不需要知道)
provide和inject
想要在provide中使用this获取data里的数据,需要把provide写成函数,函数才有作用域
provide和inject

非父子组件通信–全局事件总线$bus(常用)

全局事件总线库–hy-event-store

通过第三方库使用事件总线
其他的第三方库也可以,逻辑都是差不多的

  1. 先安装pnpm install hy-event-store
  2. 创建总线js
    创建一个文件夹utils/event-bus.js
    1
    2
    3
    import {HEventBus} from 'hy-event-store'
    const eventBus = new HYEventBus()
    export default eventBus
  3. 在组件中导入
    1
    import eventBus from './utils/event-bus'
  4. 发送事件
    1
    eventBus.emit("事件名",有要传的参数写后面)
  5. 监听事件
    1
    eventBus.on("事件名",(有参数的话这里写形参接收)=>{})
  6. 事件总线的移除
    如果使用时间总线的组件被移除, 那么这个书剑总线也应该被移除
    使用methods进行时间总线逻辑的处理,在移除的时候要有事件名和移除的方法
    事件总线的移除
  7. 注意
    父子组件可以用但是不推荐,父子组件还是乖乖用前面的父子通信吧
    在真实开发中使用总线的时候把移除的代码写上最好

组件化补充

组件生命周期

组件创建、挂载、更新、卸载的过程
要在这个过程的某个阶段添加自己的逻辑代码,使用生命周期
在vue2中有很详细的理解过程,这里不重新写了,看这个图片,很清晰
这些函数写在export default里面,和data(){}通级别
两个最重要的,其他几乎不用
事件总线的移除

组件中的$ref引用

在vue中不推荐使用原生DOM操作,那么想要获取一个元素或组件,就要用$ref
在标签上绑定一个ref属性和属性值
<h3 ref="title"></h3>
在js代码中this.$refs.title获取元素

ref获取组件实例

组件实例有一个$refs属性,他有一个对象Objetc,有注册过ref attributed的所有DOM元素和组件实例
如果用ref拿组件<banner ref="banner"/>this.$refs.banner拿到的是组件实例
拿到组件实例之后,父组件可以主动调用子组件中的方法
(两个图和一个图)
父组件还可以拿到组件实例后拿到组件的根元素this.$refs.banner.$el,如果子组件有两个跟元素,那么拿到的是第一个node节点
要拿到组件实例跟元素中的节点,this.$refs.banner.$el.nextElementSibling(真实开发中几乎不会这样用)
开发中尽量一个组件只有一个根元素

动态组件(了解)

内置组件keep-alive

让某一个组件保持存活的状态
在一个组件不再使用时,默认情况下会被销毁掉,下次用再创建,如果切换频繁,使用keep-alive会让它依然保持存活
在使用组件时,用<keep-alive></keep-alive>标签把组件包裹起来,还有匹配对应组件名
同时组件要添加name属性
内置组件keep-alive

存活组件的生命周期

使用activated(进入活跃状态,如选中),和deactivated函数

方式一:v-if逻辑判断

方式二:动态组件comonent(简洁)

is中的组件要来自两个地方,全局注册的组件和局部注册的组件

CompositionAPI

options api

之前学的都是这个,弊端是对同一个数据的操作有些零散
在vue3的composition API中,对一个数据的所有操作都在一个地方完成,把一些分散的东西放在一起,封装一个函数,维护简单,使用的时候注册使用
在vue3中,composition API是主流,要向这边转换

Setup函数的基本使用

setup函数默认有两个参数:props和context
setup里定义的数据和函数需要返回
setup里定义的数据默认不是响应式的,要变成响应式,需要用ref()进行包裹
template中,对于ref对象中的内容会自动解包,所以模板语法中直接用即可,不需要.value
setup里面不能使用this
Setup函数的基本使用

setup逻辑的封装思路

如果一套逻辑不止在一个组件中使用,可以把setup里面的逻辑封装成一个.js文件,在文件中记得return;在setup中引入这个.js文件,解构使用即可
Setup函数的基本使用
Setup函数的基本使用
使用时

1
2
3
4
5
6
upset(){
....
return {counter,increment,decrement}
//上面这行return可以写成下面这样
return { ...useCounter()}
}

Setup中的响应式数据

数据响应式reactive函数

  1. 基本使用
    如果想为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive的函数:
    1
    2
    3
    4
    5
    import {reactive} from 'vue'
    const state =reactive({
    name:"coderwhy",counter:100
    })
    return {state}
  • 因为当使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集(数据会被劫持)
  • 其实编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的;
  1. 数据限制
    reactive API对传入的类型是有限制的,它要求必须传入对象或者数组类型

数据响应式ref函数(常用)

定义简单类型的数据(也可也定义复杂类型的数据)
ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源
它内部的值是在ref的value属性中被维护的;
模板中引入ref的的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过ref.value的方式来使用;
但是在setup函数内部,它依然是一个ref引用,所以对其进行操作时,需要使用 ref.value的方式

1
2
3
4
5
6
import {ref} from 'vue'
const counter = ref(100)
function increment(){
counter.value++
}
return {counter,increment}
  1. 解包小细节
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!--默认情况下在template中使用ref时,vue会自动对其进行解包(取出其中value)·-->
    <h2>当前计数:{{counter}}く/h2>
    <button @click="increment">+1</button>
    <button @click="counter++">+1</button>
    <hr>
    <! ---使用的时候不需要写.value--
    <h2>当前计数:{{info.counter}}</h2>
    <!---修改的时候需要写.value-
    <button @click="info.counter.value++">+1</button>

ref和reactive的使用场景

  1. reactive的使用场景
    reactive多应用于本地的数据
    多个数据之间是由关系的(有联系,比如数据的聚合,组织在一起会有特定的作用)
    1
    2
    3
    4
    5
    const account=reactive({
    username:"nie"
    password:"123456"
    })
    return {account}
  2. ref的使用场景(其他场景几乎都用ref)
    定义本地的一些简单数据
    定义从网络中获取的数据使用ref
    1
    2
    3
    4
    5
    const musics = ref([])
    onMounted(() => {
    const serverMusics·=["虞寻歌","雾刃","枫糖"]
    musics.value =serverMusics
    })

readonly(用较少)

vue代码要符合单向数据流原则,如果在开发中遵循单向数据流,那么不用这个函数也一样
比如,父组件把Info对象传给子组件,子组件可以获取info对象展示。默认情况子组件可以把info对象中的数据进行修改,同时父组件的Info数据也会被修改。
实际中不希望子组件轻易修改数据,如果要修改,那么应该让子组件把事件传递给父组件,让父组件来修改
规范的写法如图:
Setup函数的基本使用
readonly使用方法:
先引入
import {readonly} from 'vue'
再用readonly函数包裹数据
const rdInfo = readonly(account)
再导出
return {rdInfo}

readonly返回的对象都是不允许修改的;
但是经过readonly处理的原来的对象是允许被修改的;
比如const info =readonly(obj),info对象是不允许被修改的;
当obj被修改时,readonly返回的info对象也会被修改;
但是不能去修改readonly返回的对象info;
其实本质上就是readonly返回的对象的setter方法被劫持了而已;

一些判断函数(了解)

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

setup语法糖

不需要再export default{setup(){ return{}}}
把setup写在script标签中即可

1
2
3
4
5
<script setup>
import{useRoute}from 'vue-router'
import {} from ''
const route = useRoute();
</script>

侦听函数watch

在某些情况下,希望在代码逻辑中侦听一个或者多个数据的变化,这个时候就需要用侦听器watch来完成了
数据变化时执行回调函数,两个额外参数immediate:true控制立刻执行,deep:true开启深度侦听

  1. 侦听单个数据
    默认有两个参数:newValue/oldValue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <script setup>
    // 1. 导入watch
    import { ref, watch } from 'vue'
    const count = ref(0)
    // 2. 调用watch 侦听变化
    watch(count, (newValue, oldValue)=>{
    console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`)
    })
    </script>
  2. 侦听多个数据
    侦听多个数据,第一个参数改写成数组的写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script setup>
    // 1. 导入watch
    import { ref, watch } from 'vue'
    const count = ref(0)
    const name = ref('cp')
    // 2. 调用watch 侦听变化
    watch([count, name], ([newCount, newName],[oldCount,oldName])=>{
    console.log(`count或者name变化了,[newCount, newName],[oldCount,oldName]`)
    })
    </script>
    1
    2
    3
    4
    5
    6
    watch:{
    //1.默认有两个参数:newValue/oldValue
    message(newValue,oldValue){console.log("message数据发生了变化:",newValue,oldValue)
    F}
    info(newValue, oldValue){console.log("info数据发生了变化:",newValue,oldValue)}
    }
  3. 监听对象类型
    如果侦听的目标是对象类型,那么拿到的是代理对象
    想获取原生对象用下面的方法:
    console.log({...newValue })
    console.log(Vue.toRaw(newValue))

  4. immediate参数
    在侦听器创建时立即出发回调,响应式数据变化之后继续执行回调

1
2
3
4
5
6
7
8
9
10
11
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const count = ref(0)
// 2. 调用watch 侦听变化
watch(count, (newValue, oldValue)=>{
console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`)
},{
immediate: true
})
</script>
  1. deep参数
    通过watch监听的ref对象默认是浅层侦听的,直接修改嵌套的对象属性不会触发回调执行,需要开启deep

这样可以监听对象的某个属性

1
2
3
4
5
6
7
8
9
10
11
watch:{
info:{
handler(newValue, oldValue){
console.log(newValue, oldValue);
};
deep: true,
immediate: true
},
'info.name': function(newValue, oldValue){
console.log(newValue,oldValue);
}}
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
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const state = ref({ count: 0 })
// 2. 监听对象state
watch(state, ()=>{
console.log('数据变化了')
})
const changeStateByCount = ()=>{
// 直接修改不会引发回调执行
state.value.count++
}
</script>

<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const state = ref({ count: 0 })
// 2. 监听对象state 并开启deep
watch(state, ()=>{
console.log('数据变化了')
},{deep:true})
const changeStateByCount = ()=>{
// 此时修改可以触发回调
state.value.count++
}
</script>

  1. 使用this.$watch侦听
    可以在created的生命周期中,使用this.$watchs来侦听:
    第一个参数是要侦听的源;
    第二个参数是侦听的回调函数callback;
    第三个参数是额外的其他选项,比如deep、immediate;
    1
    2
    created(){
    this.$watch('message',(newValue, oldValue)=>{console.log(newValue,oldValue);},{deep: true,imediate: true})}

侦听

侦听多个数据源

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

1
2
3
4
5
6
const name =ref("NIE");
const age =ref(18)
const changeName=()=>{
name.value="james";}
watch([name, age],(newValues, oldValues)=>{
console.log(newValues,oldValues);})

watchEffect停止侦听

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

1
2
3
4
5
6
const stopWatch=watchEffect(()=>{console.log("watchEffect执行~",name.value, age.value);
const changeAge =()=>{
age.value++;
if(age.value>20){
stopWatch();}
}})

Setup-Hooks思想

把一些操作封装一个.js,在setup中调用即可,看下面代码就明白了
Setup-Hooks思想

vue router

路由的基本使用

路由的基本使用

1.创建路由对象routes: 映射关系history: 修改url的模式(hash/history)
2.让路由对象生效app.use(router)
3.<router-view></router-view>的占位
4.<router-link to="/home/news">新闻</router-link>进行路由的切换
路由的基本使用

路由的默认路径

path配置的是根路径:/
redirect是重定向,也就是我们将根路径重定向到/home的路径下,这样就可以得到想要的结果了.

1
2
3
4
5
6
const routes=createRouter({
[
{path:'/',redirect:'/home'},
{path:'/home',component:Home },
{ path:'/about',component:About }
]})

路由的嵌套

嵌套路由将路由配置到路由对象中,然后通过router-view进行展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes=[
{ path:'/',redirect:'/home'},
{ path:'/home',component:()=>import('../views/Home.vue')
,children:[
{ path:'/home/news',
component:()=>import('../views/HomeNews.vue')
},
{ path:'/home/message',
component:()=>import('../views/HomeMessage.vue')
}
]},
]
<router-link to="/home/news">新闻</router-link>
<router-link to="/home/message">消息</router-link>
<router-view></router-view>

router-link属性

router-link事实上有很多属性可以配置:
to属性:是一个字符串,或者是一个对象
replace属性:当点击时,会调用router.replace(),而不是 routerpush();
active-class属性:设置激活a元素后应用的class,默认是router-link-active
exact-active-class属性:链接精准激活时,应用于渲染的<a>的 class,默认是' router-link-exact-active;

路由懒加载

Vue Router默认就支持动态来导入组件:
这是因为component可以传入一个组件,也可以接收一个函数,该函数 需要放回一个Promise;而import函数就是返回一个Promise
使用/*webpackChunkName:"home-chunk"*/对分包进行命名

1
2
3
4
5
6
7
const routes=[
{ path:'/',redirect:'/home'},
{ path:'/home',component:()=>import(/*webpackChunkName:"home-chunk"*/'../pages/Home.vue')},
{ path:'/about',component:()=>import('../pages/About.vue')}
]
//在router-link中进行如下跳转:
<router-link to="/user/123">用户:123</router-link>

在模板中获取到ID
<h2>User:{{ $route.params.id }}</h2>
在代码中获取ID
console.log(route.params.id);
在created中获取值
this.$route.params

路由的传参–query

1
2
3
4
this.$router.push({
path:'/user',
query:{name:'nie',age:23}
})

接受用{{$route.query}}he
这样传递的参数会在地址的后面跟?name=nie&age=23

动态路由基本匹配

很多时候我们需要将给定匹配模式的路由映射到同一个组件:

1
2
3
{path: '/user/:id'
component:()=>import('../pages/User.vue')}

路由匹配规则加*

/:pathMatch(.*) 后面又加了一个 *

1
2
{path:'/:pathMatch(.*)*',
component:()=>import('../pages/NotFound.vue')}

它们的区别在于解析的时候,是否解析/:
path:/:pathMatch(.*)*'
Not Found: [ “user”, “hahah”, “123” ]
path:'/:pathMatch(.*)'
Not Found: user/hahah/123

URL的hash

URL的hash也就是锚点(#),本质上是改变window.location的href属性
URL的hash

页面的前进后退

router也有back:
通过调用history.back()回溯历史,相当于router.go(-1);
router也有forward:
通过调用 history.forward()在历史中前进。相当于 router.go(1);
router的go方法:

1
2
3
4
5
6
7
8
//向前移动一条记录,与router.forward()相同
router.go(1)
//返回一条记录,与router.back()相同
router.go(-1)
//前进3条记录
router.go(3)
//如果没有那么多记录,静默失败router.go(-100)
router.go(100)

动态管理路由

动态添加路由

比如说,向服务器发送一个请求,判断用户是管理员还是普通用户,根据用户角色的不同动态加载路由

1
2
3
4
5
6
7
8
9
10
11
let isAdmin = true
if(isAdmin){
router.addRoute({
path:'/admin',
component:()=>import('../pages/Admin.vue')
})
router.addRoute({
path:'/vip',
component:()=>import('../pages/Vip.vue')
})
}

删除路由(了解,很少用到)

方法1:添加一个name相同的路由,由于路由name是唯一的,所以后面的name会替代前面相同name的路由,进行删除

1
2
3
4
5
6
router.addRoute({
path:'/vip',name:'admin',component:()=>import('../pages/Vip.vue')
})
router.addRoute({
path:'/admin',name:'admin',component:()=>import('../pages/Admin.vue')
})

方法2:通过removeRoute方法,传入路由的名称name,进行删除;

1
2
3
4
router.addRoute({ 
path:'/admin',name:'admin',component:()=>import('../pages/Admin.vue')
})
router.removeRoute('admin')

方法3:通过addRoute方法的返回值回调(特少用):

1
2
let removeRoute = router.addRoute({routeRecord})
removeRoute()

路由其他方法

router.hasRoute():检查路由是否存在
router.getRoutes():获取一个包含所有路由记录的数组

路由导航守卫

在跳转路由之前拦截并进行判断
守卫有很多很多,但是最常用的是全局前置守卫

全局的前置守卫–beforeEach

  1. 语法
    在进行任何的路由跳转之前,传入这个函数的都会被回调,在导航触发时会被回调
    router.beforeEach((to,from)=>{})
    to 到哪里去,即将进入的路由对象
    from 从哪里来,即将厉害的路由对象
    还有第三个参数next可选,不推荐使用
    有返回值:
    false:取消当前导航
    不返回或undefined:进行默认导航
    返回一个路由地址(可以是string类型路径,也可以是一个对象包含path\query\params等信息),进行跳转
  2. 在进入订单页面时,判断用户是否登录

    如何判断用户是否登录呢
    一般在localStorage中存储用户信息,保存一个token如果存在,则用户登录,否则未登录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    router.beforeEach((to,from)=>{
    const token = localStorage.getItem('token')
    if(!token && to.path.indexOf('/order')!==-1){
    //或if(!token && to.path==="/order"){
    return '/login'
    }
    })
    //想要推出登录,直接删除token即可
    localStorage.removeItem('token')

完整的导航解析流程(来自官方文档)(了解)

导航被触发->在失活的(要离开的)组件里调用 beforeRouteLeave 守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve(异步组件被解析之后,跳转之前)
调用全局的 afterEach 钩子。触发 DOM 更新。
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

Vuex状态管理

虽然pinia比vuex更优秀,但是vuex还是要掌握
vuex有五个核心概念
vuex的学习笔记在vue2中也有,那边记得很详细清楚,这里不再写了

pinia状态管理

pinia学习笔记在pinia文章中也有,那边记得很详细清楚,这里不再写了

网络请求

这部分学习笔记看网络请求文章

综合小例子

模板语法案例 – 书籍购物车(重要)

案例说明

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

思路

先把大的架子搭起来,再完成一个个模块

  1. 搭建界面内容
    表格标题+表格标题简单美化
    实际开发中表格内的数据量和内容取决于服务器传过来的,所以表格内容用v-for
    在现在练习时,books数据是自己写的,但是内容都写进data中有些多了不好看,放到data.js文件夹,用script引入文件夹,在data中直接可以读取到data.js文件夹的books
    原因是script引入的是全局作用域,所以可以直接用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    <style>
    table{
    border-collapse: collapse;
    }
    th,td{
    border: 1px solid #aaa;
    padding: 8px 16px;
    }
    </style>
    <div id="app">
    <table>
    <thead><tr>
    <th>序号</th>
    <th>书籍名称</th>
    <th>出版日期</th>
    ....
    </tr></thead>
    <tbody><tr v-for="item in books" :key="item.id">
    <td>{{item.index+1}}</td>
    <td>{{item.name}}</td>
    <td>{{item.price}}</td>
    ...
    </tr></tbody>
    </table>
    </div>
    <script>
    const app = Vue.createApp({
    data(){
    return{
    books: books
    }
    }
    //真实的开发一般这样做
    created(){
    fetch("url").then(res=>{
    this.books=res
    })
    }
    })
    </script>
  2. 完善–计算价格,价格前拼接人民币符号
    购买数量的前后应该有加号减号两个按钮
    用计算属性计算总价
    关于价格的前面要加人民币符号,使用一个methods方法给所有的价格前面拼接符号,牢记这种思想
    (vue2有过滤器,vue3删掉了,用拼接的方法也好用)

    1
    methods:{formatPrice(price){return "$"+ price}}
  3. 数量加减操作
    前面使用v-for循环渲染表格和生成加减号,那么点击加减号如何判断改变的是哪条数据的加减号呢
    思路:让加减方法传递和接收 index或者item,拿到这两个数据之一,对index.count或者item.count进行操作

  4. 减号失效禁用–v-bind动态绑定
    注意减号减到1就不能再减了,让这个减号按钮禁用(添加属性disable)
    <button v-bind:disabled="item.count <=1" @click="decrement(index,item)">-</button>
    使用v-bind动态绑定

  5. 移除书籍操作
    还是和减号的思路一样,要确定移除的是哪一本,传递并接收参数,在方法中对这个参数进行操作
    this.books.split(item,1)

  6. 购物车是否有内容不同展示
    当移除掉所有数据后,清除购物车表格,进行汉字提示
    使用v-if,v-else条件渲染,根据books.length判断,不等于0那就是有,等于0就是没有数据

<template v-if="books.length">

购物车案例完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
  <style>
table {
border-collapse: collapse;
/* text-align: center; */
}

thead {
background-color: #f5f5f5;
}

th,
td {
border: 1px solid #aaa;
padding: 8px 16px;
}

.active {
background-color: skyblue;
}
</style>
</head>
<body>
<div id="app">
<!-- 1.搭建界面内容 -->
<template v-if="books.length">
<table>
<thead>
<tr>
<th>序号</th>
<th>书籍名称</th>
<th>出版日期</th>
<th>价格</th>
<th>购买数量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in books"
:key="item.id"
@click="rowClick(index)"
:class="{ active: index === currentIndex }"
>
<td>{{ index + 1 }}</td>
<td>{{ item.name }}</td>
<td>{{ item.date }}</td>
<td>{{ formatPrice(item.price) }}</td>
<td>
<button :disabled="item.count <= 1" @click="decrement(index, item)">-</button>
{{ item.count }}
<button @click="increment(index, item)">+</button>
</td>
<td>
<button @click="removeBook(index, item)">移除</button>
</td>
</tr>
</tbody>
</table>

<h2>总价: {{ formatPrice(totalPrice) }}</h2>
</template>

<template v-else>
<h1>购物车为空, 请添加喜欢的书籍~</h1>
<p>商场中有大量的IT类的书籍, 请选择添加学习</p>
</template>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="data.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
// data: option api
data() {
return {
books: books,
currentIndex: 0
}
},
// computed
computed: {
totalPrice() {
// 1.直接遍历books
// let price = 0
// for (const item of this.books) {
// price += item.price * item.count
// }
// return price

// 2.reduce(自己决定)
return this.books.reduce((preValue, item) => {
return preValue + item.price * item.count
}, 0)
}
},
methods: {
formatPrice(price) {
return '¥' + price
},

// 监听-和+操作
decrement(index, item) {
console.log('点击-')
// this.books[index].count--
item.count--
},
increment(index, item) {
console.log('点击+:', index)
// this.books[index].count++
item.count++
},
removeBook(index, item) {
this.books.splice(index, 1)
},
rowClick(index) {
this.currentIndex = index
}
}
})

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

选中某一行 – 排他思想

点击某一行的数据,选中这一行数据(用active类,控制active添加到这一行数据上)
对这个类进行动态绑定
书籍购物车

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

<div id="app">
<ul>
<li :class="{active: index === currentIndex}"
@click="liClick(index)"
v-for="(item, index) in movies">
{{item}}
</li>
</ul>
</div>
<script src="../lib/vue.js"></script>
<script>
// 1.创建app
const app = Vue.createApp({
// data: option api
data() {
return {
movies: ["星际穿越", "阿凡达", "大话西游", "黑客帝国"],
currentIndex: -1
}
},
methods: {
liClick(index) {
console.log("liClick:", index)
this.currentIndex = index
}
}
})

app.mount("#app")
</script>

组件间通信案例

组件动态使用小例子(视频08)

组件的封装和抽取(视频0506)