v3项目实战

378 阅读14分钟

开始

1.v3特性简介

1.MVVM模式

2.重写双向数据绑定

v2基于object.defineProperty实现,有缺陷
v3基于proxy和object.defineProperty(obj,prop,desc)实现

3.优化Vdom

v2每次更新diff都是全量对比
v3通过补丁标记patch flag

4.fragment支持多个根节点

5.tree shaking

简单的说,就是保持代码允许结果不变的前提下,去除无用的代码

6.composition API

函数式编程,也叫vue hook

2.构建vite

// 创建项目app
npm init vite@latest

// 进入app目录
npm i

// 启动
npm run dev


// vite的优势
1.冷服务
2.热更新HMR
3.rollup打包

3.nvm nrm

// node版本管理工具


// 常用指令
nvm list // 本机上的node版本
nvm list available // 官方版本
nvm install 18.9.0 // 下载某个版本
nvm use 18.9.0 // 使用

4.npm run dev的启动原理、v3插件volar

node_modules/vite/package.json下
"bin": {
    "vite": "bin/vite.js"
},
启动.bin/vite中的脚本


// 插件
TypeScript Vue Plugin (Volar)
Vue Language Features (Volar)

语法

1.模板语法、指令

// 模板插值语法{{}}
<template>
  {{nm}} // 小白
  {{num?'顶针':'顶假'}}
</template> 
<script setup lang="ts">
const nm: string = '小白'
const num: number = 1
</script>

相关的运算、方法api都是可以的
指令

// v-text   显示文本
<template>
  <div v-text="nm"></div> // 小白
</template> 
<script setup lang="ts">
const nm: string = '小白'
</script>

// v-html   显示富文本,常用于放入标签
<div v-html="msg"></div>

<script setup lang="ts">
const msg:string = '<ul><li>1</li><li>2</li></ul>'
</script>


// v-if/v-else-if/v-else v-show   显示隐藏
<div v-if="flag">1</div>
<div v-show="flag">2</div>

<script setup lang="ts">
const flag: boolean = false
</script>



// v-on @   绑定事件

// 事件修饰符
//.stop阻止冒泡 
//.prevent阻止默认行为:如表单提交,a链接跳转等

<div @click="father">
    <button @click.stop="child">点击</button>
</div>
<script setup lang="ts">
const father = () => {
  console.log('点击-父');
}
const child = () => {
  console.log('点击-子');
}
</script>


// v-bind :   动态绑定
// 可以动态绑定style/class

<div v-bind:style="sty">你好</div>
<div :class="['a','b']">好你</div>

<script setup lang="ts">
type Style = {
  color: string,
  fontSize: string,
}
const sty: Style = {
  color: 'red',
  fontSize: '40px'
}
</script>

<style scoped>
.a {
  color: red;
}

.b {
  font-size: 60px;
}
</style>


// v-for 遍历
<template>
  <div v-for="item in Arr" :key="item">{{item}}</div>
  <div v-for="item in Arr2" :key="item">{{item}}</div>
</template> 
<script setup lang="ts">
const Arr: Array<number> = [1, 2, 3]
const Arr2: Array<any> = [{ name: "1" }, { name: "2" }, { name: "3" }]
</script>



// v-model   双向绑定
<template>
  <input type="text" v-model="msg">
  {{msg}}
</template> 
<script setup lang="ts">
import { ref } from "vue";
const msg = ref('123')
</script>

2.Vdom和diff算法

为什么要Vdom?

通过for循环遍历一个div元素,会得到所有的属性值,操作dom是非常的消耗资源的
vdom就是通过JS的计算性能生成一个ast节点树

diff算法

key值时,会比较两者的长度,走一个patchUnkeyedChildred函数,一样的跳过,不一样的覆盖,多的remove少的mount

有key值时,在patchKeyChildren函数中,有一个clone操作,判断是不是同一节点isSameVNodeType,对比类型和key值进行复用。会从前往后、从后往前的进行对比,多的remove,少的挂载patch函数,第一个参数为Null时作mount操作。特殊情况乱序会尽量作一个复用(数组遍历逐步对比)

3.ref全家桶

1.ref

<template>
  {{man}}
  <button @click="change">+</button>
</template>

<script setup lang='ts'>
import { ref } from 'vue'
let man = ref({ name: '小白' })
const change = () => {
  man.value.name = '小黑'
  console.log(man.value);
}
</script>


// ref也支持类型
import { ref } from 'vue'
type M = {
  name: string
}
let man = ref<M>({ name: '小白' })

// 另一种写法
import type { Ref } from "vue"
type M = {
  name: string
}
let man: Ref<M> = ref({ name: "小白" });

// 也可以不写,让ref自行推断

2.isRef

<script setup lang="ts">
import { ref, isRef } from "vue"

const man = ref({ name: "小白" });
const woman = { name: '小黑' }

const change = () => {
  console.log(isRef(man)); // true
  console.log(isRef(woman)); // false
};
</script>

3.shallowRef , triggerRef

/* 
  和ref的区别:
  ref是深层次的
  shallowRef是浅层次的
*/
<script setup lang="ts">
import { shallowRef, } from "vue"

const man2 = shallowRef({ name: "小白2" });

const change = () => {
  // man2.value.name = "小黑2" // 控制台的值改变了,页面的值没有改变
  man2.value = { name: "小黑2" } // 改变了
  console.log(man2);
};
</script>


/* 
  注:ref和shallowRef千万不能混用!
  原因:triggerRef强制更新收集的依赖,在ref的源码中就调用了triggerRef
*/

