Vue3
组合式API
1.钩子函数steup
- 函数的普通用法
<script>export default { setup() { return {} }}</script><template> </template>
- 简写使用setup
<script setup></script><template> </template>
2.响应式API
- ref函数
<script setup>import { ref } from 'vue'const state = ref(0)function increment() { state.value++}</script><template> <button @click="increment"> {{ state }} </button></template>
- reactive函数
<script setup>import { reactive } from 'vue'const state = reactive({ count: 0 })function increment() { state.count++}</script><template> <button @click="increment"> {{ state.count }} </button></template>
3.计算属性API
- 单向响应
<script setup>import { computed,reactive } from 'vue'const Person=reactive({X:'张',M:'三'}) Person.XM=computed(()=>{ return Person.X+'-'+Person.M })</script><template> 姓:<input v-model="Person.X"><br> 名:<input v-model="Person.M"><br> 单向响应:<input v-model="Person.XM"></template>
- 双向响应
<script setup>import { computed,reactive } from 'vue'const Person=reactive({X:'张',M:'三'})Person.AXM=computed({ get(){ return Person.X+'-'+Person.M }, set(value){ const arr=value.split('-') Person.X=arr[0] Person.M=arr[1] }})</script><template> 姓:<input v-model="Person.X"><br> 名:<input v-model="Person.M"><br> 双向响应:<input v-model="Person.AXM"></template>
4.监听属性API
- 监听整个对象
<!-- <script setup>import {reactive,watch} from 'vue'const Person=reactive({name:'张三',age:18, job:{salary:20}}) watch(Person,(newVal,oldVal)=>{ console.log('用户信息发生了变化',newVal,oldVal); })</script><template> <h2>年龄:{{Person.age}}</h2> <button @click="Person.age++">+1</button></template>
- 监听对象中单个属性
<!-- 监听对象中单个属性,监听单个属性可以检测到新旧值 --><script setup>import {reactive,watch} from 'vue'const Person=reactive({name:'张三',age:18, job:{salary:20}}) watch(()=>Person.age,(newVal,oldVal)=>{ console.log('用户年龄发生了变化',newVal,oldVal); })</script><template> <h2>年龄:{{Person.age}}</h2> <button @click="Person.age++">+1</button></template>
- 监听多个对象
<!-- 监听对象中多个个属性,监听单个属性可以检测到新旧值 --><script setup>import {reactive,watch} from 'vue'const Person=reactive({name:'张三',age:18, job:{salary:20}}) watch([()=>Person.name,()=>Person.age],(newValue,oldValue)=>{ console.log('person.name或者person.age的值变化了',newValue,oldValue); })</script><template> <h2>姓名:{{Person.name}}</h2> <button @click="Person.name+='~'">修改</button> <h2>年龄:{{Person.age}}</h2> <button @click="Person.age++">+1</button></template>
- 监听对象中对象(深度监听)
<!-- 监听对象中对象,必须开启深度监听,一般情况不监听对象 --><script setup>import {reactive,watch} from 'vue'const Person=reactive({name:'张三',age:18, job:{salary:20}}) watch(()=>Person.job,(newValue,oldValue)=>{ console.log('person.job的值变化了',newValue,oldValue); },{ deep:true })</script><template> <h2>薪资:{{Person.job.salary}}K</h2> <button @click="Person.job.salary++">+1</button></template>
5.高级监听API
- 基本使用(默认执行一次)
<!-- watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。 --><script setup>import {reactive,watchEffect} from 'vue'const Person=reactive({ name:'张三' }) watchEffect(()=>{ Person.name console.log('姓名发送了变化'); })</script><template> <h2>姓名:{{Person.name}}</h2> <button @click="Person.name+='~'">修改</button></template>
- 监听御前处理oninvalidate参数
<script setup lang="ts">import { reactive, watchEffect } from "vue";const Person = reactive({ name: "张三",});watchEffect((oninvalidate) => { oninvalidate(() => { console.log("before"); }); Person.name; console.log("姓名发送了变化");});</script><template> <h2>姓名:{{ Person.name }}</h2> <button @click="Person.name += '~'">修改</button></template>
- 停止监听
<script setup lang="ts">import { reactive, watchEffect } from "vue";const Person = reactive({ name: "张三",});const stop = watchEffect((oninvalidate) => { oninvalidate(() => { console.log("before"); }); Person.name; console.log("姓名发送了变化");</script><template> <h2>姓名:{{ Person.name }}</h2> <button @click="Person.name += '~'">修改</button> <button @click="stop">停止</button></template>
6.响应式对象解构API
- toRef函数
<script setup>import {reactive,toRef} from 'vue'const person=reactive({A:1,B:2})const A=toRef(person,'A')</script><template> <h2>姓名:{{A}}</h2> <button @click="person.A+='~'">修改</button></template>
- toRefs
<script setup lang="ts"> import {reactive,toRefs} from 'vue' const person=reactive({A:1,B:2}) const {A,B}=toRefs(person) </script> <template> <h2>姓名:{{A}}</h2> <button @click="A+=1">修改</button> </template>
7.生命周期API
<script setup> import {onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted,ref} from "vue"; onBeforeMount(()=>{ console.log('---挂载之前---'); }) onMounted(()=>{ console.log('---挂载---'); }) onBeforeUpdate(()=>{ console.log('---更新之前---'); }) onUpdated(()=>{ console.log('---更新---'); }) onBeforeUnmount(()=>{ console.log('---卸载之前---'); }) onUnmounted(()=>{ console.log('---卸载---'); })</script>
8.ref获取dom
<template> <div> <div ref="box">我是div</div> </div></template><script>import { ref,onMounted } from "vue";export default { setup() { let box = ref(null); onMounted(()=>{ console.log(box.value); }) console.log(box.value); return { box }; },};</script>
9.Hooks
(1)官方hooks
- useAttrs()
<!-- 父组件 --><template> <Acom a="456" title="789" /></template><!-- 子组件 --><!-- 获取父组件传过来的全部参数 --><script setup lang="ts">import { useAttrs } from 'vue'let attr = useAttrs()console.log(attr)</script>
(2)自定hooks
- 自定义hooks转换图片
import { onMounted } from 'vue'type Options = { el: string}export default function (options: Options): Promise<{ baseUrl: string }> { return new Promise(resolve => { onMounted(() => { const 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/jpg') } })}
- 使用hooks
<script setup lang="ts">import BASE64 from './hooks'BASE64({ el: '#img' }).then(resolve => { console.log(resolve.baseUrl)})</script>
(3)第三方hooks
- 安装依赖
yarn add @vueuse/core
- 简单使用
<script setup lang="ts">import { ref } from 'vue'import { useDraggable } from '@vueuse/core'const el = ref<HTMLElement | null>(null)const { x, y, style } = useDraggable(el, { initialValue: { x: 40, y: 40 }})</script><template> <div ref="el" :style="style" style="position: fixed"> Drag me! I am at {{ x }}, {{ y }} </div></template>
组件间通讯
1.props父传子
- 父组件
<script setup >import HelloWorld from './components/HelloWorld.vue'</script><template> <HelloWorld msg="1"/></template>
- 子组件
<script setup>const props=defineProps({msg:String})console.log(props.msg)</script>
2.emit子传父
- 父组件
<script setup >import HelloWorld from './components/HelloWorld.vue'const getuser=(a)=>{ console.log(a)}</script><template> <HelloWorld @getuser="getuser"/></template>
- 子组件
<script setup lang="ts">const emit = defineEmits(['getuser'])function buttonClick() { emit('getuser',1)} </script><template> <button @click="buttonClick">传输</button></template>
- 自定义事件事件校检
<script setup>const emit = defineEmits({ click: null, submit: ({ email, password }) => { if (email && password) { return true } else { console.warn('Invalid submit event payload!') return false } }})function submitForm(email, password) { emit('submit', { email, password })}</script>
3.插槽通讯
(1)匿名插槽
- 子组件
<template> <!-- slot插槽占位 --> <slot></slot></template>
- 父组件
<script setup lang="ts">import HelloWorld from "./components/HelloWorld.vue";</script><template> <HelloWorld> 插槽传递 </HelloWorld></template>
(2)具名插槽
- 父组件
<script setup lang="ts">import HelloWorld from "./components/HelloWorld.vue";</script><template> <HelloWorld> <!-- v-slot:简写# --> <template v-slot:btn> <button>具名插槽</button> </template> </HelloWorld></template>
- 子组件
<template> <!-- slot插槽占位 --> <slot name="btn"></slot></template>
(3)作用域插槽
- 理解:数据在子组件的自身,但根据数据生成的结构需要父组件决定。
- 父组件
<script setup lang="ts">import HelloWorld from "./components/HelloWorld.vue";const person=[{name:'小明',age:18},{name:'小红',age:20}]</script><template><HelloWorld :person="person"> <template #tab="scope"><tr v-for="(item,index) in scope.person" :key="index"> <th>{{item.name}}</th> <th>{{item.age}}</th> <th><button >编辑</button></th></tr></template></HelloWorld></template>
- 子组件
<script setup lang="ts">const props=defineProps<{person:{name:string,age:number}[]}>()</script><template> <table border="1"> <tr> <th>姓名</th> <th>年龄</th> <th>操作</th> </tr> <!-- 作用域插槽命名 --> <slot name="tab" :person="props.person"></slot> </table></template>
4.依赖注入
- 父组件(祖先组件)
<!-- 依赖注入传的参可以在子组件中改变 --><template> <div class="App"> <button>我是App</button> <A></A> </div></template><script setup lang="ts">import { provide, ref } from 'vue'import A from './components/Acom.vue'let flag = ref<number>(1)provide('flag', flag)</script>
- 子组件(后代组件)
<template> <div> 我是B <div>{{ flag }}</div> <button @click="flag++">+1</button> </div></template><script setup lang="ts">import { inject, ref } from 'vue'const flag = inject('flag', ref(1))</script>
5.兄弟传参
(1)父组件当成一个桥梁
(2)发布订阅模式
- Bus传递
type BusClass = { emit: (name: string) => void on: (name: string, callback: Function) => void}type PramsKey = string | number | symboltype List = { [key: PramsKey]: Array<Function>}class Bus implements BusClass { list: List constructor() { this.list = {} } emit(name: string, ...args: Array<any>) { const evnentName: Array<Function> = this.list[name] evnentName.forEach(fn => { fn.apply(this, args) }) } on(name: string, callback: Function) { const fn: Array<Function> = this.list[name] || [] fn.push(callback) this.list[name] = fn }}export default new Bus()
- A组件传递数值
<script setup lang="ts">import { ref } from 'vue'import Bus from '../utils/Bus'const flag = ref(1)const Pass = () => { Bus.emit('pass', flag)}</script><template> <div> 我是A <div>{{ flag }}</div> <button @click="Pass">Pass</button> </div></template><style scoped lang="less"></style>
- B组件接收数值
<script setup lang="ts">import Bus from '../utils/Bus'import { ref, type Ref } from 'vue'const flag = ref(0)Bus.on('pass', (Flag: Ref<number>) => { console.log(Flag) flag.value = Flag.value})</script><template> <div> 我是B <div>{{ flag }}</div> <button @click="flag++">+</button> </div></template><style scoped lang="less"></style>
(3)第三方库mitt
- 安装
yarn add mitt
- 全局挂载mit
import { createApp } from 'vue'import { createPinia } from 'pinia'import App from './App.vue'import './assets/main.css'import mitt from 'mitt'const Mit = mitt()const app = createApp(App)declare module 'vue' { export interface ComponentCustomProperties { $Bus: typeof Mit }}app.use(createPinia())app.config.globalProperties.$Bus = Mitapp.mount('#app')
- A组件传递数值
<script setup lang="ts">import { getCurrentInstance, ref } from 'vue'const instance = getCurrentInstance()const flag = ref(1)const Pass = () => { instance?.proxy?.$Bus.emit('pass', flag)}</script><template> <div> 我是A <div>{{ flag }}</div> <button @click="Pass">Pass</button> </div></template><style scoped lang="less"></style>
- B组件接收数值
<script setup lang="ts">import { getCurrentInstance, ref, type Ref } from 'vue'const instance = getCurrentInstance()const flag = ref(0)instance?.proxy?.$Bus.on('pass', Flag => { flag.value = (Flag as Ref<number>).value})</script><template> <div> 我是B <div>{{ flag }}</div> <button @click="flag++">+</button> </div></template><style scoped lang="less"></style>
- *监听事件
<script setup lang="ts">import { getCurrentInstance, ref, type Ref } from 'vue'const instance = getCurrentInstance()const flag = ref(0)instance?.proxy?.$Bus.on('*', (type, Flag) => { flag.value = (Flag as Ref<number>).value})</script>
- 取消监听事件
<script setup lang="ts">import { getCurrentInstance, ref, type Ref } from 'vue'const instance = getCurrentInstance()const flag = ref(0)instance?.proxy?.$Bus.off('pass', Flag => { flag.value = (Flag as Ref<number>).value})</script>
- 取消全部监听事件
<script setup lang="ts">import { getCurrentInstance, ref, } from 'vue'const instance = getCurrentInstance()instance?.proxy?.$Bus.all.clear()</script>
Typescript的支持
1.全局接口的抽取
- src下定义types文件夹命名xx.d.ts
- 建立Person接口person.d.ts
interface personInterface{ name:string age:number}
- 组件中直接使用
<script setup lang="ts">const props=defineProps<{person:personInterface[]}>()</script>
- 如果不是在src下或src文件下的xx.d.ts文件则需要在tsconfig.json中配置
{ { ... }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }]}
2.类型增强
- 使用环境:全局定义的数据,函数在vue组件中直接访问报错
- index.html中定义数据
<!DOCTYPE html><html lang="en"> <head> ... </head> <script> const global=1 </script> <body> ... </body></html>
- 定义类型增强
declare const global:string;
- 组件中直接读取
<script setup lang="ts">console.log(global)</script>
3.第三方库类型声明
- 安装一个库
- 安装库的ts类型声明@types/xxxx
4.props组件通讯TS
- 父组件
<script setup lang="ts">import HelloWorld from './components/HelloWorld.vue'</script><template> <HelloWorld msg="1"/></template>
- 子组件
<script setup lang="ts">interface msgIterface{ msg:string}const props = withDefaults(defineProps<msgIterface>(),{ msg:'默认值'})console.log(props.msg)</script>
5.emit组件通讯TS
- 父组件
<script setup lang="ts">import HelloWorld from './components/HelloWorld.vue'const getuser=(a:number)=>{ console.log(a)}</script><template> <HelloWorld @getuser="getuser"/></template><style scoped></style>
- 子组件
<script setup lang="ts"> const emit = defineEmits<{(e: 'getuser', id: number): void}>() function buttonClick() { emit('getuser',1)} </script><template> <button @click="buttonClick">传输</button></template><style scoped></style>
6.依赖注入类型推断
- 父组件(祖先组件)
<template> <div class="App"> <button>我是App</button> <A></A> </div></template><script setup lang="ts">import { provide, ref } from 'vue'import A from './components/Acom.vue'let flag = ref<number>(1)provide('flag', flag)</script>
- 子组件(后代组件)
<template> <div> 我是B <div>{{ flag }}</div> <button @click="flag++">+1</button> </div></template><script setup lang="ts">import { inject, ref , type Ref} from 'vue'const flag<Ref<number>> = inject('flag', ref(1))</script>
7.定义全局函数和全局函数的类型支持
import { createApp } from 'vue'...const app = createApp(App)type Fileter = { format: <T>(str: T) => string}declare module '@vue/runtime-core' { export interface ComponentCustomProperties { $filters: Fileter $env: string }}app.config.globalProperties.$filters = { format<T>(str: T): string { return `真${str}` }}app.config.globalProperties.$env = '全局变量'...
脚手架Vite
1.基本使用
- 创建vue3的项目
yarn create vite || npm init vite@latest
- 安装插件
Volar
2.配置项目路径
- tsconfig.json中添加
{ "compilerOptions": { ... "baseUrl": "./", "paths": { "@/*":[ "src/*" ] } }, ...}
- vite.config.ts中添加
export default defineConfig({ plugins: [vue()], resolve:{ alias:{ "@":join(__dirname,'src') } }})
3.eslint和prettierrc的配置
- .prettierrc.json
{ "arrowParens": "always", "bracketSameLine": true, "bracketSpacing": true, "embeddedLanguageFormatting": "auto", "htmlWhitespaceSensitivity": "css", "insertPragma": false, "jsxSingleQuote": false, "printWidth": 120, "proseWrap": "never", "quoteProps": "as-needed", "requirePragma": false, "semi": false, "singleQuote": true, "tabWidth": 2, "trailingComma": "all", "useTabs": false, "vueIndentScriptAndStyle": false, "singleAttributePerLine": false }
- .eslintrc.cjs
/* eslint-env node */require('@rushstack/eslint-patch/modern-module-resolution')module.exports = { root: true, extends: [ 'plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier' ], rules: { 'vue/multi-word-component-names': 'off', // 关闭命名 semi: 0 // 结尾无分号 }, parserOptions: { ecmaVersion: 'latest' }}
4.vite环境变量的配置
- vite的环境在import中
<script setup lang="ts">console.log(import.meta.env)</script>
- 创建
.env.development .env.production
- package.json中配置运行生产环境,会自动注入
{ ... "scripts": { "dev": "vite --mode development", ... }, }
- vite.config.ts中读取环境变量
import { fileURLToPath, URL } from 'node:url'import { defineConfig, loadEnv } from 'vite'import unocss from 'unocss/vite'import vue from '@vitejs/plugin-vue'import { presetIcons, presetAttributify, presetUno } from 'unocss'export default ({ mode }: any) => { console.log(loadEnv(mode, process.cwd())) return defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })}
- 找不到模块“./App.vue”或其相应的类型声明
declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component}
- 类型“ImportMeta”上不存在属性“env”
{ ... "compilerOptions": { ... "types": [ "vite/client" ], }, ...}
指令的重构
1.v-model指令
(1)v-model实现组件间数据双向绑定
- 父组件
<script setup lang="ts">import HelloWorld from "./components/HelloWorld.vue";import { ref } from "vue";const num=ref(1)</script><template> <HelloWorld v-model="num"/></template>
- 子组件
<script setup lang="ts">import { computed } from 'vue';const props=defineProps<{modelValue:number}>()const emit = defineEmits<{(e: 'update:modelValue', id: number): void}>()const value=computed({ get(){ return +props.modelValue }, set(value){ emit('update:modelValue',+value) }})</script><template> <input type="text" v-model="value"></template>
- v-model的原理
<template><!-- <HelloWorld v-model="num"/> --> <HelloWorld :modelValue="num" @update:modelValue="num = $event"/></template>
(2)v-model传递特定的名称
- 父组件
<script setup lang="ts">import { ref } from "vue";import HelloWorld from "./components/HelloWorld.vue";const num=ref(1)</script><template> <!-- <HelloWorld :num="num @update:="num = $event""/> --> <HelloWorld v-model:num="num"/></template>
- 子组件
<script setup lang="ts">import { computed } from 'vue';const props=defineProps<{num:number}>()const emit = defineEmits<{(e: 'update:num', id: number): void}>()const value=computed({ get(){ return +props.num }, set(value){ emit('update:num',+value) }})</script><template> <input type="text" v-model="value"></template>
2.自定义指令
(1)自定义指令的简单使用
- 全局自定义指令
import { createApp } from 'vue'import './style.css'import App from './App.vue'const app=createApp(App)app.directive('focus',{ mounted(el){ el.focus() } })app.mount('#app')
- 使用自定义指令
<template> <input type="text" v-model="value" v-focus></template>
- 局部自定义指令
<script setup>const vFocus = { mounted: (el) => el.focus()}</script><template> <input v-focus /></template>
(2)自定义指令详解
- 自定义指令的生命周期
<script setup lang="ts">import type { Directive, DirectiveBinding } from 'vue'type Dir = { background: string }const vMove: Directive = { created() {}, beforeMount() {}, mounted(el: HTMLElement, dir: DirectiveBinding<Dir>) { console.log(dir.value.background) el.style.background = dir.value.background }, beforeUpdate() {}, updated() {}, beforeUnmount() {}, unmounted() {} }</script><template> <!-- 自定义指令,参数,修饰符 --> <div v-move:a.x="{ background: 'red' }">自定义指令</div></template><style scoped lang="less"></style>
- 生命周期的简写
<script setup lang="ts">import type { Directive, DirectiveBinding } from 'vue'type Dir = { background: string }const vMove: Directive = (el: HTMLElement, dir: DirectiveBinding<Dir>) => { el.style.background = dir.value.background}</script><template> <!-- 自定义指令,参数,修饰符 --> <div v-move:a.x="{ background: 'red' }">自定义指令</div></template><style scoped lang="less"></style>
- 自定义拖拽指令
<script setup lang="ts">import type { Directive } from 'vue'const vMove: Directive = (el: HTMLElement) => { const move = (e: MouseEvent) => { console.log(e) el.style.left = e.clientX + 'px' el.style.top = e.clientY + 'px' } el.addEventListener('mousedown', () => { document.addEventListener('mousemove', move) document.addEventListener('mouseup', () => { document.removeEventListener('mousemove', move) }) })}</script><template> <!-- 自定义指令,参数,修饰符 --> <div v-move style=" background-color: red; width: 200px; height: 200px; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); " > <div style="background-color: black; width: 200px; color: white"> 自定义指令 </div> </div></template>
响应式原理
1.了解Proxy
- Proxy代理的get方法
<script> let obj={ name:'Vue', age:8 } let obj2=new Proxy(obj,{ get(target,property){ console.log('执行了get'); return target[property] } }) console.log(obj2.age) </script>
- Proxy代理的set方法
<script> let obj={ name:'Vue', age:8 } let obj2=new Proxy(obj,{ set(target,property,newValue){ console.log('执行了set') target[property]=newValue } }) obj2.age=7 console.log(obj2.age) </script>
2.了解Object.defineProperty
- Object.defineProperty(对象.定义属性,用来为一个对象添加新属性)
<script> let person = {name:'张三',sex:'男',}Object.defineProperty(person,'age',{value=18})console.log(person)</script>
- Object.defineProperty属性的可枚举可修改的实现
<script> let person = {name:'张三',sex:'男',}Object.defineProperty(person,'age',{ enumerable=true writable=true configurable:true value=18})console.log(person)</script>
- Object.defineProperty() 的get()方法
<script> let person = { name: '张三', sex: '男', } function Observer(obj) { const keys = Object.keys(obj) keys.forEach((key) => { Object.defineProperty(this,key,{ get() { return obj[key] } }) }) } const obs = new Observer(person) console.log(obs.sex); </script>
- Object.defineProperty() 的set()方法
<script> let person = { name: '张三', sex: '男', } function Observer(obj) { const keys = Object.keys(obj) keys.forEach((key) => { Object.defineProperty(this,key,{ set(val) { console.log('set方法调用了') obj[key] = val } }) }) } const obs = new Observer(person) obs.name=15 </script>
3.Vue双向绑定的实现的对比
- Vue3的Proxy实现
<body> <input type="text" id="ipt"> <p id='op'></p> <script> function reactive(obj) { return new Proxy(obj,{ get(target,property) { return target[property] }, set(target,property,newVal) { target[property] = newVal } }) } let newObj = reactive([1,2]) console.log(newObj[1]) const ipt = document.querySelector('#ipt') ipt.value = newObj[1] document.querySelector('#op').innerHTML = newObj[1] ipt.addEventListener('input',function (e) { newObj[1] = e.target.value document.querySelector('#op').innerHTML = newObj[1] }) </script></body>
- Vue2的Object.defineProperty实现
<body> <input type="text" id="ipt"> <p id='op'></p> <script> function Observer(obj) { const keys = Object.keys(obj) keys.forEach((key) => { Object.defineProperty(this,key,{ get() { console.log('get方法被调用了'); return obj[key] }, set(val) { console.log('set方法调用了') obj[key] = val } }) }) } const obs = new Observer([1,2,3]) const ipt = document.querySelector('#ipt') ipt.value = obs[1] document.querySelector('#op').innerHTML = obs[1] ipt.addEventListener('input',function (e) { obs[1] = e.target.value document.querySelector('#op').innerHTML = obs[1] }) </script></body>
- 上面的测试,Object.property是可以检测到通过索引改变数组的操作的,而Vue没有实现,Object.defineProperty表示这个锅我不背
内置组件
1.内置组件
(1)Teleport组件
- 可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去
- 父组件
<!-- 遮罩层组件传送到body下 --><script setup lang="ts">import Acom from './components/Acom.vue'</script><template><div class="app"></div><Acom/></template><style scoped >.app{ width: 200px; height: 200px; background-color: pink;}</style>
- 子组件
<script setup lang="ts"> import { ref } from 'vue' const open = ref(false) </script> <template> <button @click="open=true">显示遮罩层</button> <!-- 传送到body --> <Teleport to="body"> <div class="cover" v-show="open"> <span @click="open=false"> X</span> </div> </Teleport> </template> <style scoped> .cover { position: absolute; z-index:2; top: 0; left: 0; bottom: 0; right: 0; background-color: rgba(0,0,0,0.5); } </style>
(2)Transition组件
- 非命名动画
<script setup lang="ts">import { ref } from 'vue';const show=ref(true)</script><template><button @click="show=!show">显示/隐藏</button><Transition><div class="div" v-if="show"></div></Transition></template><style scoped>.div{ background-color: pink; width: 200px; height: 200px; margin: auto;}.v-enter-active,.v-leave-active { transition: opacity 0.5s ease;}.v-enter-from,.v-leave-to { opacity: 0;}</style>
- 命名动画
<script setup lang="ts">import { ref } from 'vue';const show=ref(true)</script><template><button @click="show=!show">显示/隐藏</button><Transition name="fade"><div class="div" v-if="show"></div></Transition></template><style scoped>.div{ background-color: pink; width: 200px; height: 200px; margin: auto;}.fade-enter-active { transition: all 0.3s ease-out;}.fade-leave-active { transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);}.fade-enter-from,.fade-leave-to { transform: translateX(20px); opacity: 0;}</style>
- 过度动画
<Transition mode="out-in"> ...</Transition>
- 结合第三方库Animate.css
<!-- yarn add animate.css --><script setup lang="ts">import { ref } from 'vue'import 'animate.css'import Acom from './components/Acom.vue'const show = ref(true)</script><template> <transition leave-active-class="animate__animated animate__fadeOut" enter-active-class="animate__animated animate__fadeIn" > <Acom v-if="show"></Acom> </transition> <button @click="show = !show">显示/隐藏</button></template><style scoped lang="less"></style>
- transition 生命周期
<script setup lang="ts">import { ref } from 'vue'import 'animate.css'import Acom from './components/Acom.vue'const show = ref(true)const beforeEnter = () => { console.log('进入之前')}const enter = (_, done: Function) => { console.log('过度曲线') setTimeout(() => { done() }, 3000)}const afterEnter = () => { console.log('过度完成')}const enterCancelled = () => { console.log('进入效果被打断')}const beforeLeave = () => { console.log('离开之前')}const leave = (_, done: Function) => { setTimeout(() => { done() }, 3000) console.log('过度曲线')}const afterLeave = () => { console.log('离开之后')}const leaveCancelled = () => { console.log('离开效果被打断')}</script><template> <transition leave-active-class="animate__animated animate__fadeOut" enter-active-class="animate__animated animate__fadeIn" @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @enter-cancelled="enterCancelled" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" @leave-cancelled="leaveCancelled" > <Acom v-if="show"></Acom> </transition> <button @click="show = !show">显示/隐藏</button></template>
- 生命周期结合第三方库gsap.js
<!-- yarn add gsap --><script setup lang="ts">import { ref } from 'vue'import Acom from './components/Acom.vue'import gsap from 'gsap'const show = ref(true)const beforeEnter = (el: Element) => { gsap.set(el, { width: 0, height: 0 })}const enter = (el: Element, done: gsap.Callback) => { gsap.to(el, { width: 200, height: 200, onComplete: done })}const beforeLeave = (el: Element) => { gsap.set(el, { width: 200, height: 200 })}const leave = (el: Element, done: gsap.Callback) => { gsap.to(el, { width: 0, height: 0, onComplete: done })}</script><template> <transition @before-enter="beforeEnter" @enter="enter" @before-leave="beforeLeave" @leave="leave" > <Acom v-if="show"></Acom> </transition> <button @click="show = !show">显示/隐藏</button></template>
- 初始化动画
<script setup lang="ts">import { ref } from 'vue'import Acom from './components/Acom.vue'const show = ref(true)</script><template> <transition appear-from-class="from" appear-active-class="active" appear-to-class="to" appear > <Acom v-if="show"></Acom> </transition> <button @click="show = !show">显示/隐藏</button></template><style scoped>.from { width: 0; height: 0;}.active { transition: all 2s ease;}.to { width: 200px; height: 200px;}</style>
- 初始化动画结合Animate.css
<script setup lang="ts">import { ref } from 'vue'import Acom from './components/Acom.vue'import 'animate.css'const show = ref(true)</script><template> <transition appear-active-class="animate__animated animate__heartBeat" appear> <Acom v-if="show"></Acom> </transition> <button @click="show = !show">显示/隐藏</button></template><style scoped></style>
(3)transition-group过度列表
- Transition组件无法对v-for的列表进行渲染
- transition-group的tag属性
<!-- tag属性可以让transition-group多加一层节点元素 --><template> <div class="wraps"> <transition-group tag="session"> <!-- 使用transition-group渲染的组件要有key--> <div class="item" v-for="item in 5" :key="item">{{ item }}</div> </transition-group> </div></template>
- 添加列表时的动画效果
<script setup lang="ts">import { ref } from 'vue'import 'animate.css'const num = ref(5)</script><template> <div class="wraps"> <transition-group leave-active-class="animate__animated animate__fadeOut" enter-active-class="animate__animated animate__fadeIn" > <!-- 使用transition-group渲染的组件要有key--> <div class="item" v-for="item in num" :key="item">{{ item }}</div> </transition-group> </div> <button @click="num++">添加</button> <button @click="num--">删除</button></template><style scoped lang="less">.wraps { display: flex; flex-wrap: wrap; word-break: break-all; border: 1px solid #ccc; .item { margin: 10px; }}</style>
- 平移动画move-class
<script setup lang="ts">import { ref } from 'vue'import _ from 'lodash'let 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)}console.log(list)</script><template> <div> <button @click="random">打乱</button> <transition-group tag="div" class="wraps" move-class="move"> <div v-for="item in list" :key="item.id" class="item"> {{ item.number }} </div> </transition-group> </div></template><style scoped lang="less">.wraps { display: flex; flex-wrap: wrap; width: calc(25px * 10 + 9px); .item { width: 25px; height: 25px; border: 1px solid #ccc; text-align: center; }}.move { transition: all 1s;}</style>
- 状态过度(数字过度颜色过度)
<script setup lang="ts">import { reactive, watch } from 'vue'import gsap from 'gsap'const num = reactive({ current: 0, tweenedNumber: 0})watch( () => num.current, newVal => { gsap.to(num, { duration: 1, tweenedNumber: newVal }) })</script><template> <div> <input type="text" v-model="num.current" step="20" /> <div> <!-- 去掉小数点 --> {{ num.tweenedNumber.toFixed(0) }} </div> </div></template><style scoped lang="less"></style>
(4)keep-alive组件
- 开启keep-alive 生命周期的变化
初次进入时: onMounted-> onActivated退出后触发: deactivated
- 缓存数据
<script setup lang="ts">import { ref } from 'vue'import Acom from './components/Acom.vue'const show = ref(true)</script><template> <keep-alive> <Acom v-if="show"></Acom> </keep-alive> <button @click="show = !show">显示/隐藏</button></template>
- include属性和exclude属性
<!-- 注意组件一定要命名才可以使用include --><script setup lang="ts">import { ref } from 'vue'import Acom from './components/Acom.vue'import Bcom from './components/Bcom.vue'const show = ref(true)</script><template> <keep-alive :include="['Acom']" :exclude="['Bcom']"> <Acom v-if="show"></Acom> <Bcom v-else></Bcom> </keep-alive> <button @click="show = !show">显示/隐藏</button></template><style scoped lang="less"></style>
2.普通组件
(1)全局组件
- 配置全局组件
import { createApp } from 'vue'import { createPinia } from 'pinia'import App from './App.vue'import Acom from './components/Acom.vue'import './assets/main.css'const app = createApp(App)app.use(createPinia())app.component('Acom', Acom)app.mount('#app')
- 使用组件
<template> <div> <Acom></Acom> </div></template>
(2)异步组件
- 子组件中发送了请求变成异步
<script setup lang="ts">interface ResItf { code: number data: { a: number; b: number }[] message: string}let p: Promise<ResItf> = new Promise(resolve => { setTimeout(() => {}, 3000) resolve({ code: 0, data: [ { a: 1, b: 2 }, { a: 11, b: 22 } ], message: '' })})const a = await pconsole.log(a)</script><template> <div>异步组件</div> <div>异步组件</div> <div>异步组件</div></template>
- 父组件异步调用组件
<script setup lang="ts">import { defineAsyncComponent } from 'vue'const Acom = defineAsyncComponent(() => import('./components/Acom.vue'))</script><template> <div> <Suspense> <template #default> <Acom></Acom> </template> <template #fallback> 加载中。。。 </template> </Suspense> </div></template><style scoped lang="less"></style>
语法糖组件命名问题
- 安装依赖
yarn add vite-plugin-vue-setup-extend
- 直接命名
<script lang="ts" setup name="xxx"></script>
常用的CSS的功能
- 样式穿透
<style scoped lang="less">:deep(input) { color: red;}</style>
- 插槽选择器
<template> <div> <slot name="nums" :nums="['1', '2', '3']"> </slot> </div></template><style scoped lang="less">:slotted(.li) { color: red;}</style>
- 全局选择器
<script setup lang="ts"></script><template> <div> <slot name="nums" :nums="['1', '2', '3']"> </slot> </div></template><style scoped lang="less">:global(.li) { color: red;}</style>
- 动态CSS
<script setup lang="ts">import { reactive } from 'vue'const style = reactive({ color: 'red'})setTimeout(() => { style.color = 'blue'}, 3000)</script><template> <div class="div">动态css</div></template><style scoped lang="less">.div { color: v-bind('style.color');}</style>
1.CSS原子化
- 安装unocss
yarn add unocss
- vite的配置文件中配置
import { fileURLToPath, URL } from 'node:url'import pxtoViewPort from 'postcss-px-to-viewport'import { defineConfig } from 'vite'import unocss from 'unocss/vite'import vue from '@vitejs/plugin-vue'export default defineConfig({ plugins: [ vue(), unocss({ rules: [ ['flex', { display: 'flex' }], ['red', { color: 'red' }], [/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })] ] }) ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }})
- main.ts中引入
import 'uno.css'
- 其他预设配置中引入
import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'import unocss from 'unocss/vite'import vue from '@vitejs/plugin-vue'import { presetIcons, presetAttributify, presetUno } from 'unocss'export default defineConfig({ plugins: [ vue(), unocss({ presets: [presetIcons(), presetAttributify(), presetUno()], rules: [ ['flex', { display: 'flex' }], ['red', { color: 'red' }], [/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })] ] }) ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }})
- 第一预设图标库
npm i -D @iconify-json/ic
- 第二预设属性语义化 无须class
<div color="red">left</div>
- 第三预设
默认的 @unocss/preset-uno 预设(实验阶段)是一系列流行的原子化框架的 通用超集,包括了 Tailwind CSS,Windi CSS,Bootstrap,Tachyons 等。例如,ml-3(Tailwind),ms-2(Bootstrap),ma4(Tachyons),mt-10px(Windi CSS)均会生效。
5.Vue3集成Tailwind CSS
- 安装依赖
yarn add -D tailwindcss@latest postcss@latest autoprefixer@latest
- 安装插件
tailwind css inteliSence
- 生成配置文件
npx tailwindcss init -p
- tailwind.config.js配置文件中添加
module.exports = { content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], theme: { extend: {} }, plugins: []}
- 创建index.css文件并且在mian.ts中引入
@tailwind base;@tailwind components;@tailwind utilities;
- 使用tailwindcss的样式
<script setup lang="ts"></script><template> <div class="w-screen h-screen bg-red-600 flex justify-center items-center text-8xl text-teal-50" > hello tailwind </div></template><style scoped lang="less"></style>
面试常用源码
1.app.use()的源码实现
- 实现myuse
import type { App } from 'vue'import { app } from '../main'interface Use { install: (app: App, ...options: any[]) => void}const installList = new Set()export function MyUse<T extends Use>(plugin: T, ...options: any[]) { if (installList.has(plugin)) { console.log('插件件已经注册') return } plugin.install(app, ...options) installList.add(plugin)}
- 使用myuse调用插件
import { createApp } from 'vue'import { createPinia } from 'pinia'import App from './App.vue'import './assets/main.css'import Loading from './components/Loading'import { MyUse } from './utils/myuse'export const app = createApp(App)MyUse(Loading)app.use(createPinia())app.mount('#app')type Lod = { show: () => void hide: () => void}declare module '@vue/runtime-core' { export interface ComponentCustomProperties { $loading: Lod }}
移动端适配
1.第一种适配方案
- 安装依赖
yarn add amfe-flexible postcss postcss-pxtorem@5.1.1
- main.ts引入amfe-flexible
import "amfe-flexible"
- 根目录下创建postcss.config.js文件并配置
module.exports = { plugins: { 'postcss-pxtorem': { rootValue: 37.5, propList: ['*'] } }}
2.第二种适配方案
- 安装依赖
yarn add postcss-px-to-viewport -D
- vite.config.ts内置
postcss.config.js
中修改配置
import { fileURLToPath, URL } from 'node:url'import pxtoViewPort from 'postcss-px-to-viewport'import { defineConfig } from 'vite'import vue from '@vitejs/plugin-vue'export default defineConfig({ plugins: [vue()], css: { postcss: { plugins: [ pxtoViewPort({ unitToConvert: 'px', viewportWidth: 750, unitPrecision: 6, propList: ['*'], viewportUnit: 'vw', fontViewportUnit: 'vw', selectorBlackList: ['ignore-'], minPixelValue: 1, mediaQuery: true, replace: true, landscape: false }) ] } }, resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }})
- 创建postcss-px-to-viewport.d.ts的声明文件
declare module 'postcss-px-to-viewport' { type Options = { unitToConvert: 'px' | 'rem' | 'cm' | 'em' viewportWidth: number viewportHeight: number unitPrecision: number viewportUnit: string fontViewportUnit: string selectorBlackList: string[] propList: string[] minPixelValue: number mediaQuery: boolean replace: boolean landscape: boolean landscapeUnit: string landscapeWidth: number } export default function (options: Partial<Options>): any}
- 在tsconfig.json中引入声明文件
{ "extends": "@vue/tsconfig/tsconfig.web.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "postcss-px-to-viewport.d.ts"], "compilerOptions": { "baseUrl": ".", "types": ["element-plus/global"], "paths": { "@/*": ["./src/*"] } }, "references": [ { "path": "./tsconfig.config.json" } ]}
- 注意:如果外面用到了
postcss.config.js
,在postcss.config.js
中添加配置文件
module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, 'postcss-px-to-viewport': { unitToConvert: 'px', viewportWidth: 320 } }}
的其他知识点
1.全局函数和全局变量
- 全局函数
import { createApp } from 'vue'import { createPinia } from 'pinia'import App from './App.vue'import './assets/main.css'const app = createApp(App)type Fileter = { format: <T>(str: T) => string}declare module '@vue/runtime-core' { export interface ComponentCustomProperties { $filters: Fileter }}app.config.globalProperties.$filters = { format<T>(str: T): string { return `真${str}` }}app.use(createPinia())app.mount('#app')
- 全局变量
import { createApp } from 'vue'import { createPinia } from 'pinia'import App from './App.vue'import './assets/main.css'const app = createApp(App)declare module '@vue/runtime-core' { export interface ComponentCustomProperties { $env: string }}app.config.globalProperties.$env = '全局变量'app.use(createPinia())app.mount('#app')
2.自定义插件
- 封装插件的样式,抛出插件的显示隐藏方法
<script setup lang="ts">import { ref } from 'vue'const isShow = ref(false)const show = () => { console.log(111) isShow.value = true}const hide = () => { isShow.value = false}defineExpose({ show, hide})</script><template> <div v-if="isShow" class="loading">loading....</div></template><style scoped lang="less"></style>
- 创建接收调用插件的方法
import { render, type App, type VNode } from 'vue'import Loading from './index.vue'import { createVNode } from 'vue'export default { install(app: App) { const Vnode: VNode = createVNode(Loading) render(Vnode, document.body) app.config.globalProperties.$loading = { show: Vnode.component?.exposed?.show, hide: Vnode.component?.exposed?.hide } }}
- main.ts中挂载上面的方方法
import { createApp } from 'vue'import { createPinia } from 'pinia'import App from './App.vue'import './assets/main.css'import Loading from './components/Loading'const app = createApp(App)app.use(Loading)app.use(createPinia())app.mount('#app')
- 对插件的方法进行声明
type Lod = { show: () => void hide: () => void}declare module '@vue/runtime-core' { export interface ComponentCustomProperties { $loading: Lod }}
- 使用插件
<script setup lang="ts">import { getCurrentInstance } from 'vue'const instance = getCurrentInstance()instance?.proxy?.$loading.show()setTimeout(() => { instance?.proxy?.$loading.hide()}, 5000)</script><template> <div></div></template><style scoped lang="less"></style>
3.函数式编程
- h函数
h 接收三个参数1.type 元素的类型2.propsOrChildren 数据对象, 这里主要表示(props, attrs, dom props, class 和 style)3.children 子节点
- h函数的多种组合
h('div')h('div', { id: 'foo' }) h('div', { class: 'bar', innerHTML: 'hello' }) h('div', { '.name': 'some-name', '^width': '100' }) h('div', { class: [foo, { bar }], style: { color: 'red' } }) h('div', { onClick: () => {} }) h('div', { id: 'foo' }, 'hello') h('div', 'hello')h('div', [h('span', 'hello')]) h('div', ['hello', h('span', 'hello')])
- 使用props传递参数
<template> <Btn text="按钮"></Btn></template> <script setup lang='ts'>import { h, } from 'vue';type Props = { text: string}const Btn = (props: Props, ctx: any) => { return h('div', { class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1', }, props.text)}</script>
- 接收emit
<template> <Btn @on-click="getNum" text="按钮"></Btn></template> <script setup lang='ts'>import { h, } from 'vue';type Props = { text: string}const Btn = (props: Props, ctx: any) => { return h('div', { class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1', onClick: () => { ctx.emit('on-click', 123) } }, props.text)} const getNum = (num: number) => { console.log(num);}</script>
- 定义插槽
<template> <Btn @on-click="getNum"> <template #default> 按钮slots </template> </Btn></template> <script setup lang='ts'>import { h, } from 'vue';type Props = { text?: string}const Btn = (props: Props, ctx: any) => { return h('div', { class: 'p-2.5 text-white bg-green-500 rounded shadow-lg w-20 text-center inline m-1', onClick: () => { ctx.emit('on-click', 123) } }, ctx.slots.default())} const getNum = (num: number) => { console.log(num);}</script>
4.vue性能优化
(1)跑分和打包体积
- 跑分vue开发工具Lighthouse
从Performance页的表现结果来看,得分37分,并提供了很多的时间信息,我们来解释下这些选项代表的意思:FCP (First Contentful Paint):首次内容绘制的时间,浏览器第一次绘制DOM相关的内容,也是用户第一次看到页面内容的时间。Speed Index: 页面各个可见部分的显示平均时间,当我们的页面上存在轮播图或者需要从后端获取内容加载时,这个数据会被影响到。LCP (Largest Contentful Paint):最大内容绘制时间,页面最大的元素绘制完成的时间。TTI(Time to Interactive):从页面开始渲染到用户可以与页面进行交互的时间,内容必须渲染完毕,交互元素绑定的事件已经注册完成。TBT(Total Blocking Time):记录了首次内容绘制到用户可交互之间的时间,这段时间内,主进程被阻塞,会阻碍用户的交互,页面点击无反应。CLS(Cumulative Layout Shift):计算布局偏移值得分,会比较两次渲染帧的内容偏移情况,可能导致用户想点击A按钮,但下一帧中,A按钮被挤到旁边,导致用户实际点击了B按钮。
- 打包后rollup的插件
yarn add rollup-plugin-visualizer
import { fileURLToPath, URL } from 'node:url'import { defineConfig, loadEnv } from 'vite'import unocss from 'unocss/vite'import vue from '@vitejs/plugin-vue'import { visualizer } from 'rollup-plugin-visualizer'export default ({ mode }: any) => { console.log(loadEnv(mode, process.cwd())) return defineConfig({ plugins: [vue(), visualizer({ open: true })], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })}
- vite配置文件中vite的优化
import { fileURLToPath, URL } from "node:url";import { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";import vueJsx from "@vitejs/plugin-vue-jsx";export default defineConfig({ ... build: { chunkSizeWarningLimit: 2000, cssCodeSplit: true, sourcemap: false, minify: 'terser', assetsInlineLimit: 5000 } })
(2)PWA离线存储技术
- 安装依赖
yarn add vite-plugin-pwa -D
- 配置
import { fileURLToPath, URL } from "node:url";import { VitePWA } from "vite-plugin-pwa";import { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";import vueJsx from "@vitejs/plugin-vue-jsx";export default defineConfig({ plugins: [ vue(), vueJsx(), VitePWA({ workbox: { cacheId: "key", runtimeCaching: [ { urlPattern: /.*\.js.*/, handler: "StaleWhileRevalidate", options: { cacheName: "XiaoMan-js", expiration: { maxEntries: 30, maxAgeSeconds: 30 * 24 * 60 * 60, }, }, }, ], }, }), ], ....});
(3)其他性能优化
- 图片懒加载
import { createApp } from 'vue'import App from './app'import lazyPlugin from 'vue3-lazy'const app = createApp(App)app.use(lazyPlugin, { loading: 'loading.png', error: 'error.png'})app.mount('#app')<img v-lazy="user.avatar" >
- 虚拟列表实现
后台返回多数据展示可视区的dom
- 多线程 使用 new Worker 创建
const myWorker1 = new Worker("./calcBox.js");worker.postMessage(arrayBuffer, [arrayBuffer]);self.onmessage = function (e) {};关闭worker.terminate();
- 防抖节流