【coderwhy前端笔记 - 阶段六 VUE 】(整理版)(更新中2023.7.16)

coderwhy前端系统课 - 阶段六VUE (整理版)(更新中2023.10.23)

1. 前言

本文以coderwhy前端系统课 - 阶段六VUE为主。

一刷版本的笔记有些乱,目前还在二刷整理,同时参考了一部分其他的资料,并加上个人使用总结

暂时停更,在备考,考完分享《软考中级-软件评测师》的复习资源和笔记
-----2023-10-23

建议使用 资源绑定链接 里的html文件进行阅读

直接下载

资源说明:

  1. 资源内有自定义的样式更便于阅读,这里的样式不做额外编写
  2. 资源内点击侧边栏开关时文章阅读位置会偏移,点击目录定位即可
  3. 资源内的图片无法像csdn的可以点击放大,但能看清的
  4. 该文章有些定位点链接,资源内可点击,文章这点了没效果,不做额外修改
  5. 该文章出现一些看不懂的符号拼接啥的,略过
    (一些自定义语法,太多了,这回应该删完了,后面更新估计懒得删)

----2023.10.23 更新----

  1. 图片经停1秒会放大显示
    (会有文章抖动现象,vscode的md目前没有像csdn的可以点击放大图片)

附上一张效果图:
在这里插入图片描述

2. vue2 vs vue3

放在开头为了方便对比,后面的内容以vue3为主

2.1. vue文件结构

/.wrap2 <
/.box <

vue2

<template>
  <div>
    <h1>{{ title }}</h1>
    <button @click="increment">{{ count }}</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: 'Hello, Vue2!',
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

/->
/.box <

vue3

<template>
  <div>
    <h1>{{ title }}</h1>
    <button @click="increment">{{ count }}</button>
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  data() {
    return {
      // 也可以在这定义data数据
    }
  },
  setup() {
    const title = 'Hello, Vue3!'
    const count = ref(0)

    function increment() {
      count.value++
    }

    return {
      title,
      count,
      increment
    }
  }
}
</script>

/->
/->

2.2. 路由

– 来自chatgpt,两者都使用Vue Router来实现路由功能

/.wrap2 <
/.box <

vue2

  1. 引入Vue Router插件及其依赖
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)
  1. 配置路由映射关系
const router = new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      component: About
    }
  ]
})

其中,path表示URL路径,name为路由名称,component指定该路由对应的组件。

  1. 将路由挂载到Vue实例上
new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

在 Vue3 中,render 函数的写法有所不同,使用了新的 createApp API。

/->
/.box <

vue3

提供了一个基于函数式API的新特性:createRouter()

  1. 引入Vue Router插件及其依赖
import { createRouter, createWebHistory } from 'vue-router'
  1. 创建Router实例并配置路由映射关系
const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/about',
    name: 'about',
    component: About
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

与Vue2不同的是,在Vue3中,需要将history传递给 createWebHistory 来指定路由模式。

routes 数组中的每个对象包含pathnamecomponent属性。

  1. 将路由挂载到Vue实例上
createApp(App).use(router).mount('#app')

这里使用createApp()方法来创建Vue实例,并通过use()方法将router实例添加到Vue实例中。

/->
/->

备注

vue2 中的 render: h => h(App)

20230503101058

2.3. JS函数

3. 基础语法

3.1. 插值语法 {{}}

/.wrap2 <
/.box <

html

<!-- 1.基本使用 -->
<h2>{{ message }}</h2>
<h2>当前计数: {{ counter }} </h2>

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

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

<!-- 4.调用methods中函数 -->
<h2>{{ formatDate(time) }}</h2>

/->
/.box <

javascript

 //data
 counter: 100,
 info: "my name is why",
 age: 22,
 time: 123
 //method
 formatDate: function(date) {
   return "2022-10-10-" + date
 }

/->/->

3.2. 内置指令

  • v-for(item in list) 遍历,内容:{{item}}
  • v-text 文本 ,等于直接用 {{}}
  • v-html  HTML插入
  • v-pre  无需编译,如{{m}},显示在浏览器还是 {{m}}
  • v-once  只传入一次
  • v-cloak 遮罩斗篷,先隐藏没有传入值的{{m}},有传入值后显示,css加上 [v-cloak]{display:none}
(1) v-memo 某值改变才更新

某值改变时才更新内容,可查看《vue3.2新增指令v-memo的使用 - 南风晚来晚相识》

/.wrap2 <
/.box <

html

<div id="app">
  <div v-memo="[name]">
    <h2>姓名: {{ name }}</h2>
    <h2>年龄: {{ age }}</h2>
    <h2>身高: {{ height }}</h2>
  </div>
  <button @click="updateInfo">改变信息</button>
</div>

/->
/.box <

javascript

data: function() {
  return {
    name: "why",
    age: 18,
    height: 1.88
  }
},

methods: {
  updateInfo: function() {
    this.name = "kobe"
    this.age = 20
  }
}

/->/->

(2) v-bind 动态绑定 :
原语句语法糖
v-bind:src="":src
v-bind:href="":href
v-bind:class="":class

绑定方法参考

/.wrap2 <
/.box lit3<

html

<!-- 1.绑定img的src属性 -->
<img v-bind:src="showImgUrl" alt="">
<!-- 语法糖: v-bind -> : -->
<img :src="showImgUrl" alt="">

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

/->
/.box lit7<

javascript

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() {
    //  图片地址 =  现在的图片地址 ===  是图片1吗 ?     是:展示图片2  不是:显示图片1
    this.showImgUrl = this.showImgUrl === this.imgUrl1 ? this.imgUrl2: this.imgUrl1
  }
}

/->/->

❥ 绑定class

/.wrap2 <
/.box lit3<

前提:

 // css
 .active {
      color: red;
    }
 //data
 isActive: false,
   // methods
    btnClick: function() {
       this.isActive = !this.isActive
    },

/->
/.box lit7<

  1. 三元:
  <!--		=isActive吗?   	是:添加active样式   否:啥也不加			-->
  <button :class="isActive ? 'active': ''" @click="btnClick">我是按钮</button>
  1. 单值 ⭐
<!--	不会影响原有class	 active样式加不加呢?    =false 不加    =true 加		-->
<button class="haha" :class="{ active: isActive }" @click="btnClick">我是按钮</button>
  1. 对象语法的多个键值对
<!--								直接添加 why		-->
<button :class="{ active: isActive, why: true, kobe: false }" @click="btnClick">我是按钮</button>
  1. 把 :class 的内容抽取出去
<!--							抽取到方法里			-->
<button class="abc cba" :class="getDynamicClasses()" @click="btnClick">我是按钮</button>
 //method
 getDynamicClasses: function() {
   return { active: this.isActive, why: true, kobe: false }
 }
  1. 数组
    <!-- 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>

/->/->

❥ 绑定style
  1. 绑定属性为对象,分隔符为,
<!--  普通的html写法 -->
<h2 style="color: red; font-size: 30px;">哈哈哈哈</h2>

<!--  对象类型  -->
<!--			调用data的fontColor		调用data的fontSize与px拼接	88px要连在一起,所以加''	-->
<h2 v-bind:style="{ color: fontColor, fontSize: fontSize + 'px', height: '88px' }">哈哈哈哈</h2>
//data
fontColor: "blue",
  1. 绑定属性为数组 (很少用)
<h2 :style="objStyle">呵呵呵呵</h2>
<h2 :style="[objStyle, { backgroundColor: 'purple' }]">嘿嘿嘿嘿</h2>
//data
objStyle: {
  fontSize: '50px',
  color: "green"
}
❥ 绑定属性名
<h2 :[name]="'aaaa'">Hello World</h2>
//data
name: "class"
❥ 绑定对象
<h2 v-bind="infos">Hello Bind</h2>
// data
infos: { name: "why", age: 18, height: 1.88, address: "广州市" },
(3) v-on 事件绑定(事件监听) @
原语句语法糖
v-on:click=" "@click=" "

绑定方法参考

/.wrap2 <
/.box lit7<

<!-- 1.基本的写法 -->
<div class="box" v-on:click="divClick"></div>

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

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

<!-- 5.元素绑定多个事件(掌握) -->
<div class="box" @click="divClick" @mousemove="divMousemove"></div>

/->
/.box lit3<

methods: {
  divClick() {
    console.log("divClick")
  },
  divMousemove() {
    console.log("divMousemove")
  }
}

/->/->

❥ 参数传递

/.wrap2 <
/.box <

<!-- 1.默认传递event对象 -->
<button @click="btn1Click">按钮1</button>

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

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

/->
/.box <

//data
age: 18
//方法 
methods: {
  // 1.默认参数: event对象
  // 总结: 如果在绑定事件的时候, 没有传递任何的参数
  //       那么event对象会被默认传递进来
  btn1Click(event) {
    console.log("btn1Click:", event)
  },

  // 2.明确参数:
  btn2Click(name, age) {
    console.log("btn2Click:", name, age) // pyy 18
  },

  // 3.明确参数+event对象
  btn3Click(name, age, event) {
    console.log("btn3Click:", name, age, event)
  }
}

/->/->

❥ 添加修饰符
<button @click.stop="btnClick">按钮</button>

使用过

/.wrap2 <
/.box lit3<

回车自动触发:@keyup.enter.native
阻止默认事件:@submit.native.prevent

/->
/.box lit7<

解决问题:
20230509005949

/->/->

其他修饰符

  • .stop - 调用 event.stopPropagation(),这是阻止事件的冒泡方法,不让事件向document上蔓延,但是默认事件任然会执行,当你掉用这个方法的时候,如果点击一个连接,这个连接仍然会被打开,解释来源
  • .prevent - 调用event.preventDefault(),这是阻止默认事件的方法,调用此方法是,连接不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素;
  • .capture - 添加事件侦听器时使用 capture 模式,事件冒泡
  • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调
  • .{keyAlias} - 仅当事件是从特定键触发时才触发回调
  • .once - 只触发一次回调
  • .left - 只当点击鼠标左键时触发
  • .right - 只当点击鼠标右键时触发
  • .middle - 只当点击鼠标中键时触发passive -{ passive: true}模式添加侦听器
(4) v-if 是否(条件渲染)

v-if="条件"     条件成立,执行该段
v-else       条件不成立,执行该段

❥ 数组

/.wrap2 <
/.box <

 <ul v-if="names.length > 0">
   <li v-for="item in names">{{item}}</li>
 </ul>
 <h2 v-else>当前names没有数据, 请求获取数据后展示</h2>

v-for(元素 in 列表)  遍历

/->
/.box <

 //data
 names:[]  // 无数据
 names:[ab, ad, ae] // 有数据

/->/->

❥ 对象

/.wrap2 <
/.box <

<!-- v-if="条件" 	 	无值 > false     有值 > true		 -->
<div class="info" v-if="Object.keys(info).length">  
  <h2>个人信息</h2>
  <ul>
    <li>姓名: {{info.name}}</li>
    <li>年龄: {{info.age}}</li>
  </ul>
</div>

<!-- v-else -->
<div v-else>
  <h2>没有输入个人信息</h2>
  <p>请输入个人信息后, 再进行展示~</p>
</div>

/->
/.box <

info: {name:"aa", age:11}

/->/->

❥ if else if
  <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>
(5) v-show 显示/隐藏 (条件渲染)

/.wrap2 <
/.box <

<div>
  <button @click="toggle">切换</button>
</div>

<div v-show="isShowCode">
  <img src="https://game.gtimg.cn/images/yxzj/web201706/images/comm/floatwindow/wzry_qrcode.jpg" alt="">
</div>

/->
/.box <

// data
isShowCode: true
//method
toggle() {
  this.isShowCode = !this.isShowCode
}

/->
/->