4.customRef自定义ref

<template>
  {{msg}}
  <button @click="change">+</button>
</template>

<script setup lang="ts">
import { customRef } from "vue"
function myRef<T>(value: T) {
  return customRef((track, trigger) => {
    return {
      get() {
        track() // 收集依赖
        return value
      },
      set(newV) {
        value = newV;
        trigger() // 更新依赖
      }
    }
  })
}

const msg = myRef<string>('小白')

const change = () => {
  msg.value = '小黑'
  console.log(msg);
}
</script>

5.小知识补充

/* 
    ref可以读取dom
*/
<template>
  <div ref="ref123">123</div>
  <button @click="show">123点击</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const ref123 = ref<HTMLDivElement>()

console.log(ref123.value?.innerHTML); // undefined,此时dom还没有渲染

const show = () => {
  console.log(ref123.value?.innerHTML); // 123
}
</script>


/* 
  f12-设置-启用自定义格式化程序
  更方便的查看ref/reactive
*/

6.读源码

ref作函数重载

createRef(value,boolean) // boolean默认为false
判断是不是ref对象,是的话直接返回,不是的话创建一个ref对象,通过refImpl(rawValue,shallow)类

refImpl类中,通过constructor接受,定义私有属性_value
_value判断是否为isShallow?是的话直接返回,不是的话作toReactive(value)响应式
get value()中作依赖收集trackRefValue
set value()中作依赖更新triggerRefValue

toReactive判断接受的value是不是引用类型,是的话作reacitve响应式,不是的话返回



shallowRef是将createRef(value,boolean)传入true,在判断isShallow的时候直接返回value,不会作reactive,∴它只会返回到value,还是会有依赖收集和依赖更新value

如果同时使用ref和shallowRef的话,ref会调用triggerRef,影响到shallowRef

4.reactive全家桶

4.1.reactive

/* 
  reactive和ref的区别:
    1.ref支持所有类型,reactive支持引用类型
    2.ref取值赋值要用.value,reactive不用
    3.reactive是proxy代理的对象,不能直接赋值,否则会破坏响应式对象
        解决方法:push+解构...
                写在对象中,对象赋值的写法
*/

const obj1 = reactive('') // 类型“string”的参数不能赋给类型“object”的参数。ts(2345)

import { reactive } from 'vue'
type M = {
  name: string,
  age: number
}
const obj = reactive<M>({
  name: '小白',
  age: 1
})


// 2.ref取值赋值要用.value,reactive不用
<button @click="change">?</button>
const change = () => {
  obj.name = '小黑'
}


// 3.reactive是proxy代理的对象,不能直接赋值,否则会破坏响应式对象
<ul v-for="v in list" :key="v">
    <li>{{v}}</li>
</ul>
<button @click="add">+++</button>

let list = reactive<number[]>([])
const add = () => {
  setTimeout(() => {
    let res = [1, 2, 3]
    list = res
    console.log(list); // 控制台上显示了,但是页面没有更新
  }, 400);
}

// 解决方法
list.push(...res)

4.2.readonly

/* 
  注:obj2是只读的,但是会被obj1的改变所影响
*/

<template>
  <button @click="change">???</button>
</template>

<script setup lang="ts">
import { reactive, readonly } from "vue";
const obj1 = reactive({ name: "小白" });
const obj2 = readonly(obj1);

const change = () => {
  obj1.name = "小黑";
  console.log(1, obj1); // 均为{name:"小黑"}
  console.log(2, obj2);
};
</script>

4.3.shallowReactive

<template>
  {{obj}}
  <button @click="change">???</button>
</template>

<script setup lang="ts">
import { shallowReactive } from "vue";

const obj = shallowReactive({
  a: {
    b: {
      c: '小白'
    }
  }
})

const change = () => {
  // obj.a.b.c = '小黑'
  // console.log(obj); // 和shallowRef同理,控制台改变,页面不改变,会被reactive所影响

  obj.a = { // 正确操作
    b: {
      c: '小黑'
    }
  }

}
</script>

4.4.读源码

reactive

判断是否为isReadonly?是的话直接返回,不是的话,进行createReactiveObject函数


createReactiveObject函数中
    传入基本数据类型,报错,value cannot be made reactive,直接返回
    传入的对象已经被proxy代理了,直接返回
    从缓存weekMap(readonly,reactiveMap)中查找,如果被代理过直接返回
    白名单的话直接返回,例如__skip__(markRaw处理的结果),经过getTargetType函数的结果进行判断返回
    以上都没有的话,进行proxy代理

5.to全家桶

5.1 toRef

/* 
  toRef常用于解构,解构出对象的属性,仍然具有响应式
  toRef只能修改响应式的对象的值
*/

{{ man }}
<button @click="change">改变</button>

const man = reactive({ name: "小白", age: 20, sex: "男" });
const manSex = toRef(man, "sex");
const change = () => {
  manSex.value = "nv";
  console.log(man);
};

5.2 toRefs

/*
  toRefs:将对象中的每一个属性都变成ref对象
*/
// 手写源码:本质上就是通过遍历的方式,将对象中的每个属性都作toRef处理
const toRefs = <T extends object>(object: T) => {
  const map: any = {}
  for (let key in object) {
    map[key] = toRef(object, key)
  }
  return map
}


html:
{{ name }}--{{ age }}--{{ sex }}
<button @click="change">???</button>

js:
import { reactive, toRef } from "vue"
const man = reactive({ name: "小白", age: 20, sex: "男" })
const { name, age, sex } = toRefs(man)
const change = () => {
  name.value = "小黑"
  age.value = 10
  sex.value = "nv"
}

