一、VUE3
1. 認識Vue3
1) 了解相關信息
- Vue.js 3.0 "One Piece" 正式版在今年9月份發布
- 2年多開發, 100+位貢獻者, 2600+次提交, 600+次PR
- Vue3支持vue2的大多數特性
- 更好的支持Typescript
2) 性能提升:
- 打包大小減少41%
- 初次渲染快55%, 更新渲染快133%
- 內存減少54%
- 使用Proxy代替defineProperty實現數據響應式
- 重寫虛擬DOM的實現和Tree-Shaking
3) 新增特性
- Composition (組合) API
- setup
- ref 和 reactive
- computed 和 watch
- 新的生命周期函數
- provide與inject
- ...
- 新組件
- Fragment - 文檔碎片
- Teleport - 瞬移組件的位置
- Suspense - 異步加載組件的loading界面
- 其它API更新
- 全局API的修改
- 將原來的全局API轉移到應用對象
- 模板語法變化
2. 創建vue3項目
1) 使用 vue-cli 創建
文檔: https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create
## 安裝或者升級
npm install -g @vue/cli
## 保證 vue cli 版本在 4.5.0 以上
vue --version
## 創建項目
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 創建
- 文檔: https://v3.cn.vuejs.org/guide/installation.html
- vite 是一個由原生 ESM 驅動的 Web 開發構建工具。在開發環境下基於瀏覽器原生 ES imports 開發,
- 它做到了本地快速開發啟動, 在生產環境下基於 Rollup 打包。
- 快速的冷啟動,不需要等待打包操作;
- 即時的熱模塊更新,替換性能和模塊數量的解耦讓更新飛起;
- 真正的按需編譯,不再等待整個應用編譯完成,這是一個巨大的改變。
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev
二、Composition API
1. Composition API(常用部分)
文檔:
https://composition-api.vuejs.org/zh/api.html
1) setup
- 新的option, 所有的組合API函數都在此使用, 只在初始化時執行一次
- 函數如果返回對象, 對象中的屬性或方法, 模板中可以直接使用
2) ref
- 作用: 定義一個數據的響應式
- 語法: const xxx = ref(initValue):
- 創建一個包含響應式數據的引用(reference)對象
- js中操作數據: xxx.value
- 模板中操作數據: 不需要.value
- 一般用來定義一個基本類型的響應式數據
<template>
<h2>{{count}}</h2>
<hr>
<button @click="update">更新</button>
</template>
<script>
import {
ref
} from 'vue'
export default {
/* 在Vue3中依然可以使用data和methods配置, 但建議使用其新語法實現 */
// data () {
// return {
// count: 0
// }
// },
// methods: {
// update () {
// this.count++
// }
// }
/* 使用vue3的composition API */
setup () {
// 定義響應式數據 ref對象
const count = ref(1)
console.log(count)
// 更新響應式數據的函數
function update () {
// alert('update')
count.value = count.value + 1
}
return {
count,
update
}
}
}
</script>
3) reactive
- 作用: 定義多個數據的響應式
- const proxy = reactive(obj): 接收一個普通對象然后返回該普通對象的響應式代理器對象
- 響應式轉換是“深層的”:會影響對象內部所有嵌套的屬性
- 內部基於 ES6 的 Proxy 實現,通過代理對象操作源對象內部數據都是響應式的
<template>
<h2>name: {{state.name}}</h2>
<h2>age: {{state.age}}</h2>
<h2>wife: {{state.wife}}</h2>
<hr>
<button @click="update">更新</button>
</template>
<script>
/*
reactive:
作用: 定義多個數據的響應式
const proxy = reactive(obj): 接收一個普通對象然后返回該普通對象的響應式代理器對象
響應式轉換是“深層的”:會影響對象內部所有嵌套的屬性
內部基於 ES6 的 Proxy 實現,通過代理對象操作源對象內部數據都是響應式的
*/
import {
reactive,
} from 'vue'
export default {
setup () {
/*
定義響應式數據對象
*/
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對對象的已有屬性值的讀取和修改進行劫持(監視/攔截)
- 數組: 通過重寫數組更新數組一系列更新元素的方法來實現元素修改的劫持
Object.defineProperty(data, 'count', {
get () {},
set () {}
})
- 問題
- 對象直接新添加的屬性或刪除已有屬性, 界面不會自動更新
- 直接通過下標替換元素或更新length, 界面不會自動更新 arr[1] = {}
Vue3的響應式
- 核心:
- 通過Proxy(代理): 攔截對data任意屬性的任意(13種)操作, 包括屬性值的讀寫, 屬性的添加, 屬性的刪除等...
- 通過 Reflect(反射): 動態對被代理對象的相應屬性進行特定的操作
- 文檔:
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: "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)
</script>
</body>
</html>
5) setup細節
- setup執行的時機
- 在beforeCreate之前執行(一次), 此時組件對象還沒有創建
- this是undefined, 不能通過this來訪問data/computed/methods / props
- 其實所有的composition API相關回調函數中也都不可以
- setup的返回值
- 一般都返回一個對象: 為模板提供數據, 也就是模板中可以直接使用此對象中的所有屬性/方法
- 返回對象中的屬性會與data函數返回對象的屬性合並成為組件對象的屬性
- 返回對象中的方法會與methods中的方法合並成功組件對象的方法
- 如果有重名, setup優先
- 注意:
- 一般不要混合使用: methods中可以訪問setup提供的屬性和方法, 但在setup方法中不能訪問data和methods
- setup不能是一個async函數: 因為返回值不再是return的對象, 而是promise, 模板看不到return對象中的屬性數據
- setup的參數
- setup(props, context) / setup(props, {attrs, slots, emit})
- props: 包含props配置聲明且傳入了的所有屬性的對象
- attrs: 包含沒有在props配置中聲明的屬性的對象, 相當於 this.$attrs
- slots: 包含所有傳入的插槽內容的對象, 相當於 this.$slots
- emit: 用來分發自定義事件的函數, 相當於 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'], // 可選的, 聲明了更利於程序員閱讀, 且可以對分發的事件數據進行校驗
data () {
console.log('data', this)
return {
// n: 1
}
},
beforeCreate () {
console.log('beforeCreate', this)
},
methods: {
// update () {
// this.n++
// this.m++
// }
},
// setup (props, context) {
setup (props, {attrs, emit, slots}) {
console.log('setup', this)
console.log(props.msg, attrs.msg2, slots, emit)
const m = ref(2)
const n = ref(3)
function update () {
// console.log('--', this)
// this.n += 2
// this.m += 2
m.value += 2
n.value += 2
// 分發自定義事件
emit('fn', '++')
}
return {
m,
n,
update,
}
},
})
</script>
6) reactive與ref-細節
- 是Vue3的 composition API中2個最重要的響應式API
- ref用來處理基本類型數據, reactive用來處理對象(遞歸深度響應式)
- 如果用ref對象/數組, 內部會自動將對象/數組轉換為reactive的代理對象
- ref內部: 通過給value屬性添加getter/setter來實現對數據的劫持
- reactive內部: 通過使用Proxy來實現對對象內部所有數據的劫持, 並通過Reflect操作對象內部數據
- ref的數據操作: 在js中要.value, 在模板中不需要(內部解析模板時會自動添加.value)
<template>
<h2>App</h2>
<p>m1: {{m1}}</p>
<p>m2: {{m2}}</p>
<p>m3: {{m3}}</p>
<button @click="update">更新</button>
</template>
<script lang="ts">
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對對象進行了深度數據劫持
console.log(m3.value.a2)
}
return {
m1,
m2,
m3,
update
}
}
}
</script>
7) 計算屬性與監視
- computed函數:
- 與computed配置功能一致
- 只有getter
- 有getter和setter
- watch函數
- 與watch配置功能一致
- 監視指定的一個或多個響應式數據, 一旦數據變化, 就自動執行監視回調
- 默認初始時不執行回調, 但可以通過配置immediate為true, 來指定初始時立即執行第一次
- 通過配置deep為true, 來指定深度監視
- watchEffect函數
- 不用直接指定要監視的數據, 回調函數中使用的哪些響應式數據就監視哪些響應式數據
- 默認初始時就會執行第一次, 從而可以收集需要監視的數據
- 監視數據發生變化時回調
<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 lang="ts">
/*
計算屬性與監視
1. computed函數:
與computed配置功能一致
只有getter
有getter和setter
2. watch函數
與watch配置功能一致
監視指定的一個或多個響應式數據, 一旦數據變化, 就自動執行監視回調
默認初始時不執行回調, 但可以通過配置immediate為true, 來指定初始時立即執行第一次
通過配置deep為true, 來指定深度監視
3. watchEffect函數
不用直接指定要監視的數據, 回調函數中使用的哪些響應式數據就監視哪些響應式數據
默認初始時就會執行第一次, 從而可以收集需要監視的數據
監視數據發生變化時回調
*/
import {
reactive,
ref,
computed,
watch,
watchEffect
} from 'vue'
export default {
setup () {
const user = reactive({
firstName: 'A',
lastName: 'B'
})
// 只有getter的計算屬性
const fullName1 = computed(() => {
console.log('fullName1')
return user.firstName + '-' + user.lastName
})
// 有getter與setter的計算屬性
const fullName2 = computed({
get () {
console.log('fullName2 get')
return user.firstName + '-' + user.lastName
},
set (value: string) {
console.log('fullName2 set')
const names = value.split('-')
user.firstName = names[0]
user.lastName = names[1]
}
})
const fullName3 = ref('')
/*
watchEffect: 監視所有回調中使用的數據
*/
/*
watchEffect(() => {
console.log('watchEffect')
fullName3.value = user.firstName + '-' + user.lastName
})
*/
/*
使用watch的2個特性:
深度監視
初始化立即執行
*/
watch(user, () => {
fullName3.value = user.firstName + '-' + user.lastName
}, {
immediate: true, // 是否初始化立即執行一次, 默認是false
deep: true, // 是否是深度監視, 默認是false
})
/*
watch一個數據
默認在數據發生改變時執行回調
*/
watch(fullName3, (value) => {
console.log('watch')
const names = value.split('-')
user.firstName = names[0]
user.lastName = names[1]
})
/*
watch多個數據:
使用數組來指定
如果是ref對象, 直接指定
如果是reactive對象中的屬性, 必須通過函數來指定
*/
watch([() => user.firstName, () => user.lastName, fullName3], (values) => {
console.log('監視多個數據', values)
})
return {
user,
fullName1,
fullName2,
fullName3
}
}
}
</script>
8) 生命周期
vue2.x的生命周期
vue3的生命周期
與 2.x 版本生命周期相對應的組合式 API
beforeCreate
-> 使用setup()
created
-> 使用setup()
beforeMount
->onBeforeMount
mounted
->onMounted
beforeUpdate
->onBeforeUpdate
updated
->onUpdated
beforeDestroy
->onBeforeUnmount
destroyed
->onUnmounted
errorCaptured
->onErrorCaptured
新增的鈎子函數
組合式 API 還提供了以下調試鈎子函數:
- onRenderTracked
- onRenderTriggered
<template>
<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>
<template>
<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>
09) 自定義hook函數
-
使用Vue3的組合API封裝的可復用的功能函數
-
自定義hook的作用類似於vue2中的mixin技術
-
自定義Hook的優勢: 很清楚復用功能代碼的來源, 更清楚易懂
-
需求1: 收集用戶鼠標點擊的頁面坐標
hooks/useMousePosition.ts
import { ref, onMounted, onUnmounted } from 'vue'
/*
收集用戶鼠標點擊的頁面坐標
*/
export default function useMousePosition () {
// 初始化坐標數據
const x = ref(-1)
const y = ref(-1)
// 用於收集點擊事件坐標的函數
const updatePosition = (e: MouseEvent) => {
x.value = e.pageX
y.value = e.pageY
}
// 掛載后綁定點擊監聽
onMounted(() => {
document.addEventListener('click', updatePosition)
})
// 卸載前解綁點擊監聽
onUnmounted(() => {
document.removeEventListener('click', updatePosition)
})
return {x, y}
}
<template>
<div>
<h2>x: {{x}}, y: {{y}}</h2>
</div>
</template>
<script>
import {
ref
} from "vue"
/*
在組件中引入並使用自定義hook
自定義hook的作用類似於vue2中的mixin技術
自定義Hook的優勢: 很清楚復用功能代碼的來源, 更清楚易懂
*/
import useMousePosition from './hooks/useMousePosition'
export default {
setup() {
const {x, y} = useMousePosition()
return {
x,
y,
}
}
}
</script>
-
利用TS泛型強化類型檢查
-
需求2: 封裝發ajax請求的hook函數
hooks/useRequest.ts
import { ref } from 'vue'
import axios from 'axios'
/*
使用axios發送異步ajax請求
*/
export default function useUrlLoader<T>(url: string) {
const result = ref<T | null>(null)
const loading = ref(true)
const errorMsg = ref(null)
axios.get(url)
.then(response => {
loading.value = false
result.value = response.data
})
.catch(e => {
loading.value = false
errorMsg.value = e.message || '未知錯誤'
})
return {
loading,
result,
errorMsg,
}
}
<template>
<div class="about">
<h2 v-if="loading">LOADING...</h2>
<h2 v-else-if="errorMsg">{{errorMsg}}</h2>
<!-- <ul v-else>
<li>id: {{result.id}}</li>
<li>name: {{result.name}}</li>
<li>distance: {{result.distance}}</li>
</ul> -->
<ul v-for="p in result" :key="p.id">
<li>id: {{p.id}}</li>
<li>title: {{p.title}}</li>
<li>price: {{p.price}}</li>
</ul>
<!-- <img v-if="result" :src="result[0].url" alt=""> -->
</div>
</template>
<script lang="ts">
import {
watch
} from "vue"
import useRequest from './hooks/useRequest'
// 地址數據接口
interface AddressResult {
id: number;
name: string;
distance: string;
}
// 產品數據接口
interface ProductResult {
id: string;
title: string;
price: number;
}
export default {
setup() {
// const {loading, result, errorMsg} = useRequest<AddressResult>('/data/address.json')
const {loading, result, errorMsg} = useRequest<ProductResult[]>('/data/products.json')
watch(result, () => {
if (result.value) {
console.log(result.value.length) // 有提示
}
})
return {
loading,
result,
errorMsg
}
}
}
</script>
10) toRefs
把一個響應式對象轉換成普通對象,該普通對象的每個 property 都是一個 ref
應用: 當從合成函數返回響應式對象時,toRefs 非常有用,這樣消費組件就可以在不丟失響應式的情況下對返回的對象進行分解使用
問題: reactive 對象取出的所有屬性值都是非響應式的
解決: 利用 toRefs 可以將一個響應式 reactive 對象的所有原始屬性轉換為響應式的 ref 屬性
<template>
<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對象的普通對象
應用: 當從合成函數返回響應式對象時,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>
11) ref獲取元素
利用ref函數獲取組件中的標簽元素
功能需求: 讓輸入框自動獲取焦點
<template>
<h2>App</h2>
<input type="text">---
<input type="text" ref="inputRef">
</template>
<script lang="ts">
import { onMounted, ref } from 'vue'
/*
ref獲取元素: 利用ref函數獲取組件中的標簽元素
功能需求: 讓輸入框自動獲取焦點
*/
export default {
setup() {
const inputRef = ref<HTMLElement|null>(null)
onMounted(() => {
inputRef.value && inputRef.value.focus()
})
return {
inputRef
}
},
}
</script>
2. Composition API(其它部分)
1) shallowReactive 與 shallowRef
- shallowReactive : 只處理了對象內最外層屬性的響應式(也就是淺響應式)
- shallowRef: 只處理了value的響應式, 不進行對象的reactive處理
- 什么時候用淺響應式呢?
- 一般情況下使用ref和reactive即可
- 如果有一個對象數據, 結構比較深, 但變化時只是外層屬性變化 ===> shallowReactive
- 如果有一個對象數據, 后面會產生新的對象來替換 ===> shallowRef
<template>
<h2>App</h2>
<h3>m1: {{m1}}</h3>
<h3>m2: {{m2}}</h3>
<h3>m3: {{m3}}</h3>
<h3>m4: {{m4}}</h3>
<button @click="update">更新</button>
</template>
<script lang="ts">
import { reactive, ref, shallowReactive, shallowRef } from 'vue'
/*
shallowReactive與shallowRef
shallowReactive: 只處理了對象內最外層屬性的響應式(也就是淺響應式)
shallowRef: 只處理了value的響應式, 不進行對象的reactive處理
總結:
reactive與ref實現的是深度響應式, 而shallowReactive與shallowRef是淺響應式
什么時候用淺響應式呢?
一般情況下使用ref和reactive即可,
如果有一個對象數據, 結構比較深, 但變化時只是外層屬性變化 ===> shallowReactive
如果有一個對象數據, 后面會產生新的對象來替換 ===> shallowRef
*/
export default {
setup () {
const m1 = reactive({a: 1, b: {c: 2}})
const m2 = shallowReactive({a: 1, b: {c: 2}})
const m3 = ref({a: 1, b: {c: 2}})
const m4 = shallowRef({a: 1, b: {c: 2}})
const update = () => {
// m1.b.c += 1
// m2.b.c += 1
// m3.value.a += 1
m4.value.a += 1
}
return {
m1,
m2,
m3,
m4,
update,
}
}
}
</script>
2) readonly 與 shallowReadonly
- readonly:
- 深度只讀數據
- 獲取一個對象 (響應式或純對象) 或 ref 並返回原始代理的只讀代理。
- 只讀代理是深層的:訪問的任何嵌套 property 也是只讀的。
- shallowReadonly
- 淺只讀數據
- 創建一個代理,使其自身的 property 為只讀,但不執行嵌套對象的深度只讀轉換
- 應用場景:
- 在某些特定情況下, 我們可能不希望對數據進行更新的操作, 那就可以包裝生成一個只讀代理對象來讀取數據, 而不能修改或刪除
<template>
<h2>App</h2>
<h3>{{state}}</h3>
<button @click="update">更新</button>
</template>
<script lang="ts">
import { reactive, readonly, shallowReadonly } from 'vue'
/*
readonly: 深度只讀數據
獲取一個對象 (響應式或純對象) 或 ref 並返回原始代理的只讀代理。
只讀代理是深層的:訪問的任何嵌套 property 也是只讀的。
shallowReadonly: 淺只讀數據
創建一個代理,使其自身的 property 為只讀,但不執行嵌套對象的深度只讀轉換
應用場景:
在某些特定情況下, 我們可能不希望對數據進行更新的操作, 那就可以包裝生成一個只讀代理對象來讀取數據, 而不能修改或刪除
*/
export default {
setup () {
const state = reactive({
a: 1,
b: {
c: 2
}
})
// const rState1 = readonly(state)
const rState2 = shallowReadonly(state)
const update = () => {
// rState1.a++ // error
// rState1.b.c++ // error
// rState2.a++ // error
rState2.b.c++
}
return {
state,
update
}
}
}
</script>
3) toRaw 與 markRaw
- toRaw
- 返回由
reactive
或readonly
方法轉換成響應式代理的普通對象。 - 這是一個還原方法,可用於臨時讀取,訪問不會被代理/跟蹤,寫入時也不會觸發界面更新。
- 返回由
- markRaw
- 標記一個對象,使其永遠不會轉換為代理。返回對象本身
- 應用場景:
- 有些值不應被設置為響應式的,例如復雜的第三方類實例或 Vue 組件對象。
- 當渲染具有不可變數據源的大列表時,跳過代理轉換可以提高性能。
<template>
<h2>{{state}}</h2>
<button @click="testToRaw">測試toRaw</button>
<button @click="testMarkRaw">測試markRaw</button>
</template>
<script lang="ts">
/*
toRaw: 得到reactive代理對象的目標數據對象
*/
import {
markRaw,
reactive, toRaw,
} from 'vue'
export default {
setup () {
const state = reactive<any>({
name: 'tom',
age: 25,
})
const testToRaw = () => {
const user = toRaw(state)
user.age++ // 界面不會更新
}
const testMarkRaw = () => {
const likes = ['a', 'b']
// state.likes = likes
state.likes = markRaw(likes) // likes數組就不再是響應式的了
setTimeout(() => {
state.likes[0] += '--'
}, 1000)
}
return {
state,
testToRaw,
testMarkRaw,
}
}
}
</script>
4) toRef
- 為源響應式對象上的某個屬性創建一個 ref對象, 二者內部操作的是同一個數據值, 更新時二者是同步的
- 區別ref: 拷貝了一份新的數據值單獨操作, 更新時相互不影響
- 應用: 當要將 某個prop 的 ref 傳遞給復合函數時,toRef 很有用
<template>
<h2>App</h2>
<p>{{state}}</p>
<p>{{foo}}</p>
<p>{{foo2}}</p>
<button @click="update">更新</button>
<Child :foo="foo"/>
</template>
<script lang="ts">
/*
toRef:
為源響應式對象上的某個屬性創建一個 ref對象, 二者內部操作的是同一個數據值, 更新時二者是同步的
區別ref: 拷貝了一份新的數據值單獨操作, 更新時相互不影響
應用: 當要將某個 prop 的 ref 傳遞給復合函數時,toRef 很有用
*/
import {
reactive,
toRef,
ref,
} from 'vue'
import Child from './Child.vue'
export default {
setup () {
const state = reactive({
foo: 1,
bar: 2
})
const foo = toRef(state, 'foo')
const foo2 = ref(state.foo)
const update = () => {
state.foo++
// foo.value++
// foo2.value++ // foo和state中的數據不會更新
}
return {
state,
foo,
foo2,
update,
}
},
components: {
Child
}
}
</script>
<template>
<h2>Child</h2>
<h3>{{foo}}</h3>
<h3>{{length}}</h3>
</template>
<script lang="ts">
import { computed, defineComponent, Ref, toRef } from 'vue'
const component = defineComponent({
props: {
foo: {
type: Number,
require: true
}
},
setup (props, context) {
const length = useFeatureX(toRef(props, 'foo'))
return {
length
}
}
})
function useFeatureX(foo: Ref) {
const lenth = computed(() => foo.value.length)
return lenth
}
export default component
</script>
5) customRef
- 創建一個自定義的 ref,並對其依賴項跟蹤和更新觸發進行顯式控制
- 需求: 使用 customRef 實現 debounce 的示例
<template>
<h2>App</h2>
<input v-model="keyword" placeholder="搜索關鍵字"/>
<p>{{keyword}}</p>
</template>
<script lang="ts">
/*
customRef:
創建一個自定義的 ref,並對其依賴項跟蹤和更新觸發進行顯式控制
需求:
使用 customRef 實現 debounce 的示例
*/
import {
ref,
customRef
} from 'vue'
export default {
setup () {
const keyword = useDebouncedRef('', 500)
console.log(keyword)
return {
keyword
}
},
}
/*
實現函數防抖的自定義ref
*/
function useDebouncedRef<T>(value: T, delay = 200) {
let timeout: number
return customRef((track, trigger) => {
return {
get() {
// 告訴Vue追蹤數據
track()
return value
},
set(newValue: T) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
// 告訴Vue去觸發界面更新
trigger()
}, delay)
}
}
})
}
</script>
6) provide 與 inject
- provide
和
inject提供依賴注入,功能類似 2.x 的
provide/inject - 實現跨層級組件(祖孫)間通信
<template>
<h1>父組件</h1>
<p>當前顏色: {{color}}</p>
<button @click="color='red'">紅</button>
<button @click="color='yellow'">黃</button>
<button @click="color='blue'">藍</button>
<hr>
<Son />
</template>
<script lang="ts">
import { provide, ref } from 'vue'
/*
- provide` 和 `inject` 提供依賴注入,功能類似 2.x 的 `provide/inject
- 實現跨層級組件(祖孫)間通信
*/
import Son from './Son.vue'
export default {
name: 'ProvideInject',
components: {
Son
},
setup() {
const color = ref('red')
provide('color', color)
return {
color
}
}
}
</script>
<template>
<div>
<h2>子組件</h2>
<hr>
<GrandSon />
</div>
</template>
<script lang="ts">
import GrandSon from './GrandSon.vue'
export default {
components: {
GrandSon
},
}
</script>
<template>
<h3 :style="{color}">孫子組件: {{color}}</h3>
</template>
<script lang="ts">
import { inject } from 'vue'
export default {
setup() {
const color = inject('color')
return {
color
}
}
}
</script>
7) 響應式數據的判斷
- isRef: 檢查一個值是否為一個 ref 對象
- isReactive: 檢查一個對象是否是由
reactive
創建的響應式代理 - isReadonly: 檢查一個對象是否是由
readonly
創建的只讀代理 - isProxy: 檢查一個對象是否是由
reactive
或者readonly
方法創建的代理
3. 手寫組合API
1) shallowReactive 與 reactive
const reactiveHandler = {
get (target, key) {
if (key==='_is_reactive') return true
return Reflect.get(target, key)
},
set (target, key, value) {
const result = Reflect.set(target, key, value)
console.log('數據已更新, 去更新界面')
return result
},
deleteProperty (target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('數據已刪除, 去更新界面')
return result
},
}
/*
自定義shallowReactive
*/
function shallowReactive(obj) {
return new Proxy(obj, reactiveHandler)
}
/*
自定義reactive
*/
function reactive (target) {
if (target && typeof target==='object') {
if (target instanceof Array) { // 數組
target.forEach((item, index) => {
target[index] = reactive(item)
})
} else { // 對象
Object.keys(target).forEach(key => {
target[key] = reactive(target[key])
})
}
const proxy = new Proxy(target, reactiveHandler)
return proxy
}
return target
}
/* 測試自定義shallowReactive */
const proxy = shallowReactive({
a: {
b: 3
}
})
proxy.a = {b: 4} // 劫持到了
proxy.a.b = 5 // 沒有劫持到
/* 測試自定義reactive */
const obj = {
a: 'abc',
b: [{x: 1}],
c: {x: [11]},
}
const proxy = reactive(obj)
console.log(proxy)
proxy.b[0].x += 1
proxy.c.x[0] += 1
2) shallowRef 與 ref
/*
自定義shallowRef
*/
function shallowRef(target) {
const result = {
_value: target, // 用來保存數據的內部屬性
_is_ref: true, // 用來標識是ref對象
get value () {
return this._value
},
set value (val) {
this._value = val
console.log('set value 數據已更新, 去更新界面')
}
}
return result
}
/*
自定義ref
*/
function ref(target) {
if (target && typeof target==='object') {
target = reactive(target)
}
const result = {
_value: target, // 用來保存數據的內部屬性
_is_ref: true, // 用來標識是ref對象
get value () {
return this._value
},
set value (val) {
this._value = val
console.log('set value 數據已更新, 去更新界面')
}
}
return result
}
/* 測試自定義shallowRef */
const ref3 = shallowRef({
a: 'abc',
})
ref3.value = 'xxx'
ref3.value.a = 'yyy'
/* 測試自定義ref */
const ref1 = ref(0)
const ref2 = ref({
a: 'abc',
b: [{x: 1}],
c: {x: [11]},
})
ref1.value++
ref2.value.b[0].x++
console.log(ref1, ref2)
3) shallowReadonly 與 readonly
const readonlyHandler = {
get (target, key) {
if (key==='_is_readonly') return true
return Reflect.get(target, key)
},
set () {
console.warn('只讀的, 不能修改')
return true
},
deleteProperty () {
console.warn('只讀的, 不能刪除')
return true
},
}
/*
自定義shallowReadonly
*/
function shallowReadonly(obj) {
return new Proxy(obj, readonlyHandler)
}
/*
自定義readonly
*/
function readonly(target) {
if (target && typeof target==='object') {
if (target instanceof Array) { // 數組
target.forEach((item, index) => {
target[index] = readonly(item)
})
} else { // 對象
Object.keys(target).forEach(key => {
target[key] = readonly(target[key])
})
}
const proxy = new Proxy(target, readonlyHandler)
return proxy
}
return target
}
/* 測試自定義readonly */
/* 測試自定義shallowReadonly */
const objReadOnly = readonly({
a: {
b: 1
}
})
const objReadOnly2 = shallowReadonly({
a: {
b: 1
}
})
objReadOnly.a = 1
objReadOnly.a.b = 2
objReadOnly2.a = 1
objReadOnly2.a.b = 2
4) isRef, isReactive 與 isReadonly
/*
判斷是否是ref對象
*/
function isRef(obj) {
return obj && obj._is_ref
}
/*
判斷是否是reactive對象
*/
function isReactive(obj) {
return obj && obj._is_reactive
}
/*
判斷是否是readonly對象
*/
function isReadonly(obj) {
return obj && obj._is_readonly
}
/*
是否是reactive或readonly產生的代理對象
*/
function isProxy (obj) {
return isReactive(obj) || isReadonly(obj)
}
/* 測試判斷函數 */
console.log(isReactive(reactive({})))
console.log(isRef(ref({})))
console.log(isReadonly(readonly({})))
console.log(isProxy(reactive({})))
console.log(isProxy(readonly({})))
4. Composition API VS Option API
1) Option API的問題
- 在傳統的Vue OptionsAPI中,新增或者修改一個需求,就需要分別在data,methods,computed里修改 ,滾動條反復上下移動
2) 使用Compisition API
我們可以更加優雅的組織我們的代碼,函數。讓相關功能的代碼更加有序的組織在一起
三、其他新組合的API
1. 新組件
1) Fragment(片斷)
- 在Vue2中: 組件必須有一個根標簽
- 在Vue3中: 組件可以沒有根標簽, 內部會將多個標簽包含在一個Fragment虛擬元素中
- 好處: 減少標簽層級, 減小內存占用
<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 lang="ts">
import ModalButton from './ModalButton.vue'
export default {
setup() {
return {
}
},
components: {
ModalButton
}
}
</script>
3) Suspense(不確定的)
- 它們允許我們的應用程序在等待異步組件時渲染一些后備內容,可以讓我們創建一個平滑的用戶體驗
<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>
2. 其他新的API
全新的全局API
- createApp()
- defineProperty()
- defineAsyncComponent()
- nextTick()
將原來的全局API轉移到應用對象
- app.component()
- app.config()
- app.directive()
- app.mount()
- app.unmount()
- app.use()
模板語法變化
- v-model的本質變化
- prop:value -> modelValue;
- event:input -> update:modelValue;
-
.sync修改符已移除, 由v-model代替
- v-if優先v-for解析
四、使用VuePress搭建在線文檔網站
0. 在線文檔
VuePress官方在線文檔(opens new window)
1. 搭建基本環境
# 將 VuePress 作為一個本地依賴安裝
npm install -D vuepress
# 新建一個 docs 文件夾
mkdir docs
# 新建一個文件: docs/README.md
echo '# Hello VuePress!' > docs/README.md
# 啟動文檔項目
npx vuepress dev docs
# 構建靜態文件
npx vuepress build docs
|-- docs
|-- .vuepress
|-- config.js
|-- README.md
2. 配置ts教程文檔
- 整體結構
|-- dist
|-- dics
|-- .vuepress
|-- public
|-- ts-logo.png
|-- config.js
|-- chapter1
|-- 01_初識TS.md
|-- 02_安裝TS.md
|-- 03_HelloWorld.md
|-- chapter2
|-- 1_type.md
|-- 2_interface.md
|-- 3_class.md
|-- 4_function.md
|-- 5_generic.md
|-- 6_other.md
|-- chapter3
|-- 01_react.md
|-- 02_vue.md
|-- chapter4
|-- README.md
|-- README.md
|-- package.json
- docs/.vuepress/config.js
// 注意: base的值為github倉庫的名稱
module.exports = {
base: '/ts-study/', /* 基礎虛擬路徑: */
dest: 'dist', /* 打包文件基礎路徑, 在命令所在目錄下 */
title: 'TypeScript 入門', // 標題
description: '學習使用 TypeScript', // 標題下的描述
themeConfig: { // 主題配置
sidebar: [ // 左側導航
{
title: '初識 TypeScript', // 標題
collapsable: false, // 下級列表不可折疊
children: [ // 下級列表
'chapter1/01_初識TS',
'chapter1/02_安裝TS',
'chapter1/03_HelloWorld'
]
},
{
title: 'TypeScript 常用語法',
collapsable: false,
children: [
'chapter2/1_type',
'chapter2/2_interface',
'chapter2/3_class',
'chapter2/4_function',
'chapter2/5_generic',
]
},
]
}
}
- docs/README.md
---
#首頁
home: true
# 圖標
heroImage: /ts-logo.png
# 按鈕文本
actionText: 開始學習 →
# 按鈕點擊跳轉路徑
actionLink: /chapter1/01_初識TS
---
- package.json
"scripts": {
"doc:dev": "vuepress dev docs",
"doc:build": "vuepress build docs",
"doc:deploy": "gh-pages -d docs/dist"
}
3. 發布到gitpage
- 使用git管理當前項目
- 將打包的項目推送到gitpage
# 下載工具包
yarn add -D gh-pages
# 執行打包命令
yarn doc:build
# 執行部署命令
yarn doc:deploy
本文來源於尚硅谷楊小帥老師