② 初識vue3.0:新特性講解
1 vue3 新特性巡禮
1.1 效能提升
-
打包減小
-
初次渲染加快、更新加快
-
記憶體使用減少
得益於重寫虛擬 DOM 的實現和 Tree shanking 的優化
1.2 Composition API
-
ref 和 reactive
-
computed 和 watch
-
新的生命週期函式
-
自定義函式 -- Hooks 函式
1.3 其他新增特性
-
Teleport -- 瞬移元件的位置
-
Suspense -- 非同步載入元件的新福音
-
全域性 API 的優化和修改
-
更多試驗性特性
1.4 更好的 ts 支援
2 為什麼會有 vue3
- vue2 遇到的難題
1. 隨著功能的增長,複雜元件的程式碼變得難以維護
2. 隨著複雜度的上升,帶來的問題
mixin 的解決方案
const filterMixin = {
data() {
return {}
},
methods: {}
}
const paginationMixin = {
data() {
return {}
},
methods: {}
}
export default {
mixin: [ filterMixin, paginationMixin ]
}
mixin 的缺點
-
命名衝突
-
不清楚暴露出來變數的作用
-
重用到其他元件時會出現問題
3. vue2 對於 ts 的支援非常有限
3 vue3 - ref 的妙用
-
setup
-
ref:處理基本型別的資料
-
computed
import { ref, computed } from 'vue' export default { name: 'App', setup() { const count = ref(0) const double = computed(() => { return count.value * 2 }) const increase = () => { count.value++ } return { count, double, increase } } }
4 更近一步 - reactive
-
reactive:處理複雜型別的資料
-
reactive + toRefs(將物件屬性轉化為響應式屬性)
使用 toRefs 保證 reactive 物件屬性保持響應性
import { computed, reactive, toRefs } from 'vue'
interface DataProps {
count: number,
double: number,
increase: () => void
}
export default {
name: 'App',
setup() {
const data: DataProps = reactive({
count: 0,
increase: () => { data.count++ },
double: computed(() => data.count * 2)
})
// 只有 data 是響應式的 其屬性非響應式
const refData = toRefs(data)
return {
...refData
}
}
}
5 vue3 響應式物件的新花樣
響應式原理
- vue2
Object.definedProperty(data, 'count', {
get() {},
set() {}
})
- vue3
new Proxy(data, {
get(key) {},
set(key, value) {}
})
- vue3應用
import { computed, reactive, toRefs } from 'vue'
interface DataProps {
numbers: number[];
person: { name?: string };
}
export default {
name: 'App',
setup() {
const data: DataProps = reactive({
numbers: [0, 1, 2],
person: {}
})
data.numbers[0] = 5
data.person.name = 'viking'
const refData = toRefs(data)
return {
...refData
}
}
}
6 老瓶新酒 - 生命週期
在 setup 中使用的 hook 名稱和原來生命週期的對應關係
-
beforeCreate
-> use setup() -
created
-> use setup() -
beforeMount
->onBeforeMount
-
mounted
->onMounted
-
beforeUpdate
->onBeforeUpdate
-
updated
->onUpdated
-
beforeUnmount
->onBeforeUnmount
-
unmounted
->onUnmounted
-
errorCaptured
->onErrorCaptured
-
renderTracked
->onRenderTracked
每次渲染後重新收集響應式依賴時執行 -
renderTriggered
->onRenderTriggered
每次觸發頁面重新渲染時執行
import { onMounted, onUpdated, onRenderTriggered, onRenderTracked } from 'vue'
export default {
name: 'App',
setup() {
onMounted(() => {
console.log('mounted')
})
onUpdated(() => {
console.log('updated')
})
onRenderTriggered(() => {
console.log('renderTriggered') // 點選按鈕時觸發
})
onRenderTracked(() => {
console.log('renderTracked') // 頁面重新整理時觸發
})
}
}
7 偵測變化 - watch
- 一般用法
setup() {
const greetings = ref('')
const updateGreeting = () => {
greetings.value += 'Hello!'
}
watch(greetings, (newVal, oldVal) => {
console.log(newVal, oldVal);
document.title = 'updated '
})
return {
updateGreeting,
}
}
- 偵聽 reactive 方法下的資料 -- getter 寫法 -- 使用箭頭函式
setup() {
const data: DataProps = reactive({
count: 0,
})
const greetings = ref('')
const updateGreeting = () => {
greetings.value += 'Hello!'
}
watch([greetings, () => data.count], (newVal, oldVal) => {
console.log(newVal, oldVal);
document.title = 'updated ' + greetings.value + data.count
})
// 只有 data 是響應式的 其屬性非響應式
const refData = toRefs(data)
return {
updateGreeting,
...refData
}
}
8 模組化開發 -- 滑鼠追蹤器
hooks > useMousePosition.ts
import { reactive, toRefs, onMounted, onUnmounted } from 'vue'
function useMousePosition() {
const data = reactive({
x: 0,
y: 0
})
const updateMouse = (e: MouseEvent) => {
data.x = e.pageX
data.y = e.pageY
}
onMounted(() => {
document.addEventListener('click', updateMouse)
})
onUnmounted(() => {
document.removeEventListener('click', updateMouse)
})
const refData = toRefs(data)
return { ...refData }
}
export default useMousePosition
App.vue
import useMousePosition from './hooks/useMousePosition'
export default {
setup() {
const { x, y } = useMousePosition()
return {
x, y,
}
}
}
優點
-
可以清楚地知道 x y 的來源,這兩個引數是幹什麼的
-
可以給 x y 設定任何別名,避免了命名衝突的風險
-
這段邏輯可脫離元件存在,只有邏輯程式碼不需要模板
9 模組化難度上升 - useURLLoader
hooks > useURLLoader.ts
import { ref } from 'vue'
import axios from 'axios'
function useURLLoader(url: string) {
const result = ref(null)
const loading = ref(true)
const loaded = ref(false)
const error = ref(null)
axios.get(url).then(rawData => {
loading.value = false
loaded.value = true
result.value = rawData.data
}).catch(e => {
error.value = e
loading.value = false
})
return {
result, loading, loaded, error
}
}
export default useURLLoader
App.vue
import useURLLoader from './hooks/useURLLoader'
export default {
setup() {
const { result, loading, loaded } = useURLLoader('https://dog.ceo/api/breeds/image/random')
return {
result, loading, loaded
}
}
}
10 模組化結合 typescript - 泛型改造
hooks > useURLLoader.ts
function useURLLoader<T>(url: string) {
const result = ref<T | null>(null)
// ...
}
export default useURLLoader
App.vue
- 展示狗狗圖片
import useURLLoader from './hooks/useURLLoader'
interface DogResult {
message: string;
status: string;
}
export default {
name: 'App',
setup() {
const { result, loading, loaded } = useURLLoader<DogResult>('https://dog.ceo/api/breeds/image/random')
watch(result, () => {
if(result.value) {
console.log(result.value.message);
}
})
return {
result, loading, loaded
}
}
}
- 展示貓貓圖片
import useURLLoader from './hooks/useURLLoader'
interface CatResult {
id: string;
url: string;
width: number;
height: number;
}
export default {
name: 'App',
setup() {
const { result, loading, loaded } = useURLLoader<CatResult[]>('https://api.thecatapi.com/v1/images/search?limit=1')
watch(result, () => {
if(result.value) {
console.log(result.value[0].url);
}
})
return {
result, loading, loaded
}
}
}
11 Typescript 對 vue3 的加持
使用 defineComponent 包裹元件 + setup 函式引數
- components > HelloWorld
import { defineComponent } from 'vue';
export default defineComponent({
name: 'HelloWorld',
props: {
msg: {
required: true,
type: String
}
},
setup(props, context) {
console.log(props.msg)
const { attrs, slots, emit } = context
}
});
12 元件 Teleport - 瞬間移動
-
Teleport
上有一個to
的屬性,它接受一個 css query selector 作為引數,代表要把這個元件渲染到哪個 dom 元素中
12.1 原始 modal
元件
1. components > Modal.vue
<teleport to="#modal">
<div id="center">
<h1>this is a modal</h1>
</div>
</teleport>
2. App.vue
<!-- ... -->
<Modal />
<!-- ... -->
<script lang="ts">
import Modal from './components/Modal.vue'
export default {
components: { Modal }
// ...
}
</script>
3. public > index.html
<div id="app"></div>
<div id="modal"></div>
4. 渲染結果
12.2 實現 modal
的開啟關閉
-
控制組件的顯示與否 -- isOpen
-
自定義 content --
slot
-
關閉 modal -- 使用
emit
觸發
1. components > Modal.vue
-
文件化 emits-- 一目瞭然要觸發的事件
-
支援執行時檢驗
-
支援 自動補全
<teleport to="#modal">
<div id="center" v-if="isOpen">
<h1>
<slot>
this is a modal
</slot>
</h1>
<button @click="buttonClick">Close</button>
</div>
</teleport>
export default defineComponent({
props: {
isOpen: Boolean
},
// 更明確的顯示元件的自定義事件
emits: {
'close-modal': null
},
setup(props, context) {
const buttonClick = () => {
context.emit('close-modal')
}
return {
buttonClick
}
}
})
2. App.vue
<Modal :isOpen="modalIsOpen" @close-modal="onModalClose">
My Modal !!!
</Modal>
<button @click="openModal">open Modal</button>
setup() {
const modalIsOpen = ref(false)
const openModal = () => {
modalIsOpen.value = true
}
const onModalClose = () => {
modalIsOpen.value = false
}
return {
modalIsOpen, openModal, onModalClose
}
}
13 元件 Suspense - 非同步請求好幫手
- 元件使用
Suspense
,在 setup 中需要返回一個Promise
13.1 簡單用法
1. components > AsyncShow.vue
<template>
<h1>{{ result }}</h1>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
return new Promise ((resolve) => {
setTimeout(() => {
return resolve({
result: 42
})
}, 3000);
})
}
})
</script>
2. App.vue
<Suspense>
<template #default>
<async-show />
</template>
<template #fallback>
<h1>Loading !...</h1>
</template>
</Suspense>
import AsyncShow from './components/AsyncShow.vue'
export default {
components: { AsyncShow }
}
13.2 使用 async await
1. components > DogShow.vue
<template>
<img :src="result && result.message" />
</template>
<script lang="ts">
import axios from 'axios'
import { defineComponent } from 'vue'
export default defineComponent({
async setup() {
const rawData = await axios.get('https://dog.ceo/api/breeds/image/random')
return {
result: rawData.data
}
}
})
</script>
2. App.vue
<Suspense>
<template #default>
<div>
<async-show />
<dog-show />
</div>
</template>
<template #fallback>
<h1>Loading !...</h1>
</template>
</Suspense>
import AsyncShow from './components/AsyncShow.vue'
import DogShow from './components/DogShow.vue'
export default {
components: { AsyncShow, DogShow }
}
13.3 Suspense 中的錯誤捕獲
import { onErrorCaptured } from 'vue'
setup() {
const error = ref(null)
onErrorCaptured((e: any) => {
error.value = e
return true
})
return {
error
}
}
14 Provide / Inject
14.1 父子元件傳值 -- prop
14.2 解決多層級傳值 -- Provide/Inject
- Provide/Inject:提供了一種在元件之間共享此類值的方式,而不必通過元件樹的每個層級顯式地傳遞 props
目的是為共享那些被認為對於一個元件樹而言是“全域性”的資料
1. provide -- 提供
export default {
provide: {
message: 'hello!'
}
}
- 應用級提供
import { createApp } from 'vue'
const app = createApp({})
app.provide('message', 'hello!')
2. inject -- 注入
export default {
inject: ['message'],
data() {
return {
fullMessage: this.message
}
}
}
- 注入別名
export default {
inject: {
localMessage: {
from: 'message'
}
}
}
- 注入預設值
export default {
inject: {
message: {
from: 'message',
default: 'default value'
},
user: {
default: () => ({ name: 'John' })
}
}
}
3. 使用響應性
- 使用
computed()
函式提供一個計算屬性 -- 使注入響應性地連結到提供者
import { computed } from 'vue'
export default {
data() {
return {
message: 'hello!'
}
},
provide() {
return {
// explicitly provide a computed property
message: computed(() => this.message)
}
}
}
15 全域性 API 修改
15.1 入口檔案
1. vue2 的全域性配置
import Vue from 'vue'
import App from './App.vue'
Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)
Vue.prototype.customProperty = () => {}
new Vue({
render: h => h(App)
}).$mount('#app')
不足
- 單元測試中,全域性配置容易汙染全域性環境
- 在不同 App 中,難以共享一份有不同配置的 Vue 物件
2. vue3 的全域性配置
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// app 就是一個 App 的例項,設定任何的配置是在不同的 app 例項上面的,不會像vue2 一樣發生任何的衝突
app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)
app.config.globalProperties.customProperty = () => {}
// 當配置結束以後,我們再把 App 使用 mount 方法掛載到固定的 DOM 的節點上。
app.mount(App, '#app')
15.2 全域性配置 Vue.config -> app.config
15.3 全域性註冊類 API
- Vue.component -> app.component
- Vue.directive -> app.directive
15.4 行為擴充套件類
- Vue.mixin -> app.mixin
- Vue.use -> app.use
15.5 Global API Treeshaking
Tree Shaking -- 用於描述移除 js 上下文中的未引用程式碼
1. vue2
import Vue from 'vue'
Vue.nextTick(() => {})
const obj = Vue.observable({})
2. vue3
import Vue, { nextTick, observable } from 'vue'
Vue.nextTick // undefined
nextTick(() => {})
const obj = observable({})