2022 Vue3+vite+ts (从零入门学习干货+案例代码,暂时停更)

439 阅读15分钟

目录

一、vue介绍

二、教你创建新项目

三、介绍Vue3 模板语法、插入指令

四、介绍虚拟Dom和Diff算法

五、认识ref全家桶

六、认识reactive全家桶

七、认识to系列的全家桶

八、认识computed计算属性

九、认识watch监听器

十、认识watchEffect高级侦听器

十一、认识组件&Vue3生命周期

十二、实操组件和认识 Less & Scoped

十三、父子组件传参

十四、使用全局组件、局部组件、递归组件

十五、简单实现动态组件

十六、认识插槽全家桶

十八、Teleport 传送组件

十九、vue-router 之 keep-alive 缓存组件

二十、 更新中...

一、vue介绍

官宣:

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

1、最主要的:重写双向绑定

vue2
基于Object.defineProperty()实现
vue3 基于Proxy
proxy与Object.defineProperty(obj, prop, desc)方式相比有以下优势:
//丢掉麻烦的备份数据
//省去for in 循环
//可以监听数组变化
//代码更简化
//可以监听动态新增的属性;
//可以监听删除的属性 ;
//可以监听数组的索引和 length 属性;
let proxyObj = new Proxy(obj,{
    get : function (target,prop) {
        return prop in target ? target[prop] : 0
    },
    set : function (target,prop,value) {
        target[prop] = 888;
    }
})

2、Vue3 优化Vdom

在Vue2中,每次更新diff,都是全量对比,Vue3则只对比带有标记的,这样大大减少了非动态内容的对比消耗

patch flag 优化静态树

新增了 patch flag 标记

TEXT = 1 // 动态文本节点
CLASS=1<<1,1 // 2//动态class
STYLE=1<<2,// 4 //动态style
PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。
HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点
STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragment
KEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有key
UNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragment
NEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slot
HOISTED = -1 // 静态节点
BALL = -2

我们发现创建动态 dom 元素的时候,Vdom 除了模拟出来了它的基本信息之外,还给它加了一个标记: 1 /* TEXT */

这个标记就叫做 patch flag(补丁标记)

patch flag 的强大之处在于,当你的 diff 算法走到 _createBlock 函数的时候,会忽略所有的静态节点,只对有标记的动态节点进行对比,而且在多层的嵌套下依然有效。

尽管 JavaScript 做 Vdom 的对比已经非常的快,但是 patch flag 的出现还是让 Vue3 的 Vdom 的性能得到了很大的提升,尤其是在针对大组件的时候。

3、Vue3 Fragment

vue3 允许我们支持多个根节点

同时支持render JSX 写法

render() {
        return (
            <>
                {this.visable ? (
                    <div>{this.obj.name}</div>
                ) : (
                    <div>{this.obj.price}</div>
                )}
                <input v-model={this.val}></input>
                {[1, 2, 3].map((v) => {
                   return <div>{v}-----</div>;
                })}
            </>
        );
    },
    

同时新增了Suspense 和 多 v-model 用法

4、Vue3 Tree shaking

简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码

在Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到。

而Vue3源码引入tree shaking特性,将全局 API 进行分块。如果你不使用其某些功能,它们将不会包含在你的基础包中

比如你要用watch 就是import {watch} from 'vue' 其他的computed 没用到就不会给你打包,减少体积

5、Vue 3 Composition Api

Setup 函数式编程 也叫vue Hook

例如 ref reactive watch computed toRefs toRaws

二、创建新项目

1、安装node(装过的忽略,-V确定一下版本)

www.runoob.com/nodejs/node…

2、构建vite项目

官方文档开始 开始 Vite中文网

vite 的优势

冷服务 默认的构建目标浏览器是能 在 script 标签上支持原生 ESM 和 原生 ESM 动态导入

HMR 速度快到惊人的 模块热更新(HMR)

Rollup打包 它使用 Rollup 打包你的代码,并且它是预配置的 并且支持大部分rollup插件

使用vite初始化一个项目

npm

npm init vite@latest

Yarn

yarn create vite

接着设置好项目名称,安装好依赖,就可以开始学习vite了

package json 命令解析

{
  "scripts": {
    "dev": "vite", // 启动开发服务器,别名:`vite dev``vite serve`
    "build": "vite build", // 为生产环境构建产物
    "preview": "vite preview" // 本地预览生产构建产物
  }
}

3、Vite目录

public 下面的不会被编译 可以存放静态资源

assets 下面可以存放可编译的静态资源

components 下面用来存放我们的组件

App.vue 是全局组件

main ts 全局的ts文件

index.html 非常重要的入口文件 (webpack,rollup 他们的入口文件都是enrty input 是一个js文件 而Vite 的入口文件是一个html文件,他刚开始不会编译这些js文件 只有当你用到的时候 如script src="xxxxx.js" 会发起一个请求被vite拦截这时候才会解析js文件)

vite config ts 这是vite的配置文件具体配置项 后面会详解

VsCode Vue3 插件推荐 Vue Language Features (Volar)

SFC 语法规范 *.vue 件都由三种类型的顶层语法块所组成: <template>、<script>、<style>

<template> 每个 *.vue 文件最多可同时包含一个顶层 <template> 块。

其中的内容会被提取出来并传递给 @vue/compiler-dom,预编译为 JavaScript 的渲染函数,并附属到导出的组件上作为其 render 选项。