注意:
v-if=true/false也可以控制元素的隐藏显示,区别在于:

  • v-show 不支持 template (见template详解
  • v-show 不可以和 v-else 一起使用
  • v-show 不管是 true 还是 false ,内容的 dom 都是存在的,只是通过 css 的 display 属性来切换
  • v-if=flase 时,对应的内容不会渲染在 dom 中(是不存在的!
  • 频繁的切换隐藏显示用v-show
(6) v-for 遍历 (列表渲染)

v-for="(item,index) in 数组"

也支持 v-for="(item,index) of 数组",但平时一般直接用in

《v-for 循环中 in 与 of 区别,以及 ES5 for in 与 ES6 for of 区别 - 雁 南飞》:

20230520135035

❥ 数组和对象

/–遍历对象 v-for="(value, key, index) in info"

/–遍历字符串 v-for="item in message"

/–遍历数字 v-for="item in 100"

写在 li 标签,会产生多个 li,默认 item 为 value 值

/.wrap2 <
/.box lit4 <

html

<!--  遍历 value  -->
<li v-for="movie in movies">{{ movie }}</li>

<!-- 遍历 value, key, index -->
<li v-for="(value, key, index) in info">
  {{value}}-{{key}}-{{index}}
</li>

<!-- 有索引   0, 1, 2, 3...     -->
<li v-for="(movie, index) in movies">
  {{index + 1}} - {{ movie }}
</li>

<!-- 3.遍历数组复杂数据 -->
<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>

/->
/.box lit6 <

javascript

 // data
 // 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秒杀, 快来抢购!" },
 ]

/->
/->

❥ 数组更新检测
 changeArray() {
   // 1.直接将数组修改为一个新的数组
   // this.names = ["why", "kobe"]
 
   // 2.通过一些数组的方法, 修改数组中的元素
   this.names.push("why")    // 加
   this.names.pop()          // 删除后面一个
   this.names.splice(2, 1, "why")
   this.names.sort()
   this.names.reverse()
 
   // 3.不修改原数组的方法是不能侦听(watch)
    // 因为this.names.map() 会产生一个新数组,而不是修改原数组,所以需要把值存入一个变量
   const newNames = this.names.map(item => item + "why")  //每个value后面拼接why    
   this.names = newNames
 }

⭐ splice!可以添加、删除、替换

names.splice(3,1,"pyy","pyyyy") // 在位置3 删除1个 添加pyy和pyyyy
❥ key的作用

官方解释key::
在这里插入图片描述

有key和没有key时执行的方法不一样
20230516144301

没key时,也会执行diff算法,对比遇到不一样的内容时,后面的全部替换:

有key时,也使用diff算法,但会尽量的复用原有节点:
20230516144534

<!-- key要求是唯一: id -->
<li v-for="item in letters" :key="item">{{item}}</li>

总结
20230520135653

key的面试问答

(7) template (列表渲染)

当div没有意义,又使用了v-xxx,那就把 div 换成 template
可以放id <template id=" ">

在这里插入图片描述

以下来自解释来自于《vue v-if与v-show的区别,template的使用 - 键盘上的那抹灰》

  • 当两个以上的div被同一元素控制时,用 template 替换该元素
  • 控制台template改良版将不会出现一个新的div,也不会出现template,减少空间
  • template是没有实际东西的dom,所以v-show与template联合使用将失效

3.3. v-model

原理和使用方法

原理:
在这里插入图片描述

写法:

✗ 手动绑定,先显示message值在input里,input输入其他值时,message同步改变

<input type="text" :value="message" @input="inputChange">
<h2>{{message}}</h2>
 //data
 message: "Hello Model",
 // methods
 inputChange(event) {
   this.message = event.target.value
 },

⭐ v-model,不用添加方法

<input type="text" v-model="message">
<h2>{{message}}</h2>

案例:登录

    <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>
// data 
account: "",
password: ""
// methods
loginClick() {    // 获取值,发送出去
  const account = this.account
  const password = this.password

  // url发送网络请求
  console.log(account, password)
}
(1) 绑定到 textarea

在这里插入图片描述

(2) 绑定到 checkbox

在这里插入图片描述

lable 里的 for 为 html 内的知识点,点击文字也可以关联到 input

(3) 绑定到 redio

在这里插入图片描述

(4) 绑定到 select

在这里插入图片描述

(5) 值绑定

选项也来自服务器时。
请添加图片描述

(6) 修饰符

v-model-lazy 会将绑定的事件切换为 change 事件,只有在提交时(比如回车)才会触发

v-model-number 转为数字类型

v-model-trim 动过滤用户输入的空白字符

使用多个

v-model.lazy.trim="" 可以组合使用

4. Options API(vue2)

Options API 包含

 export default {
   data() { return { }  },  // 数据   
   props: [ ]               // 接收父组件传递过来的属性
   methods:{ },            // 方法
   watch: { },              // 监听(数据变化)
   computed: { },           // 复杂数据处理
   minxins: { },            // 混入(合并),vue2用的多
   components: { },         // 局部组件
   created() { },           // 监听(创建后)
+
  // lifecycle hooks 生命周期钩子函数,如
  // created、mounted、updated、destroyed 等

  // 以下不确定 
   provide: [ ]             // 提供数据给inject
   inject: [ ]              // 使用props这些数据
 }

4.1. 复杂数据处理 ⭐computed

(1) 方案对比
  1. 插值语法 
    <!-- 插值语法表达式直接进行拼接 -->
    <!-- 1.拼接名字 -->
    <h2>{{ firstName + " " + lastName }}</h2>

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

    <!-- 3.反转单词显示文本 -->
    <h2>{{ message.split(" ").reverse().join(" ") }}</h2>

/–split 将字符串转化为数组
/–reverse 反转
/–join 用xx拼接

  1. method 方法 

函数调用

/.wrap2 <
/.box <

    <!-- 1.拼接名字 方便多次调用-->
    <h2>{{ getFullname() }}</h2>
    <h2>{{ getFullname() }}</h2>
    <h2>{{ getFullname() }}</h2>

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

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

/->
/.box <

 // methods
 getFullname() {
   return this.firstName + " " + this.lastName
 },
 getScoreLevel() {
   return this.score >= 60 ? "及格": "不及格"
 },
 reverseMessage() {
   return this.message.split(" ").reverse().join(" ")
 }

/->/->

弊端:所有的data使用过程都变成了方法的调用

  1. computed 计算属性 ⭐

官方:任何包含响应式数据的复杂逻辑,都应该使用计算属性(案例里都算相应式数据的复杂逻辑)

数据更新时会自动处理

computed 位置

 const app = Vue.createApp({
   data() { return { }  },
   methods:{ },
   computed: { }
 }).mount("#app")

处理案例:

/.wrap2 <
/.box lit4 <

    <!-- 1.拼接名字 -->
    <h2>{{ fullname }}</h2>
    <h2>{{ fullname }}</h2>
    <h2>{{ fullname }}</h2>

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

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

/->
/.box lit6 <

 // 1.创建app
 const app = Vue.createApp({
   // data: option api
   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(" ")
     }
   }
 })
 
 // 2.挂载app
 app.mount("#app")

/->/->

(2) method 与 computed 区别
  • 表现形式:computed 稍微简洁一些
    在这里插入图片描述

  • computed 有缓存
    在这里插入图片描述
    解释:第一次变化时,数据未发生变化,computed方法仅调用了一次,缓存下来直接用,而method方法重复调用(执行)了三次

(3) 计算属性的 set get

20230517145402

4.2. 监听器 watch

位置

 const app = Vue.createApp({
   data() { return { }  },
   methods:{ },
   computed: { },
   watch:{ }
 }).mount("#app")
(1) 监听新旧值

dataName(newValue, oldValue) {}

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

       console.log(newValue.name, oldValue.name)    // 两个值
 
       // 3.获取原生对象  (不想获取proxy对象,想要获取的原生对象方法)
       console.log(...newValue)              // 原生的方法
       console.log(Vue.toRaw(newValue))      //vue专门提供的方法,{name:kobe}
     }
   }
 }).mount("#app")

如果原来是对象类,那么监听时获取到的也是proxy对象

对象类型

proxy对象 👉 Vue.toRaw(newValue)

(2) 深度监听

methods方法修改内容时,虽然内容会有变化,但是默认的watch不会进行深度监听!,所以在watch里默认没有监听到

修改info.name,监听变化

在这里插入图片描述

  • handler(){}相当于info(newValue,oldValue){}的语法糖
  • deep: true 启动深度监听
  • 但!点击改变 info.name:kobe 后改深度对象返回的newValue,oldValue是相同的
    20230520015729
    因为改变的不是info对象是info里的属性, (所以也没能监听到啊,deep:true到底有什么用)
  • immediate,第一次渲染时执行一次
  • 这里的 info.name function 是vue2的知识点,能监听到name的变化
(3) 声明周期的监听

监听message

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

4.3. Options API的弊端

vue2编写组件的方式是Options API

  • Options API一大特点是在对应的属性中编写对应的功能模块
  • data定义数据methods定义方法computed定义计算属性watch监听属性变化,也包括生命周期钩子

但是这种代码有很大的弊端

  • 当实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中
  • 当组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散
  • 对于一开始没有编写这些组件的人(包括阅读组件的其他人)来说,这个组件的代码是难以阅读和理解

20230705070008

当组件非常大时,这种碎片化的代码使用、理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题。并且处理单个逻辑关注点时,需要不断地跳转到相应的代码中

5. 组件

概念及思想

2023052204403620230522043929

5.1. 注册 调用

全局组件
在任何其他的组件中都可以使用的组件,app.component( 组件名称, 对象 ) 方法

局部组件
只有在注册的组件中才能使用的组件,components( 组件名称, 对象 ) 属性

在这里插入图片描述

(1) 全局组件

app.component( 组件名称, 对象 ) 方法,注册全局组件,html 里直接用组件名<组件名称></组件名称>

⭐ vue全局注册案例 —— chatgpt

1.假设你有一个名为"my-component"的组件

<template>
  <div>{{ message }}</div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello World!'
    }
  }
}
</script>

2.要在整个应用程序中使用该组件,你需要先调用Vue全局方法"Vue.component()"进行注册,如下所示:

// main.js
import Vue from 'vue'
import MyComponent from './MyComponent.vue'

Vue.component('my-component', MyComponent)

new Vue({
  el: '#app',
  // ...
})

现在,可以在整个应用程序中使用"my-component"组件,如下所示:

// App.vue
<template>
  <div id="app">
    <my-component></my-component>
  </div>
</template>

<script>
import MyComponent from './MyComponent.vue'

export default {
  components: {
    MyComponent
  }
}
</script>

在这个例子中,我们已经在"main.js"文件中全局注册了"my-component"组件,因此我们不再需要在"App.vue"文件中再次导入和注册该组件。相反,我们可以直接在模板中使用该组件。

总结一下,Vue的全局组件注册使得我们可以在整个应用程序中使用该组件,而不需要重复导入或注册该组件。

课程内容

/.wrap2 <
/.box lit3 <
框架
在这里插入图片描述
/->
/.box lit3 <
案例
在这里插入图片描述

/->
/.box lit3 <
案例
在这里插入图片描述
/->
/->

全局组件的特点: 一旦注册成功后, 可以在任意其他组件的template中使用
在这里插入图片描述

以前写法对比图:
在这里插入图片描述

案例:

在这里插入图片描述

(2) 局部组件

components( 组件名称, 对象 ) 属性

 const app = Vue.createApp({
   components: { },    // 局部组件
   data() { return { }  },  // 数据
   methods:{ },     // 方法
   computed: { },    // 复杂数据处理
   watch:{ }      // 监听
 }).mount("#app")

⭐ vue局部注册案例 —— chatgpt

以下是一个简单的 Vue 局部组件注册案例:

<template>
  <div>
    <!-- 使用局部组件 -->
    <my-component></my-component>
  </div>
</template>

<script>
import MyComponent from './MyComponent.vue'

export default {
  components: {
    // 注册局部组件
    'my-component': MyComponent
  }
}
</script>

在上面的代码中,我们首先引入了 MyComponent 组件,然后在 Vue 实例的 components 选项中注册了该组件。这个组件现在就可以在当前 Vue 组件中使用,并且可以通过 标签来调用。

需要注意的是,在进行局部组件注册时,我们需要指定组件的名称,这个名称将作为组件的标签名在模板中使用。在上面的例子中,我们注册了名为 my-component 的组件,并在模板中使用了它。

值得一提的是,Vue 还支持使用字符串数组来一次性注册多个局部组件,例如:

<script>
import MyComponent1 from './MyComponent1.vue'
import MyComponent2 from './MyComponent2.vue'
import MyComponent3 from './MyComponent3.vue'

export default {
  components: {
    'my-component-1': MyComponent1,
    'my-component-2': MyComponent2,
    'my-component-3': MyComponent3
  }
}
</script>

在上述代码中,我们同时注册了三个局部组件,并分别指定了它们的名称。这样,在模板中就可以使用 < my-component-1 >、< my-component-2 > 和 < my-component-3 > 这三个标签来调用这三个组件了。

课程

案例:

  <div id="app">
    <home-nav></home-nav>
    <product-item></product-item>
  </div>
  
  <template id="product">
    <div class="product">
      <h2>{{title}}</h2><p>商品描述, 限时折扣, 赶紧抢购</p><p>价格: {{price}}</p><button>收藏</button>
    </div>
  </template>

  <template id="nav">
    <div>-------------------- nav start ---------------</div>
    <product-item></product-item>
    <div>-------------------- nav end ---------------</div>
  </template>
 // 1.创建app
    const ProductItem = {
      template: "#product",
      data() {
        return {
          title: "我是product的title",
          price: 9.9
        }
      }
    }

    // 1.1.组件打算在哪里被使用
    const app = Vue.createApp({
      // components: option api
      components: {
        ProductItem,
        HomeNav: {
          template: "#nav",
          components: {
            ProductItem
          }
        }
      },
      // data: option api
      data() {
        return {
          message: "Hello Vue"
        }
      }
    })

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

图解:
在这里插入图片描述

案例:
在这里插入图片描述在这里插入图片描述

组件间的嵌套

在这里插入图片描述

(3) 组件名字

两种方式

  1. - 连接
  2. 使用驼峰标识符

组件名称使用驼峰式时,components:{ MyItem } ,在 <template></template> 内使用时,以下两种方法都可以调用
<MyItem></MyItem> <my-item></my-item>

5.2. 通信

父组件传递给子组件: 通过 props 属性
子组件传递给父组件: 通过 $emit 触发事件
在这里插入图片描述

(1) 父传子 子用 props 接收 ⭐

子组件放props:[ ],props 位置

 // vue3
 export default{
   components: { },    // 局部组件
   data() { return { }  },  // 数据
   methods:{ },     // 方法
   computed: { },    // 复杂数据处理
   watch:{ },     // 监听
   props:[] // 或{}    //接收父组件传递过来的属性
 }

