指令
v- 开头都是vue 的指令
v-text 用来显示文本
v-html 用来展示富文本
v-if 用来控制元素的显示隐藏(切换真假DOM) 会重新渲染组件和销毁组件 触发onBeforeUnmount/onUnmounted
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>
.prevent 阻止表单提交
<template>
<form action="/">
<button @click.prevent="submit" type="submit">submit</button>
</form>
</template>
<script setup lang="ts">
const submit = () => {
console.log('child');
}
</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: false,
h: true
};
</script>
<style>
.active {
color: red;
}
.other {
color: blue;
}
.h {
height: 300px;
border: 1px 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-model")
</script>
虚拟Dom和diff算法
介绍虚拟dom
虚拟DOM就是通过js来生成的节点树
为什么要有虚拟DOM?
我们可以通过下面的例子
let div = document.createElement('div')
let str = ''
for (const key in div) {
str += key + ''
}
console.log(str)
可以发现dom上面的属性非常多
所以直接操作dom会非常浪费性能
解决方案就是 我们可以用JS的计算性能来换取操作DOM所消耗的性能,既然我们逃不掉操作DOM这道坎,但是我们可以尽可能少的操作DOM
ref 全家桶
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>
<template>
<div>{{ mes }}</div>
<button @click='change'>修改</button>
</template>
<script setup lang="ts">
import {ref} from "vue";
const mes = ref({name: 'haoran'})
const change = () => {
mes.value.name = '浩然'
console.log(mes)
}
</script>
ifRef
判断是不是一个ref对象
<template>
<button @click='change'>修改</button>
</template>
<script setup lang="ts">
import {ref, isRef} from "vue";
const mes = ref({name: 'haoran'})
const mes2 = {name: 'zhangSan'}
const change = () => {
console.log(isRef(mes)) // true
console.log(isRef(mes2)) // false
}
</script>
shallowRef
ref 是深层响应式
shallowRef 是浅层响应式
点击修改后页面数据并没有发生改变 但是内部已经改变
<template>
<div>ref:{{ mes }}</div>
<hr>
<div>shallowRef:{{ mes2 }}</div>
<button @click='change'>修改</button>
</template>
<script setup lang="ts">
import {ref, isRef, shallowRef} from "vue";
const mes = ref({name: 'haoran'})
const mes2 = shallowRef({name: 'zhangSan'})
const change = () => {
mes2.value.name = '张三'
console.log(mes2)
}
</script>
要直接修改value 页面才会对视图造成更新
const change = () => {
mes2.value = {name: '张三'}
console.log(mes2)
}
ref和shallowRef不能一起写,如果一起写shallowRef会被ref影响 :::tips 因为ref底层更新逻辑的时候,他会调用triggerRef这个函数 :::
const change = () => {
mes.value.name = '浩然' // 如果这个数据在页面渲染了 那么shallowReactive会收到影响
mes2.value.name = '张三'
}
triggerRef
强制更新页面DOM,也可以改变shallowRef里面的值
使用ref时 底层会调用triggerRef,所以会影响shallowRef
const change = () => {
mes2.value.name = '张三'
triggerRef(mes2)
}
customRef
自定义ref
customRef 是个工厂函数要求我们返回一个对象 并且实现 get 和 set 适合去做防抖之类的
跟ref原理差不多
<template>
<div>customRef: {{ obj }}</div>
<hr>
<button @click='change'>修改</button>
</template>
<script setup lang="ts">
import {customRef} from "vue";
// 适合做防抖,连续点击500毫秒内触发一次
function MyRef<T>(value: T) {
return customRef((track, trigger) => {
let timer:any
// 要返回一个对象
return {
get() {
track() // 收集依赖
return value
},
set(newVal) {
clearTimeout(timer)
timer = setTimeout(() => {
console.log('触发了')
value = newVal
trigger() // 触发依赖
},500)
}
}
})
}
const obj = MyRef<string>('浩然')
const change = () => {
obj.value = '我可以被修改'
}
</script>
ref小妙招
是个这玩意 查看起来很不方便 Vue 已经想到 了 帮我们做了格式化
此时观看打印的值就很明了
想要查看原来的对象形式,可以右键Ref<"小满">,选择 show as JavaScript object
使用ref获取dom元素
<template>
<div ref="dom">我是dom</div>
</template>
<script setup lang="ts">
import {ref} from "vue";
const dom = ref()
console.log(dom.value.innerHTML) // 在这里打印会undefined 因为还没有渲染dom结构
const change = () => {
console.log(dom.value.innerHTML) // 我是dom
}
</script>
Reactive 全家桶
reactive
用来绑定复杂的数据,例如:数组、对象
reactive 源码约束了我们的类型
他是不可以绑定普通的数据类型这样是不允许 会给我们报错
import { reactive} from 'vue'
let person = reactive('sad')
要绑定普通的数据类型
可以使用ref
如果使用ref来绑定数组或者对象等复杂数据类型,从源码中可以看出也是去调用reactive
使用reactive修改值无需.value
reatcive 基础用法
import { reactive } from 'vue'
let person = reactive({
name:"小满"
})
person.name = "大满"
数组异步赋值问题
不能给reactive直接覆盖复制,因为reactive是Proxy代理
这样赋值页面是不会变化的因为会脱离响应式
let person = reactive<number[]>([])
setTimeout(() => {
person = [1, 2, 3]
console.log(person);
},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)
readonly
拷贝一份proxy对象将其设置为只读
<template>
<div>
<button @click.prevent="show">查看</button>
</div>
</template>
<script setup lang="ts">
import {ref, reactive, readonly} from "vue";
let obj = reactive({
name: '浩然'
})
const read = readonly(obj)
const show = () => {
read.name = 'test' // 直接对read进行修改会报错
console.log(read)
}
</script>
如果更改obj 那么readonly会受影响
const show = () => {
obj.name = 'test'
console.log(read)
}
shallowReactive
<template>
<div>
<div>{{ obj2 }}</div>
<button @click.prevent="edit">修改</button>
</div>
</template>
<script setup lang="ts">
import { reactive, shallowReactive } from "vue";
let obj = reactive({name: '浩然'})
const obj2:any = shallowReactive({
foo: {
bar: {
num : 1
}
}
})
const edit = () => {
// obj2.foo.bar.num = 2 // 页面数据不会发生改变 非响应式
obj2.foo = {name: '浩然'} // 页面数据发生改变 响应式数据
console.log(obj2)
}
</script>
shallowReactive 与 shallowRef一样,不能同reactive同时写
<template>
<div>
<div>obj: {{obj}}</div>
<div>obj2: {{ obj2 }}</div>
<button @click.prevent="edit">修改</button>
</div>
</template>
<script setup lang="ts">
import {reactive, shallowReactive} from "vue";
let obj = reactive({name: '浩然'})
const obj2: any = shallowReactive({
foo: {
bar: {
num: 1
}
}
})
const edit = () => {
obj.name = 'test' // 如果这个数据在页面渲染了 那么shallowReactive会收到影响
obj2.foo.bar.num = 2 // 页面数据不会发生改变 非响应式
console.log(obj2)
}
</script>
to系列全家桶
toRef
非响应式数据使用
只能修改响应式对象的值 非响应式数据的视图毫无变化(非响应式数据使用toRef没有任何作用)
<template>
<div>
{{ hao }}
</div>
<hr>
<div>toRef: {{like}}</div>
<hr>
<button @click='change'>修改</button>
</template>
<script setup lang="ts">
import {reactive, toRef, toRefs, toRaw} from "vue";
const hao = {name: '浩然', age: 19, like: 'zjl'}
const like = toRef(hao,'like')
const change = () => {
like.value = 'eat'
console.log(like) // eat
}
</script>
响应式数据使用
// 改为响应式数据
const hao = reactive({name: '浩然', age: 19, like: 'zjl'})
用途:单独提取reactive对象中的一个属性进行操作、传递
toRefs
结构响应式数据对象,对每个属性进行toRef提取(结构响应式数据)
<template>
<div>
{{ hao }}
</div>
<hr>
<div>toRefs_name: {{name}}</div>
<hr>
<div>toRefs_age: {{age}}</div>
<hr>
<div>toRefs_like: {{like}}</div>
<hr>
<button @click='change'>修改</button>
</template>
<script setup lang="ts">
import {reactive,toRefs, toRef, toRaw} from "vue";
const hao = reactive({name: '浩然', age: 19, like: 'zjl'})
const {name, age, like} = toRefs(hao) // 结构hao
const change = () => {
like.value = 'JK'
console.log(name)
console.log(age)
console.log(like)
}
</script>
手写源码
<script setup lang="ts">
import {reactive, toRef, toRaw} from "vue";
const hao = reactive({name: '浩然', age: 19, like: 'zjl'})
const toRefs = <T extends object>(object:T) => {
const map:any = {}
for (let key in object) {
map[key] = toRef(object, key)
}
return map
}
const {name, age, like} = toRefs(hao)
const change = () => {
like.value = 'JK'
console.log(name)
console.log(age)
console.log(like)
}
</script>
toRaw
当你不想这个数据作为响应式,可以使用toRaw
<script setup lang="ts">
import {reactive,toRefs, toRef, toRaw} from "vue";
const hao = reactive({name: '浩然', age: 19, like: 'zjl'})
const change = () => {
// 打印响应式数据hao和使用toRaw的响应式数据
console.log(hao, toRaw(hao))
}
</script>
另一种实现方式,与toRaw效果相同
<script setup lang="ts">
import {reactive} from "vue";
const hao = reactive({name: '浩然', age: 19, like: 'zjl'})
const change = () => {
console.log(hao, hao['__v_raw'])
}
</script>
computed计算属性
计算属性就是当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。
写法一:
回调函数的方法
<template>
<input v-model="firstname">
<input v-model="lastname">
<div>{{name}}</div>
</template>
<script setup lang="ts">
import {computed, ref} from "vue";
let firstname = ref('')
let lastname = ref('')
// 当firstname、lastname 发生变化会触发computed
const name = computed(() => {
return firstname.value + '---' + lastname.value
})
</script>
写法二:
对象的方法
const name = computed({
get() {
return firstname.value + '---' + lastname.value
},
set() {
// firstname.value + '---' + lastname.value
}
})
</script>
watch侦听器
watch第一个参数监听源
watch第二个参数回调函数cb(newVal,oldVal)
watch第三个参数一个options配置项是一个对象{
immediate:true //是否立即调用一次
deep:true //是否开启深度监听
}
<template>
<input v-model="message" type="text">
</template>
<script setup lang="ts">
import {ref, watch} from "vue";
let message = ref<string>('')
watch(message, (value, oldValue) => {
console.log(value)
console.log(oldValue)
console.log('-------------')
})
</script>
侦听多个值
<template>
<input v-model="message" type="text">
<input v-model="message2" type="text">
</template>
<script setup lang="ts">
import {ref, watch} from "vue";
let message = ref<string>('')
let message2 = ref<string>('')
watch([message,message2], (value, oldValue) => {
console.log('new',value)
console.log('old',oldValue)
console.log('-------------')
})
</script>
深度侦听
ref数据是无法侦听到深层数据的 ,reactive可以侦听深层数据
let message = ref<object>({
nav: {
bar: {
name: '浩然'
}
}
})
当值有多层时,watch侦听就会失效,需要我们手动开启深度侦听
watch(message, (value, oldValue) => {
console.log('new',value)
console.log('old',oldValue)
console.log('-------------')
},{
deep: true
})
:::tips
但是也会出现一个bug,newVal和oldVal相同
:::
watch默认不会去执行,加上
immediate: true会默认执行
侦听reactive单一值
<template>
<input v-model="message.name1" type="text">
<input v-model="message.name2" type="text">
</template>
<script setup lang="ts">
import {reactive, watch} from "vue";
let message = reactive<object>({
name1: 'haoran', // 监听这个值
name2: '浩然'
})
watch(()=>message.name1, (value, oldValue) => {
console.log('new',value)
console.log('old',oldValue)
console.log('-------------')
})
watchEffect 高级侦听器
立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
如果用到message 就只会监听message 就是用到几个监听几个 而且是非惰性 会默认调用一次
<template>
<input v-model="message" type="text">
<input v-model="message2" type="text">
</template>
<script setup lang="ts">
import { ref, watchEffect } from "vue";
let message = ref('飞机')
let message2 = ref('大大的飞机')
watchEffect(() => {
console.log('message====>',message.value)
})
</script>
清除副作用
就是在触发监听之前会调用一个函数可以处理你的逻辑例如防抖
watchEffect((onCleanup) => {
console.log('message====>',message.value)
onCleanup(() => {
console.log('清除副作用')
})
})
停止侦听器
const stop = watchEffect(() => {})
// 当不再需要此侦听器时:
stop()
更多的配置项和调试
副作用刷新时机 flush 一般使用post
{
flush: 'post'
}
| pre | sync | post | |
|---|---|---|---|
| 更新时机 | 组件更新前执行 | 强制效果始终同步触发 | 组件更新后执行 |
watchEffect(() => {}, {
flush: 'post',
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
组件
每一个.vue 文件呢都可以充当组件来使用
每一个组件都可以复用
引入组件
<template>
<hello-world></hello-world>
</template>
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue' // 组件的地址
</script>
组件的生命周期
简单来说就是一个组件从创建 到 销毁的 过程 成为生命周期
在我们使用Vue3 组合式API 是没有 beforeCreate 和 created 这两个生命周期的
onBeforeMount()
在组件DOM实际渲染安装之前调用。在这一步中,根元素还不存在。
onMounted()
在组件的第一次渲染后调用,该元素现在可用,允许直接DOM访问
onBeforeUpdate()
数据更新时调用,发生在虚拟 DOM 打补丁之前。
updated()
DOM更新后,updated的方法即会调用。
onBeforeUnmount()
在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
onUnmounted()
卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
[
<template>
<hello-world v-if="flag"></hello-world>
<br>
<button @click="flag = !flag">销毁和启用helloWord组件</button>
</template>
<script setup lang="ts">
import {ref} from "vue";
import HelloWorld from './components/HelloWorld.vue'
const flag = ref(true)
</script>
<template>
<h1>{{val}}</h1>
<button @click="change">更改数据</button>
</template>
<script setup lang="ts">
import {ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from "vue";
const val = ref('helloWord组件')
console.log('setup')
// onBeforeMount 挂载之前 获取不到dom
onBeforeMount(() => {
console.log('挂载之前》》'+document.querySelector('h1')) // null
console.log('onBeforeMount 挂载之前')
console.log(' ')
})
// onMounted 挂载完成
onMounted(() => {
console.log('挂载之后》》'+document.querySelector('h1')) // null
console.log('onMounted 挂载完成')
console.log(' ')
})
// onBeforeUpdate 数据更新前
onBeforeUpdate(() => {
console.log('onBeforeUpdate 数据更新前')
console.log(' ')
})
// onUpdated 数据更新完成
onUpdated(() => {
console.log('onUpdated 数据更新完成')
console.log(' ')
})
// onBeforeUnmount 组件卸载前
onBeforeUnmount(() => {
console.log('onBeforeUnmount 组件卸载前')
console.log(' ')
})
// onUnmounted 组件完成
onUnmounted(() => {
console.log('onUnmounted 组件卸载完成')
console.log(' ')
})
const change = () => {
val.value = '我被修改了'
}
</script>
<style scoped>
</style>
初始
更改数据
销毁组件
实操组件和认识less和scoped
less概览
什么是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 -D 安装即可
在style标签注明即可
<style lang="less">
</style>
& 父级拼接
.layout {
height: 100%;
overflow: hidden;
display: flex;
/* .layout_right */
&_right {
background: red;
}
}
什么是scoped
实现组件的私有化, 当前style属性只属于当前模块.
在DOM结构中可以发现,vue通过在DOM结构以及css样式上加了唯一标记,达到样式私有化,不污染全局的作用,
实操组件(自适应)
项目文件目录
/*这里存放全局样式*/
*{
padding: 0;
margin: 0;
}
html,body,#app {
height: 100%;
overflow: hidden;
}
<template>
<Layout></Layout>
</template>
<script setup lang="ts">
import Layout from './layout/index.vue'
</script>
<style lang="less" scoped>
</style>
<template>
<div class="layout">
<Menu></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'
</script>
<style lang="less" scoped>
.layout {
height: 100%;
overflow: hidden;
display: flex;
&_right {
flex: 1;
display: flex;
flex-direction: column;
}
}
</style>
<template>
<div class="menu">
menu
</div>
</template>
<script setup lang="ts">
</script>
<style lang="less" scoped>
.menu {
width: 200px;
border-right: 1px solid black;
}
</style>
<template>
<div class="header">
header
</div>
</template>
<script setup lang="ts">
</script>
<style lang="less" scoped>
.header {
height: 60px;
border-bottom: 1px solid black;
}
</style>
<template>
<div class="content">
<div class="content_items" :key="item" v-for="item in 100">
{{item}}
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="less" scoped>
.content {
flex: 1;
margin: 20px;
border: 1px solid black;
overflow: auto;
&_items {
margin: 5px;
padding: 10px;
border: 1px solid #ccc;
}
}
</style>
最终效果
父子组件传参
父传子
父组件通过v-bind绑定一个数据,然后子组件通过defineProps接受传过来的值,
如以下代码
给Menu组件 传递了一个title 字符串类型是不需要v-bind
<template>
<div class="layout">
<Menu v-bind:data="data" 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 data = reactive<number[]>([1, 2, 3])
</script>
子组件接受值
通过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
defineProps({
title:{
default:"",
type:string
},
data:Array
})
TS 特有的默认值方式
withDefaults是个函数也是无须引入开箱即用接受一个props函数第二个参数是一个对象设置默认值
type Props = {
title?: string,
data?: number[]
}
withDefaults(defineProps<Props>(), {
title: "张三",
data: () => [1, 2, 3]
})
子传父
子组件给父组件传参
是通过defineEmits派发一个事件
<template>
<div class="menu">
<button @click="clickTap">派发给父组件</button>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const list = reactive<number[]>([4, 5, 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"></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 data = reactive<number[]>([1, 2, 3])
const getList = (list: number[]) => {
console.log(list,'父组件接受子组件');
}
</script>
我们从Menu 组件接受子组件派发的事件on-click 后面是我们自己定义的函数名称getList
会把参数返回过来
父获取子组件内部属性
子组件暴露给父组件内部属性
通过defineExpose
我们从父组件获取子组件实例通过ref
<Menu ref="menus"></Menu>
<script setup lang="ts">
const menus = ref(null)
</script>
这时候打印menus.value 可以发现没有任何属性
如果父组件想要读到子组件的属性可以通过 defineExpose暴露
const list = reactive<number[]>([4, 5, 6])
defineExpose({
list
})
配置全局组件
例如组件使用频率非常高(table,Input,button,等)这些组件 几乎每个页面都在使用便可以封装成全局组件
案例------我这儿封装一个Card组件想在任何地方去使用
组件目录
<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">
defineProps<{
content?:string
}>()
</script>
<style scoped lang="less">
@border:1px solid #ccc;
.card {
border: @border;
cursor: pointer;
&:hover {
box-shadow: 0 0 2px 3px #ccc;
}
&-header {
display: flex;
justify-content: space-between;
padding: 10px;
border-bottom: @border;
}
&-content {
padding: 10px;
}
}
</style>
全局注册
在main.ts 引入我们的组件跟随在createApp(App) 后面 切记不能放到mount 后面这是一个链式调用用
其次调用 component 第一个参数组件名称 第二个参数组件实例
import {createApp} from 'vue'
import './style.css'
import App from './App.vue'
import Card from './components/Card/index.vue'
createApp(App).component('Card', Card).mount('#app')
使用方法
直接在其他vue页面 立即使用即可 无需引入
<template>
<Card></Card>
</template>
递归组件
<template>
<div class="menu">
menu
<Tree @on-item="getItem" :data="data"></Tree>
</div>
</template>
<script setup lang="ts">
import {reactive} from "vue";
import Tree from '../../components/Tree/index.vue'
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: 'test'
}
])
const getItem = (item:TreeList) => {
console.log(item)
}
</script>
<style lang="less" scoped>
.menu {
width: 200px;
border-right: 1px solid black;
}
</style>
<template>
<div>
<div @click.stop="clickItem(item)" style="margin-left:16px " :key="index" v-for="(item,index) in data">
{{item.name}}
<TreeItem @on-item="clickItem" v-if="item?.children?.length" :data="item.children"></TreeItem>
</div>
</div>
</template>
<script setup lang="ts">
import TreeItem from './index.vue'
type TreeList = {
name: string,
icon?: string,
children?: TreeList[] | []
}
defineProps<{
data?: TreeList[]
}>()
const emit = defineEmits(['on-item'])
const clickItem = (item:TreeList) => {
// console.log(item)
emit('on-item',item)
}
</script>
<style scoped lang="less">
</style>
<TreeItem @on-item="clickItem" v-if="item?.children?.length" :data="item.children">
不写问号获取不存在的数据length 会报错
动态组件实现tab栏切换
什么是动态组件 就是:让多个组件使用同一个挂载点,并动态切换,这就是动态组件。
在挂载点使用component标签,然后使用v-bind:is=”组件”
用法如下
引入组件
import A from './A.vue'
import B from './B.vue'
<component :is="A"></component>
<template>
<div class="content">
<div class="tab">
<div @click="tabCom(item)" v-for="(item,i) in data" :key="i">
{{item.name}}
</div>
<component :is="current.comName"></component>
</div>
</div>
</template>
<script setup lang="ts">
import A from './A.vue'
import B from './B.vue'
import C from './C.vue'
import {reactive, markRaw} from "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 tabCom = (item:Tabs) => {
current.comName = item.comName
}
</script>
<style lang="less" scoped>
.tab{
display: flex;
.active{
background: skyblue;
color: #fff;
}
div{
padding: 5px 10px;
border: 1px solid #ccc;
margin: 6px 10px;
}
}
</style>
// A
<template>
<div>AAAAAAAAAAA</div>
</template>
// B
<template>
<div>B</div>
</template>
// A
<template>
<div>CCCCCCC</div>
</template>
通过is 切换 A B 组件
使用场景
tab切换 居多
注意事项
1.在Vue2 的时候is 是通过组件名称切换的 在Vue3 setup 是通过组件实例切换的
2.如果你把组件实例放到Reactive Vue会给你一个警告runtime-core.esm-bundler.js:38 [Vue warn]: Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with markRaw or using shallowRef instead of ref.
Component that was made reactive:
这是因为reactive 会进行proxy 代理 而我们组件代理之后毫无用处 节省性能开销 推荐我们使用shallowRef 或者 markRaw 跳过proxy 代理
修改如下
const tab = reactive<Com[]>([
{
name: "A组件",
comName: markRaw(A)
}, {
name: "B组件",
comName: markRaw(B)
}
])
markRaw:
import {reactive, markRaw} from "vue";
let obj = {name: 1}
console.log(markRaw(obj))
插槽slot
插槽就是子组件中的提供给父组件使用的一个占位符,用 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的标签。
匿名插槽
1.在子组件放置一个插槽
<template>
<div>
<slot></slot>
</div>
</template>
2.父组件使用插槽
在父组件给这个插槽填充内容
<Dialog>
<template v-slot>
<div>2132</div>
</template>
</Dialog>
具名插槽
具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中
<div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
父组件使用需对应名称
<Dialog>
<template v-slot:header>
<div>1</div>
</template>
<template v-slot>
<div>2</div>
</template>
<template v-slot:footer>
<div>3</div>
</template>
</Dialog>
插槽简写
<Dialog>
<template #header>
<div>1</div>
</template>
<template #default>
<div>2</div>
</template>
<template #footer>
<div>3</div>
</template>
</Dialog>
作用域插槽
在子组件动态绑定参数 派发给父组件的slot去使用
<div>
<slot name="header"></slot>
<div>
<div v-for="item in 100">
<slot :data="item"></slot>
</div>
</div>
<slot name="footer"></slot>
</div>
通过结构方式取值
<Dialog>
<template #header>
<div>1</div>
</template>
<template #default="{ data }">
<div>{{ data }}</div>
</template>
<template #footer>
<div>3</div>
</template>
</Dialog>
动态插槽
插槽可以是一个变量名
<Dialog>
<template #[name]>
<div>
23
</div>
</template>
</Dialog>
异步组件&代码分包&suspense
异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块 并且减少主包的体积
这时候就可以使用异步组件
顶层 await
在setup语法糖里面 使用方法
<script setup>
const post = await fetch(`/api/post/1`).then(r => r.json())
</script>
父组件引用子组件 通过defineAsyncComponent加载异步配合import 函数模式便可以分包
<script setup lang="ts">
import { reactive, ref, markRaw, toRaw, defineAsyncComponent } from 'vue'
const Dialog = defineAsyncComponent(() => import('../../components/Dialog/index.vue'))
suspense
组件有两个插槽。它们都只接收一个直接子节点。default 插槽里的节点会尽可能展示出来。如果不能,则展示 fallback 插槽里的节点。
<Suspense>
<template #default>
<Dialog>
<template #default>
<div>我在哪儿</div>
</template>
</Dialog>
</template>
<template #fallback>
<div>loading...</div>
</template>
</Suspense>
Teleport传送组件&源码解析
Teleport Vue 3.0新特性之一。
Teleport 是一种能够将我们的模板渲染至指定DOM节点,不受父级style、v-show等属性影响,但data、prop数据依旧能够共用的技术;类似于 React 的 Portal。
主要解决的问题 因为Teleport节点挂载在其他指定的DOM节点下,完全不受父级style样式影响
使用方法
通过to 属性 插入指定元素位置 to="body" 便可以将Teleport 内容传送到指定位置
<Teleport to="body">
<Loading></Loading>
</Teleport>
也可以自定义传送位置 支持 class id等 选择器
<div id="app"></div>
<div class="modal"></div>
<template>
<div class="dialog">
<header class="header">
<div>我是弹框</div>
<el-icon>
<CloseBold />
</el-icon>
</header>
<main class="main">
我是内容12321321321
</main>
<footer class="footer">
<el-button size="small">取消</el-button>
<el-button size="small" type="primary">确定</el-button>
</footer>
</div>
</template>
<script setup lang='ts'>
import { ref, reactive } from 'vue'
</script>
<style lang="less" scoped>
.dialog {
width: 400px;
height: 400px;
background: #141414;
display: flex;
flex-direction: column;
position: absolute;
left: 50%;
top: 50%;
margin-left: -200px;
margin-top: -200px;
.header {
display: flex;
color: #CFD3DC;
border-bottom: 1px solid #636466;
padding: 10px;
justify-content: space-between;
}
.main {
flex: 1;
color: #CFD3DC;
padding: 10px;
}
.footer {
border-top: 1px solid #636466;
padding: 10px;
display: flex;
justify-content: flex-end;
}
}
</style>
多个使用场景
<Teleport to=".modal1">
<Loading></Loading>
</Teleport>
<Teleport to=".modal2">
<Loading></Loading>
</Teleport>
动态控制teleport
使用disabled 设置为 true则 to属性不生效 false 则生效
<teleport :disabled="true" to='body'>
<A></A>
</teleport>
源码解析
在创建teleport 组件的时候会经过patch 方法 然后调用teleport 的process 方法
主要是创建 更新 和删除的逻辑
他通过 resolveTarget 函数 获取了props.to 和 querySelect 获取 了目标元素
然后判断是否有disabled 如果有则 to 属性不生效 否则 挂载新的位置
新节点disabled 为 true 旧节点disabled false 就把子节点移动回容器
如果新节点disabled 为 false 旧节点为true 就把子节点移动到目标元素
遍历teleport 子节点进行unmount方法去移除
keep-alive缓存组件
内置组件keep-alive
有时候我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就需要用到keep-alive组件。
开启keep-alive 生命周期的变化
- 初次进入时: onMounted> onActivated
- 退出后触发 onDeactivated
- 再次进入:
- 只会触发 onActivated
- 事件挂载的方法等,只执行一次的放在 onMounted中;组件每次进去执行的方法放在 onActivated中
<!-- 基本 -->
<keep-alive>
<component :is="view"></component>
</keep-alive>
<!-- 多个条件判断的子组件 -->
<keep-alive>
<comp-a v-if="a > 1"></comp-a>
<comp-b v-else></comp-b>
</keep-alive>
<!-- 和 `<transition>` 一起使用 -->
<transition>
<keep-alive>
<component :is="view"></component>
</keep-alive>
</transition>
include 和 exclude
<keep-alive :include="" :exclude="" :max=""></keep-alive>
include 和 exclude 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:
max
<keep-alive :max="10">
<component :is="view"></component>
</keep-alive>
transition动画组件
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:
- 条件渲染 (使用 v-if)
- 条件展示 (使用 v-show)
- 动态组件
- 组件根节点
自定义 transition 过度效果,你需要对transition组件的name属性自定义。并在css中写入对应的样式
1.过渡的类名
在进入/离开的过渡中,会有 6 个 class 切换。
- #过渡 class
- 在进入/离开的过渡中,会有 6 个 class 切换。
- v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
- v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
- v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
- v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
- v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
- v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。
如下
<button @click='flag = !flag'>切换</button>
<transition name='fade'>
<div v-if='flag' class="box"></div>
</transition>
//开始过度
.fade-enter-from{
background:red;
width:0px;
height:0px;
transform:rotate(360deg)
}
//开始过度了
.fade-enter-active{
transition: all 2.5s linear;
}
//过度完成
.fade-enter-to{
background:yellow;
width:200px;
height:200px;
}
//离开的过度
.fade-leave-from{
width:200px;
height:200px;
transform:rotate(360deg)
}
//离开中过度
.fade-leave-active{
transition: all 1s linear;
}
//离开完成
.fade-leave-to{
width:0px;
height:0px;
}
2.自定义过渡 class 类名
trasnsition props
- enter-from-class=“class”
- enter-active-class=“class”
- enter-to-class=“class”
- leave-from-class=“class”
- leave-active-class=“class”
- leave-to-class=“class”
<transition enter-from-class="e-from" leave-from-class="l-from" name="fade">
<div v-if="flag" class="box"></div>
</transition>
自定义过度时间 单位毫秒
你也可以分别指定进入和离开的持续时间:
// 自定义过度时间 单位毫秒
<transition :duration="1000">...</transition>
// 分别指定进入和离开的持续时间
<transition :duration="{ enter: 500, leave: 800 }">...</transition>
通过自定义class 结合css动画库animate css
安装库 npm install animate.css
引入 import 'animate.css'
使用方法
官方文档 Animate.css | A cross-browser library of CSS animations.
<transition
leave-active-class="animate__animated animate__bounceInLeft"
enter-active-class="animate__animated animate__bounceInRight"
>
<div v-if="flag" class="box"></div>
</transition>
3.transition 生命周期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"//离开过度打断
当只用 JavaScript 过渡的时候,在 enter 和 leave 钩子中必须使用 done 进行回调
结合gsap 动画库使用 GreenSock
const beforeEnter = (el: Element) => {
console.log('进入之前from', el);
}
const Enter = (el: Element,done:Function) => {
console.log('过度曲线');
setTimeout(()=>{
done()
},3000)
}
const AfterEnter = (el: Element) => {
console.log('to');
}
4.appear
通过这个属性可以设置初始节点过度 就是页面加载完成就开始动画 对应三个状态
appear-active-class=""
appear-from-class=""
appear-to-class=""
appear
transition-group过度列表
列表的添加删除
- 单个节点
- 多个节点,每次只渲染一个
那么怎么同时渲染整个列表,比如使用 v-for?在这种场景下,我们会使用 组件。在我们深入例子之前,先了解关于这个组件的几个特点:
- 默认情况下,它不会渲染一个包裹元素,但是你可以通过 tag attribute 指定渲染一个元素。
- 过渡模式不可用,因为我们不再相互切换特有的元素。
- 内部元素总是需要提供唯一的 key attribute 值。
- CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。
<template>
<div class="content">
<el-button @click="add">add</el-button>
<el-button @click="pop">pop</el-button>
<div class="wraps">
<transition-group
enter-active-class="animate__animated animate__rollIn"
leave-active-class="animate__animated animate__rollOut">
<div class="item" :key="item" v-for="item in list">{{item}}</div>
</transition-group>
</div>
</div>
</template>
<script setup lang="ts">
import "animate.css"
import {reactive, ref} from "vue";
const list = reactive<number[]>([1,2,3,4,5])
const add = () => {
list.push(list.length + 1)
}
const pop = () => {
list.pop()
}
</script>
<style lang="less" scoped>
.content {
button {
padding: 8px 14px;
margin-left: 10px;
}
}
.wraps {
display: flex;
flex-wrap: wrap;
word-break: break-all;
border: 1px solid black;
.item {
margin: 20px;
font-size: 40px;
}
}
</style>
依赖注入(Provide&Inject)
Provide&Inject
通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。
官网的解释很让人疑惑,那我翻译下这几句话:
provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。
父组件传递数据
<template>
<div class="App">
<button>我是App</button>
<A></A>
</div>
</template>
<script setup lang='ts'>
import { provide, ref } from 'vue'
import A from './components/A.vue'
let flag = ref<number>(1)
provide('flag', flag)
</script>
<style>
.App {
background: blue;
color: #fff;
}
</style>
子组件接受
<template>
<div style="background-color: green;">
我是B
<button @click="change">change falg</button>
<div>{{ flag }}</div>
</div>
</template>
<script setup lang='ts'>
import { inject, Ref, ref } from 'vue'
const flag = inject<Ref<number>>('flag', ref(1))
const change = () => {
flag.value = 2
}
</script>
<style>
</style>
TIPS 你如果传递普通的值 是不具有响应式的 需要通过ref reactive 添加响应式
使用场景
当父组件有很多数据需要分发给其子代组件的时候, 就可以使用provide和inject。
兄弟组件传参和Bus
1.借助父组件传参
例如父组件为App 子组件为A 和 B他两个是同级的
<template>
<div>
<A @on-click="getFalg"></A>
<B :flag="Flag"></B>
</div>
</template>
<script setup lang='ts'>
import A from './components/A.vue'
import B from './components/B.vue'
import { ref } from 'vue'
let Flag = ref<boolean>(false)
const getFalg = (flag: boolean) => {
Flag.value = flag;
}
</script>
<style>
</style>
A 组件派发事件通过App.vue 接受A组件派发的事件然后在Props 传给B组件 也是可以实现的
缺点就是比较麻烦 ,无法直接通信,只能充当桥梁
2.Event Bus
我们在Vue2 可以使用on监听 emit传递过来的事件
这个原理其实是运用了JS设计模式之发布订阅模式
我写了一个简易版
type BusClass<T> = {
emit: (name: T) => void
on: (name: T, callback: Function) => void
}
type BusParams = string | number | symbol
type List = {
[key: BusParams]: Array<Function>
}
class Bus<T extends BusParams> implements BusClass<T> {
list: List
constructor() {
this.list = {}
}
emit(name: T, ...args: Array<any>) {
let eventName: Array<Function> = this.list[name]
eventName.forEach(ev => {
ev.apply(this, args)
})
}
on(name: T, callback: Function) {
let fn: Array<Function> = this.list[name] || [];
fn.push(callback)
this.list[name] = fn
}
}
export default new Bus<number>()
Mitt
在vue3中off 和 $once 实例方法已被移除,组件实例不再实现事件触发接口,因此大家熟悉的EventBus便无法使用了。然而我们习惯了使用EventBus,对于这种情况我们可以使用Mitt库(其实就是我们视频中讲的发布订阅模式的设计)
安装:
:::tips npm install mitt -S :::
main.ts 初始化
import { createApp } from 'vue'
import App from './App.vue'
import mitt from 'mitt'
const Mit = mitt()
//TypeScript注册
// 由于必须要拓展ComponentCustomProperties类型才能获得类型提示
declare module "vue" {
export interface ComponentCustomProperties {
$Bus: typeof Mit
}
}
const app = createApp(App)
//Vue3挂载全局API
app.config.globalProperties.$Bus = Mit
app.mount('#app')
方法
使用方法通过emit派发, on 方法添加事件,off 方法移除,clear 清空所有
A组件派发(emit)
<template>
<div>
<h1>我是A</h1>
<button @click="emit1">emit1</button>
<button @click="emit2">emit2</button>
</div>
</template>
<script setup lang='ts'>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance();
const emit1 = () => {
instance?.proxy?.$Bus.emit('on-num', 100)
}
const emit2 = () => {
instance?.proxy?.$Bus.emit('*****', 500)
}
</script>
<style>
</style>
B组件监听(on)
<template>
<div>
<h1>我是B</h1>
</div>
</template>
<script setup lang='ts'>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
instance?.proxy?.$Bus.on('on-num', (num) => {
console.log(num,'===========>B')
})
</script>
<style>
</style>
监听所有事件( on("*") )
instance?.proxy?.$Bus.on('*',(type,num)=>{
console.log(type,num,'===========>B')
})
移除监听事件(off)
const Fn = (num: any) => {
console.log(num, '===========>B')
}
instance?.proxy?.$Bus.on('on-num',Fn)//listen
instance?.proxy?.$Bus.off('on-num',Fn)//unListen
清空所有监听(clear)
instance?.proxy?.$Bus.all.clear()
TSX
我们之前呢是使用Template去写我们模板。现在可以扩展另一种风格TSX风格
vue2 的时候就已经支持jsx写法,只不过不是很友好,随着vue3对typescript的支持度,tsx写法越来越被接受
安装插件
npm install @vitejs/plugin-vue-jsx -D
vite.config.ts 配置
使用TSX
tsx支持 v-model 的使用
import { ref } from 'vue'
let v = ref<string>('')
const renderDom = () => {
return (
<>
<input v-model={v.value} type="text" />
<div>
{v.value}
</div>
</>
)
}
export default renderDom
v-show
import { ref } from 'vue'
let flag = ref(false)
const renderDom = () => {
return (
<>
<div v-show={flag.value}>景天</div>
<div v-show={!flag.value}>雪见</div>
</>
)
}
export default renderDom
v-if是不支持的
需要改变风格
import { ref } from 'vue'
let flag = ref(false)
const renderDom = () => {
return (
<>
{
flag.value ? <div>景天</div> : <div>雪见</div>
}
</>
)
}
export default renderDom
v-for也是不支持的
需要使用Map
import { ref } from 'vue'
let arr = [1,2,3,4,5]
const renderDom = () => {
return (
<>
{
arr.map(v=>{
return <div>${v}</div>
})
}
</>
)
}
export default renderDom
v-bind使用
直接赋值就可以
import { ref } from 'vue'
let arr = [1, 2, 3, 4, 5]
const renderDom = () => {
return (
<>
<div data-arr={arr}>1</div>
</>
)
}
export default renderDom
v-on绑定事件 所有的事件都按照react风格来
- 所有事件有on开头
- 所有事件名称首字母大写
const renderDom = () => {
return (
<>
<button onClick={clickTap}>点击</button>
</>
)
}
const clickTap = () => {
console.log('click');
}
export default renderDom
Props 接受值
import { ref } from 'vue'
type Props = {
title:string
}
const renderDom = (props:Props) => {
return (
<>
<div>{props.title}</div>
<button onClick={clickTap}>点击</button>
</>
)
}
const clickTap = () => {
console.log('click');
}
export default renderDom
Emit派发
type Props = {
title: string
}
const renderDom = (props: Props,content:any) => {
return (
<>
<div>{props.title}</div>
<button onClick={clickTap.bind(this,content)}>点击</button>
</>
)
}
const clickTap = (ctx:any) => {
ctx.emit('on-click',1)
}
深入v-model
v-model
TIps 在Vue3 v-model 是破坏性更新的 v-model在组件里面也是很重要的
v-model 其实是一个语法糖 通过props 和 emit组合而成的
1.默认值的改变
- prop:value -> modelValue;
- 事件:input -> update:modelValue;
- v-bind 的 .sync 修饰符和组件的 model 选项已移除
- 新增 支持多个v-model
- 新增 支持自定义 修饰符
案例 子组件
<template>
<div v-if='propData.modelValue ' class="dialog">
<div class="dialog-header">
<div>标题</div><div @click="close">x</div>
</div>
<div class="dialog-content">
内容
</div>
</div>
</template>
<script setup lang='ts'>
type Props = {
modelValue:boolean
}
const propData = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const close = () => {
emit('update:modelValue',false)
}
</script>
<style lang='less'>
.dialog{
width: 300px;
height: 300px;
border: 1px solid #ccc;
position: fixed;
left:50%;
top:50%;
transform: translate(-50%,-50%);
&-header{
border-bottom: 1px solid #ccc;
display: flex;
justify-content: space-between;
padding: 10px;
}
&-content{
padding: 10px;
}
}
</style>
父组件
<template>
<button @click="show = !show">开关{{show}}</button>
<Dialog v-model="show"></Dialog>
</template>
<script setup lang='ts'>
import Dialog from "./components/Dialog/index.vue";
import {ref} from 'vue'
const show = ref(false)
</script>
<style>
</style>
绑定多个案例
子组件
<template>
<div v-if='modelValue ' class="dialog">
<div class="dialog-header">
<div>标题---{{title}}</div><div @click="close">x</div>
</div>
<div class="dialog-content">
内容
</div>
</div>
</template>
<script setup lang='ts'>
type Props = {
modelValue:boolean,
title:string
}
const propData = defineProps<Props>()
const emit = defineEmits(['update:modelValue','update:title'])
const close = () => {
emit('update:modelValue',false)
emit('update:title','我要改变')
}
</script>
<style lang='less'>
.dialog{
width: 300px;
height: 300px;
border: 1px solid #ccc;
position: fixed;
left:50%;
top:50%;
transform: translate(-50%,-50%);
&-header{
border-bottom: 1px solid #ccc;
display: flex;
justify-content: space-between;
padding: 10px;
}
&-content{
padding: 10px;
}
}
</style>
父组件
<template>
<button @click="show = !show">开关{{show}} ----- {{title}}</button>
<Dialog v-model:title='title' v-model="show"></Dialog>
</template>
<script setup lang='ts'>
import Dialog from "./components/Dialog/index.vue";
import {ref} from 'vue'
const show = ref(false)
const title = ref('我是标题')
</script>
<style>
</style>
自定义修饰符
添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件。在下面的示例中,我们创建了一个组件,其中包含默认为空对象的 modelModifiers prop
<script setup lang='ts'>
type Props = {
modelValue: boolean,
title?: string,
modelModifiers?: {
default: () => {}
}
titleModifiers?: {
default: () => {}
}
}
const propData = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'update:title'])
const close = () => {
console.log(propData.modelModifiers);
emit('update:modelValue', false)
emit('update:title', '我要改变')
}
自定义指令directive
directive-自定义指令(属于破坏性更新)
Vue中有v-if,v-for,v-bind,v-show,v-model等等一系列方便快捷的指令 今天一起来了解一下vue里提供的自定义指令
1.Vue3指令的钩子函数
- created 元素初始化的时候
- beforeMount 指令绑定到元素后调用 只调用一次
- mounted 元素插入父级dom调用
- beforeUpdate 元素被更新之前调用
- update 这个周期方法被移除 改用updated
- beforeUnmount 在元素被移除前调用
- unmounted 指令被移除后调用 只调用一次
Vue2 指令 bind inserted update componentUpdated unbind
2.在setup内定义局部指令
但这里有一个需要注意的限制:必须以 vNameOfDirective 的形式来命名本地自定义指令,以使得它们可以直接在模板中使用。
js代码
<script setup lang="ts">
import A from './components/A.vue'
import {Directive, DirectiveBinding , ref} from "vue";
const flag = ref<boolean>(true)
const vMove:Directive = {
created() {
console.log('created------')
},
beforeMount() {
console.log('beforeMount-----')
},
mounted(el:HTMLElement, dir:DirectiveBinding) {
console.log('mounted-----')
el.style.background = dir.value.background
},
beforeUpdate(){
console.log('beforeUpdate-----')
},
updated() {
console.log('updated--------')
},
beforeUnmount() {
console.log('beforeUnmount------')
},
unmounted() {
console.log('unmounted-----')
}
}
created------ beforeMount----- mounted-----
<template>
<div class="App">
<el-button>toggle</el-button>
<A v-move="{background:'red'}"></A>
</div>
</template>
beforeUnmount------ unmounted-----
<template>
<div class="App">
<el-button @click="flag = !flag">toggle</el-button>
<A v-if="flag" v-move="{background:'red'}"></A>
</div>
</template>
beforeUpdate----- updated--------
<template>
<div class="App">
<el-button @click="flag = !flag">toggle</el-button>
<A v-move="{background:'red',flag: flag}"></A>
</div>
</template>
3.生命周期钩子参数详解
第一个 el 当前绑定的DOM 元素
第二个 binding
- instance:使用指令的组件实例。
- value:传递给指令的值。例如,在 v-my-directive="1 + 1" 中,该值为 2。
- oldValue:先前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否有更改都可用。
- arg:传递给指令的参数(如果有的话)。例如在 v-my-directive:foo 中,arg 为 "foo"。
- modifiers:包含修饰符(如果有的话) 的对象。例如在 v-my-directive.foo.bar 中,修饰符对象为 {foo: true,bar: true}。
- dir:一个对象,在注册指令时作为参数传递。例如,在以下指令中
第三个 当前元素的虚拟DOM 也就是Vnode
第四个 prevNode 上一个虚拟节点,仅在 beforeUpdate 和 updated 钩子中可用
4.函数简写
你可能想在 mounted 和 updated 时触发相同行为,而不关心其他的钩子函数。那么你可以通过将这个函数模式实现
<template>
<div class="App">
<input type="text" v-model="value">
<A v-move="{background: value}"></A>
</div>
</template>
<script setup lang="ts">
import A from './components/A.vue'
import {Directive, DirectiveBinding , ref} from "vue";
const value = ref<string>('')
const vMove:Directive = (el:HTMLElement,binding:DirectiveBinding) => {
el.style.background = binding.value.background
}
</script>
案例自定义拖拽指令
<template>
<div v-move class="box">
<div class="header"></div>
<div>content</div>
</div>
</template>
<script setup lang="ts">
import {Directive, DirectiveBinding} from "vue";
const vMove: Directive = (el: HTMLElement, binding: DirectiveBinding) => {
let moveEl: HTMLElement = el.firstElementChild as HTMLDivElement
const mouseDown = (e:MouseEvent) => {
let X = e.clientX - el.offsetLeft
let 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', mouseDown)
}
</script>
<style lang="less" scoped>
.box {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
border: 1px solid #ccc;
user-select: none;
.header {
height: 20px;
background: black;
cursor: move;
}
}
</style>
自定义Hooks
Vue3 自定义Hook
主要用来处理复用代码逻辑的一些封装
这个在vue2 就已经有一个东西是Mixins
mixins就是将这些多个相同的逻辑抽离出来,各个组件只需要引入mixins,就能实现一次写代码,多组件受益的效果。
弊端就是 会涉及到覆盖的问题
组件的data、methods、filters会覆盖mixins里的同名data、methods、filters。
第二点就是 变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护。
Vue3 的自定义的hook
Vue3 的 hook函数 相当于 vue2 的 mixin, 不同在与 hooks 是函数 Vue3 的 hook函数 可以帮助我们提高代码的复用性, 让我们能在不同的组件中都利用 hooks 函数 Vue3 hook 库Get Started | VueUse
案例
import {onMounted} from "vue";
type Options = {
el: string
}
export default function (options: Options):Promise<{baseUrl:string}> {
return new Promise((resolve) => {
onMounted(() => {
let img: HTMLImageElement = document.querySelector(options.el) as HTMLImageElement
console.log(img)
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')
}
})
}
使用定义的hook
<template>
<div>
<img id="img" src="src/assets/vue.svg" alt="" srcset="">
<A a="123" title="456"></A>
</div>
</template>
<script setup lang="ts">
import A from './components/A.vue'
import userBase64 from './hooks'
userBase64({
el: '#img'
}).then(data => {
console.log(data.baseUrl)
})
</script>
定义全局函数和变量
globalProperties
由于Vue3 没有Prototype 属性 使用 app.config.globalProperties 代替 然后去定义变量和函数
Vue2
// 之前 (Vue 2.x)
Vue.prototype.$http = () => {}
Vue3
// 之后 (Vue 3.x)
const app = createApp({})
app.config.globalProperties.$http = () => {}
过滤器
在Vue3 移除了
我们正好可以使用全局函数代替Filters
案例: main.ts
type Filter = {
format: <T>(str:T) => string
}
declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$filters: Filter,
$uname: string
}
}
const app = createApp(App)
app.config.globalProperties.$filters = {
format<T>(str: T): string {
return `真·${str}`
}
}
app.config.globalProperties.$uname = '帅哥'
app.mount('#app')
app.vue
<div>
{{$filters.format('我是你爹')}}
{{$uname}}
</div>
编写vue3插件
插件
插件是自包含的代码,通常向 Vue 添加全局级功能。你如果是一个对象需要有install方法Vue会帮你自动注入到install 方法 你如果是function 就直接当install 方法去使用
使用插件
在使用 createApp() 初始化 Vue 应用程序后,你可以通过调用 use() 方法将插件添加到你的应用程序中。
实现一个Loading
Loading.Vue
<template>
<div v-if="isShow" class="loading">
<div class="loading-content">Loading...</div>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
const isShow = ref(false)//定位loading 的开关
const show = () => {
isShow.value = true
}
const hide = () => {
isShow.value = false
}
//对外暴露 当前组件的属性和方法
defineExpose({
isShow,
show,
hide
})
</script>
<style scoped lang="less">
.loading {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
&-content {
font-size: 30px;
color: #fff;
}
}
</style>
Loading.ts
import { createVNode, render, VNode, App } from 'vue';
import Loading from './index.vue'
export default {
install(app: App) {
//createVNode vue提供的底层方法 可以给我们组件创建一个虚拟DOM 也就是Vnode
const vnode: VNode = createVNode(Loading)
//render 把我们的Vnode 生成真实DOM 并且挂载到指定节点
render(vnode, document.body)
// Vue 提供的全局配置 可以自定义
app.config.globalProperties.$loading = {
show: () => vnode.component?.exposed?.show(),
hide: () => vnode.component?.exposed?.hide()
}
}
}
Main.ts
import Loading from './components/loading'
let app = createApp(App)
app.use(Loading)
type Lod = {
show: () => void,
hide: () => void
}
//编写ts loading 声明文件放置报错 和 智能提示
declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$loading: Lod
}
}
app.mount('#app')
了解UI库ElementUI,AntDesigin等
vue作为一款深受广大群众以及尤大崇拜者的喜欢,特此列出在github上开源的vue优秀的UI组件库供大家参考
这几套框架主要用于后台管理系统和移动端的制作,方便开发者快速开发
Element UI Plus
官网:element-plus.gitee.io/zh-CN/
安装方法
# NPM
$ npm install element-plus --save
# Yarn
$ yarn add element-plus
# pnpm
$ pnpm install element-plus
main ts引入
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
volar插件支持
{
"compilerOptions": {
// ...
"types": ["element-plus/global"]
}
}
Ant Design Vue
安装
使用 npm 或 yarn 安装
$ npm install ant-design-vue --save
yarn add ant-design-vue
示例
import { DatePicker } from 'ant-design-vue';
app.use(DatePicker);
引入样式:
import 'ant-design-vue/dist/antd.css'; // or 'ant-design-vue/dist/antd.less'
Iview
基于 Vue.js 3 的企业级 UI 组件库和前端解决方案,
安装:
npm install view-ui-plus --save
引入:
import { createApp } from 'vue'
import ViewUIPlus from 'view-ui-plus'
import App from './App.vue'
import router from './router'
import store from './store'
import 'view-ui-plus/dist/styles/viewuiplus.css'
const app = createApp(App)
app.use(store)
.use(router)
.use(ViewUIPlus)
.mount('#app')
Vant 移动端
官网:vant-contrib.gitee.io/vant/v4/#/z…
安装
npm i vant -S
使用
import Vant from 'vant'
import 'vant/lib/index.css';
createApp(App).use(vant).$mount('#app)
Scoped和样式穿透deep()
主要是用于修改很多vue常用的组件库(element, vant, AntDesigin),虽然配好了样式但是还是需要更改其他的样式
就需要用到样式穿透
scoped的原理
vue中的scoped 通过在DOM结构以及css样式上加唯一不重复的标记:data-v-hash的方式,以保证唯一(而这个工作是由过PostCSS转译实现的),达到样式私有化模块化的目的。
总结一下scoped三条渲染规则:
- 给HTML的DOM节点加一个不重复data属性(形如:data-v-123)来表示他的唯一性
- 在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器(如[data-v-123])来私有化样式
- 如果组件内部包含有其他组件,只会给其他组件的最外层标签加上当前组件的data属性
PostCSS会给一个组件中的所有dom添加了一个独一无二的动态属性data-v-xxxx,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom, 从而达到了'样式模块化'的效果.
案例修改Element ui Input样式
发现没有生效
<template>
<div class="bg">
<el-input class="ipt"></el-input>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="less" scoped>
.bg {
background-color: black;
height: 100vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
.ipt {
width: 200px;
.el-input__inner {
background-color: red;
}
}
}
</style>
如果不写Scoped 就没问题
原因就是Scoped 搞的鬼 他在进行PostCss转化的时候把元素选择器默认放在了最后
Vue 提供了样式穿透:deep() 他的作用就是用来改变 属性选择器的位置
css Style完整新特性
插槽选择器:slotted
父组件
<template>
<div>
<A>
<div class="a">私人定制div</div>
</A>
</div>
</template>
<script setup lang="ts">
import A from "./components/A.vue"
</script>
子组件
<template>
<div>
我是插槽
<slot></slot>
</div>
</template>
在子组件写css,改变父组件class=a的样式
<style scoped>
.a{
color:red
}
</style>
可以发现并无效果
默认情况下,作用域样式不会影响到 <slot/> 渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。
解决方案 slotted
<style scoped lang="less">
:slotted(.a) {
color: red;
}
</style>
全局选择器:global
在之前我们想加入全局 样式 通常都是新建一个style 标签 不加scoped 现在有更优雅的解决方案
<style>
div{
color:red
}
</style>
<style lang="less" scoped>
</style>
<style lang="less" scoped>
:global(div){
color:red
}
</style>
效果等同于上面
动态css
单文件组件的 <style> 标签可以通过 v-bind 这一 CSS 函数将 CSS 的值关联到动态的组件状态上:
<template>
<div class="div">
动态css
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue'
const style = ref('red')
</script>
<style lang="less" scoped>
.div {
color: v-bind(style);
}
</style>
如果是对象 v-bind 请加引号
const style = ref({
color: 'red'
})
.div {
color: v-bind('style.color');
}
css module
标签会被编译为 [CSS Modules](https://github.com/css-modules/css-modules) 并且将生成的 CSS 类作为 $style对象的键暴露给组件 <template> <div :class="$style.red"> 小满是个弟弟 </div> </template> ```vue <template> <div :class="$style.red"> 小满是个弟弟 </div> </template> <style module> .red { color: red; font-size: 20px; }
自定义注入名称(多个可以用数组)
你可以通过给 `module` attribute 一个值来自定义注入的类对象的 property 键
```vue
<template>
<div :class="[zs.red,zs.border]">
小满是个弟弟
</div>
</template>
<style module="zs">
.red {
color: red;
font-size: 20px;
}
.border{
border: 1px solid #ccc;
}
</style>
与组合式 API 一同使用
注入的类可以通过 useCssModule API 在 setup() 和
<template>
<div :class="[zs.red,zs.border]">
小满是个弟弟
</div>
</template>
<script setup lang="ts">
import { useCssModule } from 'vue'
const css = useCssModule('zs')
</script>
<style module="zs">
.red {
color: red;
font-size: 20px;
}
.border{
border: 1px solid #ccc;
}
</style>
Vue3集成Tailwind CSS
Tailwind CSS 是一个由js编写的CSS框架他是基于postCss 去解析的
官网地址Tailwind CSS 中文文档 - 无需离开您的HTML,即可快速建立现代网站。
对于PostCSS的插件使用,我们再使用的过程中一般都需要如下步骤:
- postCss 功能介绍PostCSS 配置文件 postcss.config.js,新增 tailwindcss 插件。
- TaiWindCss插件需要一份配置文件,比如:tailwind.config.js。
PostCSS - 是一个用 JavaScript 工具和插件来转换 CSS 代码的工具 | PostCSS 中文网
postCss 功能介绍
1.增强代码的可读性 (利用从 Can I Use 网站获取的数据为 CSS 规则添加特定厂商的前缀。 Autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。)
2.将未来的 CSS 特性带到今天!(PostCSS Preset Env 帮你将最新的 CSS 语法转换成大多数浏览器都能理解的语法,并根据你的目标浏览器或运行时环境来确定你需要的 polyfills,此功能基于 cssdb 实现。)
3.终结全局 CSS(CSS 模块 能让你你永远不用担心命名太大众化而造成冲突,只要用最有意义的名字就行了。)
. 4.避免 CSS 代码中的错误(通过使用 stylelint 强化一致性约束并避免样式表中的错误。stylelint 是一个现代化 CSS 代码检查工具。它支持最新的 CSS 语法,也包括类似 CSS 的语法,例如 SCSS 。)
postCss 处理 tailWind Css 大致流程
- 将CSS解析成抽象语法树(AST树)
- 读取插件配置,根据配置文件,生成新的抽象语法树
- 将AST树”传递”给一系列数据转换操作处理(变量数据循环生成,切套类名循环等)
- 清除一系列操作留下的数据痕迹
- 将处理完毕的AST树重新转换成字符串
安装
1.初始化项目
npm init vue@latest
2.安装 Tailwind 以及其它依赖项
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
3.生成配置文件
npx tailwindcss init -p
4.修改配置文件 tailwind.config.js
2.6版本
module.exports = {
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
3.0版本
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
5.创建一个index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
在main.ts 引入
<template>
<div class="w-screen h-screen bg-red-600 flex justify-center items-center text-5xl" >
hello tailwind
</div>
</template>
Evnet Loop 和 nextTick&源码解析
JS 执行机制
在我们学js 的时候都知道js 是单线程的如果是多线程的话会引发一个问题在同一时间同时操作DOM 一个增加一个删除JS就不知道到底要干嘛了,所以这个语言是单线程的但是随着HTML5到来js也支持了多线程webWorker 但是也是不允许操作DOM
单线程就意味着所有的任务都需要排队,后面的任务需要等前面的任务执行完才能执行,如果前面的任务耗时过长,后面的任务就需要一直等,一些从用户角度上不需要等待的任务就会一直等待,这个从体验角度上来讲是不可接受的,所以JS中就出现了异步的概念。
同步任务
代码从上到下按顺序执行
异步任务
宏任务
script(整体代码)、setTimeout、setInterval、UI交互事件、postMessage、Ajax
微任务
Promise.then catch finally、MutaionObserver、process.nextTick(Node.js 环境)
<template>
<div>
<input v-model="message" type="text">
<div ref="div">{{message}}</div>
<button @click="change">change div</button>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
const message = ref('浩然')
const div = ref<HTMLElement>()
const change = () => {
message.value = '浩然最帅'
console.log(div.value?.innerHTML)
}
</script>
打印出来的是浩然,并不是新的数据
运行机制
所有的同步任务都是在主进程执行的形成一个执行栈,主线程之外,还存在一个"任务队列",异步任务执行队列中先执行宏任务,然后清空当次宏任务中的所有微任务,然后进行下一个tick如此形成循环
nextTick 就是创建一个异步任务,那么它自然要等到同步任务执行完成后才执行。
<script setup lang='ts'>
import { ref,nextTick } from 'vue';
const message = ref('浩然')
const div = ref<HTMLElement>()
const change = async () => {
message.value = '浩然最帅'
await nextTick()
console.log(div.value?.innerHTML)
}
</script>
源码地址 core\packages\runtime-core\src\scheduler.ts
const resolvedPromise: Promise<any> = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null
export function nextTick<T = void>(
this: T,
fn?: (this: T) => void
): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
nextTick 接受一个参数fn(函数)定义了一个变量P 这个P最终返回都是Promise,最后是return 如果传了fn 就使用变量P.then执行一个微任务去执行fn函数,then里面this 如果有值就调用bind改变this指向返回新的函数,否则直接调用fn,如果没传fn,就返回一个promise,最终结果都会返回一个promise
在我们之前讲过的ref源码中有一段 triggerRefValue 他会去调用 triggerEffects
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
if (ref.dep) {
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
//当响应式对象发生改变后,执行 effect 如果有 scheduler 这个参数,会执行这个 scheduler 函数
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
}
那么scheduler 这个函数从哪儿来的 我们看这个类 ReactiveEffect
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null, //我在这儿
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
scheduler 作为一个参数传进来的
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update),
instance.scope // track it in component's effect scope
))
他是在初始化 effect 通过 queueJob 传进来的
//queueJob 维护job列队,有去重逻辑,保证任务的唯一性,每次调用去执行,被调用的时候去重,每次调用去执行 queueFlush
export function queueJob(job: SchedulerJob) {
// 判断条件:主任务队列为空 或者 有正在执行的任务且没有在主任务队列中 && job 不能和当前正在执行任务及后面待执行任务相同
// 重复数据删除:
// - 使用Array.includes(Obj, startIndex) 的 起始索引参数:startIndex
// - startIndex默认为包含当前正在运行job的index,此时,它不能再次递归触发自身
// - 如果job是一个watch()回调函数或者当前job允许递归触发,则搜索索引将+1,以允许他递归触发自身-用户需要确保回调函数不会死循环
if (
(!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) &&
job !== currentPreFlushParentJob
) {
if (job.id == null) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}
queueJob 维护job列队 并且调用 queueFlush
function queueFlush() {
// 避免重复调用flushJobs
if (!isFlushing && !isFlushPending) {
isFlushPending = true
//开启异步任务处理flushJobs
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
queueFlush 给每一个队列创建了微任务
Vue开发移动端&打包
开发移动端最主要的就是适配各种手机,为此我研究了一套解决方案
在之前我们用的是rem 根据HTML font-size 去做缩放
现在有了更好用的vw vh
vw 视口的最大宽度,1vw等于视口宽度的百分之一
vh 视口的最大高度,1vh等于视口高度的百分之一
1.安装依赖
npm install postcss-px-to-viewport -D
因为vite中已经内联了postcss,所以并不需要额外的创建 postcss.config.js文件
vite.config.ts
import { fileURLToPath, URL } from 'url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import postcsspxtoviewport from "postcss-px-to-viewport" //插件
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx()],
css: {
postcss: {
plugins: [
postcsspxtoviewport({
unitToConvert: 'px', // 要转化的单位
viewportWidth: 750, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
selectorBlackList: ['ignore-'], // 指定不转换为视窗单位的类名,
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
landscape: false // 是否处理横屏情况
})
]
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
如果用的vite 是 ts 他这个插件并没有提供声明文件已经写好了声明文件
declare module 'postcss-px-to-viewport' {
type Options = {
unitToConvert: 'px' | 'rem' | 'cm' | 'em',
viewportWidth: number,
viewportHeight: number, // not now used; TODO: need for different units and math for different properties
unitPrecision: number,
viewportUnit: string,
fontViewportUnit: string, // vmin is more suitable.
selectorBlackList: string[],
propList: string[],
minPixelValue: number,
mediaQuery: boolean,
replace: boolean,
landscape: boolean,
landscapeUnit: string,
landscapeWidth: number
}
export default function(options: Partial<Options>):any
}
引入声明文件 tsconfig.app postcss-px-to-viewport.d.ts跟vite.ts同级
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "postcss-px-to-viewport.d.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
代码案例
<template>
<div class="wraps">
<header class="header">
<div>left</div>
<div>中间</div>
<div>right</div>
</header>
<main class="main">
<div class="main-items" v-for="item in 100">
<div class="main-port">头像</div>
<div class="main-desc">
<div>小满{{item}}</div>
<div>你妈妈喊你回家穿丝袜啦</div>
</div>
</div>
</main>
<footer class="footer">
<div class="footer-items" v-for="item in footer">
<div>{{ item.icon }}</div>
<div>{{ item.text }}</div>
</div>
</footer>
</div>
</template>
<script setup lang='ts'>
import { reactive } from 'vue';
type Footer<T> = {
icon: T,
text: T
}
const footer = reactive<Footer<string>[]>([
{
icon: "1",
text: "首页"
},
{
icon: "2",
text: "商品"
},
{
icon: "3",
text: "信息"
},
{
icon: "4",
text: "我的"
}
])
</script>
<style lang="less">
@import url('@/assets/base.css');
html,
body,
#app {
height: 100%;
overflow: hidden;
font-size: 14px;
}
.wraps {
height: inherit;
overflow: hidden;
display: flex;
flex-direction: column;
}
.header {
background-color: pink;
display: flex;
height: 30px;
align-items: center;
justify-content: space-around;
div:nth-child(1) {
width: 40px;
}
div:nth-child(2) {
text-align: center;
}
div:nth-child(3) {
width: 40px;
text-align: right;
}
}
.main {
flex: 1;
overflow: auto;
&-items {
display: flex;
border-bottom: 1px solid #ccc;
box-sizing: border-box;
padding: 5px;
}
&-port {
background: black;
width: 30px;
height: 30px;
border-radius: 200px;
}
&-desc{
margin-left:10px;
div:last-child{
font-size: 10px;
color:#333;
margin-top: 5px;
}
}
}
.footer {
border-top: 1px solid #ccc;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
&-items {
font-size: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
</style>
unocss原子化
重新构想原子化CSS - 知乎
什么是css原子化?
CSS原子化的优缺点
1.减少了css体积,提高了css复用
2.减少起名的复杂度
3.增加了记忆成本 将css拆分为原子之后,你势必要记住一些class才能书写,哪怕tailwindcss提供了完善的工具链,你写background,也要记住开头是bg
接入unocss tips:最好用于vite webpack属于阉割版功能很少
安装
npm i -D unocss
vite.config.ts
import unocss from 'unocss/vite'
plugins: [vue(), vueJsx(),unocss({
rules:[
]
})],
main.ts 引入
import 'uno.css'
配置静态css
rules: [
['flex', { display: "flex" }]
]
使用
<div class="flex red">
此刻
</div>
配置动态css(使用正则表达式)
m-参数*10 例如 m-10 就是 margin:100px
rules: [
[/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })]
]
<div class="flex red m-2">
此刻
</div>
shortcuts 可以自定义组合样式
plugins: [vue(), vueJsx(), unocss({
rules: [
[/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],
['flex', { display: "flex" }],
['red', { color: 'red' }]
],
shortcuts: {
cike: "pink flex"
}
})],
<div class="cike">
此刻
</div>
unocss 预设
plugins: [vue(), vueJsx(), unocss({
presets:[presetIcons(),presetAttributify(),presetUno()]
})],
1.presetIcons Icon图标预设
首先我们去icones官网(方便浏览和使用iconify)浏览我们需要的icon,比如这里我用到了Google Material Icons图标集里面的baseline-add-circle图标
图标集合安装
npm i -D @iconify-json/ic
ic是指icones官网图标库里的名称,在浏览器地址栏可以查看到
使用图标
<div class="i-ic-baseline-alarm-on"></div>
2.presetAttributify 属性化模式支持
属性语义化 无须class
<div red m="3">
此刻
</div>
3.presetUno 工具类预设
默认的 @unocss/preset-uno 预设(实验阶段)是一系列流行的原子化框架的 通用超集,包括了 Tailwind CSS,Windi CSS,Bootstrap,Tachyons 等。
例如,ml-3(Tailwind),ms-2(Bootstrap),ma4(Tachyons),mt-10px(Windi CSS)均会生效。
Tailwind:
<div class="text-3xl">
此刻
</div>
函数式编程&h函数
<template>
<div>
<Btn></Btn>
</div>
</template>
<script setup lang='ts'>
import { h } from 'vue'
type Props = {
text: string
}
const Btn = (propos: Props, ctx: any) => {
return h()
}
</script>
h 接收三个参数
- type 元素的类型
- propsOrChildren 数据对象, 这里主要表示(props, attrs, dom props, class 和 style)
- children 子节点
h函数拥有多种组合方式
// 除类型之外的所有参数都是可选的
h('div')
h('div', { id: 'foo' })
//属性和属性都可以在道具中使用
//Vue会自动选择正确的分配方式
h('div', { class: 'bar', innerHTML: 'hello' })
// props modifiers such as .prop and .attr can be added
// with '.' and `^' prefixes respectively
h('div', { '.name': 'some-name', '^width': '100' })
// class 和 style 可以是对象或者数组
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 定义事件需要加on 如 onXxx
h('div', { onClick: () => {} })
// 子集可以字符串
h('div', { id: 'foo' }, 'hello')
//如果没有props是可以省略props 的
h('div', 'hello')
h('div', [h('span', 'hello')])
// 子数组可以包含混合的VNode和字符串
h('div', ['hello', h('span', 'hello')])
使用props传递参数
<template>
<div>
<Btn text="浩然写的h函数"></Btn>
</div>
</template>
<script setup lang='ts'>
import { h } from 'vue'
type Props = {
text: string
}
const Btn = (propos: Props, ctx: any) => {
return h('div', {
// css使用的是tailwind
class: ['rounded', 'bg-red-500', 'text-white', 'text-center']
},
propos.text
)
}
</script>
接受emit
<template>
<div>
<Btn @on-click="getBtn" text="浩然写的h函数"></Btn>
</div>
</template>
<script setup lang='ts'>
import { h } from 'vue'
type Props = {
text: string
}
const Btn = (propos: Props, ctx: any) => {
return h('div', {
class: ['rounded', 'bg-red-500', 'text-white', 'text-center'],
onclick: () => {
ctx.emit('on-click','我是按钮')
}
},
propos.text
)
}
const getBtn = (str:string) => {
console.log(str);
}
</script>
<style lang="less">
</style>
定义插槽
<template>
<div>
<Btn @on-click="getBtn">
<template #default>123</template>
</Btn>
</div>
</template>
<script setup lang='ts'>
import { h } from 'vue'
type Props = {
text?: string
}
const Btn = (propos: Props, ctx: any) => {
return h('div', {
class: ['rounded', 'bg-red-500', 'text-white', 'text-center'],
onclick: () => {
ctx.emit('on-click','我是按钮')
}
},
ctx.slots.default()
)
}
const getBtn = (str:string) => {
console.log(str);
}
</script>
Vue响应性语法糖
小提示 本章内容所讲的东西都是实验性的产物 暂时不要再生产环境使用,自己开发玩可以使用,不过大体框架应该不会变了。
要求 vue版本 3.2.25 及以上
1.开启配置(开启之后才能使用新特性)
vite 开启 reactivityTransform
import { fileURLToPath, URL } from 'url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 3000
},
plugins: [
vue({
// 配置这条命令
reactivityTransform:true
}),
vueJsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
})
如果是 vue-cli
// vue.config.js
module.exports = {
chainWebpack: (config) => {
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => {
return {
...options,
reactivityTransform: true
}
})
}
}
第一个例子 $ref
在之前ref 修改值 和 获取值 都要.value 一下 感觉很繁琐,不想用.value 我们可以使用vue3的新特性$ref 。
我们可以直接使用$ref 宏函数 就不需要.value 了。能帮我们快速书写,但是宏函数是基于运行时的他最终还是会转换成ref 加.value 只不过vue帮我们做了这个操作了
<template>
<div>
<button @click="add">增加</button>
</div>
<div>{{count}}</div>
</template>
<script setup lang='ts'>
import {$ref} from "vue/macros";
let count = $ref(0)
const add = () => {
count++
}
</script>
当然跟ref 有关的函数都做处理 都不需要.value了
- ref ->
$ref - computed ->
$computed - shallowRef ->
$shallowRef - customRef ->
$customRef - toRef ->
$toRef
$ref 的弊端
应为他编译之后就是 count.value 并不是一个ref对象所以watch 无法监听而且会抛出一个警告
[Vue warn]: Invalid watch source: 0 A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.
at <App>
<script setup lang='ts'>
import {$ref} from "vue/macros";
import {watch} from "vue";
let count = $ref(0)
const add = () => {
count++
}
watch(count,(v) => {
console.log(v)
})
</script>
解决这个问题需要$$ 符号 就是再让他编译的时候变成一个ref 对象不加.value
<script setup lang='ts'>
import {$ref,$$} from "vue/macros";
import {watch} from "vue";
let count = $ref(0)
const add = () => {
count++
}
watch($$(count),(v) => {
console.log(v)
})
</script>
解构
在之前我们解构一个对象使用toRefs 解构完成之后 获取值和修改值 还是需要.value
vue3 也提供了语法糖 $() 解构完之后可以直接赋值
<template>
<div>{{name}}</div>
<div>{{desc}}</div>
</template>
<script setup lang='ts'>
import {$} from "vue/macros";
import {reactive, watch} from "vue";
const obj = reactive({
name: '浩然',
desc: '长得很帅'
})
// let {name,desc} = obj
// setTimeout(() => {
// desc ='帅死了' // 无法做出响应式
// },1000)
let {name,desc} = $(obj)
setTimeout(() => {
desc ='帅死了'
},1000)
</script>
环境变量
环境变量:他的主要作用就是让开发者区分不同的运行环境,来实现 兼容开发和生产
例如 npm run dev 就是开发环境 npm run build 就是生产环境等等
Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量
console.log(import.meta.env)
{
"BASE_URL":"/", //部署时的URL前缀
"MODE":"development", //运行模式
"DEV":true," //是否在dev环境
PROD":false, //是否是build 环境
"SSR":false //是否是SSR 服务端渲染模式
}
需要注意的一点就是这个环境变量不能使用动态赋值import.meta.env[key] 应为这些环境变量在打包的时候是会被硬编码的通过JSON.stringify 注入浏览器的
配置额外的环境变量
在根目录新建env 文件 可以创建多个
如下 env.[name]
修改启动命令
在 package json 配置 --mode env文件名称
webpack 构建 Vue3项目
1.初始化项目结构(跟cli 结构保持一致)
2.安装所需要的依赖包
{
"name": "webpack-vue",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@vue/compiler-sfc": "^3.2.38", //解析vue文件
"clean-webpack-plugin": "^4.0.0", //打包 的时候清空dist
"css-loader": "^6.7.1", //处理css文件
"friendly-errors-webpack-plugin": "^1.7.0", //美化dev
"html-webpack-plugin": "^5.5.0", //html 模板
"less": "^4.1.3", //处理less
"less-loader": "^11.0.0", //处理less文件
"style-loader": "^3.3.1", //处理style样式
"ts-loader": "^9.3.1", //处理ts
"typescript": "^4.8.2", //ts
"vue": "^3.2.38", //vue
"vue-loader": "^17.0.0", //解析vue
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.10.0"
}
}
// 生成package.json
npm init -y
// 生成tsconfig.json
tsc --init
// 安装webpack和webpack-cli
npm i webpack webpack-cli
// 安装启动dev的环境
npm i webpack-dev-server
// webpack插件:html模板 html-webpack-plugin
npm i html-webpack-plugin
// 安装vue
npm i vue
// 安装解析vue的vue-loader @vue/compiler-sfc
npm i vue-loader @vue/compiler-sfc
//打包 的时候清空dist
npm i clean-webpack-plugin
、、、、、、、、、
按着上面的包管理文件安装就行了
如果 tsc --init 不能使用, 安装npm install typescript -g
webpack 版本3以上的版本要安装webpack-cli
配置package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev" : "webpack-dev-server",
"build": "webpack"
},
配置webpack.config.js
const { Configuration } = require('webpack')
const path = require('path')
const htmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader/dist/index');
const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");
/**
* @type {Configuration} //配置智能提示
*/
const config = {
mode: "development",
entry: './src/main.ts', //入口文件
output: {
filename: "[hash].js",
path: path.resolve(__dirname, 'dist') //出口文件
},
module: {
// rules基本是处理文件的
rules: [
{
test: /\.vue$/, //解析vue 模板
use: "vue-loader"
},
{
test: /\.less$/, //解析 less
use: ["style-loader", "css-loader", "less-loader"],
},
{
test: /\.css$/, //解析css
use: ["style-loader", "css-loader"],
},
{
test: /\.ts$/, //解析ts
loader: "ts-loader",
options: {
configFile: path.resolve(process.cwd(), 'tsconfig.json'),
appendTsSuffixTo: [/\.vue$/]
},
}
]
},
// 放在plugins里面的都是webpack插件
plugins: [
new htmlWebpackPlugin({
template: "./public/index.html" //html模板
}),
new CleanWebpackPlugin(), //打包清空dist
new VueLoaderPlugin(), //解析vue
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo:{ //美化样式
messages:['You application is running here http://localhost:9001']
}
})
],
resolve: {
alias: {
"@": path.resolve(__dirname, './src') // 别名
},
extensions: ['.js', '.json', '.vue', '.ts', '.tsx'] //识别后缀
},
stats:"errors-only", //取消提示,与美化样式一起使用
devServer: {
proxy: {},
port: 9001,
hot: true,
open: true,
},
externals: {
vue: "Vue" //CDN 引入,要在index.html 引入vue的cdn
},
}
module.exports = config
测试打包
main.ts
const a = 1
执行命令: npm run build
在dist目录下可以看到我们声明的变量