<script> 每一个 *.vue 文件最多可同时包含一个 <script> 块 (不包括

该脚本将作为 ES Module 来执行。

其默认导出的内容应该是 Vue 组件选项对象,它要么是一个普通的对象,要么是 defineComponent 的返回值。

<script setup> 每个 *.vue 文件最多可同时包含一个 <script setup> 块 (不包括常规的 <script>)

该脚本会被预处理并作为组件的 setup() 函数使用,也就是说它会在每个组件实例中执行。

<style> 一个 *.vue 文件可以包含多个 <style> 标签。

<style> 标签可以通过 scoped 或 module attribute (更多详情请查看 SFC 样式特性) 将样式封装在当前组件内。多个不同封装模式的 <style> 标签可以在同一个组件中混

三、模板语法、指令

1、模板插值语法 或者使用v-text

在script 声明一个变量可以直接在template 使用用法为{{变量名称}}

模板语法是可以编写条件运算的、操作API 也是支持的

<template>
  <div>{{ message.split(",").map((v) => `666${v}`) }}</div>
</template><script setup lang="ts">
const message: string = "你,是,好,人";
</script><style></style>

img

编辑切换为居中

添加图片注释,不超过 140 字(可选)

2、指令

v- 开头都是vue 的指令

v-text 用来显示文本

v-html 用来展示富文本

v-if 用来控制元素的显示隐藏(切换真假DOM)

v-else-if 表示 v-if 的“else if 块”。可以链式调用

v-else v-if条件收尾语句

v-show 用来控制元素的显示隐藏(display none block Css切换)

v-on 简写@ 用来给元素添加事件

v-bind 简写: 用来绑定元素的属性Attr

v-model 双向绑定

v-for 用来遍历元素

v-on修饰符 冒泡案例 .stop阻止事件冒泡

<template>
  <div @click="parent">
    <div @click.stop="child">child</div>
  </div>
</template>
 
 
<script setup lang="ts">
const child = () => {
  console.log('child');
 
}
const parent = () => {
  console.log('parent');
}
 
</script>

阻止表单提交案例

<template>
  <form action="/">
    <button @click.prevent="submit" type="submit">submit</button>
  </form>
</template>
 
 
<script setup lang="ts">
const submit = () => {
  console.log('child');
}
</script>
 
 
 
<style>
</style>

v-bind 绑定style案例

<template>
  <div :style="style">学习前端</div>
</template>
 
 
<script setup lang="ts">
type Style = {
  color: string,
  height: string
}
​
const style:Style = {
  color: "blue",
  height: "300px"
}
 
</script>

v-bind 绑定class案例

<template>
  <div :class="flag">{{flag}}</div>
</template>
 
 
<script setup lang="ts">
type Cls = {
  other: boolean,
  h: boolean
}
const flag: Cls = {
  other: true,
  h: true
};
</script>
 
 
 
<style>
.active {
  color: red;
}
.other {
  color: blue;
}
.h {
  border: 3px solid #ccc;
}
</style>

v-model 双向绑定案例

<template>
  <input v-model="message" type="text" />
  <div>{{ message }}</div>
</template>
 
 
<script setup lang="ts">
import { ref } from 'vue'
const message = ref("v-model123")
</script>
 
 
 
<style>
.active {
  color: red;
}
.other {
  color: blue;
}
.h {
  height: 300px;
  border: 1px solid #ccc;
}
</style>

四、虚拟Dom和Diff算法

为什么要学习源码

1.可以提升自己学习更优秀的API设计和代码逻辑

2.面试的时候也会经常问源码相关的东西

3.更快的掌握vue和遇到问题可以定位

介绍虚拟Dom

虚拟DOM就是通过JS来生成一个AST节点树

img

编辑切换为居中

添加图片注释,不超过 140 字(可选)

为什么要有虚拟DOM?

  • MVVM框架解决视图和状态同步问题
  • 模板引擎可以简化视图操作,没办法跟踪状态
  • 虚拟DOM跟踪状态变化
  • 参考github上virtual-dom[1]的动机描述
  • 虚拟DOM可以维护程序的状态,跟踪上一次的状态
  • 通过比较前后两次状态差异更新真实DOM
  • 跨平台使用
  • 浏览器平台渲染DOM
  • 服务端渲染SSR(Nuxt.js/Next.js),前端是vue向,后者是react向
  • 原生应用(Weex/React Native)
  • 小程序(mpvue/uni-app)等
  • 真实DOM的属性很多,创建DOM节点开销很大
  • 虚拟DOM只是普通JavaScript对象,描述属性并不需要很多,创建开销很小
  • 复杂视图情况下提升渲染性能(操作dom性能消耗大,减少操作dom的范围可以提升性能)

我们可以通过下面的例子

let div = document.createElement('div')
let str = ''
for (const key in div) {
  str += key + ''
}
console.log(str)

发现一个dom上面的属性是非常多的

aligntitlelangtranslatedirhiddenaccessKeydraggablespellcheckautocapitalizecontentEditableisContentEditableinputModeoffsetParentoffsetTopoffsetLeftoffsetWidthoffsetHeightstyleinnerTextouterTextonbeforexrselectonabortonbluroncanceloncanplayonca.......文章原因此处省略

所以直接操作DOM非常浪费性能

解决方案就是 我们可以用JS的计算性能来换取操作DOM所消耗的性能,既然我们逃不掉操作DOM这道坎,但是我们可以尽可能少的操作DOM

灵魂发问:使用了虚拟DOM就一定会比直接渲染真实DOM快吗? 答案当然是否定的,且听我说:

举例:当一个节点变更时DOMA->DOMB

img

编辑切换为居中

添加图片注释,不超过 140 字(可选)

上述情况: 示例1是创建一个DOMB然后替换掉DOMA; 示例2去创建虚拟DOM+DIFF算法比对发现DOMB跟DOMA不是相同的节点,最后还是创建一个DOMB然后替换掉DOMA; 可以明显看出1是更快的,同样的结果,2还要去创建虚拟DOM+DIFF算啊对比 所以说使用虚拟DOM比直接操作真实DOM就一定要快这个说法是错误的,不严谨的

举例:当DOM树里面的某个子节点的内容变更时:

img

编辑切换为居中

添加图片注释,不超过 140 字(可选)

当一些复杂的节点,比如说一个父节点里面有多个子节点,当只是一个子节点的内容发生了改变,那么我们没有必要像示例1重新去渲染这个DOM树,这个时候虚拟DOM+DIFF算法就能够得到很好的体现,我们通过示例2使用虚拟DOM+Diff算法去找出改变了的子节点更新它的内容就可以了

总结:复杂视图情况下提升渲染性能,因为虚拟DOM+Diff算法可以精准找到DOM树变更的地方,减少DOM的操作(重排重绘)

介绍Diff算法

在看完上述的文章之后相信大家已经对Diff算法有一个初步的概念,没错,Diff算法其实就是找出两者之间的差异;

diff 算法首先要明确一个概念就是 Diff 的对象是虚拟DOM(virtual dom),更新真实 DOM 是 Diff 算法的结果。

到这里就与snabbdom源码核心部分离不开了

snabbdom的核心

  • init()设置模块.创建patch()函数
  • 使用h()函数创建JavaScript对象(Vnode)描述真实DOM
  • patch()比较新旧两个Vnode
  • 把变化的内容更新到真实DOM树

init函数

当init使用了导入的模块就能够在h函数中用这些模块提供的api去创建虚拟DOM(Vnode)对象;在上文中就使用了样式模块以及事件模块让创建的这个虚拟DOM具备样式属性以及事件属性,最终通过patch函数对比两个虚拟dom(会先把app转换成虚拟dom),更新视图;

h函数

有些地方也会用createElement来命名,它们是一样的东西,都是创建虚拟DOM的,在上述文章中相信大伙已经对h函数有一个初步的了解并且已经联想了使用场景,就不作场景案例介绍了,直接上源码部分:

总结:h函数先生成一个vnode函数,然后vnode函数再生成一个Vnode对象(虚拟DOM对象)

补充:

在h函数源码部分涉及一个函数重载的概念,简单说明一下:

  • 参数个数或参数类型不同的函数()
  • JavaScript中没有重载的概念
  • TypeScript中有重载,不过重载的实现还是通过代码调整参数

重载这个概念个参数相关,和返回值无关

patch函数(核心)

要是看完前面的铺垫,看到这里你可能走神了,醒醒啊,这是核心啊,上高地了兄弟;

  • pactch(oldVnode,newVnode)
  • 把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点(核心)
  • 对比新旧VNode是否相同节点(节点的key和sel相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode的text不同直接更新文本内容(patchVnode)
  • 如果新的VNode有children,判断子节点是否有变化(updateChildren,最麻烦,最难实现)

源码:

return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {    
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    // cbs.pre就是所有模块的pre钩子函数集合
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
    // isVnode函数时判断oldVnode是否是一个虚拟DOM对象
    if (!isVnode(oldVnode)) {
        // 若不是即把Element转换成一个虚拟DOM对象
        oldVnode = emptyNodeAt(oldVnode)
    }
    // sameVnode函数用于判断两个虚拟DOM是否是相同的,源码见补充1;
    if (sameVnode(oldVnode, vnode)) {
        // 相同则运行patchVnode对比两个节点,关于patchVnode后面会重点说明(核心)
        patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
        elm = oldVnode.elm! // !是ts的一种写法代码oldVnode.elm肯定有值
        // parentNode就是获取父元素
        parent = api.parentNode(elm) as Node
​
        // createElm是用于创建一个dom元素插入到vnode中(新的虚拟DOM)
        createElm(vnode, insertedVnodeQueue)
​
        if (parent !== null) {
            // 把dom元素插入到父元素中,并且把旧的dom删除
            api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))// 把新创建的元素放在旧的dom后面
            removeVnodes(parent, [oldVnode], 0, 0)
        }
    }
​
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
        insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    return vnode
}