根(父)组件
(app.vue)

<!-- 没加:的,传的是字符串类型        加了:的,会变为js代码,自动变成数字类型 --><show-info name="why" :age="18" :height="1.88" address="广州市" abc="cba" class="active" /><show-info name="kobe" :age="30" :height="1.87" />

子vue

script用下面的语法接收,接收后template{{}}调用

  • props 数组语法props: ["name", "age", "height"]

    弊端: 1、不能对类型进行验证;2、没有默认值的

  • props 对象语法

  export default {
    props: {
      name: {
        type: String,
        default: "我是默认name"
      },
      age: {
        type: Number,
        required: true,
        default: 0
      },
      height: {
        type: Number,
        default: 2
      },
      // 重要的原则: 对象类型写默认值时, 需要编写default的函数, 函数返回默认值
      friend: {
        type: Object,
        default() {   // 或 dafault: () => ({ name: "james" })
          return { name: "james" }
        }
      },
      hobbies: {
        type: Array,
        default: () => ["篮球", "rap", "唱跳"]
      },
      showMessage: {
        type: String,
        default: "我是showMessage"
      }
    }
  }
(2) 子传父 子用 $emit 发送⭐

在这里插入图片描述

添加emits属性,方便查看发送的参数名,且父组件编写时会自动提示

export default {
    // 1.emits数组语法
    emits: ["add"],
    methods: {}
  }

全貌:
在这里插入图片描述
关于emits验证语法,(先执行函数,再验证,即使为false也是先执行函数,仅为提醒作用)
在这里插入图片描述

(3) 非Prop的Attribute

官方概念

未传递的属性
在这里插入图片描述

解释

用props演示来解释

父组件有传 address="广州市" abc="cba" class="active" (👆①)
子组件没有{{address}}接收语句,但vue会自动帮我们接收,添加到子组件的根元素上(在浏览器页面代码上
在这里插入图片描述

inheritAttrs属性决定要不要自动接收

export default {
    inheritAttrs: false,  // 不接收
    props: {}
  }

第一种情况
设置了inheritAttrs: false,但又想在某元素上调用,则设置$attrs
:class="$attrs.class"

第二种情况
不设置inheritAttrs(就是要接收),又有多个根(如下图同级的div)的情况下,使用v-bind="$attrs",告诉浏览器把属性传到哪个根(div)上

在这里插入图片描述

(4) 通信案例(一)

chatgpt提供案例,简单的 父传子+子传父

20230529061837

(5) 通信案例(二)

课程提供的综合案例

在这里插入图片描述

代码:
在这里插入图片描述

代码分析:
在这里插入图片描述

default:()=>[] 默认值:返回空数组

(6) 非父子通信 事件总线 ⭐

provide  Inject

最上层父组件有一个 provide 选项来提供数据
(孙)子组件有一个 inject 选项来开始使用这些数据
在这里插入图片描述

provide提供数据

import { computed } from 'vue'   // 有@click= 事件

 // provide一般都是写成函数
 provide() {
   return {
     name: "why",
     age: 18,
     message: computed(() => this.message) // computed 复杂数据处理,数据发生变化时自动更新
   }           // this.message 数据来自于data
 }

Inject 使用数据

inject: ["name", "age", "message"]

全局事件总线

跨层级较多时采用事件总线方法
在这里插入图片描述

vue2 有事件总线,vue3移除了,官方推荐了mitt和tiny-emitter库,这里我们使用hy-event-store

安装库npm install hy-event-store,里面有提供HYEventBusHYEventStore

创建全局总线,utils > event-bus.js

import { HYEventBus } from 'hy-event-store'

const eventBus = new HYEventBus()

export default eventBus

A 发出事件eventBus.emit("名称", 数据, 数据)
B 监听事件created() { eventBus.on("名称", (数据名, 数据名) => { } ) }

created() { 
 eventBus.on("名称", (数据名, 数据名) => {  
 })
}

A.vue

  import eventBus from './utils/event-bus'

  export default {
    methods: {
      bannerBtnClick() {
        console.log("bannerBtnClick")
        eventBus.emit("whyEvent", "why", 18, 1.88)
      }
    }
  }

B.vue

 import eventBus from './utils/event-bus'
   export default {
    created() {           //声明周期函数
      eventBus.on("whyEvent",(name, age, height)=>{
       console.log("whyEvent事件在app中监听", name, age, height)
       this.message = `name:${name}, age:${age}, height:${height}`  // 更改data的message数据
      })
    }
  }

监听后一般要做移除工作,以C.vue文件,监听总线为例

  import eventBus from './utils/event-bus'

  export default {
    methods: {
      whyEventHandler() {
        console.log("whyEvent在category中监听")
      }
    },
    created() {            // 声明周期函数
      eventBus.on("whyEvent", this.whyEventHandler)   // 监听的是whyEvent,变化时执行方法
    },
    unmounted() {
      console.log("category unmounted") 
      eventBus.off("whyEvent", this.whyEventHandler)  // 销毁监听
    }
  }

5.3. 插槽

定义

  • 抽取共性,预留不同

  • 将共同元素、内容依然在组件内进行封装

  • 不同的元素使用slot作为占位,让外部决定显示什么元素

(1) 基础用法(单插槽)

<slot></slot>
在这里插入图片描述

(2) 具名插槽(多插槽)

子组件传递:<slot name="插槽名称">默认值</slot>"

父组件接收,方式一:v-slot:插槽名称
      方式二:#插槽名称

(父组件未指定接收名字时,名称为default 。v-slot:default

子组件

 <div class="right">
   <slot name="right">right</slot> 
 </div>

里面的right为默认内容,当父组件调用又无实际内容时,显示right

父组件

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

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

在这里插入图片描述

(3) 动态插槽

在这里插入图片描述

(4) 作用域插槽

即使有通讯,vue - template - {{ }}也只是获取自己的 vue-script-data,这称之为渲染作用域

改良目的:不想制作文字使用,也可以是button
在这里插入图片描述
更多:
在这里插入图片描述
ppt内的解释:
在这里插入图片描述

5.4. 生命周期

速览

创建 -> 加载(挂载) -> 更新 -> 销毁(卸载)

/–创建前 beforeCreate
/–创建后 created

/–加载前 beforeMount
/–加载后 mounted      DOM渲染在此周期中已经完成

/–更新前 beforeUpdate
/–更新后 updated

/–销毁(卸载)前 beforeDestroy / beforeUnmount
/–销毁(卸载)后 destroyed / Unmounted

销毁和卸载的区别

  1. vue版本
    vue2 -> 销毁 destroyed
    vue3 -> 卸载 Unmounted

  2. 处理工作上 —— chatgpt
    destroyed适合处理一些清理工作,如清除计时器、取消网络请求、销毁第三方库等
    Unmounted适合做一些操作DOM的工作,如获取元素高度、保存滚动位置等

  3. 执行时机 —— chatgpt
    destroyed:组件实例完全销毁之后调用,此时所有的指令以及事件监听器都已经被移除,数据绑定也被解绑。
    Unmounted:组件从DOM中卸载之前调用,此时可以访问到组件实例、指令、事件以及DOM元素,但是该组件的实例上的所有指令和事件监听器都已经被移除。

生命周期详解

生命周期函数是一些钩子函数(回调函数)。P1027
请添加图片描述  在这里插入图片描述  在这里插入图片描述

  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 - 组件template准备被挂载")
    },
    // 4.组件template被挂载: 虚拟DOM -> 真实DOM ⭐
    mounted() {
      console.log("mounted - 组件template被挂载")
      console.log("1.获取DOM")
      console.log("2.使用DOM")
    },
    // 5.数据发生改变
    // 5.1. 准备更新DOM
    beforeUpdate() {
      console.log("数据发生改变");
      console.log("beforeUpdate - 准备更新DOM")
    },
    // 5.2. 更新DOM
    updated() {
      console.log("updated - 更新DOM")
    },

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

移除案例

20230531065944

5.5. ref引用 $refs

ref是什么?
在Vue中,ref是一种特殊的属性,用于给元素或组件指定一个唯一的标识符,以便可以在JavaScript代码中访问该元素或组件。通过this.$refs对象,我们可以访问所有具有 ref 属性的元素或组件,并且可以执行操作,例如访问DOM元素的属性或调用组件的方法。

作用
帮助获取DOM

在Vue开发中我们是不推荐进行原生DOM操作的;
这个时候,我们可以给元素或者组件绑定一个ref的attribute属性

使用方法
html元素加上ref="名称",方法 methods 中通过 this.$名称 获取

<h2 ref="title" class="title" :style="{ color: titleColor }">{{ message }}</h2>
<button ref="btn" @click="changeTitle">修改title</button>

<banner ref="banner"/>
  export default {
    components: { Banner  },  
    data() { return {  message: "Hello World", titleColor: "red"  }  },
    methods: {
      changeTitle() {
        // 2.获取h2/button元素
        console.log(this.$refs.title)  // 打印整个h2代码
        console.log(this.$refs.btn)   // 打印整个button代码

        // 3.获取banner组件: 组件实例
        console.log(this.$refs.banner)  //  打印代理对象
        
        // 3.1.在父组件中可以主动的调用子组件的对象方法
        this.$refs.banner.bannerClick()  // 执行方法

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

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

        // 4.组件实例还有两个属性(了解):
        console.log(this.$parent) // 获取父组件  打印代理对象
        console.log(this.$root) // 获取根组件   打印代理对象
      }
    }
  }

效果
20230601021942

5.6. 动态组件 :is="组件名称"

通过 <component :is="组件名称"> </component> 绑定

效果
20230601031744

解释
组件名Home和home的大小写没关系

在这里插入图片描述
里边的props + $emit 知识点
在这里插入图片描述

5.7. keep-alive 保持存活

如:tabA有计数器,选择了10。此时切换到tabB,再切换回tabA,我的计数器是多少?

  • 用了 <keep-alive></keep-alive> ,计数器仍保持在10的状态
  • 没有 <keep-alive></keep-alive> ,计数器还原(原因:切换到tabB时,tabA已经被卸载,使用unmounted 可看到提示已被卸载)

属性:

  • include 属性决定哪个要保持存活,不被销毁,(字符串、正则、数组)
  • exclude 属性决定哪个不被缓存,要销毁,(字符串、正则、数组)
  • max 属性决定最多可以缓存多少组件实例,一旦达到这个数字,那么最近没有被访问的实例会被销毁,(数字、字符串)
  • 注意组件name中间,后不加空格,直接写组件名称
<keep-alive include="组件A定义的name,组件B定义的name">
    <component :is="组件名称"></component>
</keep-alive>

在这里插入图片描述

created 创建后、unmounted 卸载后(卸载成功)

当使用了keep-alive保持存活后,该组件就不会执行unmounted函数

缓存组件的生命周期

// 对于保持keep-alive组件, 监听有没有进行切换
// keep-alive组件进入活跃状态
activated() {
  console.log("home activated")      // 进入该组件
},
deactivated() {
  console.log("home deactivated")    // 退出该组件
}

5.8. 异步组件 (了解)

异步组件不常用,一般使用懒加载的方式

学习异步组件前,先了解一下webpack 代码分包

(1) webpack 代码分包

在这里插入图片描述

当自己编写的页面代码过多时,首屏渲染速度就会延长。

(2) 异步组件使用

打包某个js文件
20230601063542

打包某个组件

方式一:

// 引入方法
import { defineAsyncComponent } from 'vue'
// 引入组件
const AsyncCategory = defineAsyncComponent(() => import("./views/Category.vue"))
// 在components中调用
export default {
  components: {
    Category: AsyncCategory
  },
}

// 注意:
// 是把 import Category from './views/Category.vue' 改为上面defineAsyncComponent方法

方式二:
20230601064300

5.9. 组件的v-model

普通v-model

<!-- 1.input v-model -->
<input v-model="message">
<input :value="message" @input="message = $event.target.value">

概念

顾名思义,在组件中使用v-model,它默认完成了两件事情:

  1. v-bind:value的数据绑定,其中modelValue 是默认名
  2. @input的事件绑定,@update:model-value 也是事件默认名v-on:update:model-value = @update:model-value

基本用法

上下两行是等价的,(modelValue 等同于 model-value)

<my-input v-model="message"/>
<my-input :model-value="message" @update:model-value="message = $event"></my-input>

/.wrap2 <
/.box <

APP.vue

<template>
  <div class="app">
    <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 {
        appCounter: 100,
      }
    }
  }
</script>

<style scoped>
</style>
  1. 上下两个counter是相等的
  2. 绑定更新事件,$event数据等于自己绑定的值

/->
/.box <

Counter.vue

<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>

<style scoped>
</style>
  1. 接收modelValue为数字类型,如无内容默认为0

/->
/->

效果:点击前100,点击后为999

组件自定义名称

<!-- 3.组件的v-model: 自定义名称counter -->
<counter2 v-model:counter="appCounter" v-model:why="appWhy"></counter2>

/.wrap2 <
/.box <
App.vue

<template>
  <div class="app">
    <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: 100,
        appWhy: "coderwhy"
      }
    }
  }
</script>

<style scoped>
</style>

/->

/.box <
counter2.vue

<template>
<div>
  <h2>Counter: {{ counter }}</h2>
  <button @click="changeCounter">修改counter</button>

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