5.3 toRaw

/* 
  toRaws:将响应式对象转换为非响应式
  在源码中,通过属性['__v_raw']
*/
const man = reactive({ name: "小白", age: 20, sex: "男" })
const rawMan = toRaw(man)
const change = () => {
  console.log(man); // reactive响应式对象
  console.log(rawMan); // 原始对象
}

5.4 读源码

toRef
核心源码:
var ObjectRefImpl = class {
  constructor(_object, _key, _defaultValue) {
    this._object = _object;
    this._key = _key;
    this._defaultValue = _defaultValue;
    this.__v_isRef = true;
  }
  get value() {
    const val = this._object[this._key];
    return val === void 0 ? this._defaultValue : val;
  }
  set value(newVal) {
    this._object[this._key] = newVal;
  }
};
function toRef(object, key, defaultValue) {
  const val = object[key];
  return isRef(val) ? val : new ObjectRefImpl(object, key, defaultValue);
}


传入toRef的对象和属性,先isRef(val)判断是否为ref对象,不是的话进ObjectRefImpl类

在ObjectRefImpl类中,没有作依赖收集和依赖更新的操作,因为在reactive对象在reactive的源码中已经做过相关操作了,也因此非响应式对象是不能更新的


toRefs
核心源码:
function toRefs(object) {
  if (!isProxy(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`);
  }
  const ret = isArray(object) ? new Array(object.length) : {};
  for (const key in object) {
    ret[key] = toRef(object, key);
  }
  return ret;
}

toRaw
核心源码:
function toRaw(observed) {
  const raw = observed && observed["__v_raw"];
  return raw ? toRaw(raw) : observed;
}

observed["__v_raw"]就是原始对象

6.computed计算属性

6.1computed

html:
<input type="text " v-model="v1">
<input type="text " v-model="v2">
<br>
{{vA}}
{{vB}}

js:
import { ref, computed } from "vue"
const v1 = ref('')
const v2 = ref('')
// 第一种写法
const vA = computed(() => {
  return v1.value + '---' + v2.value
})


// 第二种写法
const vB = computed({
  get() {
    return v1.value + '---' + v2.value
  }
  ,
  set() {
    v1.value + '---' + v2.value
  },
})

6.2实战--购物车,总价用computed实现

7.侦听器watch

7.1 watch

/* 
  watch侦听器
*/
html:
<input type="text" v-model="msg"><input type="text" v-model="msg2">

js:
import { ref, watch } from "vue"
let msg = ref<string>('')
let msg2 = ref<string>('')

watch([msg, msg2], (newV, oldV) => {
  console.log(newV, '新'); // 同时侦听2个值,得到新值的数组
  console.log(oldV, '旧');
})


/* 
  watch侦听器的第三个参数
  {
      deep:true,
      immediate:true // 默认直接监听
  }
  
  深层次的,监听不到
      解决方法:deep深度监听
                  注:deep在v3中存在一个bug,newV和oldV是同样的
                  注:reactive声明的msg3不需要deep深度监听
              第一个参数用函数返回需要监听的值
                  注:需要只监听一个值的时候,同样可以使用
*/

html:
<input type="text" v-model="msg3.nav.bar.num">

js:
watch(msg3, (newV, oldV) => {
  console.log(newV, '新msg3')
  console.log(oldV, '旧msg3')
}
  , {
    deep: true,
  }
)

7.2 watchEffect高级侦听器

/* 
  watchEffect高级侦听器
  使用:直接放进去就可以使用了 

  oninvalidate:侦听之前的回调

  返回值:可以用于停止监听

  其他配置项:
    flush:'post',用于组件后更新后侦听
    onTrigger:断点调试
*/

// 基本使用
// oninvalidate:侦听之前的触发
// 返回值:可以用于停止监听

html:
<input type="text" v-model="msg1" />
<input type="text" v-model="msg2" />
<button @click="stopWatch">停止侦听</button>

js:
import { watchEffect, ref } from "vue"

const msg1 = ref<string>("A")
const msg2 = ref<string>("B")

const stop = watchEffect(() => {
    console.log(msg1.value, msg2.value)
    oninvalidate(() => {
        console.log('侦听之前触发');
  })
})
const stopWatch = () => stop()


// 其他配置项
// flush: 'post',在组件更新后监听
// console.log(inp);本来在页面刷新时会打印null,现在会正确的打印inp节点

// onTrigger用于调试debugger
html:
<input type="text" v-model="msg1" id="inp" />

JS:
import { watchEffect, ref } from "vue"

const msg1 = ref<string>("A")

watchEffect(() => {
  const inp: HTMLInputElement = document.querySelector('#inp') as HTMLInputElement
  console.log(inp);
}, {
  flush: 'post',
  onTrigger(event) {
      debugger
    },
})

8.生命周期

8.1 生命周期

v2 选项式api option-api
beforeCreated,created,
beforeMount,mounted,
beforeUpdate,updated
beforeDestroy,destroyed



v3 组合式api
没有beforeCreated,created,已有setup取代
onBeforeMount,onMount
onBeforeUpdate,onUpdated
onBeforeUnmount,onUnmounted



onBeforeMount(() => {
  console.log('onBeforeMount');
})
onMounted(() => {
  console.log('onMounted');
})
onBeforeUpdate(() => {
  console.log('onBeforeUpdate');
})
onUpdated(() => {
  console.log('onUpdated');
})


/* 
  注:onBeforeMount是读不到节点的,onMounted可以读到节点
*/
html:
<div ref="refDiv">{{Str}}</div>

JS:
const Str = ref<string>('A')
const refDiv = ref<HTMLDivElement>()

onBeforeMount(() => {
  console.log('onBeforeMount', refDiv.value); // undefined

})
onMounted(() => {
  console.log('onMounted', refDiv.value); // <div>A</div>
})


/* 
    onRenderTracked,onRenderTriggered调试
*/
onRenderTracked((e) => {
  console.log(e, '挂载时调试');
})
onRenderTriggered((e) => {
  console.log(e, '修改时调试');
})

8.2 读源码

9. sass,elementUI

/*
    安装sass
*/
npm i sass -D

/*
    css-reset
    vite可以直接放进style.scss
    或者放在asset/css下,import './assets/css/reset.scss'
*/
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}

/*
    安装ElementPlus
*/
npm install element-plus --save

main.ts
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
.use(ElementPlus)

10.传参

10.1 父子传参

/*
    父传子:defineProps
*/
父组件:
<Header title="父-标题栏" :arr="headerArr" />
const headerArr = reactive<number[]>([1, 2, 3])

子组件:
{{title}}--{{arr}}
type Props = {
    title: string,
    arr: number[]
}
defineProps<Props>()



/*
    父传子--默认值withDefaults
*/
子组件:
type Props = {
    t1?: string,
    t2?: number[]
}

withDefaults(defineProps<Props>(), {
    t1: '我是默认的t1',
    t2: () => [4, 5, 6] // 对象数组写成函数返回形式
})



/*
    子传父:defineEmit
*/
子组件:
<button @click="clickSend">子传父</button>

const exam2 = reactive<number[]>([4, 5, 6])
const emit = defineEmits(['ZIsendFU'])
const clickSend = () => {
    emit('ZIsendFU', true, exam2)
}

父组件:
<Header @ZIsendFU="ZIsendFU" />

const ZIsendFU = (boo: boolean, list: number[]) => {
    console.log('子传父实例', boo, list);
}


 
/* 
    defineExpose:派发
*/

子组件:
// 从子组件中暴露出2个属性
const msgA = ref(true)
const msgB = reactive<number[]>([1, 2])
defineExpose({
    msgA, msgB
})

父组件
<Header ref="refHeader" @ZIsendFU="ZIsendFU" />

const refHeader = ref(null) // 注意名称要相同
const ZIsendFU = (boo: boolean, list: number[]) => {
    console.log(refHeader, '1-'); // 可以拿到msgA,msgB
}

11.组件

11.1 全局组件

// 1.创建一个组件Card
// 2.main.ts中全局引入
import Card from './components/Card/index.vue'
.component('Card', Card)

11.2 局部组件

常用的,通过import引入的,就是局部组件

11.3 递归组件

父组件中:
html:
<Tree :A="A" @on-click="click1" />

js:
// 1.引入组件tree,将树结构数据A传给Treee
import Tree from '../../components/Tree/index.vue';
type TreeList = {
    mc: string,
    child?: TreeList[] | [] // child是TreeList型的数组,也可以是空数组,也可以没有
}
const A = reactive<TreeList[]>([
    {
        mc: '1',
        child: [{
            mc: '1-1',
            child: [{
                mc: '1-1-1',
            }]
        }]
    },
    {
        mc: '2',
        child: [{
            mc: '2-1',
        }]
    },
    {
        mc: '3',
    },
])

// emit接受tree的数据
const click1 = (i: TreeList) => {
    console.log(i, 1);
}



/*
    Tree组件
*/
<template>
    <!-- tree -->
    <div v-for="(i,index) in A" :key="index" style="margin-left: 5px;" @click.stop="click(i)"> {{i.mc}}
        <treeItem :A="i.child" v-if="i?.child?.length" @on-click="click" />
    </div>
</template>

<script setup lang='ts'>
import treeItem from "./index.vue";

type TreeList = {
    mc: string,
    child?: TreeList[] | []
}
type Props = {
    A?: TreeList[], // A的类型是一个TreeList数组,也可以没有
}
defineProps<Props>()


const emit = defineEmits(['on-click'])
const click = (i: TreeList) => {
    emit('on-click', i)
}

11.4 动态组件

/*
    扩展:?可选链操作符

*/
const item = { name: 'A' }

// 可以读到age为undefined,读不到.length
console.log(item.age); // undefined
console.log(item.age.length); // Cannot read properties of undefined (reading 'length')

// 可以用?修饰
console.log(item.age?.length); // undefined

// ?? 当前值为undefined或null时,返回后值
// 注:0返回0, false返回false
console.log(item.age?.length ?? []); // []
/*
    动态组件:<component></component>
*/
html:
<div v-for="i in data" :key="i.name" @click="changeTab(i)">
    {{i.name}}
</div>
<component :is="cur.comp"></component>


js:
import { reactive, markRaw } from 'vue'

import A from './A.vue'
import B from './B.vue'
import C from './C.vue'

type Tabs = {
    name: string,
    comp: any,
}

type Comp = Pick<Tabs, 'comp'>

const data = reactive<Tabs[]>([
    { name: 'A组件', comp: markRaw(A) },
    { name: 'B组件', comp: markRaw(B) },
    { name: 'C组件', comp: markRaw(C) }
])

let cur = reactive<Comp>({
    comp: data[0].comp
})

const changeTab = (i: Tabs) => {
    cur.comp = i.comp
}



// markRaw
// 注:在v2中动态引入组件,写成{ name: 'A组件', comp: 'A' }是可以识别的,但在reactive中要写成{ name: 'A组件', comp: A }
// 注2:控制台报错原因:reactive会进行proxy代理,会把组件也进行动态代理,建议不进行代理,推荐使用shallowRef或markRaw

let obj = {
    name: '123'
}
let o = markRaw(obj)
console.log('o=', o);// {name: '123', __v_skip: true}
// 多出了一个v_skip属性,vue会识别为true就跳过proxy代理

12.插槽

/*
    插槽的使用:
        匿名插槽/具名插槽
*/
父组件:
html:
<Dialog>
    <template v-slot:Header>
        具名插槽-Header
    </template>
    <template v-slot>
        匿名插槽
    </template>
    <template v-slot:Footer>
        具名插槽-Footer
    </template>
</Dialog>

js:
import Dialog from '../../components/Dialog/index.vue'


dialog中:
<header class="header">
    <slot name="Header"></slot>
</header>
<main class="main">
    <slot></slot>
</main>
<footer class="footer">
    <slot name="Footer"></slot>
</footer>
/*
    插槽作用域:
        在父组件中拿到子组件的值
*/
dialog中:
 <main class="main">
    <div v-for="(i,index) in man">
        <!-- 直接v-bind就可以将值传出去到content中 -->
        <slot :data="i" :index="index"></slot>
    </div>
</main>

js:
type People = {
    name: string,
    age: number,
}

const man = reactive<People[]>([{
    name: '小白',
    age: 11,
}, {
    name: '小黑',
    age: 12,
}, {
    name: '小花',
    age: 13,
}])


父组件:
html:
<!-- v-slot=接收值 -->
<template v-slot="{data,index}">
    {{data.name}}--{{data.age}}--{{index}}
</template>


/*
    v-slot可以简写为#,如#Header,v-slot=简写为#default=
*/
/*
    动态插槽
*/
<Dialog>
    <template #[name]>
        动态插槽
    </template>
</Dialog>

let name = ref('Header') // header
let name = ref('Footer') // footer
let name = ref('default') // 匿名的

13.性能优化--异步组件/await/Suspense

// mock模拟异步操作,请求本地public/data.json,手写XHR部分
server.ts:
type NameList = {
    name: string
}
export const axios = (url: string): Promise<NameList> => {
    return new Promise((resolve) => {
        let xhr: XMLHttpRequest = new XMLHttpRequest();
        xhr.open('GET', url)
        xhr.onreadystatechange = () => {
            if (xhr.readyState === 4 && xhr.status === 200) {
                setTimeout(() => {
                    resolve(JSON.parse(xhr.responseText));
                }, 2000);
            }
        }
        xhr.send(null)
    })
}


// 在A中使用
A.vue:
<template>
    // 遍历时,页面加载失败--此时需要父组件在引用A时使用Suspense
    <div v-for="i in list">{{i}}</div>
</template>

<script setup lang='ts'>
import { axios } from './server'
// 语法糖:可以直接await调用接口
// ./直接访问public下的内容
const list = await axios('./data.json')
console.log(list, '1-');
</script>


父组件中:
// defineAsyncComponent异步引入
js:
const A = defineAsyncComponent(() => import("../../components/A/index.vue"))


html:
<Suspense>
    <template #default>
        <A></A>
    </template>
    <template #fallback>
        Loading... // 在加载时展示的内容
    </template>
</Suspense>


// 此时打包会多出一个.js文件,异步组件被拆分开了

14.传送组件Teleport

/*
    传送组件不受父组件的style/v-show所影响,data/prop等依旧能共存
        如:子组件的定位absolute会受父组件的relative影响
*/
<!-- to:插入到对应的dom元素,disabled:是否禁用 -->
<Teleport to="body"></Teleport>

15.缓存组件keep-alive

/*
    keep-alive的使用
*/
<keep-alive>
    <A v-if="flag" />
    <B v-else />
</keep-alive>


可用属性:
include:只缓存A
    <keep-alive :include="['A']">
        <A v-if="flag" />
        <B v-else />
    </keep-alive>
exclude:不缓存XX

max:最大值
/*
    keep-alive会多出2个生命周期
*/
onActivated(() => {
    console.log('keep-alive初始化');
})
onDeactivated(() => {
    console.log('keep-alive卸载');
})
/*
    读源码
*/
在setup函数中,return了一个渲染函数,读取keep-alive中的插槽子节点,它只能渲染单个组件,多了会报错的,最后将该节点返回.
声明了一个Map集合的cache和Set集合的keys,在cacheSubTree中判断有pendingCacheKey.,将缓存组件存入.pendingCacheKey是在render函数执行完后赋值的,∴第一次没有缓存,会走销毁操作,后面会走缓存的组件实例

16.transition过渡动画

16.1使用

/*
    基本使用
*/
html部分:
<button @click="flag = !flag">切换</button>
<transition name="box">
    <div class="box" v-show="flag"></div>
</transition>

js部分:
import { ref } from "vue";
const flag = ref<boolean>(true)

css部分:
.box {
    width: 100px;
    height: 100px;
    background-color: red
}

// 从隐藏到显示
// to默认可以不写,∵最后会恢复到box的样式
.box-enter-from {
    width: 0;
    height: 0;
}

.box-enter-active {
    transition: all 1s linear;
}

.box-enter-to {}

// 从显示到隐藏
// from默认可以不写
.box-leave-from {}

.box-leave-active {
    transition: all 2s ease;
}

.box-leave-to {
    width: 0;
    height: 0;
    background-color: gold;
}

16.2结合animate.css

/*
    如果不想使用.box-enter-form/.box-enter-active,想要使用自定义的class类名的话,可以尝试以下写法
*/

html部分:
<button @click="flag = !flag">切换</button>
<transition name="box" enter-from-class="e-from" enter-active-class="e-active">
    <div class="box" v-if="flag"></div>
</transition>

css部分:
.box {
    width: 100px;
    height: 100px;
    background-color: red;
}
.e-from {
    width: 0;
    height: 0;
}
.e-active {
    transition: all 1s linear;
}


/*
    这样写的好处是:可以结合第三方库使用,如animate.css
*/

// 安装animate.css
npm install animate.css --save

// 引入
import 'animate.css';

// 我们使用的animate是4版本的,在class中要加一个前缀animate__animated

// duration:动画时间
// 用法:
//:duration="500"
//duration="{enter:5000,leave:500}"


<template>
    <button @click="flag = !flag">切换</button>
    <transition name="box" :duration="{ enter: 5000, leave: 1000 }" enter-from-class="e-from"
        enter-active-class="animate__animated animate__backInDown">
        <div class="box" v-if="flag"></div>
    </transition>
</template>

<script setup lang="ts">
import { ref } from "vue"
import "animate.css"
const flag = ref<boolean>(true)
</script>
<style lang="scss" scoped>
.box {
    width: 100px;
    height: 100px;
    background-color: red;
}

.e-from {
    width: 0;
    height: 0;
}
</style>

16.3生命周期

生命周期共有8个
  @before-enter="beforeEnter" //对应enter-from
  @enter="enter"//对应enter-active
  @after-enter="afterEnter"//对应enter-to
  @enter-cancelled="enterCancelled"//显示过度打断
  @before-leave="beforeLeave"//对应leave-from
  @leave="leave"//对应enter-active
  @after-leave="afterLeave"//对应leave-to
  @leave-cancelled="leaveCancelled"//离开过度打断
<template>
    <button @click="flag = !flag">切换</button>
    <transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @enter-cancelled="enterCancelled"
        @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" @leave-cancelled="leaveCancelled">
        <div class="box" v-if="flag"></div>
    </transition>
</template>

<script setup lang="ts">
import { ref } from "vue"
import "animate.css"
const flag = ref<boolean>(true)

// 进入动画
const beforeEnter = (el: Element) => {
    console.log("显示之前")
}
const enter = (el: Element, done: Function) => {
    console.log("过渡曲线")
    setTimeout(() => {
        done()
    }, 3000)
}
const afterEnter = (el: Element) => {
    console.log("过渡完成")
}
const enterCancelled = (el: Element) => {
    console.log("过渡被打断")
}
// 离开动画
const beforeLeave = (el: Element) => {
    console.log("离开之前")
}
const leave = (el: Element, done: Function) => {
    console.log("过渡曲线--离开")
    setTimeout(() => {
        done()
    }, 5000)
}
const afterLeave = (el: Element) => {
    console.log("过渡完成--离开")
}
const leaveCancelled = (el: Element) => {
    console.log("过渡被打断--离开")
}
</script>
扩展:GSAP库,一个JS的动画库
    可用于16.6相关的状态数字过渡

16.4 appear

appear:页面加载完成时就执行一次的动画
<template>
    <button @click="flag = !flag">切换</button>
    <transition appear appear-from-class="appear-from" appear-to-class="appear-to" appear-active-class="appear-active">
        <div class="box" v-if="flag"></div>
    </transition>
</template>
<script setup lang="ts">
import { ref } from "vue"
import "animate.css"
const flag = ref<boolean>(true)
</script>
<style lang="scss" scoped>
.box {
    width: 100px;
    height: 100px;
    background-color: red;
}

.appear-from {
    width: 0;
    height: 0;
}

.appear-active {
    transition: all 1s ease;
}

// 可以不写
.appear-to {}
</style>


// 注:可以结合animate使用
<transition appear appear-active-class="animate__animated animate__bounce">
    <div class="box" v-if="flag"></div>
</transition>

16.5 transitionGroup

列表过渡
<template>
    <div class="app">
        <button @click="plus">+</button>
        <button @click="cut">-</button>
        <transition-group leave-active-class="animate__animated   animate__backOutDown"
            enter-active-class="animate__animated  animate__bounce">
            <div v-for="i in list" :key="i">
                {{ i }}
            </div>
        </transition-group>
    </div>
</template>
<script setup lang="ts">
import { reactive } from "vue"
import "animate.css"

const list = reactive<number[]>([1, 2, 3, 4, 5, 6])
const plus = () => {
    list.push(list.length + 1)
}
const cut = () => {
    list.pop()
}
</script>
<style lang="scss" scoped>
.app {
    border: 1px solid;
    display: flex;
    flex-wrap: wrap;
    word-break: break-all;
}
</style>

16.6 9X9列表过渡动画

<template>
    <div>
        <button @click="random">change</button>
        <transition-group tag="div" class="all" move-class="mc">
            <div v-for="i in list" :key="i.id" class="single">
                {{ i.number }}
            </div>
        </transition-group>
    </div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import _ from "lodash"

// 创建一个9x9的数组对象,这个方法可以创建81个undefined的数组值
const list = ref(
    Array.apply(null, { length: 81 } as number[]).map((_, index) => {
        return {
            id: index,
            number: (index % 9) + 1,
        }
    })
)

const random = () => {
    list.value = _.shuffle(list.value)
}
</script>
<style lang="scss" scoped>
.all {
    display: flex;
    flex-wrap: wrap;
    width: calc(22px * 9);

    .single {
        width: 20px;
        border: 1px solid;
        justify-content: center;
    }
}

.mc {
    transition: all 0.3s;
}
</style>
lodash

// 安装
npm i lodash -S
// 引入
import _ from "lodash"
// shuffle
打乱数组的顺序


过渡动画
move-class

17.provide/inject

爷组件
// 这里引入父组件的操作省略
// 如果不想让子组件改值的话,使用readonly
<template>
    <h1>爷爷组件--{{ flag }}</h1>
</template>

<script setup lang="ts">
import { ref, provide, readonly } from "vue"
const flag = ref<number>(1)
provide("flag", readonly(flag))
</script>


父组件
// inject接受的flag可能是number或undefined,
<template>
    <h2>父组件--{{ flag }}</h2>
    <son></son>
</template>
<script setup lang="ts">
import son from "./son.vue"
import { ref, inject } from "vue"
import type { Ref } from "vue"
const flag = inject<Ref<number>>("flag")
</script>

\

子组件
// 子组件改值
// flag?.value = 2 赋值表达式的左侧不能是可选属性访问。ts(2779)可选链无法赋值,∵可能取值number或undefined,需要使用非空断言flag!.value = 2,或者给flag一个默认值const flag = inject<Ref<number>>("flag", ref(1))
<template>
    <h3>子组件--{{ flag }}</h3>
    <button @click="change">change</button>
</template>

<script setup lang="ts">
import { ref, inject } from "vue"
import type { Ref } from "vue"
const flag = inject<Ref<number>>("flag", ref(1))
const change = () => {
    flag.value = 2
}
</script>

18.兄弟组件传参

发布-订阅者模式
手写eventBus

19.Mitt

20.TSX,babel

21.v-model的双向数据流实现

v-model在v3是破坏性更新,本质上是一个语法糖,是props+emit组合而成的


和v2的区别
value-->modelValue
input-->update:modelValue
支持多个v-model
.sync已被移除,支持自定义修饰符

单个v-model的用法


// 父组件控制子组件的显示隐藏
// 子组件控制自身的显示隐藏,出于单向数据流,需要使用defineEmits
父组件:
<template>
  <div style="border: 1px solid">
    父组件--isShow:{{ isShow }}
    <button @click="isShow = !isShow">控制子组件的显示隐藏</button>
  </div>
  <vmodel v-model="isShow"></vmodel>
</template>

<script setup lang="ts">
import vmodel from "./components/v-model.vue"
import { ref } from "vue"

let isShow = ref<boolean>(true)
</script>

子组件:
<template>
    <div style="border: 1px solid red" v-if="modelValue">
        子组件--{{ modelValue }}
        <button @click="close">关闭</button>
    </div>
</template>

<script setup lang="ts">
// defineProps接收modelValue值
defineProps<{
    modelValue: boolean
}>()
// defineEmits改变modelValue值
const emit = defineEmits(["update:modelValue"])
const close = () => {
    emit("update:modelValue", false)
}
</script>

多个v-model的用法

父组件:
<template>
  <div style="border: 1px solid">
    父组件--isShow:{{ isShow }}--{{ text }}
    <button @click="isShow = !isShow">控制子组件的显示隐藏</button>
  </div>
  <vmodel v-model="isShow" v-model:textVal="text"></vmodel>
</template>

<script setup lang="ts">
import vmodel from "./components/v-model.vue"
import { ref } from "vue"

let isShow = ref<boolean>(true)
let text = ref<string>("你好")
</script>

子组件:
<template>
  <div style="border: 1px solid red" v-if="modelValue">
    子组件--{{ modelValue }}
    <input type="text" :value="textVal" @input="change" />
    <button @click="close">关闭</button>
  </div>
</template>

<script setup lang="ts">
defineProps<{
  modelValue: boolean
  textVal: string
}>()

const emit = defineEmits(["update:modelValue", "update:textVal"])
const close = () => {
  emit("update:modelValue", false)
}

const change = (e: Event) => {
  const target = e.target as HTMLInputElement
  emit("update:textVal", target.value)
}
</script>

自定义修饰符

默认的.number/.trim/.lazy可以正常使用


父组件:
// 父组件自定义修饰符.isBT
<template>
  <div style="border: 1px solid">
    父组件
    <button @click="isShow = !isShow">控制子组件的显示隐藏</button>
  </div>
  <vmodel v-model:textVal.isBT="text"></vmodel>
</template>

<script setup lang="ts">
import vmodel from "./components/v-model.vue"
import { ref } from "vue"

let text = ref<string>("你好")


子组件:
// textValModifiers接收
<template>
    <div style="border: 1px solid red">
        子组件
        <input type="text" :value="textVal" @input="change" />
    </div>
</template>

<script setup lang="ts">
const props = defineProps<{
    textVal: string
    textValModifiers?: {
        isBT: boolean,
    }
}>()

const emit = defineEmits(["update:textVal"])

const change = (e: Event) => {
    const target = e.target as HTMLInputElement
    emit("update:textVal", props.textValModifiers?.isBT ? target.value + '变态' : target.value)
}
</script>

22.自定义指令

1.生命周期

created 元素初始化的时候
beforeMount 指令绑定到元素后调用 只调用一次
mounted 元素插入父级dom调用
beforeUpdate 元素被更新之前调用
update 这个周期方法被移除 改用updated
beforeUnmount 在元素被移除前调用
unmounted 指令被移除后调用 只调用一次

常用的mounted
<template>
  <div style="border: 1px solid">
    父组件
    <button @click="isShow = !isShow">切换</button>
    <vmodel v-color="{ backgroundColor: 'yellow', isShow: isShow }"></vmodel>
  </div>
</template>

<script setup lang="ts">
import vmodel from "./components/v-model.vue"
import { Directive, DirectiveBinding, ref } from "vue"

const isShow = ref<boolean>(true)

// v-color 可以加参数,可以加修饰符
// args四个参数
// 第一个参数el即这个元素
// 第二个参数dir, 值 / 修饰符等参数都在这里
// 第三个参数vnode即虚拟dom
// 第四个参数为上一个vnode, 没有则为null

const vColor: Directive = {
  created() {
    console.log("created")
  },
  beforeMount() {
    console.log("beforeMount")
  },
  mounted(el: HTMLElement, dir: DirectiveBinding) {
    console.log("mounted")
    el.style.backgroundColor = dir.value.backgroundColor
  },
  beforeUpdate() {
    console.log("beforeUpdate")
  },
  updated() {
    console.log("updated")
  },
  beforeUnmount() {
    console.log("beforeUnmount")
  },
  unmounted() {
    console.log("unmounted")
  },
}
</script>

2.简写

不关心其他钩子函数,只关注mounted/updated时,可以使用简写
<template>
  <!-- <layout></layout> -->
  <div style="border: 1px solid">
    父组件
    <vmodel v-color="{ backgroundColor: 'yellow' }"></vmodel>
  </div>
</template>

<script setup lang="ts">
import vmodel from "./components/v-model.vue"
import { Directive, DirectiveBinding, ref } from "vue"

const vColor: Directive = (el: HTMLElement, dir: DirectiveBinding) => {
  el.style.backgroundColor = dir.value.backgroundColor
}
</script>

案例:拖拽

<template>
  <div style="border: 1px solid">
    父组件
    <div v-move class="box">
      <div class="header"></div>
      <div>内容</div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { Directive, DirectiveBinding, ref } from "vue"

const vMove: Directive = (el: HTMLElement, dir: DirectiveBinding) => {
  const moveEl = el.firstElementChild as HTMLElement
  const down = (e: MouseEvent) => {
    const X = e.clientX - el.offsetLeft
    const Y = e.clientY - el.offsetTop
    const move = (e: MouseEvent) => {
      el.style.left = e.clientX - X + "px"
      el.style.top = e.clientY - Y + "px"
    }
    document.addEventListener("mousemove", move)
    document.addEventListener("mouseup", () => {
      document.removeEventListener("mousemove", move)
    })
  }
  moveEl.addEventListener("mousedown", down)
}
</script>

<style lang="scss">
html,
body,
#app {
  height: 100%;
  overflow: hidden;

  .box {
    width: 100px;
    height: 100px;
    border: 1px solid;
    position: fixed;
    left: 50%;
    top: 50%;

    .header {
      height: 40px;
      background-color: orange;
    }
  }
}
</style>

23. 自定义hooks

定义:
v3的自定义hooks相当于v2的mixin,将一些相同的逻辑抽离出来

缺点:
1.覆盖问题,mixin的组件比引入mixin的组件要快,所以会有同名data组件被覆盖的问题
2.变量来源不确定问题,mixin的数据可以在引入组件中使用,但是不容易找到源头
自定义hooks就没有这样的问题
vue有自带的hooks,如useAttrs/useSlots


// useAttrs:查看所有属性
父组件:
<A a="A" b="B" c="C"></A>

A中:
<script setup lang="ts">
    import { useAttrs } from "vue"
    const attrs = useAttrs()
    console.log(attrs) // {a: 'A', b: 'B', c: 'C'}
</script>



// useSlots:查看所有插槽
自定义一个Hooks,将一个图片转换为base64格式的

// 1.app中放入img图片(图片放在assets文件夹下)
// 2.src下新建hooks文件夹,新建index.ts
// 3.在app中引入index.ts命名useBase64,将图片的el传入
// 3.1 写index.ts,定义一个Options,接收一个元素
// 3.2 在onMounted中读el
// 3.3 在图片加载完成之后,再使用定义的base64函数处理
// 3.4 定义base64函数,用canvas
// 3.5 返回一个promise
// 3.6 app中then取出使用即可

APP中:
<img id="img" width="200" height="200" src="./assets/123.png"/>

<script setup lang="ts">
import useBase64 from "./hooks/index"
useBase64({
  el: "#img"
}).then(res => {
  console.log(res.baseUrl, 'res');

})
</script>



index.ts中:
// 定义一个Options,接收一个元素
type Options = {
    el: string
}

import { onMounted } from "vue"

export default function (options: Options): Promise<{ baseUrl: string }> {
    // 返回一个promise
    return new Promise((resolve) => {
        onMounted(() => {
            // 在onMounted中读el
            let img: HTMLImageElement = document.querySelector(
                options.el
            ) as HTMLImageElement
            // 在图片加载完成之后,再使用定义的base64函数处理
            img.onload = () => {
                // base64(img)
                resolve({
                    baseUrl: base64(img),
                })
            }
        })
        // 定义base64函数,用canvas
        const base64 = (el: HTMLImageElement) => {
            const canvas = document.createElement("canvas")
            const ctx = canvas.getContext("2d")
            canvas.width = el.width
            canvas.height = el.height
            ctx?.drawImage(el, 0, 0, canvas.width, canvas.height)
            return canvas.toDataURL("image/png")
        }
    })
}