题外话:diff算法简介

传统diff算法

  • 虚拟DOM中的Diff算法
  • 传统算法查找两颗树每一个节点的差异
  • 会运行n1(dom1的节点数)*n2(dom2的节点数)次方去对比,找到差异的部分再去更新

img

编辑切换为居中

添加图片注释,不超过 140 字(可选)

snabbdom的diff算法优化

  • Snbbdom根据DOM的特点对传统的diff算法做了优化
  • DOM操作时候很少会跨级别操作节点
  • 只比较同级别的节点

img

五、认识ref全家桶

文章列出包括(ref、isRef、shallowRef、triggerRef、customRef)的用法

1、ref

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。

案例

我们这样操作是无法改变message 的值 应为message 不是响应式的无法被vue 跟踪要改成ref

<template>
  <div>
    <button @click="changeMsg">change</button>
    <div>{{ message }}</div>
  </div>
</template>
 
 
 
<script setup lang="ts">
let message: string = "我是message"
 
const changeMsg = () => {
   message = "change msg"
}
</script>
 
 
<style>
</style>

改为ref

Ref TS对应的接口

interface Ref<T> {
  value: T
}

注意被ref包装之后需要.value 来进行赋值

<template>
  <div>
    <button @click="changeMsg">change</button>
    <div>{{ message }}</div>
  </div>
</template>
 
<script setup lang="ts">
import {ref,Ref} from 'vue'
let message:Ref<string> = ref("我是message")
 
const changeMsg = () => {
   message.value = "change msg"
}
</script>
 
 
<style>
</style>
//--------------------------------ts两种方式
<template>
  <div>
    <button @click="changeMsg">change</button>
    <div>{{ message }}</div>
  </div>
</template>
 
 
 
<script setup lang="ts">
import { ref } from 'vue'
let message = ref<string | number>("我是message")
 
const changeMsg = () => {
  message.value = "change msg"
}
</script>
 
 
<style>
</style>

2、isRef

判断是不是一个ref对象

import { ref, Ref,isRef } from 'vue'
let message: Ref<string | number> = ref("我是message")
let notRef:number = 123
const changeMsg = () => {
  message.value = "change msg"
  console.log(isRef(message)); //true
  console.log(isRef(notRef)); //false
  
}

3、shallowRef

创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的

例子1

修改其属性是非响应式的这样是不会改变的

<template>
  <div>
    <button @click="changeMsg">change</button>
    <div>{{ message }}</div>
  </div>
</template>
 
 
 
<script setup lang="ts">
import { shallowRef } from 'vue';
let message = shallowRef({
  name: "yzf"
})
​
const changeMsg = () => {
  message.value.name = '666'
}
</script>
 
 
<style>
</style>

例子2

这样是可以被监听到的修改value,需要赋值整个对象

<template>
  <div>
    <button @click="changeMsg">change</button>
    <div>{{ message }}</div>
  </div>
</template>
 
 
 
<script setup lang="ts">
import { shallowRef } from 'vue';
let message = shallowRef({
  name: "yzf"
})
​
const changeMsg = () => {
  message.value = { name: "666" }
}
</script>
 
 
<style>
</style>

img

编辑

添加图片注释,不超过 140 字(可选)

可以看到这里的属性值已经被我们改变了(监听到了修改的value)

思考: 如果我们不想赋值整个对象,而只是想修改一个属性值,应该怎么做呢?

这里就需要引入一个新的ref概念 --- triggerRef

4、triggerRef

强制更新页面DOM

这样也是可以改变值的

<template>
  <div>
    <button @click="changeMsg">change</button>
    <div>{{ message }}</div>
  </div>
</template>
 
 
 
<script setup lang="ts">
import { Ref, shallowRef, triggerRef } from 'vue';
type Obj = {
  name: string
}
let message: Ref<Obj> = shallowRef({
  name: "yzf"
})
​
const changeMsg = () => {
  message.value.name = '666'
  triggerRef(message)
}
</script>
 
 
<style>
</style>

5、customRef

自定义ref

customRef 是个工厂函数要求我们返回一个对象 并且实现 get 和 set

这块稍微有些复杂

<template>
  <div>
    <button @click="changeMsg">change</button>
    <div>{{ message }}</div>
  </div>
</template>
 
 
 
<script setup lang="ts">
import { customRef } from 'vue'
 
function Myref<T>(value: T) {
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newVal: T) {
        console.log('set');
        value = newVal
        trigger()
      }
    }
  })
}
 
let message = Myref('yzf')
const changeMsg = () => {
  message.value = '666'
}
</script> 
 
 
<style>
</style>

六、认识reactive全家桶

用来绑定复杂的数据类型 例如 对象 数组

reactive 源码约束了我们的类型

img

编辑切换为居中

添加图片注释,不超过 140 字(可选)

他不允许绑定普通的数据类型 会给我们报错

import { reactive} from 'vue'
 
let person = reactive('yzf')

img

编辑

添加图片注释,不超过 140 字(可选)

绑定普通的数据类型 我们可以使用ref,使用reactive 去修改值无须.value

1、reactive 基础用法

<script setup lang="ts">
import { reactive } from 'vue';
let person = reactive({
   name:"yaozaofeng"
})
person.name = "YAOZAOFENG"
</script> 

数组异步赋值问题

这样赋值页面是不会变化的应为会脱离响应式

let person = reactive<number[]>([])
setTimeout(() => {
  person = [1, 2, 3]
  console.log(person); // [1,2,3] 打印出来的值是已经变化了的
  
},1000)

解决方案1

使用push