<script>
export default {
  props: {
    // 接收counter组件v-model,默认值为数字0,有接收值,为100
    counter: {
      type: Number,
      default: 0
    },
    // 接收why组件v-model,默认值为空字符串,有接收值,为coderwhy
    why: {
      type: String,
      default: ""
    }
  },
  // 发送
  emits: ["update:counter", "update:why"],
  methods: {
    changeCounter() {
      this.$emit("update:counter", 999)
    },
    changeWhy() {
      this.$emit("update:why", "kobe")
    }
  }
}
</script>

<style scoped>
</style>


/->
/->

5.10. 混入Mixin

组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取,vue2和vue都支持使用Mixin来完成(vue2使用较多,vue3已经不怎么用了)

作用

  1. 分发Vue组件中的可复用功能
  2. 一个min对象可以包含任何组件选项
  3. 当组件使用Mixin对象时,所有Mixin对象的选项将被 混合 进入该组件本身的选项中

使用方法

  1. views文件夹同级新建一个mixins文件夹

20230619153407

/.wrap2 <
/.box <

  1. 新建message-mixin.js文件
export default {
  data() {
    return {
      message: "Hello World"
    }
  },
  created() {
    console.log("message:", this.message)
  }
}

/->
/.box <

  1. About.vue引用文件
<template>
  <h2>About组件</h2>
</template>

<script>
  // 引入方法
  import messageMixin from '../mixins/message-mixin'

  export default {
    // 调用
    mixins: [messageMixin]
  }
</script>

<style scoped>
</style>

/->
/->

混入的含义

message-mixin.js的data数据会自动合并(混入)到About.vue的data里,包括生命周期(created)等其他函数都会自动合并(混入)并执行

合并规则

Mixin对象中的选项和组件对象中的选项发生冲突分成不同情况来处理

情况一:如果是data函数的返回值对象

  • 返回值对象默认情况下会进行合并
  • data返回值对象的属性发生冲突时保留组件自身的数据
    (如message-mixin.jsAbout.vue都有message,则以About.vue的message数据为准)

情况二:如果有相同的生命周期钩子函数

  • 生命周期的钩子函数会被合并到数组中,都会被调用

情况三:值为对象的选项,例如methodscomponentsdirectives,将被合并为同一个对象

  • 比如都有methods选项,并且都定义的方法,那么它们都会生效
  • 但如果对象的key相同(key:value),那么会取组件自身的对象键值(如上案例,假设message-mixin.jsAbout.vue都有massage,则优先取About.vue的值)

全局混入
app.mixin({})

6. Composition API (vue3)

6.1. 含义和区别

Composition API是什么,跟options API有什么区别 —— chatgpt

Composition API是Vue 3中提供的一种新的API风格,用于组织和重用组件逻辑。与Options API不同,Composition API将相关的代码组合在一起,以便更好地组织和重用代码。它基于函数而不是对象的形式,使得代码更容易拆分成可组合的逻辑块。此外,使用Composition API可以更好地封装和隐藏信息,并在组件之间共享逻辑,从而提高代码的可维护性和可重用性。总之,Composition API是Vue 3中一个非常实用且令人期待的改进,可以帮助我们更轻松地构建复杂的Web应用程序。

6.2. 包含内容

Composition API包含什么 —— chatgpt

  1. reactive:用于将一个普通对象转换为响应式对象,可以监听该对象属性的变化。

  2. ref:用于将基本类型数据转换为响应式对象,可以监听该值的变化。

  3. computed:用于创建计算属性,依赖于其他响应式对象和计算属性。

  4. watchEffect:用于监听响应式对象的变化,并在变化时执行回调函数。

  5. 生命周期钩子:包括onMounted(挂载到DOM后)、onUpdated(更新后)、onUnmounted(卸载后)等,用于在组件生命周期的不同阶段执行相应的操作。

  6. provideinject:用于跨层级传递数据。

  7. 封装复用逻辑:通过使用函数封装可复用的逻辑,实现组件之间逻辑共享。

6.3. setup函数

(1) 参数

props 和 context

props 从父组件传递过来的属性会被放在props对象中,setup中需要使用时直接通过props参数获取:

  • 定义props的类型和之前的规则是一样的,再props选项中定义
  • 再template中依然是可以正常使用props中的属性
  • 再setup函数中想要使用props,不可以通过this去获取
  • 因为props有直接作为参数传递到setup函数中,可以直接通过参数来使用即可

context 也称之为SetupContext,包含三个属性:

  • attrs:所有的非props的attribute
  • slots:父组件传递过来的插槽
  • emit:组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过this.$emit发出事件)

案例

计数器
20230707054946

设:计数器代码需要复用,要将相关代码提取出来
20230707055728

总结:使用setup方法,函数的复用性、简洁性更强

(2) 返回值 return

作用

  • 可以在模板template中被使用
  • 可以通过setup的返回值来替代data选项
  • 可以返回一个执行函数来替代在methods中定义的方法👇
    20230712063848

注意: 此时counter不是响应式数据,因为对于一个定义的变量来说,默认情况下,Vue并不会跟踪它的变化来引起界面的响应式操作

(3) 数据响应式
❥ Reactive API
  • 对传入类型有限制,必须是一个对象或者数组

  • 一般用在复杂类型的数据,如账号密码

  • 传入基本数据类型(String、Number、Boolean)时会有警告

<template>
  {{state.name}}
  {{state.counter}}
</template>
import {reactive} from 'vue'
export default {
  setup() {
    const state = reactive({
      name:'pyy',
      counter: 100
    })
    return {
      state
    }
  }
}

原理:

  • 这是因为当我们使用reactive函数处理我们的数据之数据再次被使用时就会进行依赖收集
  • 数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);
  • 事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的;

注意:不能随便对reactive做解构

❥ Ref API
  • ref会返回一个可变的响应式对象

  • 该对象作为一个响应式的引用,维护着它内部的值,这就是ref名称的来源

  • 它内部的值是在ref的value属性中被维护着

  • 在模板(template)引入ref时,vue会自动解包(浅层解包),所以在模板中不需要写ref.value

<template>
  <div class='app'>
    {{counter}}  <!-- 会自动解包 -->
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    let counter = ref(100)

    const increment = () => {
    counter.value++   // 表示ref.value
    }

    const decrement = () => {
    counter.value--
    }

    return { counter, increment, decrement }
  }
}

</script>

一点小瑕疵

“ref是浅层解包”说法里有点小瑕疵,估计是

20230712110724

相关js👇

20230712111444

const info = {
  counter // 语法糖写法,全称为 counter: counter,表示counter的值为上方的counter = ref(0)
}
❥ Reactive VS Ref

/–ref 可以定义简单的数据,也可以定义复杂的数据

方法定义的数据应用场景
ref简单与复杂数据都可以1、其他的场景基本都用ref
2、定义从网络中获取的数据(案例👇)
reactive复杂数据1、本地(生成的)数据,如:账号密码
2、多个数据之间是有关系的,组合在一起有特定联系(表单)

案例

// 2.定义从网络中获取的数据也是使用ref
// const musics = reactive([])  但是一般用ref
const musics = ref([])
onMounted(() => {
  const serverMusics = ["晴天", "屋顶", "听妈妈的话"]
  musics.value = serverMusics
})
❥ reactive —— 数据只读
❥❥ readonly 概念(了解)

readonly:一般我们通过reactiveref可以获取到一个响应式的对象。但某些情况下,我们传给其他组件的这个响应式对象希望另外一个组件被使用,但不能被修改

(👆总结:响应式对象在其他组件可以被使用,但不能修改

readonly会返回原始对象的只读代理(也就是它依然是一个Proxy)

一般组件之间的数据传递要符合单项数据流的规范

❥❥ 单向数据流 概念

数据传递给另一个组件时,只允许阅读,不允许修改,称为单向数据流

规范

  1. 子组件拿到数据后只能使用不能修改
  2. 若要定义(改变数据的)方法,需要从子组件发送方法父组件,父组件监听事件,并修改
    父组件负责给子组件传递数据,子组件不能修改,告诉父组件我要修改,在父组件里修改

(同理,react框架有个互通的知识点:react的使用是非常灵活的,但是它有一个重要的原则 —— 任何一个组件都应该像纯函数一样,不能修改传入的props)

方案对比

(旨在表达要修改数据的话,需要把子组件的事件发送到父组件,由父组件决定更改权,明白这个意思可以直接去下一段)(疑问:这不也是在子组件改的kobe?)

/.wrap2 <
/.box <

不规范的方式,(不能在子组件修改数据)

app.vue

<template>
  <h2>父组件App: {{ info }}</h2>
  <show-info :info="info" ></show-info>
</template>

<script>
  import { reactive } from 'vue'
  import ShowInfo from './ShowInfo.vue'

  export default {
    components: {
      ShowInfo
    },
    setup() {
      // 本地定义多个数据, 都需要传递给子组件
      const info = reactive({
        name: "pyy",
        age: 18,
      })
      return {
        info
      }
    }
  }
</script>

showInfo.vue

<template>
  <div>
    <h2>ShowInfo: {{ info }}</h2>
    <!-- 代码没有错误, 但是违背规范(单项数据流) -->
    <button @click="info.name = 'kobe'">ShowInfo按钮</button>
  </div>
</template>

<script>
  export default {
    props: {
      // reactive数据
      info: {
        type: Object,
        default: () => ({})
      },
    }
  }
</script>

/->
/.box <

规范的方式,(需要在父组件修改数据)

app.vue

<template>
  <h2>App: {{ info }}</h2> <!-- 👇@子组件发来的名称=本父组件下的方法 -->
  <show-info :info="info" @changeInfoName="changeInfoName"></show-info>
</template>

<script>
  import { reactive} from 'vue'
  import ShowInfo from './ShowInfo.vue'

  export default {
    components: {ShowInfo},
    setup() {
      // 本地定义多个数据, 都需要传递给子组件
      const info = reactive({
        name: "why",
        age: 18,
      })
      // 接收改名字的方法
      function changeInfoName(payload) {
        info.name = payload
      }
      return {info,changeInfoName}
    }
  }
</script>

showInfo.vue

<template>
  <div>
    <h2>ShowInfo: {{ info }}</h2>
    <!-- 正确的做法: 符合单项数据流-->
    <button @click="showInfobtnClick">ShowInfo按钮</button>
  </div>
</template>

<script>
  export default {
    props: {
      // reactive数据
      info: {
        type: Object,
        default: () => ({})
      },
    },
    emits: ["changeInfoName"],
    // (1)中表明了setup有两个参数 props、context
    setup(props, context) {
      function showInfobtnClick() {
        // 不能通过this.emit发送数据
        context.emit("changeInfoName", "kobe")
      }
      return {
        showInfobtnClick
      }
    }
  }
</script>

/->
/->

❥❥ readonly 使用

方案对比

/.wrap2 <
/.box <
单项数据流写法,(即 上方右侧的规范写法)

app.vue

<template>
  <show-info :info="info"
             @changeInfoName="changeInfoName">
  </show-info>
</template>

<script>
  import { reactive } from 'vue'
  import ShowInfo from './ShowInfo.vue'

  export default {
    components: {
      ShowInfo
    },

    setup() {
      // 本地定义多个数据, 都需要传递给子组件
      const info = reactive({
        name: "why",
        age: 18,
      })

      // 接收改名字的方法
      function changeInfoName(payload) {
        info.name = payload
      }

      return {
        info,
        changeInfoName
      }
    }
  }
</script>

showInfo.vue

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

<script>
  export default {
    props: {
      // reactive数据
      info: {
        type: Object,
        default: () => ({})
      },
    },

    emits: ["changeInfoName"],

    setup(props, context) {
      function showInfobtnClick() {
        context.emit("changeInfoName", "kobe")
      }
      return {
        showInfobtnClick
      }
    }
  }
</script>

/->
/.box <

readonly写法

app.vue

<template>
  <show-info :roInfo="roInfo" 
             @changeRoInfoName="changeRoInfoName">
  </show-info>
</template>

<script>
  import { readonly } from 'vue'
  import ShowInfo from './ShowInfo.vue'

  export default {
    components: {
      ShowInfo
    },
    
    setup() {
      // 本地定义多个数据, 都需要传递给子组件
      const info = reactive({
        name: "why",
        age: 18,
      })

      // 使用readOnly包裹info
      const roInfo = readonly(info)
      function changeRoInfoName(payload) {
        info.name = payload
      }

      return {
        roInfo,
        changeRoInfoName
      }
    }
  }
</script>

showInfo.vue

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

<script>
  export default {
    props: {
      // readonly数据
      roInfo: {
        type: Object,
        default: () => ({})
      }
    },

    emits: ["changeRoInfoName"],

    setup(props, context) {
      function roInfoBtnClick() {
        context.emit("changeRoInfoName", "james")
      }
      return {
        roInfoBtnClick
      }
    }
  }
</script>

/->
/->

总结:在编写代码时遵循单项数据流原则(规范),也可以不使用readonly,readonly是vue3有的

(疑问:这也是在子组件修改的数据kobe啊,只是在父组件调用子组件的修改数据代码,感觉该案例并没有做到我理解的统一在父组件修改数据)

❥❥ chatgpt的解答readonly ⭐

看完视频仍有些懵懵懂懂

readonly的理解

在Vue3中,readonly是一个响应式数据的修饰符,用于创建一个只读的响应式数据。它可以应用于普通的JavaScript对象、数组或者Map。

使用readonly修饰符后,被修饰的数据将变成只读的,即不能通过赋值的方式改变其值。这样做的好处是,当试图修改只读数据时,Vue会发出警告,以防止意外的数据变更引起的问题。

以下是一个使用readonly的示例:

import { readonly } from 'vue';

const data = readonly({
  name: 'John',
  age: 25
});

console.log(data.name); // 输出: "John"
data.name = 'Bob'; // 试图修改只读数据,将触发警告

在上面的示例中,我们使用readonly创建了一个只读的响应式对象data。由于name属性是只读的,所以我们可以读取它的值,但是不能修改它。

深度冻结(deepFreeze)

需要注意的是,readonly只能确保直接修改属性的操作被拦截,如果属性值是一个对象或数组,仍然可以通过改变对象或数组内部的属性或元素来间接修改值。因此,如果需要完全禁止所有级别的修改,可以考虑使用深度冻结(deepFreeze)

import { readonly, deepFreeze } from 'vue';

const data = readonly(deepFreeze({
  name: 'John',
  hobbies: ['reading', 'swimming']
}));

console.log(data.name); // 输出: "John"  ⭐⭐
data.name = 'Bob'; // 试图修改只读数据,将触发警告    ⭐⭐

data.hobbies.push('coding'); // 试图修改只读数据,将触发警告

在上面的示例中,使用deepFreeze对data进行了深度冻结,这样任何级别的修改都将被拦截,确保了完全的只读性。

父子组件间的readonly

在Vue中,父子组件之间可以通过props来传递数据。如果你想在父组件中传递一个只读的数据给子组件,可以使用readonly修饰符。

下面是一个示例,展示了如何在父组件中使用readonly修饰符来传递只读的props给子组件

/.wrap2 <
/.box <

父组件


<template>
  <div>
    <child-component :data="readonlyData">
    </child-component>
  </div>
</template>

<script>
import { readonly } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      data: {
        name: 'John',
        age: 25
      }
    };
  },
  computed: {
    readonlyData() {
      return readonly(this.data);
    }
  }
}
</script>

