1. 認識Vue3
1) 了解相關信息
Vue 團隊于 2020 年 9 月 18 日晚 11 點半發(fā)布了 Vue 3.0 版本
耗時2年多徘郭,4000+次提交,40+RFC妒蔚,2500+PR穿挨,99+貢獻者月弛。
Vue3支持vue2的大多數(shù)特性
更好的支持Typescript
2) 性能提升:
打包大小減少41%
初次渲染快55%, 更新渲染快133%
內(nèi)存減少54%
使用Proxy代替defineProperty實現(xiàn)數(shù)據(jù)響應式
重寫虛擬DOM的實現(xiàn)和Tree-Shaking
3) 新增特性
Composition (組合) API
-
setup
ref 和 reactive
computed 和 watch
新的生命周期函數(shù)
provide與inject
...
-
新組件
Fragment - 文檔碎片
Teleport - 瞬移組件的位置
Suspense - 異步加載組件的loading界面
-
其它API更新
全局API的修改
將原來的全局API轉(zhuǎn)移到應用對象
模板語法變化
2. 創(chuàng)建vue3項目
1) 使用 vue-cli 創(chuàng)建
文檔: https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create
npm install -g @vue/cli
## 保證 vue cli 版本在 4.5.0 以上
vue --version
## 創(chuàng)建項目
vue create my-project
然后的步驟
Please pick a preset - 選擇 Manually select features
Check the features needed for your project - 選擇上 TypeScript ,特別注意點空格是選擇科盛,點回車是下一步
Choose a version of Vue.js that you want to start the project with - 選擇 3.x (Preview)
Use class-style component syntax - 直接回車
Use Babel alongside TypeScript - 直接回車
Pick a linter / formatter config - 直接回車
Use history mode for router? - 直接回車
Pick a linter / formatter config - 直接回車
Pick additional lint features - 直接回車
Where do you prefer placing config for Babel, ESLint, etc.? - 直接回車
Save this as a preset for future projects? - 直接回車
2) 使用 vite 創(chuàng)建
vite 是一個由原生 ESM 驅(qū)動的 Web 開發(fā)構(gòu)建工具帽衙。在開發(fā)環(huán)境下基于瀏覽器原生 ES imports 開發(fā),
-
它做到了本地快速開發(fā)啟動, 在生產(chǎn)環(huán)境下基于 Rollup 打包贞绵。
快速的冷啟動厉萝,不需要等待打包操作;
即時的熱模塊更新榨崩,替換性能和模塊數(shù)量的解耦讓更新飛起谴垫;
真正的按需編譯,不再等待整個應用編譯完成母蛛,這是一個巨大的改變翩剪。
3. Composition API(常用部分)
文檔:
https://composition-api.vuejs.org/zh/api.html
1) setup
新的option, 所有的組合API函數(shù)都在此使用, 只在初始化時執(zhí)行一次
函數(shù)如果返回對象, 對象中的屬性或方法, 模板中可以直接使用
2) ref
作用: 定義一個數(shù)據(jù)的響應式
-
語法: const xxx = ref(initValue):
創(chuàng)建一個包含響應式數(shù)據(jù)的引用(reference)對象
js中操作數(shù)據(jù): xxx.value
模板中操作數(shù)據(jù): 不需要.value
一般用來定義一個基本類型的響應式數(shù)據(jù)
<template>
<h2>{{count}}</h2>
<hr>
<button @click="update">更新</button>
</template>
<script>
import { ref } from 'vue'
export default {
/* 使用vue3的composition API */
setup () {
// 定義響應式數(shù)據(jù) ref對象
const count = ref(1)
console.log(count)
// 更新響應式數(shù)據(jù)的函數(shù)
function update () {
// alert('update')
count.value += 1
}
return {
count,
update
}
}
}
</script>
3) reactive
作用: 定義多個數(shù)據(jù)的響應式
const proxy = reactive(obj): 接收一個普通對象然后返回該普通對象的響應式代理器對象
響應式轉(zhuǎn)換是“深層的”:會影響對象內(nèi)部所有嵌套的屬性
內(nèi)部基于 ES6 的 Proxy 實現(xiàn),通過代理對象操作源對象內(nèi)部數(shù)據(jù)都是響應式的
<template>
<h2>name: {{state.name}}</h2>
<h2>age: {{state.age}}</h2>
<h2>wife: {{state.wife}}</h2>
<hr>
<button @click="update">更新</button>
</template>
<script>
/*
reactive:
作用: 定義多個數(shù)據(jù)的響應式
const proxy = reactive(obj): 接收一個普通對象然后返回該普通對象的響應式代理器對象
響應式轉(zhuǎn)換是“深層的”:會影響對象內(nèi)部所有嵌套的屬性
內(nèi)部基于 ES6 的 Proxy 實現(xiàn)彩郊,通過代理對象操作源對象內(nèi)部數(shù)據(jù)都是響應式的
*/
import { reactive } from 'vue'
export default {
setup () {
/*
定義響應式數(shù)據(jù)對象
*/
const state = reactive({
name: 'tom',
age: 25,
wife: {
name: 'marry',
age: 22
},
})
console.log(state, state.wife)
const update = () => {
state.name += '--'
state.age += 1
state.wife.name += '++'
state.wife.age += 2
}
return {
state,
update,
}
}
}
</script>
4) 比較Vue2與Vue3的響應式(重要)
vue2的響應式
-
核心:
對象: 通過defineProperty對對象的已有屬性值的讀取和修改進行劫持(監(jiān)視/攔截)
數(shù)組: 通過重寫數(shù)組更新數(shù)組一系列更新元素的方法來實現(xiàn)元素修改的劫持
Object.defineProperty(data, 'count', {
get () {},
set () {}
})
-
問題
對象直接新添加的屬性或刪除已有屬性, 界面不會自動更新
直接通過下標替換元素或更新length, 界面不會自動更新 arr[1] = {}
Vue3的響應式
-
核心:
通過Proxy(代理): 攔截對data任意屬性的任意(13種)操作, 包括屬性值的讀寫, 屬性的添加, 屬性的刪除等...
通過 Reflect(反射): 動態(tài)對被代理對象的相應屬性進行特定的操作
-
文檔:
new Proxy(data, {
// 攔截讀取屬性值
get (target, prop) {
return Reflect.get(target, prop)
},
// 攔截設置屬性值或添加新屬性
set (target, prop, value) {
return Reflect.set(target, prop, value)
},
// 攔截刪除屬性
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})
proxy.name = 'tom'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proxy 與 Reflect</title>
</head>
<body>
<script>
const user = {
name: "apple",
age: 12
};
/*
proxyUser是代理對象, user是被代理對象
后面所有的操作都是通過代理對象來操作被代理對象內(nèi)部屬性
*/
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);
},
deleteProperty (target, prop) {
console.log('劫持到了delete屬性~', prop)
return Reflect.deleteProperty(target, prop)
}
});
// 讀取屬性值
//console.log(proxyUser===user)
//console.log('proxyUser.name:::', proxyUser.name)
//console.log('proxyUser.user:::', proxyUser.age)
// 設置屬性值
//proxyUser.name = 'orange'
//proxyUser.age = 13
//console.log('user:::', user)
// 添加屬性
//proxyUser.sex = '男'
//console.log('user:::', user)
// 刪除屬性
//delete proxyUser.sex
//console.log('user:::', user)
</script>
</body>
</html>
5) setup細節(jié)
-
setup執(zhí)行的時機
在beforeCreate之前執(zhí)行(一次), 此時組件對象還沒有創(chuàng)建
this是undefined, 不能通過this來訪問data/computed/methods / props
其實所有的composition API相關回調(diào)函數(shù)中也都不可以
-
setup的返回值
一般都返回一個對象: 為模板提供數(shù)據(jù), 也就是模板中可以直接使用此對象中的所有屬性/方法
返回對象中的屬性會與data函數(shù)返回對象的屬性合并成為組件對象的屬性
返回對象中的方法會與methods中的方法合并成功組件對象的方法
如果有重名, setup優(yōu)先
-
注意:
一般不要混合使用: methods中可以訪問setup提供的屬性和方法, 但在setup方法中不能訪問data和methods
setup不能是一個async函數(shù): 因為返回值不再是return的對象, 而是promise, 模板看不到return對象中的屬性數(shù)據(jù)
-
setup的參數(shù)
setup(props, context) / setup(props, {attrs, slots, emit})
props: 包含props配置聲明且傳入了的所有屬性的對象
attrs: 包含沒有在props配置中聲明的屬性的對象, 相當于 this.$attrs
slots: 包含所有傳入的插槽內(nèi)容的對象, 相當于 this.$slots
emit: 用來分發(fā)自定義事件的函數(shù), 相當于 this.$emit
<template>
<h2>App</h2>
<p>msg: {{msg}}</p>
<button @click="fn('--')">更新</button>
<child :msg="msg" msg2="cba" @fn="fn"/>
</template>
<script lang="ts">
import { reactive, ref } from 'vue'
import child from './child.vue'
export default {
components: { child },
setup () {
const msg = ref('abc')
function fn (content: string) {
msg.value += content
}
return {
msg,
fn
}
}
}
</script>
<template>
<div>
<h3>{{n}}</h3>
<h3>{{m}}</h3>
<h3>msg: {{msg}}</h3>
<h3>msg2: {{$attrs.msg2}}</h3>
<slot name="xxx"></slot>
<button @click="update">更新</button>
</div>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue'
export default defineComponent({
name: 'child',
props: ['msg'],
emits: ['fn'], // 可選的, 聲明了更利于程序員閱讀, 且可以對分發(fā)的事件數(shù)據(jù)進行校驗
setup (props, {attrs, emit, slots}) {
console.log(props.msg, attrs.msg2, slots, emit)
const m = ref(2)
const n = ref(3)
function update () {
m.value += 2
n.value += 2
// 分發(fā)自定義事件
emit('fn', '++')
}
return {
m,
n,
update,
}
},
})
</script>
6) reactive與ref-細節(jié)
是Vue3的 composition API中2個最重要的響應式API
ref用來處理基本類型數(shù)據(jù), reactive用來處理對象(遞歸深度響應式)
如果用ref對象/數(shù)組, 內(nèi)部會自動將對象/數(shù)組轉(zhuǎn)換為reactive的代理對象
ref內(nèi)部: 通過給value屬性添加getter/setter來實現(xiàn)對數(shù)據(jù)的劫持
reactive內(nèi)部: 通過使用Proxy來實現(xiàn)對對象內(nèi)部所有數(shù)據(jù)的劫持, 并通過Reflect操作對象內(nèi)部數(shù)據(jù)
ref的數(shù)據(jù)操作: 在js中要.value, 在模板中不需要(內(nèi)部解析模板時會自動添加.value)
<template>
<h2>App</h2>
<p>m1: {{m1}}</p>
<p>m2: {{m2}}</p>
<p>m3: {{m3}}</p>
<button @click="update">更新</button>
</template>
<script>
import {
reactive,
ref
} from 'vue'
export default {
setup () {
const m1 = ref('abc')
const m2 = reactive({x: 1, y: {z: 'abc'}})
// 使用ref處理對象 ==> 對象會被自動reactive為proxy對象
const m3 = ref({a1: 2, a2: {a3: 'abc'}})
console.log(m1, m2, m3)
console.log(m3.value.a2) // 也是一個proxy對象
function update() {
m1.value += '--'
m2.x += 1
m2.y.z += '++'
m3.value = {a1: 3, a2: {a3: 'abc---'}}
m3.value.a2.a3 += '==' // reactive對對象進行了深度數(shù)據(jù)劫持
console.log(m3.value.a2)
}
return {
m1,
m2,
m3,
update
}
}
}
</script>
7) 計算屬性與監(jiān)視
-
computed函數(shù):
與computed配置功能一致
只有g(shù)etter
有g(shù)etter和setter
-
watch函數(shù)
與watch配置功能一致
監(jiān)視指定的一個或多個響應式數(shù)據(jù), 一旦數(shù)據(jù)變化, 就自動執(zhí)行監(jiān)視回調(diào)
默認初始時不執(zhí)行回調(diào), 但可以通過配置immediate為true, 來指定初始時立即執(zhí)行第一次
通過配置deep為true, 來指定深度監(jiān)視
-
watchEffect函數(shù)
不用直接指定要監(jiān)視的數(shù)據(jù), 回調(diào)函數(shù)中使用的哪些響應式數(shù)據(jù)就監(jiān)視哪些響應式數(shù)據(jù)
默認初始時就會執(zhí)行第一次, 從而可以收集需要監(jiān)視的數(shù)據(jù)
監(jiān)視數(shù)據(jù)發(fā)生變化時回調(diào)
<template>
<h2>App</h2>
fistName: <input v-model="user.firstName"/><br>
lastName: <input v-model="user.lastName"/><br>
fullName1: <input v-model="fullName1"/><br>
fullName2: <input v-model="fullName2"><br>
fullName3: <input v-model="fullName3"><br>
</template>
<script>
import {
reactive,
ref,
computed,
watch,
watchEffect
} from 'vue'
export default {
setup () {
const user = reactive({
firstName: 'A',
lastName: 'B'
})
// 1.只有g(shù)etter的計算屬性
const fullName1 = computed(() => {
console.log('fullName1')
return user.firstName + '-' + user.lastName
})
// 2.有g(shù)etter與setter的計算屬性
const fullName2 = computed({
get () {
console.log('fullName2 get')
return user.firstName + '-' + user.lastName
},
set (value) {
console.log('fullName2 set')
const names = value.split('-')
user.firstName = names[0]
user.lastName = names[1]
}
})
const fullName3 = ref('')
/* 使用watch的2個特性:深度監(jiān)視和初始化立即執(zhí)行 */
watch(user, () => {
fullName3.value = user.firstName + '-' + user.lastName
}, {
immediate: true, // 是否初始化立即執(zhí)行一次, 默認是false
deep: true, // 是否是深度監(jiān)視, 默認是false
})
/*
watch一個ref對象數(shù)據(jù)
默認在數(shù)據(jù)發(fā)生改變時執(zhí)行回調(diào)
*/
watch(fullName3, (value) => {
console.log('watch')
const names = value.split('-')
user.firstName = names[0]
user.lastName = names[1]
})
/*
watch多個數(shù)據(jù):
使用數(shù)組來指定
如果是ref對象, 直接指定
如果是reactive對象中的屬性, 必須通過函數(shù)來指定
*/
watch([() => user.firstName, () => user.lastName, fullName3], (values) => {
console.log('監(jiān)視多個數(shù)據(jù)', values)
})
/*
watchEffect: 監(jiān)視所有回調(diào)中使用的數(shù)據(jù)
*/
/*
watchEffect(() => {
console.log('watchEffect')
fullName3.value = user.firstName + '-' + user.lastName
})
*/
return {
user,
fullName1,
fullName2,
fullName3
}
}
}
</script>
8) 生命周期
vue2.x的生命周期
[圖片上傳失敗...(image-3484e0-1644895211640)]
vue3的生命周期
[圖片上傳失敗...(image-cb9666-1644895211640)]
與 2.x 版本生命周期相對應的組合式 API
beforeCreate
-> 使用setup()
created
-> 使用setup()
beforeMount
->onBeforeMount
mounted
->onMounted
beforeUpdate
->onBeforeUpdate
updated
->onUpdated
beforeDestroy
->onBeforeUnmount
destroyed
->onUnmounted
errorCaptured
->onErrorCaptured
新增的鉤子函數(shù)
組合式 API 還提供了以下調(diào)試鉤子函數(shù):
onRenderTracked
onRenderTriggered
<div class="about">
<h2>msg: {{msg}}</h2>
<hr>
<button @click="update">更新</button>
</div>
</template>
<script lang="ts">
import {
ref,
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount
} from "vue"
export default {
beforeCreate () {
console.log('beforeCreate()')
},
created () {
console.log('created')
},
beforeMount () {
console.log('beforeMount')
},
mounted () {
console.log('mounted')
},
beforeUpdate () {
console.log('beforeUpdate')
},
updated () {
console.log('updated')
},
beforeUnmount () {
console.log('beforeUnmount')
},
unmounted () {
console.log('unmounted')
},
setup() {
const msg = ref('abc')
const update = () => {
msg.value += '--'
}
onBeforeMount(() => {
console.log('--onBeforeMount')
})
onMounted(() => {
console.log('--onMounted')
})
onBeforeUpdate(() => {
console.log('--onBeforeUpdate')
})
onUpdated(() => {
console.log('--onUpdated')
})
onBeforeUnmount(() => {
console.log('--onBeforeUnmount')
})
onUnmounted(() => {
console.log('--onUnmounted')
})
return {
msg,
update
}
}
}
</script>
<h2>App</h2>
<button @click="isShow=!isShow">切換</button>
<hr>
<Child v-if="isShow"/>
</template>
<script lang="ts">
import Child from './Child.vue'
export default {
data () {
return {
isShow: true
}
},
components: {
Child
}
}
</script>
9) toRefs
把一個響應式對象轉(zhuǎn)換成普通對象前弯,該普通對象的每個 property 都是一個 ref
應用: 當從合成函數(shù)返回響應式對象時,toRefs 非常有用焦辅,這樣消費組件就可以在不丟失響應式的情況下對返回的對象進行分解使用
問題: reactive 對象取出的所有屬性值都是非響應式的
解決: 利用 toRefs 可以將一個響應式 reactive 對象的所有原始屬性轉(zhuǎn)換為響應式的 ref 屬性
<h2>App</h2>
<h3>foo: {{foo}}</h3>
<h3>bar: {{bar}}</h3>
<h3>foo2: {{foo2}}</h3>
<h3>bar2: {{bar2}}</h3>
</template>
<script lang="ts">
import { reactive, toRefs } from 'vue'
/*
toRefs:
將響應式對象中所有屬性包裝為ref對象, 并返回包含這些ref對象的普通對象
應用: 當從合成函數(shù)返回響應式對象時博杖,toRefs 非常有用,
這樣消費組件就可以在不丟失響應式的情況下對返回的對象進行分解使用
*/
export default {
setup () {
const state = reactive({
foo: 'a',
bar: 'b',
})
const stateAsRefs = toRefs(state)
setTimeout(() => {
state.foo += '++'
state.bar += '++'
}, 2000);
const {foo2, bar2} = useReatureX()
return {
// ...state,
...stateAsRefs,
foo2,
bar2
}
},
}
function useReatureX() {
const state = reactive({
foo2: 'a',
bar2: 'b',
})
setTimeout(() => {
state.foo2 += '++'
state.bar2 += '++'
}, 2000);
return toRefs(state)
}
</script>
10) provide和inject
利用ref函數(shù)獲取組件中的標簽元素
provide :向子組件以及子孫組件傳遞數(shù)據(jù)筷登。接收兩個參數(shù)剃根,第一個參數(shù)是 key,即數(shù)據(jù)的名稱前方;第二個參數(shù)為 value狈醉,即數(shù)據(jù)的值 inject :接收父組件或祖先組件傳遞過來的數(shù)據(jù)。接收一個參數(shù) key惠险,即父組件或祖先組件傳遞的數(shù)據(jù)名稱
import {provide} from 'vue'
export default {
setup() {
const obj= {
name: 'apple',
color: 'red'
}
// 向子組件以及子孫組件傳遞名為info的數(shù)據(jù)
provide('info', obj)
}
}
// B.vue
<div>{{ form.name }}</div>
import {inject} from 'vue'
export default {
setup() {
// 接收A.vue傳遞過來的數(shù)據(jù)
const form= inject('info') // { name: 'apple', color: 'red'}
}
}
11) ref獲取元素
利用ref函數(shù)獲取組件中的標簽元素
功能需求: 讓輸入框自動獲取焦點
<template>
<h2>App</h2>
<input type="text">
<input type="text" ref="inputRef">
</template>
<script>
import { onMounted, ref } from 'vue'
/*
ref獲取元素: 利用ref函數(shù)獲取組件中的標簽元素
功能需求: 讓輸入框自動獲取焦點
*/
export default {
setup() {
const inputRef = ref(null)
onMounted(() => {
inputRef.value && inputRef.value.focus()
})
return {
inputRef
}
},
}
</script>
4. 新組件
1) Fragment(片斷)
在Vue2中: 組件必須有一個根標簽
在Vue3中: 組件可以沒有根標簽, 內(nèi)部會將多個標簽包含在一個Fragment虛擬元素中
好處: 減少標簽層級, 減小內(nèi)存占用
<template>
<h2>aaaa</h2>
<h2>aaaa</h2>
</template>
2) Teleport(瞬移)
- Teleport 提供了一種干凈的方法, 讓組件的html在父組件界面外的特定標簽(很可能是body)下插入顯示
ModalButton.vue
<template>
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'modal-button',
setup () {
const modalOpen = ref(false)
return {
modalOpen
}
}
}
</script>
<style>
.modal {
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
background-color: rgba(0,0,0,.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.modal div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: white;
width: 300px;
height: 300px;
padding: 5px;
}
</style>
App.vue
<template>
<h2>App</h2>
<modal-button></modal-button>
</template>
<script>
import ModalButton from './ModalButton.vue'
export default {
setup() {
return {
}
},
components: {
ModalButton
}
}
</script>
3) Suspense(不確定的)
- 它們允許我們的應用程序在等待異步組件時渲染一些后備內(nèi)容苗傅,可以讓我們創(chuàng)建一個平滑的用戶體驗
<template>
<Suspense>
<template v-slot:default>
<AsyncComp/>
<!-- <AsyncAddress/> -->
</template>
<template v-slot:fallback>
<h1>LOADING...</h1>
</template>
</Suspense>
</template>
<script lang="ts">
/*
異步組件 + Suspense組件
*/
// import AsyncComp from './AsyncComp.vue'
import AsyncAddress from './AsyncAddress.vue'
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))
export default {
setup() {
return {
}
},
components: {
AsyncComp,
AsyncAddress
}
}
</script>
- AsyncComp.vue
<template>
<h2>AsyncComp22</h2>
<p>{{msg}}</p>
</template>
<script lang="ts">
export default {
name: 'AsyncComp',
setup () {
// return new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve({
// msg: 'abc'
// })
// }, 2000)
// })
return {
msg: 'abc'
}
}
}
</script>
- AsyncAddress.vue
<template>
<h2>{{data}}</h2>
</template>
<script lang="ts">
import axios from 'axios'
export default {
async setup() {
const result = await axios.get('/data/address.json')
return {
data: result.data
}
}
}
</script>