import { reactive } from 'vue'
let person = reactive<number[]>([])
setTimeout(() => {
  const arr = [1, 2, 3]
  person.push(...arr)
  console.log(person);
  
},1000)

方案2包裹一层对象

type Person = {
  list?:Array<number>
}
let person = reactive<Person>({
   list:[]
})
setTimeout(() => {
  const arr = [1, 2, 3]
  person.list = arr;
  console.log(person);
  
},1000)

2、readonly

拷贝一份proxy对象将其设置为只读

import { reactive ,readonly } from 'vue'
const person = reactive({count:1})
const copy = readonly(person)
copy.count++ // 这里不能改变值,因为设置了只读状态

3、shallowReactive

只能对浅层的数据 如果是深层的数据只会改变值 不会改变视图

案例

<template>
  <div>
    <div>{{ state }}</div>
    <button @click="change1">test1</button>
    <button @click="change2">test2</button>
  </div>
</template>
 
 
 
<script setup lang="ts">
import { shallowReactive } from 'vue'
 
 
const obj = {
  a: 1,
  first: {
    b: 2,
    second: {
      c: 3
    }
  }
}
 
const state = shallowReactive(obj)
 
function change1() {
  state.a = 100
}
function change2() {
  state.first.b = 200
  state.first.second.c = 300
  console.log(state);
}
</script> 
 
 
<style>
</style>

七、 认识to系列的全家桶

toRef 、toRefs、toRaw

1、toRef

如果原始对象是非响应式的就不会更新视图 数据是会变的

<template>
   <div>
      <button @click="change">按钮</button>
      {{state}}
   </div>
</template>
 
<script setup lang="ts">
import { reactive, toRef } from 'vue'
 
const obj = {
   foo: 1,
   bar: 1
}
​
// 如果原始对象是响应式的是会更新视图并且改变数据的
// const obj = reactive({
//   foo: 1,
//   bar: 1
// })
 
 
const state = toRef(obj, 'bar')
// bar 转化为响应式对象
 
const change = () => {
   state.value++
   console.log(obj, state);
 
}
</script>

如果原始对象是响应式的是会更新视图并且改变数据的

// const obj = reactive({
//   foo: 1,
//   bar: 1
// })

2、toRefs

可以帮我们批量创建ref对象主要是方便我们解构使用

<template>
  <div>
    <div>foo---{{ foo }}</div>
    <div>bar---{{ bar }}</div>
    <div>
      <button @click="change">按钮change</button>
    </div>
  </div>
</template>
 
<script setup lang="ts">
import { reactive, toRefs } from 'vue'
const obj = reactive({
  foo: 1,
  bar: 1
})
​
let { foo, bar } = toRefs(obj)
​
foo.value++
console.log(foo, bar);
const change = () => {
  foo.value++
  bar.value--
}
</script>

toRefs能够帮助做到响应式 视图发生改变

img

编辑

添加图片注释,不超过 140 字(可选)

3、toRaw

将响应式对象转化为普通对象

<template>
  <div>
    <div>foo---{{ obj.foo }}</div>
    <div>bar---{{ obj.bar }}</div>
    <div>
      <button @click="change">按钮change</button>
    </div>
  </div>
</template>
 
<script setup lang="ts">
import { reactive, toRaw } from 'vue'
 
const obj = reactive({
   foo: 1,
   bar: 1
})
 
 
const state = toRaw(obj)
// 响应式对象转化为普通对象
 
const change = () => {
 
   console.log(obj, state);
 
}
</script>

此时点击按钮视图不会发生变化

八、认识computed计算属性

计算属性就是当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。

1、 函数形式

<template>
  <div>
    {{price}} --- {{m}} <!-- 500 --- $500 -->
  </div>
</template>
 
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
let price = ref(0)//$0
 
let m = computed<string>(()=>{
   return `$` + price.value
})
 
price.value = 500
</script>

2、对象形式

<template>
   <div>{{ mul }}</div>
   <button @click="mul = 100">click</button>
</template>
 
<script setup lang="ts">
import { computed, ref } from 'vue'
let price = ref<number | string>(1)//$0
let mul = computed({
   get: () => {
      console.log('get')
      return price.value
   },
   set: (value) => {
      console.log('set')
      price.value = 'set' + value
   }
})
</script>
 
<style>
</style>

接下来使用一个购物车案例来学习巩固computed的知识

<template>
  <div>
    <table style="width:800px" border>
      <thead>
        <tr>
          <th>名称</th>
          <th>数量</th>
          <th>价格</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr :key="index" v-for="(item, index) in data">
          <td align="center">{{ item.name }}</td>
          <td align="center">
            <button @click="addAndSub(item, false)">-</button>
            {{ item.num }}
            <button @click="addAndSub(item, true)">+</button>
          </td>
          <td align="center">{{ item.num * item.price }}</td>
          <td align="center">
            <button @click="del(index)">删除</button>
          </td>
        </tr>
      </tbody>
      <tfoot>
        <tr>
          <td align="center"></td>
          <td align="center"></td>
          <td align="center"></td>
          <td align="center">总价:{{ $total }}</td>
        </tr>
      </tfoot>
    </table>
  </div>
</template>
 
<script setup lang="ts">
import { reactive, ref, computed } from 'vue'
type Shop = {
  name: string,
  num: number,
  price: number
}
let $total = ref(0)
const data = reactive<Shop[]>([
  {
    name: "衣服",
    num: 1,
    price: 100
  },
  {
    name: "裤子",
    num: 2,
    price: 200
  },
  {
    name: "鞋子",
    num: 3,
    price: 300
  }
​
])
​
const addAndSub = (item: Shop, type: boolean): void => {
  if (item.num > 1 && !type) {
    item.num--
  }
  if (item.num < 99 && type) {
    item.num++
  }
}
​
const del = (index: number) => {
  data.splice(index, 1)
}
​
$total = computed<number>(() => {
  return data.reduce((prev, next) => {
    console.log(prev + (next.num * next.price))
    return prev + (next.num * next.price)
  }, 0)
})
</script>
 
<style>
</style>

img

编辑切换为居中

添加图片注释,不超过 140 字(可选)

这里实现一个简易的购物车,computed的使用主要体现再了总价的计算方法调用上,利用computed可以避免重复调用总价的计算方法。

拓展:reduce()方法的使用

reduce方法虽然参数比较多,有回调函数中的prev,cur,index,arr,还有reduce的第二个参数init,但是常用的也就prev(上一次回调的返回值)和cur(当前值)

arr.reduce(function(prev,cur,index,arr){
...
}, init);
​
其中,
arr 表示原数组;
prev 表示上一次调用回调时的返回值,或者初始值 init;
cur 表示当前正在处理的数组元素;
index 表示当前正在处理的数组元素的索引,若提供 init 值,则索引为0,否则索引为1init 表示初始值。
​
看上去是不是感觉很复杂?没关系,只是看起来而已,其实常用的参数只有两个:prev 和 cur。接下来我们跟着实例来看看具体用法吧~