在上面的示例中,父组件中的data数据被赋值给了readonlyData计算属性,并使用readonly修饰符将其变为只读数据。然后,将readonlyData作为props传递给子组件ChildComponent。

/->
/.box <

子组件 ChildComponent.vue 中,你可以像访问普通的props一样访问只读的props。子组件可以读取只读props的值,但不能修改它。

<template>
  <div>
    <p>Name: {{ data.name }}</p>
    <p>Age: {{ data.age }}</p>
  </div>
</template>

<script>
export default {
  props: {
    data: {
      required: true,  // 必填
      type: Object
    }
  }
}
</script>

在上面的子组件中,我们定义了一个名为data的props,类型为对象。我们可以在模板中直接访问data.name和data.age来显示父组件传递过来的只读props的值。

/->
/->

这样,父组件的数据将以只读的形式传递给子组件,子组件无法直接修改父组件的数据,从而确保了数据的安全性。

数据父组件获取到的,设置只读的操作也是在父组件完成的,子组件负责接收并显示

点击按钮修改readonly的值

/.wrap2 <
/.box <

父组件

<template>
  <div>
    <button @click="changeData">Change Data</button>
    <child-component :data="readonlyData"></child-component>
  </div>
</template>

<script>
import { ref, readonly } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  setup() {
    const data = ref({
      name: 'John',
      age: 25
    });

    const readonlyData = readonly(data);

    const changeData = () => {
      data.value.name = 'Alice'; // 修改只读数据
    };

    return {
      readonlyData,
      changeData
    };
  }
}
</script>

/->
/.box <

子组件 ChildComponent.vue

<template>
  <div>
    <p>Name: {{ data.name }}</p>
    <p>Age: {{ data.age }}</p>
  </div>
</template>

<script>
export default {
  props: {
    data: {
      required: true,
      type: Object
    }
  }
}
</script>

/->
/->

读取的是readonlyData,修改的是data

❥ reactive —— 数据判断

Reactive判断的API

/–isProxy 检查对象是否是reactivereadonly创建

/–isReactive 检查对象是否是reactive创建的响应式代理
/–. 如果该代理是readonly建的,但包裹了由reactive创建的另一个代理,它也会返回 true;

/–isReadonly 检查对象是否是readonly创建的只读代理

/–toRaw 返回reactivereadonly代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。

/–shallowReactive 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。

/–shallowReadonly 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。

以上函数的使用方法:如第一个 isProxy(info)

❥ toRefs 和 toRef 只读

toRefs

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

20230719093145

toRefs函数的作用是将一个响应式对象转换为一个普通的对象,该对象的每个属性都是一个只读的响应式引用。这个函数在将响应式对象传递给子组件时特别有用,以确保子组件能够访问到父组件的响应式数据。(简单说:将一个响应式对象转换为只读的响应式对象

以下是一个使用toRefs的示例:

<!-- 父组件 -->
<template>
  <div>
    <!-- 更改的是原来的data,而非只读的readonlyData -->
    <button @click="changeData">Change Data</button>
    <!-- 调用的是已经设置为只读的数据 -->
    <child-component v-bind="readonlyData"></child-component>
  </div>
</template>

<script>
import { ref, toRefs } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  setup() {
    const data = ref({
      name: 'John',
      age: 25
    });

    const readonlyData = toRefs(data);    // ⭐⭐  将一个响应式对象转换为只读的响应式对象

    const changeData = () => {
      data.value.name = 'Alice';
    };

    return {
      readonlyData,
      changeData
    };
  }
}
</script>

在上面的代码示例中,我们使用toRefs将data对象转换为只读的响应式引用对象readonlyData。然后,我们通过v-bind将readonlyData对象的属性传递给子组件,以便子组件能够访问和响应父组件的数据变化。

toRef

toRef函数的作用是将一个响应式对象的 某个属性 转换只读 的响应式引用。这在一些特定的场景下很有用,例如将父组件的响应式数据传递给子组件时,可以使用toRef创建只读的引用,以确保子组件不能修改父组件的数据。

以下是一个使用toRef的示例:

<!-- 父组件 -->
<template>
  <div>
    <button @click="changeData">Change Data</button>
    <child-component :data="readonlyName"></child-component>
  </div>
</template>

<script>
import { ref, toRef } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  setup() {
    const data = ref({
      name: 'John',
      age: 25
    });

    const readonlyName = toRef(data, 'name');   // ⭐⭐  将一个响应式对象的 某个属性 转换为只读的响应式引用

    const changeData = () => {
      data.value.name = 'Alice';
    };

    return {
      readonlyName,
      changeData
    };
  }
}
</script>

在上面的代码示例中,我们使用toRef将data.value.name属性转换为只读的响应式引用readonlyName。然后,我们通过:data将readonlyName传递给子组件,以便子组件能够访问父组件的只读数据。

❥ ref其他的API

/–unref 如果参数是一个ref,则返回内部值,否则返回参数本身
/–. unref 是val = isRef(val) ? val.value : val的语法糖

/–isRef 判断值是否是一个ref对象

/–shallowRef 创建一个浅层的ref对象;

/–triggerRef 手动触发和 shallowRef 相关联的副作用
/–. (没看懂,反正意思是强制触发一个响应式引用的重新渲染,举个例子👇)

<template>
  <div>
    <p>{{ message }}</p>
    <button @click="changeMessage">Change Message</button>
    <button @click="forceRender">Force Render</button>
  </div>
</template>

<script>
import { ref, triggerRef } from 'vue';

export default {
  setup() {
    const messageRef = ref('Hello, Vue 3');   // 创建响应式数据

    const changeMessage = () => {
      messageRef.value = 'Hello, MOSSAI';     // 点击第一个按钮后数据发生变化
    };

    // ⭐⭐
    const forceRender = () => { // 点击第二个按钮后强制刷新messageRef的渲染值
      triggerRef(messageRef);   // 即使其值没有发生变化
    };  // 我们可以在需要的时候手动触发响应式数据重新渲染,而不必等待其发生实际的变化

    return {
      message: messageRef,
      changeMessage,
      forceRender
    };
  }
}
</script>

当我们没有使用ref或者reactive定义数据时,修改数据并不是响应式的,此时我们就可以在修改数据的代码下加上triggerRef,渲染定义的数据

(4) setup中不能使用this

不可以使用this是因为组件实例还没有被创建出来的说法是错误的,正确的说法是原为源码中并没有绑定this

绑定this的方法有:

apply/call、fn.bind()、instance.fn()

视频位置P1049

6.4. computed函数

在Options Api中,我们是使用computed选项来完成

在Composition Api中,我们可以在setup函数中使用computed方法来编写计算属性

computed方法案例

<template>
  <h2>{{ fullname }}</h2>
  <button @click="setFullname">设置fullname</button>
  <h2>{{ scoreLevel }}</h2>
</template>

<script>
import { reactive, computed, ref } from 'vue'

  export default {
    setup() {
      // 1.定义数据
      const names = reactive({
        firstName: "kobe",
        lastName: "bryant"
      })

      // ⭐⭐ 拼接字符串
      const fullname = computed(() => {
        return names.firstName + " " + names.lastName
      })


      // 2.⭐⭐ 判断是否及格
      const score = ref(89)
      const scoreLevel = computed(() => {
        return score.value >= 60 ? "及格": "不及格"
      })

      return {
        names,
        fullname,
        scoreLevel
      }
    }
  }
</script>

当你给他写函数的时候,本质上是在写它的get语法(复杂,可以略过)👇

const fullname = computed(() => {
  return names.firstName + " " + names.lastName
})

// 👇修改成get set语法

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
  }
})
console.log(fullname)

function setFullname() {
  fullname.value = "coder why"
  console.log(names)
}

return {
  fullname,
  setFullname,
  scoreLevel
}

6.5. 组件的生命周期函数

vue3中已经不再使用options里相关的生命周期

(1) 前言:在setup中使用ref

只需要定义一个ref对象,绑定到元素或者组件的ref属性上即可

20230723021300

代码示例

<template>
  <!-- 1.获取元素 -->
  <h2 ref="titleRef">我是标题</h2>

  <button @click="getElements">获取元素</button>
</template>

<script>
  import { ref } from 'vue'

  export default {
    setup() {
      const titleRef = ref()

      console.log(titleRef.value) // ⭐⭐ undefind 此时获取不到元素
                                  // 因为此时setup内还没有挂载titleRef

      function getElements() {
        console.log(titleRef.value) // ⭐⭐ 此时可以获取到元素,已经挂载
      }                             // <h2>我是标题</h2>

      return {
        titleRef,
        getElements
      }
    }
  }
</script>

生命周期

onMounted 挂载后

/.wrap2 <
/.box lit6 <

app.vue

<template>
  <!-- 1.获取元素 -->
  <h2 ref="titleRef">我是标题</h2>
  <button ref="btnRef">按钮</button>

  <!-- 2.获取组件实例 -->
  <show-info ref="showInfoRef"></show-info>

  <button @click="getElements">获取元素</button>
</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) // ⭐⭐ 输出一个proxy

        showInfoRef.value.showInfoFoo() // ⭐⭐ 调用组件showInfoRef里的showInfoFoo方法
      })

      function getElements() {
        console.log(titleRef.value) // ⭐⭐ 挂载后,手动点击按钮去获取的
      }

      return {
        titleRef,
        btnRef,
        showInfoRef,
        getElements
      }
    }
  }
</script>

/->
/.box lit4 <

ShowInfo.vue

<template>
  <div>ShowInfo</div>
</template>

<script>
  export default {
    // vue2写法 
    methods: {
      showInfoFoo() {
        console.log("showInfo foo function")
      }
    }

    // vue3写法 
    setup() {
      function showInfoFoo() {
        console.log("showInfo foo function")
      }

      return {
        showInfoFoo
      }
    }
  }
</script>

/->
/->

(2) 生命周期钩子

setup可以用来替代data、methods、computed等等选项,也可以替代生命周期钩子

写法对比

/.wrap2 <
/.box <

vue2 options api
20230723131331

/->
/.box <

vue3 compositions api
20230723130417

/->
/->

完整对比
/.wrap2 <
/.box <

vue2

new Vue({
  beforeCreate() {
    // 实例初始化后
  },
  created() {
    // 实例创建完成后
  },
  beforeMount() {
    // 挂载前
  },
  mounted() {
    // 挂载后
  },
  beforeUpdate() {
    // 数据更新前
  },
  updated() {
    // 数据更新后
  },
  beforeDestroy() {
    // 实例销毁前
  },
  destroyed() {
    // 实例销毁后
  }
})

/->
/.box <

vue3

import { onBeforeCreate, onCreated, onBeforeMount,
 onMounted, onBeforeUpdate, onUpdated,
  onBeforeUnmount, onUnmounted } from 'vue'

createApp({
  setup() {
    onBeforeCreate(() => {
      // 实例初始化后
    })
    onCreated(() => {
      // 实例创建完成后
    })
    onBeforeMount(() => {
      // 挂载前
    })
    onMounted(() => {
      // 挂载后
    })
    onBeforeUpdate(() => {
      // 数据更新前
    })
    onUpdated(() => {
      // 数据更新后
    })
    onBeforeUnmount(() => {
      // 实例销毁前
    })
    onUnmounted(() => {
      // 实例销毁后
    })
  }
}).mount('#app')

/->
/->

表格对比

