好处
-
性能提升
打包大小减少41% 初次渲染快55%, 更新渲染快133% 内存减少54% 使用Proxy代替defineProperty实现数据响应式 重写虚拟DOM的实现和Tree-Shaking
-
新增特性
Composition (组合) API setup ref 和 reactive computed 和 watch 新的生命周期函数 provide与inject ... 新组件 Fragment - 文档碎片 Teleport - 瞬移组件的位置 Suspense - 异步加载组件的loading界面 其它API更新 全局API的修改 将原来的全局API转移到应用对象 模板语法变化
vue-cli创建vue3项目结构分析(选择ts)
- main.ts
import { createApp } from 'vue'
//引入createApp 函数,创建对应的应用,产生应用的实例对象
import App from './App.vue'
//创建App应用返回对应的实例对象,调用mount挂载
createApp(App).mount('#app')
- App.vue
vue2中template只能有一个根标签,而vue3可以有多个
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
</template>
<script lang="ts">
//目的是定义一个组件,内部可以传入一个配置对象
import { defineComponent } from 'vue';
//引入子级组件
import HelloWorld from './components/HelloWorld.vue';
//暴露出去一个定义好的组件
export default defineComponent({
name: 'App',
//注册组件
components: {
HelloWorld
}
});
</script>
setup
官网地址:https://v3.cn.vuejs.org/api/composition-api.html
- vue2与vue3响应式原理
vue2 :
Object.defineProperty(data, 'count', {
get () {},
set () {}
})
对象: 通过defineProperty对对象的已有属性值的读取和修改进行劫持(监视/拦截)
数组: 通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
问题:
对象直接新添加的属性或删除已有属性, 界面不会自动更新
直接通过下标替换元素或更新length, 界面不会自动更新 arr[1] = {}
vue3:
通过Proxy(代理): 拦截对data任意属性的任意(13种)操作, 包括属性值的读写, 属性的添加, 属性的删除等…(将普通对象变成响应式对象的操作)
const p = new Proxy(target, handler)
//target 目标对象
//handler处理器对象。 获取对象get() 设置对象set() 删除deleteProperty()等等十来种
详解见:proxy-MDN
Reflect:配合handler中的方法使用。详情:Reflect-MDN
const user = {
name: "John",
age: 12
};
/*
proxyUser是代理对象, user是被代理对象
后面所有的操作都是通过代理对象来操作被代理对象内部属性
*/
const proxyUser = new Proxy(user, {
get(target, prop) {
console.log('劫持get()', prop)
return Reflect.get(target, prop)
},
set(target, prop, val) {
console.log('劫持set()', prop, val)
return Reflect.set(target, prop, val); // (2)
},
deleteProperty (target, prop) {
console.log('劫持delete属性', prop)
return Reflect.deleteProperty(target, prop)
}
});
// 读取属性值
console.log(proxyUser===user)
console.log(proxyUser.name, proxyUser.age)
// 设置属性值
proxyUser.name = 'bob'
proxyUser.age = 13
console.log(user)
// 添加属性
proxyUser.sex = '男'
console.log(user)
// 删除属性
delete proxyUser.sex
console.log(user)
setup详解
1.setup执行的时机
-
在beforeCreate之前执行(一次), 此时组件对象还没有创建 this是undefined,
不能通过this来访问data/computed/methods / props 其实所有的composition -
API相关回调函数中也都不可以
2.setup的返回值
- 一般都返回一个对象: 为模板提供数据, 也就是模板中可以直接使用此对象中的所有属性/方法
- 返回对象中的属性会与data函数返回对象的属性合并成为组件对象的属性,最好不要同时使用data和setup
- 返回对象中的方法会与methods中的方法合并成功组件对象的方法
- 如果有重名, setup优先
注意:
- 一般不要混合使用: methods中可以访问setup提供的属性和方法, 但在setup方法中不能访问data和methods
- setup不能是一个async函数: 因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性数据
3.setup的参数
setup(props, context) / setup(props, {attrs, slots, emit})
- props: 包含props配置声明且传入了的所有属性的对象(父组件传递过来的所有属性值)
- attrs: 包含没有在props配置中声明的属性的对象, 相当于 this.$attrs
- slots: 包含所有传入的插槽内容的对象, 相当于 this.$slots
- emit: 用来分发自定义事件的函数, 相当于 this.$emit
ref家族
ref、shallowRef :ref创建的在js中要使用.value获取值。
- ref:创建的对象,里面任意深度的属性与视图都是响应性的
- shallowRef:只有更改整个对象才会更新视图
- triggerRef:手动执行与 shallowRef 关联的任何副作用,强制更新视图。
let c = shallowRef({
num: 0,
numarr: [{ a: 1 }],
});
c.value = { num: 1, numarr: [{ a: 2 }] };//会更新视图
c.value.numarr[0].a += 2;//不会
triggerRef(c);//会更新视图
- customRef :
(1)customRef 用于自定义返回一个ref对象,可以显式地控制依赖追踪和触发响应,接受工厂函数
(2)两个参数分别是用于追踪的 track 与用于触发响应的 trigger,并返回一个带有 get 和 set 属性的对象
下面使用customRef 实现防抖
function useDebouncedRef<T>(value: T, delay = 200) {
let timeId: number;
return customRef((track, trigger) => {
return {
get() {
track(); //告诉Vue追踪数据
return value;
},
set(newval: T) {
clearTimeout(timeId);
timeId = setTimeout(() => {
value = newval;
trigger(); //告诉Vue更新界面
}, delay);
},
};
});
}
let d = useDebouncedRef(2, 2000); //两秒之后再更新
Reactive家族
- reactive用来定义对象、数组。(源码已经决定)
- shallowReactive :只能对浅层的数据 如果是深层的数据只会改变值 不会改变视图
let obj2 = shallowReactive({
a: 1,
b: {
c: "ddd",
},
});
function addValue() {
//obj.a=9 //会更新
obj2.b.c = "ceshh";//页面不变
}
to家族
- toRef:将数据转成响应式的,参数一为一个响应对象,参数二为参数一这个对象中的某个属性。并将这个属性扎转换为响应式数据。
const name = toRef(person,'name')
- toRefs:批量创建ref对象主要是方便我们解构使用
const obj = reactive({
foo: 1,
bar: 1
})
let { foo, bar } = toRefs(obj)
foo.value++
console.log(foo, bar);
- toRaw: 将响应式对象转化为普通对象
const obj = reactive({
foo: 1,
bar: 1
})
const state = toRaw(obj)
// 响应式对象转化为普通对象
const change = () => {
console.log(obj, state);
}
computed计算属性
改变值才会触发更改,否则取缓存中的
let m = computed<string>(()=>{
return `$` + price.value
})
先执行set再执行get
const newName = computed({
get: () => { // 3. 当 name 的值被修改后,触发 get 方法
return name.value + 10 // 95 + 10 = 105,所以newName 的值是 105
},
set: ( param ) => { // 2. 赋值的 100 ,会作为参数传递到 set 方法 ,
name.value = param - 5 // name.value 被修改 100 - 5 = 95 ,所以 name 的值是95
},
})
watch
共有三个参数,分别为:
name:需要帧听的属性;
(curVal,preVal)=>{ //业务处理 } 箭头函数,是监听到的最新值和本次修改之前的值,此处进行逻辑处理。
options :配置项,对监听器的配置,如:是否深度监听。
watch(name,(newval,oldval)=>{ },
{
deep:true,
immediate: true
})
可以放在一个数组里面监听
watch([name1,name2],(newval,oldval)=>{ },
{
deep:true,
immediate: true
})
监听reactive数据时,会默认打开deep:true,不用手动设置,获取到的oldval和newval一致。
监听reactive对象中的某一个属性,此时可以获取到oldval
注意:当reactive数据的一个属性是对象时,监听该对象,需手动设置深度监听,且设置深度监听后无法获取oldValue的值
watch(()=>obj.name,(newval,oldval)=>{ },
{//配置项})
watchEffect
传入的一个函数,当依赖项变化的时候,重新执行该函数。
- 非惰性:一旦运行就会立即执行;
- 使用时不需要具体指定监听的谁,回调函数内直接使用就可以;
- 不可访问之前的值:只能访问当前最新的值,访问不到修改之前的值;
watchEffect 会返回一个用于停止这个监听的函数,如法如下:
const stop = watchEffect(() => {
/* ... */
})
stop()
- onInvalidate
1.该函数总是在watchEffect执行的时候再次执行
2.当组件被销毁的时候该函数再次执行
3.该函数总是优先于watchEffect中的同步/异步代码执行
4.Promize函数的执行应该在该函数下面
watchEffect((onInvalidate) => {
// 异步api调用,返回一个操作对象
const apiCall = someAsyncMethod(props.userID)
onInvalidate(() => {
// 取消异步api的调用。
apiCall.cancel()
})
})
侦听器调试
watchEffect(() => {
console.log(`${sum.person.age} 的值变化了!`)
}, {
onTrack(e) { //追踪其依赖的时候触发,只能在开发者模式下使用
console.log(e.target)
},
onTrigger(e) { //依赖项被改变的时候触发,只能在开发者模式下使用
console.log(e.target)
}
})
生命周期
onMounted(() => { });
父子组件传值
- defineProps 父组件通过v-bind传递值给子组件,子组件使用defineProps接收,返回对象
//ts写法
const props = defineProps<{
title:string,
data:number[]
}>()
defineProps({
title:{
default:"",
type:string
},
data:Array
})
- defineEmits 派发一个事件
const emits = defineEmits(['changeTab']);//
emits('changeTab',data)
- defineExpose子组件暴露给父组件内部属性
//子组件
const list = reactive<number[]>([4, 5, 6])
defineExpose({
list
})
//父组件
const childReactiveRef = ref<HTMLDivElement | null>(null);
childReactiveRef.value.list
配置全局组件
在很多页面都会使用的一个组件,想要封装一个组件想在任何地方去使用,可以在main.ts中
import Card from './components/Card/index.vue'
createApp(App).component('Card',Card).mount('#app')//component 第一个参数组件名称 第二个参数组件实例
在其他页面直接使用即可
<template>
<Card></Card>
</template>
动态组件
切换componentId来切换组件
<component :is="componentId" ></component>
而我们组件代理之后毫无用处 节省性能开销 推荐我们使用shallowRef 或者 markRaw 跳过proxy 代理
import a from './a.vue'
import b from './b.vue'
import c from './c.vue'
import {markRaw, reactive, shallowRef} from 'vue'
// 这里也需要使用 shallowRef 去指定默认的
let componentId = shallowRef(a)
console.log(componentId)
let tabList = reactive([
{
name: '新闻',
com: markRaw(a)
},
{
name: '音乐',
com: shallowRef(b)
},
{
name: '教育',
com: markRaw(c)
}
])
const changeTab = (item) => {
componentId.value = item.com
}
插槽
- 匿名插槽
子组件有一个插槽
<template>
<div>
<slot></slot>
</div>
</template>
父组件使用插槽
<child>
<template v-slot>
<div>2132</div>
</template>
</child>
- 具名插槽
具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中
子组件
<slot name="header"></slot>
父组件
<child>
<template v-slot:name>
<div>2132</div>
</template>
</child>
简写:
<child>
<template #name>
<div>2132</div>
</template>
</child>
- 作用域插槽
子组件:
<slot :today="'星期二'"></slot>
父组件:
<template #default="{ today }">
<div>{{today}}</div>
</template>
具名使用传值
<template #name="{ today }">
<div>{{today}}</div>
</template>
Suspense和代码分包
为了让用户拥有更好的体验,在正式页面没有加载出来之前,给一个页面
等待异步组件时渲染一些额外内容,让应用有更好的用户体验
父组件引用子组件 通过defineAsyncComponent
加载异步配合import 函数模式便可以分包
const child = defineAsyncComponent(() => import("@/components/child.vue"));
<Suspense>
组件有两个插槽。它们都只接收一个直接子节点。default 插槽里的节点会尽可能展示出来。如果不能,则展示 fallback 插槽里的节点。
<Suspense>
<template v-slot:default>
<child></child>
</template>
<template v-slot:fallback>
<span>加载中...请稍等</span>
</template>
</Suspense>
- 例如搭配 Suspense搭配async函数的setup
会等3000ms后才展示该组件
async setup() {
const p = new Promise((resolve) => {
setTimeout(() => {
resolve({ arr });
}, 3000);
});
return await p;
},
Teleport传送组件
Teleport 是一种能够将我们的模板渲染至指定DOM节点,不受父级style、v-show等属性影响 (当然v-if还是会影响显示)
可以使用多个
<Teleport to=".modal1">
<child></child>
</Teleport>
<Teleport to="body">
<child></child>
</Teleport>
Provide / Inject
有时我们可能不仅仅是从父组件传递到子组件,可能传递到孙组件等等…使用props一层一层传递非常麻烦,此时使用Provide / Inject。
父组件中
provide(‘num’,num) : 前一个参数想要传递的参数名字,第二个参数:参数值
<script setup lang="ts">
import { provide} from "vue";
let num =ref(0)
provide('num',num)
<script>
子组件:inject来接收
let num =inject('num')
ts版本的eventBus
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>()
TSX写法
因为个人习惯,主要使用模板语法,故简单了解TSX,不做赘述。
安装插件 npm install @vitejs/plugin-vue-jsx -D
注意: tsx不会自动解包使用ref加.vlaue
支持v-model v-show
import { ref } from 'vue'
let v = ref<string>('')
let flag = ref(false)
let arr=reactive([1,2])
const renderDom = () => {
return (
<>
<input v-model={v.value} type="text" />
<div>
{v.value}
</div>
<div v-show={flag.value}>景天</div>
{
flag.value ? <div>景天</div> : <div>雪见</div>
}
<div data-arr={arr}>1</div>
</>
)
}
export default renderDom
v-if 、v-for不支持
事件使用:onClick 格式
v-model
- 新增 支持多个v-model
- 新增 支持自定义 修饰符
- 将移除v-bind的.sync修饰符使用带参数的v-model替代。
<!-- 简写 -->
<child v-model="xxx">
<!-- 完整语法 -->
<child :model-value="xxx" @update:model-value="newValue => { xxx = newValue }"/>
在 vue3 中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件
使用:
父组件:可以绑定多个v-model
<child v-model:num='num'></child>
子组件:使用update:参数名
就可以更改 父组件中num的值
type Props = {
num: number;
};
let props = defineProps<Props>();
const emit = defineEmits(["update:num"]);
function addValue() {
emit("update:num", 9);
}
在vue2中,使用 v-bind.sync实现,vue3废弃
父组件:
<comp2 v-bind.sync="propsObj"></comp2>
propsObj: {
title:'一个标题',
name: 'wang'
}
子组件
changeName() {
this.$emit('update:name', '新的名字')
}
props: {
// propsObj 对象里的属性分开写
title: String,
name: String
},
自定义指令directive
- created:在绑定元素的 attribute 或事件监听器被应用之前调用。在指令需要附加在普通的 v-on事件监听器调用前的事件监听器中时,这很有用。
- beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用。
- mounted:在绑定元素的父组件被挂载后调用,大部分自定义指令都写在这里。
- beforeUpdate:在更新包含组件的 VNode之前调用。
- updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用。
- beforeUnmount:在卸载绑定元素的父组件之前调用
- unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次。
Vue2 指令 bind inserted update componentUpdated unbind
Vue3定义全局函数和变量
vue2定义全局变量使用prototype
Vue.prototype.$http = () => {}
vue3:使用globalProperties
const app = createApp({})
app.config.globalProperties.$http = () => {}
hooks
Vue3 的 hook函数 相当于 vue2 的 mixin, 不同在与 hooks 是函数
Vue3 的 hook函数 可以帮助我们提高代码的复用性, 让我们能在不同的组件中都利用 hooks 函数
Style
- slotted
作用域样式不会影响<slot>
渲染的内容,要使用slotted会影响。
:slotted(.a) {
color:red
}
- 全局选择器
:global
- v-bind
<template>
<div class="div">
小满是个弟弟
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue"
const red = ref({
color:'pink'
})
</script>
<style lang="less" scoped>
.div {
color: v-bind('red.color');
}
</style>
- css module
TSX 和 render 函数使用居多
<style module>
标签会被编译为 CSS Modules 并且将生成的 CSS 类作为 $style 对象的键暴露给组件
<template>
<div :class="$style.red">
小满是个弟弟
</div>
</template>
<style module>
.red {
color: red;
font-size: 20px;
}
</style>
可以通过给 module attribute 一个值来自定义注入的类对象的 property 键
<template>
<div :class="[zs.red,zs.border]">
小满是个弟弟
</div>
</template>
<style module="zs">
.red {
color: red;
font-size: 20px;
}
.border{
border: 1px solid #ccc;
}
</style>