开始
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
npm init vite@latest
npm i
npm run dev
1.冷服务
2.热更新HMR
3.rollup打包
3.nvm nrm
nvm list
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>
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: "小白" });
2.isRef
<script setup lang="ts">
import { ref, isRef } from "vue"
const man = ref({ name: "小白" });
const woman = { name: '小黑' }
const change = () => {
console.log(isRef(man));
console.log(isRef(woman));
};
</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);
const show = () => {
console.log(ref123.value?.innerHTML);
}
</script>
/*
f12-设置-启用自定义格式化程序
更方便的查看ref/reactive
*/
6.读源码
ref作函数重载
createRef(value,boolean)
判断是不是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
const obj1 = reactive('')
import { reactive } from 'vue'
type M = {
name: string,
age: number
}
const obj = reactive<M>({
name: '小白',
age: 1
})
<button @click="change">?</button>
const change = () => {
obj.name = '小黑'
}
<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)
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: '小黑'
}
}
}
</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
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)
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高级侦听器
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()
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');
})
html:
<div ref="refDiv">{{Str}}</div>
JS:
const Str = ref<string>('A')
const refDiv = ref<HTMLDivElement>()
onBeforeMount(() => {
console.log('onBeforeMount', refDiv.value);
})
onMounted(() => {
console.log('onMounted', refDiv.value);
})
onRenderTracked((e) => {
console.log(e, '挂载时调试');
})
onRenderTriggered((e) => {
console.log(e, '修改时调试');
})
8.2 读源码
9. sass,elementUI
npm i sass -D
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}
npm install element-plus --save
main.ts
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
.use(ElementPlus)
10.传参
10.1 父子传参
父组件:
<Header title="父-标题栏" :arr="headerArr" />
const headerArr = reactive<number[]>([1, 2, 3])
子组件:
{{title}}--{{arr}}
type Props = {
title: string,
arr: number[]
}
defineProps<Props>()
子组件:
type Props = {
t1?: string,
t2?: number[]
}
withDefaults(defineProps<Props>(), {
t1: '我是默认的t1',
t2: () => [4, 5, 6]
})
子组件:
<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);
}
子组件:
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-');
}
11.组件
11.1 全局组件
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' }
console.log(item.age);
console.log(item.age.length);
console.log(item.age?.length);
console.log(item.age?.length ?? []);
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
}
let obj = {
name: '123'
}
let o = markRaw(obj)
console.log('o=', o);
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">
<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:
<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')
let name = ref('Footer')
let name = ref('default')
13.性能优化--异步组件/await/Suspense
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.vue:
<template>
// 遍历时,页面加载失败--此时需要父组件在引用A时使用Suspense
<div v-for="i in list">{{i}}</div>
</template>
<script setup lang='ts'>
import { axios } from './server'
const list = await axios('./data.json')
console.log(list, '1-');
</script>
父组件中:
js:
const A = defineAsyncComponent(() => import("../../components/A/index.vue"))
html:
<Suspense>
<template #default>
<A></A>
</template>
<template #fallback>
Loading... // 在加载时展示的内容
</template>
</Suspense>
14.传送组件Teleport
/*
传送组件不受父组件的style/v-show所影响,data/prop等依旧能共存
如:子组件的定位absolute会受父组件的relative影响
*/
<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:最大值
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
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;
}
npm install animate.css --save
import 'animate.css';
<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"
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"
打乱数组的顺序
过渡动画
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
input
支持多个v-model
.sync已被移除,支持自定义修饰符
单个v-model的用法
父组件:
<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: boolean
}>()
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>("你好")
子组件:
<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)
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>
<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
父组件:
<A a="A" b="B" c="C"></A>
A中:
<script setup lang="ts">
import { useAttrs } from "vue"
const attrs = useAttrs()
console.log(attrs)
</script>
自定义一个Hooks,将一个图片转换为base64格式的
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中:
type Options = {
el: string
}
import { onMounted } from "vue"
export default function (options: Options): Promise<{ baseUrl: string }> {
return new Promise((resolve) => {
onMounted(() => {
let img: HTMLImageElement = document.querySelector(
options.el
) as HTMLImageElement
img.onload = () => {
resolve({
baseUrl: base64(img),
})
}
})
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")
}
})
}