vue2vue3含义备注
beforeCreatesetup

setup是围绕beforeCreate、created运行的
所以不需要显式的定义它们
在这两个钩子中编写的任何代码都应该直接在setup函数中编写
实例初始化后数据观测 (data observer) 和 event/watcher 事件配置之前被调用
created实例创建后,被立即调用此时实例已完成数据观测 (data observer),属性和方法的运算,watch/event 事件回调
beforeMountonBeforeMount挂载前此时模板编译/解析已完成,但尚未将组件挂载到DOM中
mountedonMounted挂载后此时组件已经被挂载到DOM中
beforeUpdateonBeforeUpdate数据更新前发生在虚拟DOM重新渲染和打补丁之前
updatedonUpdated数据更新后发生在虚拟DOM重新渲染和打补丁之后
BeforeUnmountonBeforeUnmount卸载前此时实例仍然完全可用
UnmountedonUnmounted卸载后调用后,所有事件监听器都会被移除,所有子实例都会被销毁
activatedonActivated进入该组件keep-alive组件监听
deactivatedonDeactivated退出该组件keep-alive组件监听

6.6. Provide/Inject 使用 (了解)

vue3不再事件总线的Provide/Inject API 而改为使用Provide/Inject函数

20230724165321

6.7. watch/watchEffect 监听

(1) watch

20230724172758

20230725180309

() => obj.address 是一个箭头函数,用于返回 obj.address 的值。

实际可用的案例

修改后,监听结果为flase,且能明确看到新旧值不一样的!!

watch(()=>({...info.friend}), (newValue, oldValue) => {
  console.log(newValue, oldValue)
  console.log(newValue === oldValue)
}, {
  immediate: true,
  deep:true
})

效果测试

监听值监听到的新旧值变化了吗===呢
info新旧值显示的都是新值true
info.friend新旧值显示的都是新值true
()=>({…info})新旧值显示的都是新值false
()=>({…info.friend})新值正确,旧值正确false

效果
20230725181910

(2) watchEffect

20230724172954

设置条件停止调用

20230724185006

6.8. Hook

(1) 概念

把相同逻辑的js代码抽取到同一个的文件夹(Hook)中

20230724191319

return里的...userCounter 等于setup里的 const { counter, increment, decrement } = useCounter()

...name为语法糖写法

(2) 网页标题

/.wrap2 <
/.box <
Hook文件夹新增useTitile.js

import { ref, watch } from "vue";

export default function useTitle(titleValue) {
  // document.title = title

  // 定义ref的引入数据
  const title = ref(titleValue)

  // 监听title的改变
  watch(title, (newValue) => {
    document.title = newValue  // 拿到新值,更新标题
  }, {
    immediate: true
  })

  // 返回ref值
  return {
    title
  }
}

/->
/.box <

第一种

<script>
  import useTitle from './hooks/useTitle'
  export default {
    setup() {
      function changeTitle() {
        useTitle("app title")
      }
      return {
        changeTitle,
      }
    }
  }
</script>

第二种
20230724225118
/->
/->

(3) 页面位置

/.wrap2 <
/.box <

useScrollPosition.js

import { reactive } from 'vue'

export default function useScrollPosition() {
  // 1.使用reative记录位置
  const scrollPosition = reactive({
    x: 0,
    y: 0
  })

  // 2.监听滚动
  document.addEventListener("scroll", () => {
    scrollPosition.x = window.scrollX
    scrollPosition.y = window.scrollY
  })


  return {
    scrollPosition
  }
}

document.addEventListener(事件名, 函数),事件名是设置好的,有滚动、点击等

/->
/.box <

调用

<template>
  <div class="scroll">
    <h2>x: {{ scrollPosition.x }}</h2>
    <h2>y: {{ scrollPosition.y }}</h2>
  </div>
</template>

<script>
  import useScrollPosition from '../hooks/useScrollPosition'

  export default {
    setup() {
      const { scrollPosition } = useScrollPosition()  // 这个是对return做解构
      // const { x, y } = scrollPosition 
      // ↑ 这个才是对xy做解构,但不能对reactive做解构,所以我们不能这样做
      console.log(scrollPosition)

      return {
        scrollPosition
      }
    }
  }
</script>

/->
/->

6.9. script setup 语法糖

省去script里的setup(){}return{},只需要在script标签后加上setup,在script里直接写上相应的代码即可,也不需要再写return

vue3.2版本后正式支持该写法

/.flexImg <
20230724233200

👉

20230724233133

👉

20230818001035
/->

完整案例

20230725002306

Props 👉 defineProps
emits 👉 defineEmits

6.10. 综合案例-房源列表

看11.3

7. 路 由

7.1. 相关知识

(1) 路由演变阶段

/.flexImg <
20230818065319

20230818065338

20230818065354
/->

(2) SPA 单页面富应用

SPA(Single Page Application,单页应用程序)是一种前端开发模式,其中整个应用程序的所有页面和资源都在一个单独的HTML页面中加载,通过JavaScript来动态更新内容,而不是每次页面切换时重新加载整个页面。SPA通常使用AJAX技术(异步JavaScript和XML)从服务器获取数据,并使用前端框架(如React、Vue或Angular)进行页面路由和组件管理。

传统的多页应用程序MPA在用户导航时会重新加载整个页面,这可能会导致页面刷新的延迟和用户体验的不连贯。相比之下,SPA在初始加载后,只需加载一次HTML、CSS和JavaScript文件,然后通过异步请求数据和动态更新内容,有效地提供了更快的页面切换和更流畅的用户体验。

SPA的优势

  • 更快的页面加载速度和响应速度,因为只需加载一次HTML、CSS和JavaScript文件。
  • 更好的用户体验,因为页面切换时无需刷新整个页面,可以实现无缝的过渡效果。
  • 更好的前后端分离,前端负责页面展示逻辑,后端负责数据处理和提供API接口。

SPA的劣势
然而,SPA也存在一些挑战和注意事项,如SEO问题(由于内容动态生成),初始加载时间(可能需要加载大量JavaScript代码)和内存管理(长时间运行的应用可能会导致内存泄漏)等。

总的来说
SPA是一种提供更好用户体验的前端开发模式,因其动态加载和更新内容的方式而受到广泛使用。

前端页面不需要再进行F5整体刷新,切换路由就好

(3) /#/ 地址
模式#怎样的优缺点简单说
hash✔,http://example.com/#/about当URL的哈希值改变时,页面不会重新加载,Vue会根据哈希值的变化来动态渲染对应的组件优:兼容性良好,即使在不支持HTML5历史API的旧版浏览器中也可以使用有#,url的hash值改变时页面不会重新加载,但是页面会根据新的hash值进行相应的内容渲染
history×,http://example.com/about路由的改变会使用HTML5的history API来修改URL,同时也会向服务器发送请求优:URL更加美观,没有了#符号,更接近传统的URL,对搜索引擎优化也更友好。
缺:需要后端服务器支持,因为URL的路径在浏览器刷新时会发送给服务器,服务器需要配置相应的路由规则,以便正确地返回对应的页面。
无#,当切换路由时,URL会发生改变,但通过使用HTML5的history API来让页面不会重新加载

router.js

const router = new VueRouter({
  // 方式一
  mode: 'hash', // 或者 'history'
  // 方式二
  history: createWebHashHistory(),  // 或者 'createWebHistory'  ⭐

  routes: [...]
})
❥ hash

可略过,看表格

通过监听URL的改变对URL和内容进行映射

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

  • 可以通过直接赋值location.hash来改变href,但是页面不发生刷新
    20230820085815

  • hash的优势是兼容性更好,在老版本IE浏览器都可以运行,但是缺陷是有一个#,显得不像一个真实的链接

❥ history

/–replaceState 替换原来的路径,跳转而不留下历史痕迹
/–pushState 使用新的路径
/–popState 路径的回退
/–go 向前或向后改变路径
/–forward 向前改变路径
/–back 向后改变路径

(chatgpt的使用方法)

// 在某个 Vue 组件的方法中
someMethod() {
  this.$router.replace('/new-route');   // 使用 replaceState 进行页面跳转
  this.$router.push('/new-route');      // 使用 push 进行页面跳转
  this.$router.go(-1);                  // 使用 go 进行前进或后退
  this.$router.back();                  // 使用 back 进行后退
  this.$router.forward();               // 使用 forward 进行前进
}

/.flexImg <
20230820093501

20230820093531
/->

7.2. router/index.js

(1)概念

前端三大框架 及其路由

  • Angular 的 ngRouter
  • React 的 ReactRouter
  • Vue 的 vue-router

认识 Vue router

Vue router 是vuejs的官方路由,它与vuejs核心深度集成,让用vuejs构建单页应用变得非常容易

vue-router是基于路由和组件的,路由用于设定访问路径, 将路径和组件映射起来;在vue-router的单页面应用中, 页面的路径的改变就是组件的切换

使用VUE-Router

  1. 安装:npm install vue-router
  2. 创建vue文件)创建路由需要映射的组件,Home.vue、About.vue等
  3. 写路由)在router/index.js 创建映射关系,修改history的url模式(hash/history)
  4. 让路由生效,main.js内写上app.use(router)
  5. app.vue内使用<router-view></router-view>占位
  6. app.vue内使用<router-link to="/about">首页</vueter-link>
(2) 重定向 redirect

router/index.js