img

编辑切换为居中

添加图片注释,不超过 140 字(可选)

reduce() 方法 的语法
arr.reduce(function(prev,cur,index,arr){ }, init);
​
arr.reduce(function(prev,cur,index,arr){
...
}, init);
​
其中,
arr 表示原数组;
prev 表示上一次调用回调时的返回值,或者初始值 init;
cur 表示当前正在处理的数组元素;
index 表示当前正在处理的数组元素的索引,若提供 init 值,则索引为0,否则索引为1init 表示初始值。

九、认识watch监听器

watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用

watch第一个参数监听源

watch第二个参数回调函数cb(newVal,oldVal)

watch第三个参数一个options配置项是一个对象{undefined

immediate:true //是否立即调用一次

deep:true //是否开启深度监听

1、监听Ref

案例

import { ref, watch } from 'vue'
 
let message = ref({
    nav:{
        bar:{
            name:""
        }
    }
})
 
 
watch(message, (newVal, oldVal) => {
    console.log('新的值----', newVal);
    console.log('旧的值----', oldVal);
},{
    immediate:true,
    deep:true
})

监听多个ref 注意监听多个的时候第一个参数变成数组啦

import { ref, watch ,reactive} from 'vue'
 
let message = ref('')
let message2 = ref('')
 
watch([message,message2], (newVal, oldVal) => {
    console.log('新的值----', newVal);
    console.log('旧的值----', oldVal);
})

2、监听Reactive

使用reactive监听深层对象开启和不开启deep 效果一样

案例1

<template>
  <div>
    <input v-model="message.nav.bar.name" type="text">
  </div>
</template><script setup lang="ts">
import { ref, watch ,reactive} from 'vue'
 
let message = reactive({
    nav:{
        bar:{
            name:""
        }
    }
})
 
 
watch(message, (newVal, oldVal) => {
    console.log('新的值----', newVal);
    console.log('旧的值----', oldVal);
})
</script>

案例2 监听reactive 单一值

<template>
  <div>
    <input v-model="message.name" type="text">
    <input v-model="message.name2" type="text">
  </div>
</template>
​
import { ref, watch ,reactive} from 'vue'
 
let message = reactive({
    name:"",
    name2:""
})
 
 
watch(()=>message.name, (newVal, oldVal) => {
    console.log('新的值----', newVal);
    console.log('旧的值----', oldVal);
})

十、认识watchEffect高级侦听器

watchEffect

立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

如果用到message 就只会监听message 就是用到几个监听几个 而且是非惰性 会默认调用一次

<template>
  <div>
    <input v-model="message" type="text" />
    <input v-model="message2" type="text" />
  </div>
</template>
 
<script setup lang="ts">
import { ref, watchEffect } from 'vue'let message = ref<string>('')
let message2 = ref<string>('')
watchEffect(() => {
  console.log('message', message.value);
  console.log('message2', message2.value);
})
</script>

清除副作用

就是在触发监听之前会调用一个函数可以处理你的逻辑例如防抖

<template>
  <div>
    <input v-model="message" type="text" />
    <input v-model="message2" type="text" />
  </div>
</template>
 
<script setup lang="ts">
import { ref, watchEffect } from 'vue'let message = ref<string>('')
let message2 = ref<string>('')
watchEffect((oninvalidate) => {
  console.log('message', message.value);
  console.log('message2', message2.value);
  oninvalidate(() => {
    console.log('before')
  })``
})
</script>

停止跟踪 watchEffect 返回一个函数 调用之后将停止更新

<template>
  <div>
    <input v-model="message" type="text" />
    <input v-model="message2" type="text" />
​
    <button @click="stopWatch">stopWatch</button>
  </div>
</template>
 
<script setup lang="ts">
import { ref, watchEffect } from 'vue'let message = ref<string>('')
let message2 = ref<string>('')
const stop = watchEffect((oninvalidate) => {
  // console.log('message', message.value);
  // console.log('message2', message2.value);
  oninvalidate(() => {
    console.log('before')
  })
})
​
const stopWatch = () => stop()
</script>

更多的配置项

副作用刷新时机 flush 一般使用post

img

编辑切换为居中

添加图片注释,不超过 140 字(可选)

onTrigger 可以帮助我们调试 watchEffect

<template>
  <div>
    <input id="ipt" v-model="message" type="text" />
    <input v-model="message2" type="text" />
​
    <button @click="stopWatch">stopWatch</button>
  </div>
</template>
 
<script setup lang="ts">
import { ref, watchEffect } from 'vue'let message = ref<string>('')
let message2 = ref<string>('')
const stop = watchEffect((oninvalidate) => {
  console.log('message', message.value);
​
  oninvalidate(() => {
    console.log('before')
  })
}, {
  flush: 'post',
  onTrigger(e) {
    debugger
  }
})
​
const stopWatch = () => stop()
</script>

十一、认识组件&Vue3生命周期

组的生命周期

简单来说就是一个组件从创建 到 销毁的 过程 成为生命周期

在我们使用Vue3 组合式API 是没有 beforeCreate 和 created 这两个生命周期的

onBeforeMount()

在组件DOM实际渲染安装之前调用。在这一步中,根元素还不存在。

onMounted()

在组件的第一次渲染后调用,该元素现在可用,允许直接DOM访问

onBeforeUpdate()

数据更新时调用,发生在虚拟 DOM 打补丁之前。

updated()

DOM更新后,updated的方法即会调用。

onBeforeUnmounted()

在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。

onUnmounted()

卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。

img

十二、实操组件和认识 Less & Scoped

1、什么是less

Less (Leaner Style Sheets 的缩写) 是一门向后兼容的 CSS 扩展语言。这里呈现的是 Less 的官方文档(中文版),包含了 Less 语言以及利用 JavaScript 开发的用于将 Less 样式转换成 CSS 样式的 Less.js 工具。

因为 Less 和 CSS 非常像,因此很容易学习。而且 Less 仅对 CSS 语言增加了少许方便的扩展,这就是 Less 如此易学的原因之一。

官方文档 Less 快速入门 | Less.js 中文文档 - Less 中文网

在vite中使用less

npm install less less-loader -D 安装即可

在style标签注明即可

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

2、样式穿透问题: ::v-deep 和 /deep/ 以及 <<<

为什么需要样式穿透

比如element ui 的原生组件,它有自己的CSS样式,我们一般的外部修改一般不起作用,那么我们如何才能让它不会影响到内容的显示呢?这里我们就通过样式穿透来强制进行修改啦。 我们先在浏览器 F12 找到这个组件的类名。

样式穿透的作用:当我们使用 UI组件库,一般不能对其样式进行修改,但我们通过穿透可以进行修改。

用法

//如果使用的是css,可以用下面这种
外层容器 >>> 组件 {}
​
//但在css预处理器中用上面这种是无法生效的,类似在scss和less中,我们可以用下面这种。
外层容器 /deep/ 组件 {}
​
//但有些时候上面那种也没反应的时候,我们可以试一下下面这种
//我也不清楚为什么,但看比较多资料说,一般用下面这种各个方面会比较好。
外层容器 :: v-deep  组件 {}
​
补充上面:vue3.0的环境下,安装项目时选择了dart-sass,这个不支持/deep/和>>>的写法,只能用::v-deep,选择node-sass就不会(咋感觉还不是根源,不知道::v-deep的出处是哪里来的)

案例

//比如我们遇到需要对element ui 里面 el-avatar这个组件进行修改
el-avatar {
    ::v-deep .iconfont {
        font-size: 24px;
        color: #6b98b7;
        background: #fff;
    }
}

什么是scoped

实现组件的私有化, 当前style属性只属于当前模块.

在DOM结构中可以发现,vue通过在DOM结构以及css样式上加了唯一标记,达到样式私有化,不污染全局的作用

img

编辑切换为居中

添加图片注释,不超过 140 字(可选)

如图,这里class="a"所在元素设置的颜色不会改变class="b"的元素颜色。

十三、父子组件传参

父组件通过v-bind绑定数据,子组件通过defineProps接受传过来的值

案例

1、父传子

如下代码,子组件使用reactive创建一个list,然后使用v-bind:(可以缩写成:的形式)传参给Menu组件

<template>
    <div class="layout">
        <Menu :data="list" title="我是父组件的值"></Menu>
        <div class="layout-right">
            <Header></Header>
            <Content></Content>
        </div>
    </div>
</template>
​
​
<script setup lang="ts">
import Menu from './Menu/index.vue';
import Header from './Header/index.vue';
import Content from './Content/index.vue';
import { reactive } from 'vue'
const list = reactive<number[]>([1, 2, 3])
</script><style lang="less" scoped>
.layout {
    display: flex;
    height: 100%;
    overflow: hidden;
    &-right {
        flex: 1;
        display: flex;
        flex-direction: column;
    }
}
</style>

子组件接收父组件传过来的值

通过 defineProps 进行接收,这里的 defineProps 无需引入,可以直接使用

如果我们使用的TypeScript

可以使用传递字面量类型的纯类型语法做为参数

如 这是TS特有的

<template>
    <div class="menu">
        菜单区域 {{ title }}
        <div>{{ data }}</div>
    </div>
</template>
 
<script setup lang="ts">
defineProps<{
    title:string,
    data:number[]
}>()
</script>

如果不是用TS

<script setup lang="ts">
defineProps({
    title:{
        default:"",
        type:string
    },
    data:Array
})
</script>

TS 特有的默认值方式

withDefaults是个函数也是无须引入开箱即用接受一个props函数第二个参数是一个对象设置默认值

<script setup lang="ts">
type Props = {
    title?: string,
    data?: number[]
}
withDefaults(defineProps<Props>(), { // 如果父组件有传data,则优先显示父组件传递的值,没有则使用这里的默认值
    title: "默认值",
    data: () => [1, 2, 3, 4, 5, 6] 
})
</script>

2、子传父

子组件给父组件传参

是通过defineEmits派发一个事件

<template>
    <div class="menu">
        menu
        <button @click="clickTap">派发emit</button>
    </div>
</template>
​
​
<script setup lang="ts">
import { reactive } from 'vue'
const list = reactive<number[]>([6, 6, 6, 6, 6, 6, 6, 6, 6])
​
const emit = defineEmits(['on-click'])
const clickTap = () => {
    emit('on-click', list)
}
</script>

我们在子组件绑定了一个click 事件 然后通过defineEmits 注册了一个自定义事件

点击click 触发 emit 去调用我们注册的事件 然后传递参数

父组件接受子组件的事件

<template>
    <div class="layout">
        <Menu @on-click="getList" title="我是父组件的值"></Menu>
    </div>
</template>
​
​
<script setup lang="ts">
import Menu from './Menu/index.vue';
import { reactive } from 'vue'const getList = (list:number[]) => {
    console.log(list, '父组件接收子组件传过来的list') // 这里会打印出子组件传过来的list数组
}
</script>

我们从Menu 组件接受子组件派发的事件on-click 后面是我们自己定义的函数名称getList

会把参数返回过来

3、子组件暴露给父组件的内部属性

通过defineExpose

我们从父组件获取子组件实例通过ref

<template>
    <div class="layout">
        <Menu ref="menus" @on-click="getList"></Menu>
    </div>
</template>
<script setup lang="ts">
  const menus = ref(null)
  
  const getList = (list:number[]) => {
    // console.log(list, '父组件接收子组件传过来的list')
    console.log(menu.value);
  }
</script>

然后打印menus.value 发现target中没有任何属性

这时候父组件想要读到子组件的属性可以通过 defineExpose暴露

<template>
    <div class="menu">
        menu
        <button @click="clickTap">派发emit</button>
    </div>
</template>
​
​
<script setup lang="ts">
import { reactive } from 'vue'
const list = reactive<number[]>([6, 6, 6, 6, 6, 6, 6, 6, 6])
​
const emit = defineEmits(['on-click'])
const clickTap = () => {
    emit('on-click', list)
}
​
defineExpose({
    list
})
</script>

现在我们再次点击button按钮就能够打印出子组件暴露给父组件的list了

补充:

这里menu.value获取的是一个proxy对象,我们需要获取其值,有两种方法:

第一种获取target值的方式:

通过vue中的响应式对象课用 toRaw 方法获取原始对象

//第一种获取target值的方式,通过vue中的响应式对象可使用toRaw()方法获取原始对象
import { toRaw } from '@vue/reactivity'
var list = toRaw(store.state.menuList)

第二种获取target值的方式,通过json序列化之后可获取值

JSON.parse(JSON.stringify(menu.value))

十四、使用全局组件、局部组件、递归组件

1、配置全局组件

例如组件使用频率非常高(table,Input,button、等等)这些组件 几乎每个页面都在使用便可以封装成全局组件

案例------我这儿封装一个Card组件想在任何地方去使用

<template>
  <div class="card">
     <div class="card-header">
         <div>标题</div>
         <div>副标题</div>
     </div>
     <div v-if='content' class="card-content">
         {{content}}
     </div>
  </div>
</template>
 
<script setup lang="ts">
type Props = {
    content:string
}
defineProps<Props>()
 
</script>
 
<style scoped lang='less'>
@border:#ccc;
.card{
    width: 300px;
    border: 1px solid @border;
    border-radius: 3px;
    &:hover{
        box-shadow:0 0 10px @border;
    }
 
    &-content{
        padding: 10px;
    }
    &-header{
        display: flex;
        justify-content: space-between;
        padding: 10px;
        border-bottom: 1px solid @border;
    }
}
</style>

img

使用方法

在main.ts 引入我们的组件跟随在createApp(App) 后面 切记不能放到mount 后面这是一个链式调用用

其次调用 component 第一个参数组件名称 第二个参数组件实例

import { createApp } from 'vue'
import App from './App.vue'
import './assets/css/reset.less'
import Card from './components/Card/index.vue' // 这是我们引入的案例组件
createApp(App).component('Card', Card).mount('#app') // 在mount前面注册组件实例

使用方法

直接在其他vue页面 立即使用即可 无需引入

<template>
 <Card></Card>
</template>

2、配置局部组件

<template>
    <div class="layout">
        <Menu ref="menu" @on-click="getList" title="我是父组件的值"></Menu>
        <div class="layout-right">
            <Header></Header>
            <Content></Content>
        </div>
    </div>
</template>
​
​
<script setup lang="ts">
import { reactive, ref } from 'vue'
import Menu from './Menu/index.vue';
import Header from './Header/index.vue';
import Content from './Content/index.vue';
</script>

就是在一个组件内(A) 通过import 去引入别的组件(B) 称之为局部组件

应为 B 组件只能在A组件内使用 所以是局部组件

如果 C 组件想用B组件 就需要 C 组件 import 引入 B 组件

3、配置递归组件

原理跟我们写js递归是一样的 自己调用自己 通过一个条件来结束递归 否则导致内存泄漏

案例:递归树

父组件配置数据结构 数组对象格式 传给子组件

<script setup lang="ts">
type TreeList = {
  name: string;
  icon?: string;
  children?: TreeList[] | [];
};
const data = reactive<TreeList[]>([
  {
    name: "no.1",
    children: [
      {
        name: "no.1-1",
        children: [
          {
            name: "no.1-1-1",
          },
        ],
      },
    ],
  },
  {
    name: "no.2",
    children: [
      {
        name: "no.2-1",
      },
    ],
  },
  {
    name: "no.3",
  }, {
       name: "no.4",
       children: []
  }
]);
</script>

子组件接收值

这里的 type TreeList 和上面的 type TreeList 可以提取出来作为公用

<script setup lang="ts">
type TreeList = {
  name: string;
  icon?: string;
  children?: TreeList[] | [];
};
 
type Props<T> = {
  data?: T[] | [];
};
 
defineProps<Props<TreeList>>();
</script>

template

TreeItem 其实就是当前组件 通过import 把自身又引入了一遍 如果他没有 children 就结束递归

<template>
    <div style="margin-left: 10px;">
        <div @click.stop="clickItem(item)" :key="index" v-for="(item, index) in data">
            {{ item.name }}
            <TreeItem @on-click="clickItem" v-if="item?.children?.length" :data="item.children"></TreeItem>
        </div>
    </div>
</template>

附完整的代码

这里增加了树每个节点的点击事件,返回该节点的值,通过父子组件传值的方式,将子组件点击的该节点值传递给父节点

Tree.vue

<template>
    <div style="margin-left: 10px;">
        <div @click.stop="clickItem(item)" :key="index" v-for="(item, index) in data">
            {{ item.name }}
            <TreeItem @on-click="clickItem" v-if="item?.children?.length" :data="item.children"></TreeItem>
        </div>
    </div>
</template><script setup lang='ts'>
import { reactive } from 'vue'
​
type TreeList = {
    name?: string,
    icon?: string,
    children?: TreeList[] | []
}
​
type Props = {
    data?: TreeList[]
}
defineProps<Props>()
​
​
const emit = defineEmits(['on-click'])
const clickItem = (item: TreeList) => {
    // console.log(item, '子组件的item')
    emit('on-click', item)
}
​
</script>
<script lang="ts">
export default {
    name: 'TreeItem'
}
</script>
<style scoped lang='less'>
</style>

Card.vue

<template>
   <div class="card">
       <div class="card-header">
           <div>主标题</div>
           <div>副标题</div>
       </div>
       <div class="card-content" v-if="content">
           {{content}}
       </div>
   </div>
</template>
​
<script setup lang="ts">
type Props = {
    content?:string
}
​
defineProps<Props>()
</script>
​
​
<style scoped lang='less'>
@border:#ccc;
.card{
    width: 100%;
    border: 1px solid @border;
    border-radius: 3px;
    &:hover{
        box-shadow:0 0 10px @border;
    }
 
    &-content{
        padding: 10px;
    }
    &-header{
        display: flex;
        justify-content: space-between;
        padding: 10px;
        border-bottom: 1px solid @border;
    }
}
</style>

Menu.vue

<template>
    <div class="menu">
        菜单区域
        <Tree @on-click="getItem" :data="data"></Tree>
    </div>
</template>
​
​
<script setup lang="ts">
import Tree from '../../components/Tree/index.vue'
import { reactive } from 'vue'
​
type TreeList = {
    name?: string,
    icon?: string,
    children?: TreeList[] | []
}
​
const getItem = (item: TreeList) => {
    console.log(item, '父组件的item')
}
​
const data = reactive<TreeList[]>([
    {
        name: "no.1",
        children: [
            {
                name: "no.1-1",
                children: [
                    {
                        name: "no.1-1-1",
                    }
                ]
            }
        ]
    },
    {
        name: "no.2",
        children: [
            {
                name: "no.2-1",
            }
        ]
    },
    {
        name: "no.3",
    }, {
        name: "no.4",
        children: []
    }
])
​
</script><style lang="less" scoped>
.menu {
    width: 200px;
    border-right: 1px solid #ccc;
}
</style>

十五、简单实现动态组件

第一步,创建A、B、C三个组件,在另外的组件中引入这三个组件;

第二步,创建Tabs类型数据;

第三步,利用Tabs类型创建data数组,赋予name和comName属性,用来记录各个组件名称;

第四步,声明Com,使用Pick将Tabs中的comName提取出来使用;

第五步,声明current,将data第一个属性值作为默认值,并创建component组件展示;

第六步,添加switchCom点击事件,实现点击tab切换功能。

<template>
    <div class="content">
        <div class="tab">
            <div @click="switchCom(item)" :key="index" v-for="(item, index) in data">{{ item.name }}</div>
        </div>
        <component :is="current.comName"></component>
    </div>
</template>
​
​
<script setup lang="ts">
import { reactive, markRaw } from 'vue';
import A from './A.vue'
import B from './B.vue'
import C from './C.vue'
​
type Tabs = {
    name: string,
    comName: any
}
​
type Com = Pick<Tabs, 'comName'>
​
const data = reactive<Tabs[]>([
    {
        name: "我是A组件",
        comName: markRaw(A)
    },
    {
        name: "我是B组件",
        comName: markRaw(B)
    },
    {
        name: "我是C组件",
        comName: markRaw(C)
    },
])
​
let current = reactive<Com>({
    comName: data[0].comName
})
​
​
const switchCom = (item: Tabs) => {
    current.comName = item.comName
}
​
</script><style lang="less" scoped>
.tab {
    display: flex;
    .active {
        background: skyblue;
        color: #fff;
    }
    div {
        border: 1px solid #ccc;
        margin: 20px 0 0 20px;
        padding: 5px;
        box-sizing: border-box;
    }
    div:hover {
        cursor: pointer;
    }
}
</style>

toRawmarkRaw

Vue3.0给我们提供的这两个方法,toRaw方法是把被reactive或readonly后的Proxy对象转换为原来的target对象,而markRaw则直接让target不能被reactive或readonly。

上面代码使用markRaw是因为组件进行了代理,不需要Tabs再次进行proxy代理了,消除警告。

十六、认识插槽全家桶

插槽就是子组件中的提供给父组件使用的一个占位符,用 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的标签。

1、匿名插槽

  1. 在子组件放置一个插槽
<template>
    <div>
       <slot></slot>
    </div>
</template>

父组件使用插槽

在父组件给这个插槽填充内容

<template>
    <Dialog>
        <template v-slot>
            <div>666</div>
        </template>
    </Dialog>
</template>

2、具名插槽

具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中

<template>
    <div>
        <header class="header">
            <slot name="header"></slot>
        </header>
        <main class="main">
            <slot></slot>
        </main>
        <footer class="footer">
            <slot name="footer"></slot>
        </footer>
    </div>
</template>

父组件使用需对应名称

注:插槽简写 v-slot:header 可以简写成 #header

<template>
    <div class="content">
        <Dialog>
            <template #header>
                <div>
                    插入上面
                </div>
            </template>
            <template v-slot>
                <div>
                    我被插入了中间
                </div>
            </template>
            <template #footer>
                <div>
                    插入下面
                </div>
            </template>
        </Dialog>
    </div>
</template>

img

编辑

添加图片注释,不超过 140 字(可选)

十六、认识插槽全家桶

插槽就是子组件中的提供给父组件使用的一个占位符,用 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的标签。

1、匿名插槽

  1. 在子组件放置一个插槽
<template>
    <div>
       <slot></slot>
    </div>
</template>

父组件使用插槽

在父组件给这个插槽填充内容

<template>
    <Dialog>
        <template v-slot>
            <div>666</div>
        </template>
    </Dialog>
</template>

2、具名插槽

具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中

<template>
    <div>
        <header class="header">
            <slot name="header"></slot>
        </header>
        <main class="main">
            <slot></slot>
        </main>
        <footer class="footer">
            <slot name="footer"></slot>
        </footer>
    </div>
</template>

父组件使用需对应名称

注:插槽简写 v-slot:header 可以简写成 #header

<template>
    <div class="content">
        <Dialog>
            <template #header>
                <div>
                    插入上面
                </div>
            </template>
            <template v-slot>
                <div>
                    我被插入了中间
                </div>
            </template>
            <template #footer>
                <div>
                    插入下面
                </div>
            </template>
        </Dialog>
    </div>
</template>

img

编辑

添加图片注释,不超过 140 字(可选)

3、作用域插槽

在子组件动态绑定参数 派发给父组件的slot去使用

<template>
    <div>
        <header class="header">
            <slot name="header"></slot>
        </header>
        <main class="main">
            <div v-for="(item, index) in data">
                <slot :index="index" :data="item"></slot>
            </div>
        </main>
        <footer class="footer">
            <slot name="footer"></slot>
        </footer>
    </div>
</template><script setup lang='ts'>
import { reactive } from 'vue'
​
type names = {
    name: string,
    age: number
}
​
const data = reactive<names[]>([{
    name: "被插入le",
    age: 201
},
{
    name: "被插入le",
    age: 202
},
{
    name: "被插入le",
    age: 203
},
{
    name: "被插入le",
    age: 204
}])
​
</script>

父组件接收子组件传过来的slot,通过解构的方式进行取值

<template>
    <div class="content">
        <Dialog>
            <template #header>
                <div>插入上面</div>
            </template>
            <template #default="{ data, index }">
                <div>{{ data.name }} --- {{ data.age }} --- {{ index }}</div>
            </template>
            <template #footer>
                <div>插入下面</div>
            </template>
        </Dialog>
    </div>
</template>

4、动态插槽

<template>
    <div class="content">
        <Dialog>
            <template #[name]>
                <div>不知道自己会被插到哪里,根据name的值来确定对应的slot位置</div>
            </template>
        </Dialog>
    </div>
</template>
​
​
<script setup lang="ts">
import { ref } from 'vue';
import Dialog from '../../components/Dialog/index.vue'
let name = ref("footer")
​
</script><style lang="less" scoped>
</style>

const name = ref("footer") 通过这个确定会被插入#footer名的slot插槽内

img

十八、Teleport 传送组件

Teleport Vue 3.0新特性之一。

Teleport 是一种能够将我们的模板渲染至指定DOM节点,不受父级stylev-show等属性影响,但dataprop数据依旧能够共用的技术;类似于 React 的 Portal

主要解决的问题 因为Teleport节点挂载在其他指定的DOM节点下,完全不受父级style样式影响,常用于一些全局控件上面,例如Loading空控件。

使用方法

通过 to 属性 插入指定元素位置 to="body" 便可以将Teleport 内容传送到指定位置

<Teleport to="body">
    <Loading></Loading>
</Teleport>

也可以自定义传送位置 支持 class、 id等 选择器

<div id="app"></div>
<div class="modal"></div>
<Teleport to=".modal">
   <Loading></Loading>
</Teleport>

也可以使用多个

<Teleport to=".modal1">
     <Loading></Loading>
</Teleport>
<Teleport to=".modal2">
     <Loading></Loading>
</Teleport>

\

十九、vue-router 之 keep-alive 缓存组件

内置组件keep-alive

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染

有时候我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就需要用到keep-alive组件。

开启keep-alive 生命周期的变化

  • 初次进入时: onMounted> onActivated
  • 退出后触发 deactivated
  • 再次进入:
  • 只会触发 onActivated
  • 事件挂载的方法等,只执行一次的放在 onMounted中;组件每次进去执行的方法放在 onActivated中
<keep-alive>
  <component>
    <!-- 该组件将被缓存! -->
  </component>
</keep-alive>
<!-- 多个条件判断的子组件 -->
<keep-alive>
  <comp-a v-if="bool === true"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>
<!-- 和 `<transition>` 一起使用 -->
<transition>
  <keep-alive>
    <component :is="view"></component>
  </keep-alive>
</transition>

1、props: include 和 exclude

<keep-alive :include="" :exclude="" :max=""></keep-alive>
<keep-alive include="a">
  <component>
    <!-- name 为 a 的组件将被缓存! -->
  </component>
</keep-alive>可以保留它的状态或避免重新渲染
<keep-alive exclude="a">
  <component>
    <!-- 除了 name 为 a 的组件都将被缓存! -->
  </component>
</keep-alive>可以保留它的状态或避免重新渲染

include & exclude prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示

2、max 最多可以缓存多少组件的实例

切记这个max属性必须大于0才能有效。。至少有一个要被缓存

<keep-alive :max="10">
  <component :is="view"></component>
</keep-alive>

学习资料来源:

1、www.bilibili.com/video/BV1dS…

2、v3.cn.vuejs.org/

3、jishuin.proginn.com/p/763bfbd65…