一般使用在给默认路径的重定向,(输入https://www.aaa.com/自动定位到home页面)

routes:[
  // 可以这么写,但是一般用重定向的方法
  {path:'/', component: Home},  

  // ⭐ redirect 重定向
  {path:'/', redirect: "/home"}
]

长这样:
20230821092821

(3) 路由懒加载 ()=>import('路径') 分包

router/index.js

解释

优化性能的方式之一!打包后默认只有app.js一个逻辑文件,把app.js的代码块按组件划分成多个文件,使首屏加载速度更快一些

懒加载的含义:不用的时候不加载用的时候再加载相应的js包

使用

打包命令npm run build

代码:router/index.js

// 以前用的是这种写法
import Home from '../Views/Home.vue' 
import About from '../Views/About.vue'

// 现在的写法(一)
const Home = ()=>import(/* webpackChunkName:'home'*/'../Views/Home.vue')    // webpackChunkName:'home' 为webpack3开始支持的魔法注释
const About = ()=>import(/* webpackChunkName:'about'*/'../Views/About.vue')
routes:[..........]


// 现在的写法(二) ⭐
routes:[
  {
    path:"/",
    redirect:"home"   // 首页重定向
  },
  {
    path:"/home",
    component: () => import(/* webpackChunkName:'home'*/"../Views/Home.vue")
  },
  {
    path:"/about",
    component: () => import(/* webpackChunkName:'about'*/"../Views/About.vue")
  }
]

效果
20230822062106

补充PPT解释

  • 当打包构建应用时,JavaScript包会变得非常大,影响页面加载
    • 如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效;
    • 也可以提高首屏的渲染效率
  • 关于讲到过的webpack的分包知识,Vue Router默认就支持动态来导入组件
    • 这是因为component可以传入一个组件,也可以接收一个函数,该函数需要放回一个Promise
    • 而import函数就是返回一个Promise
(4) 其他属性 name meta

router/index.js

  • /–name 独一无二的名称
  • /–meta 自定义属性
{
  path: "/home",      // 网址路径
  name: "home",       // 独一无二的名字
  component: () => import("../Views/Home.vue")
  meta: {             // 自定义数据
    name: "pyy",
    age: 88
  }
},
(5) 动态路由
❥ 同组件 不同地址 $route.params.id

http://localhost:8080/#/user/123
http://localhost:8080/#/user/321
同一个组件,显示不同的id

html

<router-link to="/user/123">(跳转链接)用户</vueter-link>
<h2>(调用)user{{$route.params.id}}</h2>

js

path:"/user/:id"
component: () => import("../Views/user.vue")

整体案例
20230828150735

调用部分未写完整,补充:

// html与图片一致
// js 部分
import { userRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id)
❥ 监听跳转变化 onBeforeRouteUpdate

在切换到同一个组件时没有经历销毁和重新创建,所以在切换时上面的console.log(route.params.id)并没有显示最新的值

这个方法获取不到

onActivation(() =>{
  const route = useRoute()
  console.log("代码中:",route.params.id)
})

这里我们使用onBeforeRouteUpdate

接动态路由部分,在vue文件使用,template相同,js部分

import { useRoute, onBeforeRouteUpdate } from 'vue-router'

// 加载进入页面时调用一次
const route = useRoute()
console.log(route.params.id)

// 后续发生变化时调用
onBeforeRouteUpdate((to, from) => {
  console.log("from:", from.params.id)
  console.log("to:", to.params.id)
})

onBeforeRouteUpdate 解释

官方:

  1. 添加一个导航守卫,不论当前位置何时被更新都会触发。类似于 beforeRouteUpdate,但可以在任何组件中使用。当组件被卸载时,该守卫会被移除。
  2. 在当前路由改变,但是该组件被复用时调用,举个例子,对于一个带有动态参数的路径/user/:id,在/users/1/users/2之间跳转
  3. Option API(vue2)使用BeforeRouteUpdate
  4. Composition API(vue3)使用onBeforeRouteUpdate

课件图

20230901210952

(6) 找不到路径
  1. 创建 NotFound.vue
<template>
  <div class="not-found">
  <!-- 具体什么路径不正确:{{ $route.params.pathMatch }} -->
    <h2>NotFound: 您当前的路径{{ $route.params.pathMatch }}不正确, 请输入正确的路径!</h2>
  </div>
</template>

<script>
import { useRoute } from 'vue-router'

// 加载进入页面时调用一次
const route = useRoute()
console.log(route.params.pathMatch)

</script>
  1. router/index.js
{
  path:'/:pathMatch(.*)',
  component: () =>import('../pages/NotFound.vue')
}

其中有两种写法,是否解析/

写法效果
path:'/:pathMatch(.*)',user/hahaha/123
path:'/:pathMatch(.*)*'["user","hahaha","123"]
(7) 路由嵌套

20230901221834

chidren里的redirect(重定向):当访问 /home 时,vue路由会自动重定向到 /home/reoduct
(看到chatgpt案例里省略了children重定向里的/home)
这里Home.vue扮演着容器的角色,它负责渲染并包含子级路由的内容。父级组件可以用来布局整个页面的结构,提供共享的样式和功能,同时也可以在子级路由之间共享数据和状态。

20230908010538

7.3. routerjs总结

(1) router

src/router/index.js,路由嵌套省略

import { createRouter, createWebHashHistory } from 'vue-router'    // 哈希模式
                   //  createWebHistory     history模式

const router = createRouter({
  history:createWebHashHistory(), // 哈希模式
  routes: [
    {
      path: "/",                  // 网址路径
      redirect: "/home"           // 重定向,输入网址后直接进入home页面
    }{
      path: "/home",              // 网址路径
      name: "home",               // 独一无二的名字
                                  // 指定文件路径,并做分包处理,包的名称为home
      component: () => import(/* webpackChunkName:'home'*/"../Views/Home.vue")  
      meta: {                     // 自定义数据
        name: "pyy",
        age: 88
      }
    },
    {
      path:"/user/:id"            // id是传过来的值
      component: () => import("../Views/user.vue")
    },
    {
      path:'/:pathMatch(.*)',     // 找不到路径,pathMatch匹配路径,.*匹配上面没有的任何东西
      component: () =>import('../pages/NotFound.vue')
    }
  ]
})
(2) template
<template>
  <!-- 切换按钮 -->
  <router-link to="/home">首页</vueter-link>
  <router-link to="/about">关于</vueter-link>
  <router-link to="/user/123">(跳转链接)用户</vueter-link>
  <!-- 内容 -->
  <router-view></router-view>

  <!-- 动态路由 user/谁的id,写在user.vue -->
  <h2>user:{{$route.params.id}}</h2>

  <!-- 找不到路径,写在NotFound.vue -->
  <h2>NotFound: 您当前的路径{{ $route.params.pathMatch }}不正确, 请输入正确的路径!</h2>

</template>


<script>
import { useRoute, onBeforeRouteUpdate } from 'vue-router'

// 加载进入页面时调用一次
const route = useRoute()
console.log(route.params.id)

// 后续发生变化时调用
onBeforeRouteUpdate((to, from) => {
  console.log("from:", from.params.id)
  console.log("to:", to.params.id)
})
</script>
❥ router-link 属性
  • /–to 跳转到,填写一个字符串,或者是一个对象(如::to="{path:'/home'}"

  • /–replace 替换,当点击时,会调用 router.replace(),而不是router.push(),这样点击后退时就退不回上一页

  • /–active-class 设置激活a元素后应用的class,默认选中的class为router-link-active,自定义方式:active-class='activeName'

  • /–exact-active-class 链接精准激活时,应用于渲染<a>的class,默认是router-link-exact-active

7.4. 页面跳转方式

方式一:路由跳转,<router-link to="/home">首页</vueter-link>

方式二:click事件跳转 >> .push 往上叠加,可后退
              .replace 替换,后退直接回到原始页面

(1) click —— router.push()

router.push()叠加形式的跳转,router.replace替换原页面的跳转形式

  • 直接跳转,router.push("/home")
  • 放入对象,router.push({ })
    • path: " " 页面路径
    • name: " " 名称,因为在首页使用时没有重定向功能,所以推荐全用path
    • query: { } 放一些需要传递的对象,网址上会附带
<span @click="homeSpanClick">首页</span>
<button @click="aboutBtnClick">关于</button>

<script setup>
  import { useRouter } from 'vue-router' // 调用方法
  
  const router = useRouter()        // 函数赋给router

  // 监听元素的点击
  function homeSpanClick() {       // 跳转到首页
    router.push("/home")           // 直接跳转过去
  }

  function aboutBtnClick() {       // 跳转到关于
    router.push({                  // 用对象方式,加点值
      path: "/about",              // 也可以用name: "home",但因为在首页使用时没有重定向功能,所以推荐全用path
      query: {                     // 用该属性带上点值
        name: "why",
        age: 18
      }
    })
  }
</script>

about.vue 获取 query 数据

<h2>About: {{ $route.query }}</h2>

效果
20230905004816

(2) click —— 前进后退(返回)

back 返回,forward 前进,go(number) 前进后退几步

<template>
  <div class="about">
    <button @click="backBtnClick">返回</button>
  </div>
</template>

<script setup>
  import { useRouter } from 'vue-router'

  const router = useRouter()

  function backBtnClick() {
    router.back()           // 后退一步   相当于 router.go(-1)
    router.forward()        // 向前一步   相当于 router.go(1)
  }

</script>

7.5. 动态管理路由

(1) 添加路由 addRoute

两种添加菜单的方法

  • 根据不同的角色,显示不同的菜单
    • 隐藏其他的菜单
    • 缺点是直接输入路径还是会加载隐藏的菜单
  • 根据不同的角色, 生成不同的菜单⭐
    • 路由也是动态注册的
    • 必须要登录,登录后判断其所属的角色

使用router里的addRoute方法

是不是某个角色 >> 是,注册这些路由
        >> 不是,下一个角色是不是

route/index.js

const router = createRouter({
  history: createWebHashHistory(),  // 采用哈希模式,有#
  routes: []                        // 省略
})
                          // 正常为登录页输入账号密码,获取到用户身份
let isAdmin = true        // 这里省略,直接判断用户是不是isAdmin
if (isAdmin) {
  // 这里添加的是一级路由
  router.addRoute({
    path: "/admin",
    component: () => import("../Views/Admin.vue")
  })

  // 这里添加的是二级路由
  router.addRoute("home",{          // 父级路由为home
    path:"vip",
    component: () => import("../Views/HomeVip.vue")
  })
}

以下为 /admin页面 的效果
/.wrap2 <
/.box <
为true时
20230911221926
/->
/.box <
为flase时
20230911222300
/->
/->

ppt的addRoute

20230910022538

(2) 删除路由

一般不用这个功能,了解一下

  1. 添加一个name相同的路由
router.addRoute({ path:'/about', name: 'about', component: About })
// 这将会删除之前已经添加的路由,因为它们具有相同的名字,且名字必须时唯一的
router.addRoute({ path:'/other', name: 'about', component: Home })
  1. 通过removeRoute方法,传入路由的名称
router.addRoute({ path:'/about', name: 'about', component: About })
// 删除路由
router.removeRoute('about')
  1. 通过addRoute方法的返回值回调
const removeRoute = router.addRoute(routeRecord)
removeRoute() // 删除路由如果存在的话

(不太懂略过,第一行会返回一个函数,第二行调用函数时会把路由删掉)

(3) 其他方法

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

7.6. 导航守卫

vue-router 提供的导航守卫主要目的:通过跳转或取消的方式守卫导航

(1) 前置导航守卫(登录) beforeEach

使用全局的前置导航守卫 beforeEach ,有个参数,种返回值

  • 参数:
    • to到哪去,return ‘/xxx’ 时默认是to
    • form从哪来
  • 返回值:
    • 无返回 / undefined 进行默认导航
    • false 取消当前导航
    • 返回一个路由地址string对象
  • 可选的第三个参数next不推荐使用
    • 在vue2中我们是通过next函数来决定如何进行跳转的
    • 但是在vue3中我们是通过返回值来控制的,不再推荐使用next函数,这是因为开发中很容易调用多次next
❥ 案例 进入订单页

20230912024502

进入订单页面:

  1. 判断用户是否登录
  2. 根据逻辑进行不同的处理
  • 用户登录成功
    • 直接进入到订单页面 —— (1) 案例 进入订单页
  • 用户没有的登录
    • 直接进入登录页面
    • 登录成功后,再次进入订单页面(或其他页面)—— (2) 案例 登录流程

文件: router > index.js

// 每次跳转页面时,都会调用 router.beforeEach
router.beforeEach((to, from) => {     // to 去到哪一页,from 从哪一页来 

// 1.进入到任何别的页面时, 都跳转到login页面      
  if (to.path !== "/login") {         // if(去的不是登录页)
     return "/login"   
   } // 不做判断直接return login的话会发生死循环,而且直接这么写是不成功的,需要加上判断

  // 2.进入到订单页面时, 判断用户是否登录
  const token = localStorage.getItem("token")  // 获取登录信息
  if (to.path === "/order" && !token) {    // 进入订单页面 且 未登录
    return "/login"
  }
})
❥ 案例 登录退出 流程

登录页 login.vue

<template>
  <div class="login">
    <h2>登录页面</h2>
    <!-- 省略账号密码 -->
    <button @click="loginClick">登录</button>
  </div>
</template>

<script>
  import { useRouter } from 'vue-router'

  function loginClick(){
    // 点击登陆后,向服务器放送请求,服务器会返回token,需要保存token
    localStorage.setItem("token","sdf4a65s1fas564gsdf")

    // 跳转到order页面
    router.push("/order")
  }
</script>

退出页 Home.vue

<template>
  <div class="home">
    <h2>首页</h2>
    <button @click="logOutClick">退出登录</button>
  </div>
</template>

<script>

  function logOutClick(){
    // 点击后删除token
    localStorage.removeItem("token")
  }
</script>

PPT登录
20230913212313

❥ 完整案例

占位
后台管理系统完整的登录流程,后面再说

(2) 其他导航守卫

导航守卫官网

完整的导航解析流程

  • 导航被触发
  • 在失活的(跳转时,离开的那个)组件里调用beforeRouteLeave守卫
  • 调用全局的beforeEach守卫⭐
  • 在重用的组件里调用beforeRouteUpdate守卫(2.2+)⭐
  • 在路由配置里调用的beforeEnter
  • 解析异步路由组件(就是那些做懒加载的组件,下载并解析那些单独的js文件)
  • 在被激活的(跳转时,打算进入的那个)组件里调用beforeRouteEnter
    • beforeRouteEnter(next){this -> 目前还获取不到组件实例}
    • 太复杂不看了,视频P1083
  • 调用全局的beforeRoutesolve守卫(2.5+)(异步组件解析之后,在跳转之前)
  • 导航被确认 (进行导航)
  • 调用全局的afterEach钩子
  • 触发DOM更新 (template -> DOM更新)
  • 调用beforeRouteEnter守卫中传给next回调函数,创建好的组件实例会作为回调函数的参数传入
    • beforeRouteEnter(to,from,next){ next(vm -> {}) }

8. vuex/Pina 状态管理

以前用Vuex(vuex4),现在用Pina(vuex5)

认识应用状态管理

在开发中,我们会的应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序中的某一个位置,对于这些数据的管理我们就称之为是状态管理

之前管理状态

  • 在Vue开发中,我们使用组件化的开发方式;
  • 而在组件中我们定义data或者在setup中返回使用的数据,这些数据我们称之为state
  • 在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View
  • 在模块中我们会产生一些行为事件处理这些行为事件时,有可能会修改state,这些行为事件我们称之为actions

20230914034134

复杂的状态管理

  • JavaScript开发的应用程序,已经变得越来越复杂了:
    • JavaScript需要管理的状态越来越多,越来越复杂;
    • 这些状态包括服务器返回的数据缓存数据用户操作产生的数据等等;
    • 也包括一些UI的状态,比如某些元素是否被选中是否显示加载动效当前分页
  • 当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏
    • 多个视图依赖同一状态;
    • 来自不同视图的行为需要变更同一状态
  • 我们是否可以通过组件数据的传递来完成呢?
    • 对于一些简单的状态,确实可以通过props的传递或者Provide的方式来共享状态;
    • 但是对于复杂的状态管理来说,显然单纯通过传递和共享的方式是不足以解决问题的,比如兄弟组件如何共享数据呢?

8.1. vuex

PPT介绍

  • 管理不断变化的state本身是非常困难的:
    • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
    • 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
  • 因此,我们是否可以考虑将组件的内部状态抽离出来,以一个全局单例的方式来管理呢?
    • 在这种模式下,我们的组件树构成了一个巨大的 “试图View”
    • 不管在树的哪个位置,任何组件都能获取状态或者触发行为
    • 通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码边会变得更加结构化和易于维护、跟踪;
  • 这就是Vuex背后的基本思想,它借鉴了Flux、Redux、Elm(纯函数语言,redux有借鉴它的思想);
  • 当然,目前Vue官方也在推荐使用Pinia进行状态管理,我们后续也会进行学习。

总结:将常用的状态在仓库统一管理
20230914034814

手动安装
npm install vuex

(1) 基本使用
❥ 简单的说

src/store/index.js中

import { createStore } from 'vuex'

const  store = createStore({ 
  // 创建状态
  state:() => ({  // {}外加() 表示返回一个对象
    a = 1
  }) 
  // 创建相关的方法
  mutations:{
    fangfa(state){
      state.a++
    }
  }
})
export default store

vue3文件

  1. tamplate中使用{{ $store.state.counter }}直接调用
  2. js,点击修改
import { useStore } from 'vuex'
const store = useStore()

function fangfa(){
  console.log(store.state.counter)  // 看看不改,注意这个复制给变量时,不是响应式的
  store.commit("increment")         // 就是要改的时候,调用一下 ⭐
}
  1. js写成响应式的变量,tamplate里直接{{counter}}
const counter = toRefs(store.state.counter)  // 只读、单个数据的、响应式数据

vue2文件(options api)

  1. tamplate中使用 {{ storeCounter }} (名字是自定义的)
  2. js
export default {
  computed: {
    stroreCounter() {
      return this.$store.state.counter
    }
  }
}
❥ 详细的说

(options api没写在这一块,直接看简单说明就好)

src/store/index.js 创建状态

import { createStore } from 'vuex'

const  store = createStore({
  // 1.箭头函数写法
  state:() => ({      // {} 加上 ({}) 表示返回一个对象,解构的知识
    counter: 0
  })

  // 2.普通写法
  // state(){  return { }  }

  // 3.定义mutations对象,在此定义状态的修改方法,
  mutations:{
    increment(state){
      state.counter++
    }
  }
})

// 导出store
export default store

main.js 调用方法
20230914042847

home.vue 使用方法

  • 在tamplate中使用{{ $store.state.counter }},它是响应式变化的
  • 在js中使用👇
<button @click="increment">+1</button>
<script>
import { useStore } from 'vuex'

const store = useStore()

function increment(){
  store.state.counter++ // 这样是不规范的,不可以这样做
  store.commit("increment") // vuex中这样才规范

}
</script>
❥ PPT解释
(❥) 创建Store
  • 每一个Vuex应用的核心就是store(仓库):
    • store本质上是一个容器,它包含着你的应用中大部分的状态(state);
  • Vuex和单纯的全局对象有什么区别呢?
  • 第一:Vuex的状态存储是响应式的
    • 当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会被更新;
  • 第二:你不能直接改变store中的状态
    • 改变store中的状态的唯一途径就显示提交 (commit) mutation;
    • 这样使得我们可以方便的跟踪每一个状态的变化,从而让我们能够通过一些工具帮助我们更好的管理应用的状态;
  • 使用步骤:
    • 创建Store对象;
    • 在app中通过插件安装;
(❥) 在组件中使用store

跟上面一样

(2) 单一状态树
  • Vuex 使用单一状态树:
    • 用一个对象就包含了全部的应用层级的状态
    • 采用的是SSOT,single Source of Truth,也可以翻译成单一数据源
  • 这也意味着,每个应用将仅仅包含一个 store 实例
    • 单状态树和模块化并不冲突,后面我们会讲到module的概念
  • 单一状态树的优势:
    • 如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难;
    • 所以Vuex也使用了单一状态树来管理应用层级的全部状态单一状态树能够让我们最直接的方式找到某个状态的片段而且在之后的维护和调试过程中,也可以非常方便的管理和维护

chatgpt

假设你正在构建一个购物车应用,其中包含商品列表、购物车中的商品数量以及用户信息等数据。在使用Vuex时,你可以将这些数据存储在Vuex的单一状态树中。

在Vuex的store中,你可以定义一个状态对象,例如:

const store = new Vuex.Store({
  state: {
    products: [], // 商品列表
    cart: [], // 购物车中的商品
    user: null // 用户信息
  },
  mutations: {},
  actions: {},
  getters: {}
});

在上述例子中,state对象包含了商品列表、购物车和用户信息。这些数据可以被多个组件共享和访问。

例如,你可以在商品列表组件中获取商品列表:

export default {
  computed: {
    products() {
      return this.$store.state.products;
    }
  },
  // ...
};

同时,在购物车组件中,你可以获取购物车中的商品数量:

export default {
  computed: {
    cartCount() {
      return this.$store.state.cart.length;
    }
  },
  // ...
};

通过单一状态树,你可以方便地在不同的组件中访问和修改这些数据,而不需要通过props或事件来传递数据。

当你在某个组件中修改了状态,例如向购物车中添加了一个商品,你可以使用mutations来更新状态:

const store = new Vuex.Store({
  state: {
    // ...
  },
  mutations: {
    addToCart(state, product) {
      state.cart.push(product);
    }
  },
  // ...
});

组件中可以通过调用mutations来更新状态:

this.$store.commit('addToCart', product);

这样,所有订阅了状态的组件都可以立即响应状态的变化。

这只是一个简单的例子,但希望能够帮助你理解如何使用Vuex的单一状态树来管理应用的状态。在实际应用中,你可以根据具体需求定义更多的状态和相应的操作。

(3) mapState映射

在tamplate中使用{{ $store.state.counter }}不太简洁,所以我们使用映射的方法来简化{{ }}的内容

几种方法

20230922061349

(注意:incrementLevel()方法这么写是不对的,只是为了快速参考)

因为mapState在vuex中不好用,所以我们使用最后一种

代码

<template>
  <h2>{{name}}</h2>
  <h2>{{level}}</h2>
</template>

<script>                  // vue2 options api
  import { mapState } from 'vuex'

  export default {
    computed: {
      ...mapState({       // 直接结构
        name: state => state.name,
        level: state => state.level
      })
    }
  }
</script>

<script setup>            // vue3 composition api
  import { useStore } from 'vuex'
  
  const store = useStore()
<script>
  const { name, level } = toRefs(store.state)     // const {顺便解构} = 响应式(state)

//       解构  起别名,给个默认值 
// const { name: sName, level: level = 0 }

</script>

8.2. 核心状态State(vuex / pnia)

8.3. 核心状态Getters(vuex / pnia)

8.4. 核心状态Mutations(vuex)

8.5. 核心状态Actions(vuex / pnia)

8.6. 核心状态Modules(vuex)

9. 搭建项目

主要有以下几种方法

  1. CDN引入方式:这种方法适用于简单的页面,只需要在HTML文件中引入Vue的CDN链接即可。这种方式在Vue 2和Vue 3中都适用。

  2. Vue CLI 脚手架:Vue CLI是Vue官方提供的脚手架工具,可以快速搭建起一个Vue项目,并且提供了一些便捷的功能,如自动生成代码打包压缩等。在Vue 2中,通过命令行输入“vue create 项目名”即可创建项目;而在Vue 3中,使用Vue CLI需要先全局安装Vue CLI 4.x或以上版本,然后通过命令行输入“vue create 项目名”创建项目。

  3. 使用Webpack手动配置:如果想要更加灵活地配置项目,可以选择手动配置Webpack。在Vue 2中,需要先安装Vue Loader和相关插件,然后通过Webpack配置文件进行相关配置;而在Vue 3中,Vue Loader已经与Vue CLI集成,只需要在Webpack配置文件中引入Vue即可开始开发。

  4. 使用Vite:Vite是Vue3官方推荐的构建工具,可以快速搭建Vue项目,并且具备高效的开发体验。Vite使用ES模块化机制来加载代码,能够极大地提升项目的启动速度和开发效率。使用Vite搭建Vue项目非常简单,只需要全局安装Vite并执行“vite create 项目名”即可创建项目。

9.1. Vue CLI 脚手架

(1) 安装
  1. 安装   npm install @vue-cli -g 不行时用 npm install -g @vue/cli
  2. 查看版本 vue --version
(2) 创建项目 —— vue create 项目名称
  1. vue create 项目名称,(底层打包工具是 webpack)

  2. 是否使用淘宝源 >> no
    在这里插入图片描述

  3. 选择预设 >> Manually select features 手动选择新特性
    在这里插入图片描述

  4. 后期做项目会选择:
    在这里插入图片描述

  5. 选择vue版本 >> 3.x
    在这里插入图片描述

  6. babel放独立文件还是package.json
    在这里插入图片描述

  7. 是否生成预设 >> yes >> 名称
    在这里插入图片描述

  8. 使用工具 >> NPM 或者 PNPM
    在这里插入图片描述

  9. 项目创建成功
    在这里插入图片描述

  10. 启动服务 npm run server ,更改了配置文件得重新启动服务

  11. 打开项目 http://localhost:8080/

(3) 创建项目 —— npm init vue@latest

(底层打包工具是vite,越来越流行,打包效率高?待核实

  • 是否安装create-vue,y
  • 项目名称
  • typeScript,n
  • jsx,n
  • 暂时都n
  • 创建成功👇
    在这里插入图片描述
(4) 区别

(来自chatgdp)

npm init vue@latestvue create 项目名称都是用于创建Vue.js项目的命令,但它们有以下不同点:

  1. npm init vue@latest是通过npm包管理器在当前目录下初始化Vue.js项目,并生成一个package.json文件。
    vue create 项目名称则是通过Vue CLI工具在指定目录下创建新的Vue.js项目。

  2. npm init vue@latest只会安装Vue.js框架本身,你需要手动安装其他依赖项,如Vue Router、Vuex等。
    vue create 项目名称可以快速构建带有预设置集成的Vue.js应用程序,包括路由、状态管理、Linter、测试等等。它还提供了各种选项和预设模板,方便你根据需求进行选择。

因此
如果你想要更加自定义化地创建Vue.js项目,则可以使用npm init vue@latest
如果你想要更快捷地创建符合标准规范的Vue.js应用程序,则可以选择vue create 项目名称

9.2. 项目结构

在这里插入图片描述

(1) .browserlistrc 浏览器视配文件

/–>1% 市场占有率大于1%
last 2 versions 支持最后两个版本
not dead 还在维护的
not ie 11 非ie11

(2) 配置别名

vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,

  configureWebpack: {
    resolve: {
      // 配置路径别名
      // @是已经配置好的路径别名: 对应的是src路径
      alias: {
        "utils": "@/utils" 
      }
    }
  }
  
})

使用 utils/文件夹/文件夹/math.js 时,可以直接写 utils/math,(此时没有文件路径提示)

添加文件路径提示:jsconfig.json

"paths": {
  "utils/*": [
    "src/utils/*"
  ]
},

解释 jsconfig.json
在这里插入图片描述

(3) vscode 关于 vue 的插件

vetur:推荐在 vue2 使用            ✗
volar:推荐在vue3使用,目前比较好用     ⭐

(4) style 作用域
  • 在 app.vue 内设置的常见样式.title { },会使底下其他组件的.title { }生效,为不影响他们,添加<style scoped></style> 生成自己的作用域

10. 概念

10.1. VNode

  1. 全程Virtual Node,虚拟节点
  2. 无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode
  3. VNode的本质是一个JavaScript的对象
  4. template 👉 VNode 👉 真实DOM

10.2. 虚拟DOM

20230516045514

  1. 由多个虚拟节点(VNode)组成一颗树(虚拟Dom)
  2. 为什么要生成虚拟DOM?
    • 便于跨平台,写一份代码渲染到多个平台上
      20230516050157

10.3. proxy

proxy 代理对象,解释:《一篇彻底理解Proxy - LBJ》

10.4. el:#app

意思是把前面html页面中id='app进行了染

10.5. $event

20230612084533

10.6. 解构

ES6语法,它是一种方便的方式来从数组或对象中提取值并将其赋给变量。

解构可以用于数组和对象

解构数组

const arr = [1, 2, 3];
const [a, b, c] = arr;  // 开始解构
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

解构对象

const obj = { x: 1, y: 2 };
const { x, y } = obj;  // 开始解构
console.log(x); // 1
console.log(y); // 2

10.7. token

保存token localStorage.setItem("token","pyy")

调用token const token = localStorage.getItem("token")

删除token localStorage.removeItem("token")

11. 案例

11.1. 购物车

效果
在这里插入图片描述

代码截图
在这里插入图片描述 在这里插入图片描述

分析

  1. books,数据来源:本地和服务器写法
    在这里插入图片描述

created(){} 生命周期回调函数:

当前的组件被创建时自动执行一般在该函数中, 会进行网络请求

  1. 总价的两种计算方法,totalPrice() {} ,有高阶函数写法 reduce 累加器
    在这里插入图片描述

该高阶仅有一句话,可省略为
在这里插入图片描述

20230521232409

  1. 有多个价格在不同的位置,但前面都要添加¥,可以通过设置一个方法的形式添加
    在这里插入图片描述
  2. 监听点击了哪一个商品的 + / -
    在这里插入图片描述
  3. 小于等于1时,按钮禁用
    在这里插入图片描述
  4. 移除商品
    在这里插入图片描述
  5. 购物车移除全部商品后,文字提示
    在这里插入图片描述
  6. 点击效果,点击行变色,其他行还原
    在这里插入图片描述

11.2. 切换+通信

5.2 (5) 通信案例(二)课程提供的综合案例

简短的切换案例

20230724210810

11.3. 列表房源

视频1063~1068

20230818054013
20230818045928

(roomlist部分放下来解释)
20230818063408

(css解释,margin左右8px,导致文字与图片没有对齐,那就在整体的inner处-8px,让div外扩16px,达到对齐效果)

(模拟网络请求:import并不是请求网络数据,而是导入的本地数据,这里模拟的是:刚刚进入网页时加载空对象ref({}),一秒后获取数据;因为import then返回的是export default对象,所以要.default)

回到7.10