一、初識vue3
1.vue3簡介
- 2020年9月18日,vue3發布3.0版本,代號大海賊時代來臨,One Piece
- 特點:
- 無需構建步驟,漸進式增強靜態的 HTML
- 在任何頁面中作為 Web Components 嵌入
- 單頁應用 (SPA)
- 全棧 / 服務端渲染 (SSR)
- Jamstack / 靜態站點生成 (SSG)
- 開發桌面端、移動端、WebGL,甚至是命令行終端中的界面
2.Vue3帶來了什么
- 打包大小減少40%
- 初次渲染快55%,更新渲染快133%
- 內存減少54%
3.分析目錄結構
- main.js中的引入
- 在模板中vue3中是可以沒有根標簽了,這也是比較重要的改變
- 應用實例并不只限于一個。createApp API 允許你在同一個頁面中創建多個共存的 Vue 應用,而且每個應用都擁有自己的用于配置和全局資源的作用域。
//main.js//引入的不再是Vue構造函數了,引入的是一個名為createApp的工廠函數
import {createApp} from 'vue
import App from './App.vue//創建應用實例對象-app(類似于之前vue2中的vm實例,但是app比vm更輕)
createApp(APP).mount('#app')
//卸載就是unmount,卸載就沒了
//createApp(APP).unmount('#app')//之前我們是這么寫的,在vue3里面這一塊就不支持了,會報錯的,引入不到 import vue from 'vue';
new Vue({render:(h) => h(App)
}).$mount('#app')//多個應用實例
const app1 = createApp({/* ... */
})
app1.mount('#container-1')const app2 = createApp({/* ... */
})
app2.mount('#container-2')
安裝vue3的開發者工具
- 方式一: 打開chrome應用商店,搜索vue: 里面有個Vue.js devtools,且下面有個角標beta那個就是vue3的開發者工具
- 方式二: 離線模式下,可以直接將包丟到擴展程序
二、 常用Composition API(組合式API)
1. setup函數
-
理解:Vue3.0中一個新的額配置項,值為一個函數
-
2.setup是所有Composition API(組合api) “表演的舞臺”
-
組件中所用到的:數據、方法等等,均要配置在setup中
-
setup函數的兩種返回值:
- 若返回一個對象,則對象中的屬性、方法,在模板中均可以直接使用。(重點關注)
- 若返回一個渲染函數:則可以自定義渲染內容。
-
注意點:
- 盡量不要與Vue2.x配置混用
- Vue2.x配置(data ,methos, computed…)中訪問到setup中的屬性,方法
- 但在setup中不能訪問到Vue2.x配置(data.methos,compued…)
- 如果有重名,setup優先
- setup不能是一個async函數,因為返回值不再是return的對象,而是promise,模板看不到return對象中的屬性
- 盡量不要與Vue2.x配置混用
import {h} from 'vue'
//向下兼容,可以寫入vue2中的data配置項
module default {name: 'App',setup(){//數據let name = '張三',let age = 18,//方法function sayHello(){console.log(name)},//f返回一個對象(常用)return {name,age,sayHello}//返回一個函數(渲染函數)//return () => {return h('h1','學習')} return () => h('h1','學習')}}
1.1關于單文件組件<script setup></script >
- 每個 *.vue 文件最多可以包含一個
<script setup>。(不包括一般的 <script>)
- 這個腳本塊將被預處理為組件的 setup() 函數,這意味著它將為每一個組件實例都執行。
<script setup>
中的頂層綁定都將自動暴露給模板。 <script setup>
是在單文件組件 (SFC) 中使用組合式 API 的編譯時語法糖。當同時使用 SFC 與組合式 API 時該語法是默認推薦。相比于普通的<script>
語法,它具有更多優勢:- 更少的樣板內容,更簡潔的代碼。
- 能夠使用純 TypeScript 聲明 props 和自定義事件。這個我下面是有說明的
- 更好的運行時性能 (其模板會被編譯成同一作用域內的渲染函數,避免了渲染上下文代理對象)。
- 更好的 IDE 類型推導性能 (減少了語言服務器從代碼中抽取類型的工作)。
(1)基本語法:
/* 里面的代碼會被編譯成組件 setup() 函數的內容。這意味著與普通的 `<script>` 只在組件被首次引入的時候執行一次不同,`<script setup>` 中的代碼會在每次組件實例被創建的時候執行。*/
<script setup>console.log('hello script setup')
</script>
頂層的綁定會被暴露給模板
當使用 <script setup>
的時候,任何在 <script setup>
聲明的頂層的綁定 (包括變量,函數聲明,以及 import 導入的內容) 都能在模板中直接使用:
<script setup>
// 變量
const msg = '王二麻子'// 函數
function log() {console.log(msg)
}
</script><template><button @click="log">{{ msg }}</button>
</template>
import 導入的內容也會以同樣的方式暴露。這意味著我們可以在模板表達式中直接使用導入的 action 函數,而不需要通過 methods 選項來暴露它:
<script setup>
import { say } from './action'
</script><template><div>{{ say ('hello') }}</div>
</template>
(2)響應式:
響應式狀態需要明確使用響應式 API 來創建。和 setup() 函數的返回值一樣,ref 在模板中使用的時候會自動解包:
<script setup>
import { ref } from 'vue'const count = ref(0)
</script><template><button @click="count++">{{ count }}</button>
</template>
(3)使用組件:
<script setup>
范圍里的值也能被直接作為自定義組件的標簽名使用:
/**
*這里 MyComponent 應當被理解為像是在引用一個變量。
*如果你使用過 JSX,此處的心智模型是類似的。
*其 kebab-case 格式的 <my-component> 同樣能在模板中使用——不過,
*強烈建議使用 PascalCase 格式以保持一致性。同時這也有助于區分原生的自定義元素。
*/
<script setup>
import MyComponent from './MyComponent.vue'
</script><template><MyComponent />
</template>
動態組件
/**
*由于組件是通過變量引用而不是基于字符串組件名注冊的,
*在 <script setup> 中要使用動態組件的時候,應該使用*動態的 :is 來綁定:
*/
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script><template><component :is="Foo" /><component :is="someCondition ? Foo : Bar" />
</template>
遞歸組件
- 一個單文件組件可以通過它的文件名被其自己所引用。例如:名為 FooBar.vue 的組件可以在其模板中用
<FooBar/>
引用它自己。 - 注意這種方式相比于導入的組件優先級更低。如果有具名的導入和組件自身推導的名字沖突了,可以為導入的組件添加別名:
import { FooBar as FooBarChild } from './components'
命名空間組件
- 可以使用帶 . 的組件標簽,例如
<Foo.Bar>
來引用嵌套在對象屬性中的組件。這在需要從單個文件中導入多個組件的時候非常有用:
<script setup>
import * as Form from './form-components'
</script><template><Form.Input><Form.Label>label</Form.Label></Form.Input>
</template>
(4)使用自定義指令:
- 全局注冊的自定義指令將正常工作。本地的自定義指令在
<script setup>
中不需要顯式注冊,但他們必須遵循 vNameOfDirective 這樣的命名規范:
<script setup>
const vMyDirective = {beforeMount: (el) => {// 在元素上做些操作}
}
</script>
<template><h1 v-my-directive>This is a Heading</h1>
</template>
- 如果指令是從別處導入的,可以通過重命名來使其符合命名規范:
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
(5)defineProps() 和 defineEmits():
- 為了在聲明 props 和 emits 選項時獲得完整的類型推導支持,我們可以使用 defineProps 和 defineEmits API,它們將自動地在
<script setup>
中可用:
<script setup>
const props = defineProps({foo: String
})
const emit = defineEmits(['change', 'delete'])
// setup 代碼
</script>
- defineProps 和 defineEmits 都是只能在
<script setup>
中使用的編譯器宏。他們不需要導入,且會隨著<script setup>
的處理過程一同被編譯掉。 - defineProps 接收與 props 選項相同的值,defineEmits 接收與 emits 選項相同的值。
- defineProps 和 defineEmits 在選項傳入后,會提供恰當的類型推導。
- 傳入到 defineProps 和 defineEmits 的選項會從 setup 中提升到模塊的作用域。因此,傳入的選項不能引用在 setup 作用域中聲明的局部變量。這樣做會引起編譯錯誤。但是,它可以引用導入的綁定,因為它們也在模塊作用域內。
(5)defineExpose:
- 使用
<script setup>
的組件是默認關閉的——即通過模板引用或者 $parent 鏈獲取到的組件的公開實例,不會暴露任何在 <script setup>
中聲明的綁定。
//可以通過 defineExpose 編譯器宏來顯式指定在 <script setup> 組件中要暴露出去的屬性:
<script setup>
import { ref } from 'vue'const a = 1
const b = ref(2)
defineExpose({a,b
})
</script>//當父組件通過模板引用的方式獲取到當前組件的實例,
//獲取到的實例會像這樣 { a: number, b: number } (ref 會和在普通實例中一樣被自動解包)
(6)useSlots() 和 useAttrs():
- 在
<script setup>
使用 slots 和 attrs 的情況應該是相對來說較為罕見的,因為可以在模板中直接通過 $slots 和 $attrs 來訪問它們。在你的確需要使用它們的罕見場景中,可以分別用 useSlots 和 useAttrs 兩個輔助函數:
<script setup>
import { useSlots, useAttrs } from 'vue'const slots = useSlots()
const attrs = useAttrs()
</script>
//useSlots 和 useAttrs 是真實的運行時函數,它的返回與 setupContext.slots 和 setupContext.attrs 等價。
//它們同樣也能在普通的組合式 API 中使用。
(7)與普通的 <script>
一起使用:
<script setup>
可以和普通的 <script>
一起使用。普通的 <script>
在有這些需要的情況下或許會被使用到:
- 聲明無法在
<script>
// 普通 <script>, 在模塊作用域下執行 (僅一次)
runSideEffectOnce()// 聲明額外的選項
export default {inheritAttrs: false,customOptions: {}
}
</script><script setup>
// 在 setup() 作用域中執行 (對每個實例皆如此)
</script>
(8)頂層 await:
<script setup>
中可以使用頂層 await。結果代碼會被編譯成 async setup():
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
// 另外,await 的表達式會自動編譯成在 await 之后保留當前組件實例上下文的格式。
2.ref 函數
- 作用:定義一個響應式的數據
- 語法: const xxx = ref(initValue)
- 創建一個包含響應式數據引用對象(reference對象)
- JS中操作數據:xxx.value
- 模板中讀取數據:不需要.value,直接: {{xxx}}
- 備注:
- 接收的數據可以是:基本類型、也可以是對象類型
- 基本類型的數據:響應式依然靠的是Object.defineProperty()的get和set完成的
- 對象類型的數據: 內部”求助“了Vue3.0中的一個新的函數——reactive函數
3.reactive 函數
- 作用:定義一個對象類型的響應式數據(基本類型別用他,用ref函數)
- 語法:const 代理對象 = reactive(被代理對象)接收一個對象(或數組),返回一個代理對象(proxy對象)
- reactive定義的響應式數據是”深層次的“
- 內部基于ES6的Proxy實現,通過代理對象操作源對象內部數據進行操作
4.Vue3.0中響應式原理
- 先來看一看vue2的響應式原理
- 對象類型: 通過Object.defineProperty()對屬性的讀取、修改進行攔截(數據劫持)
- 數組類型:通過重寫更新數組的一系列方法來實現攔截。(對數組的變更方法進行了包裹)
Object.defineProperty( data, 'count', {get(){},set(){}
})//模擬實現一下
let person = {name: '張三',age: 15,
}
let p = {}
Object.defineProperty( p, 'name', {configurable: true, //配置這個屬性表示可刪除的,否則delete p.name 是刪除不了的 falseget(){//有人讀取name屬性時調用return person.name},set(value){//有人修改時調用person.name = value}
})
- 存在問題:
1. 新增屬性。刪除屬性。界面不會更新
2. 直接通過下表修改數組,界面不會自動更新
- vue3的響應式
- 實現原理:
- 通過Proxy(代理):攔截對象中任意屬性的變化,包括:屬性值的讀寫、屬性的添加、屬性的刪除等等。
- 通過Reflect(反射):對被代理對象的屬性進行操作
- MDN文檔中描述的Proxy與Reflect:可以參考對應的文檔
- 實現原理:
//模擬vue3中實現響應式
let person = {name: '張三',age: 15,
}
//我們管p叫做代理數據,管person叫源數據
const p = new Proxy(person,{//target代表的是person這個源對象,propName代表讀取或者寫入的屬性名get(target,propName){console.log('有人讀取了p上面的propName屬性')return target[propName]},//不僅僅是修改調用,增加的時候也會調用set(target,propName,value){console.log(`有人修改了p身上的${propName}屬性,我要去更新界面了`)target[propName] = value},deleteProperty(target,propName){console.log(`有人刪除了p身上的${propName}屬性,我要去更新界面了`)return delete target[propName]}
})
//映射到person上了,捕捉到修改,那就是響應式啊
//vue3底層源碼不是我們上面寫的那么low,實現原理一樣,但是用了一個新的方式
window.Reflect
let obj = {a: 1,b:2,
}
//傳統的只能通過try catch去捕獲異常,如果使用這種那么底層源碼將會有一堆try catch
try{Object.defineProperty( obj, 'c', {get(){ return 3 },})Object.defineProperty( obj, 'c', {get(){ return 4 },})
} catch(error) {console.log(error)
}//新的方式: 通過Reflect反射對象去操作,相對來說要舒服一點,不會要那么多的try catch
const x1 = Reflect.defineProperty( obj, 'c', {get(){ return 3 },
})
const x2 = Reflect.defineProperty( obj, 'c', {get(){ return 3 },
})
//x1,和x2是有返回布爾值的
if(x2){console.log('某某操作成功了')
}else {console.log('某某操作失敗了')
}
- 所以vue3最終的響應式原理如下:
let person = {name: '張三',age: 15,
}
//我們管p叫做代理數據,管person叫源數據
const p = new Proxy(person,{//target代表的是person這個源對象,propName代表讀取或者寫入的屬性名get(target,propName){console.log('有人讀取了p上面的propName屬性')return Reflect.get(target, propName)},//不僅僅是修改調用,增加的時候也會調用set(target,propName,value){console.log(`有人修改了p身上的${propName}屬性,我要去更新界面了`)Reflect.set(target, propName, value)},deleteProperty(target,propName){console.log(`有人刪除了p身上的${propName}屬性,我要去更新界面了`)return Reflect.deleteProperty(target,propName) }
})
5.reactive對比ref
-
從定義數據角度對比:
- ref用來定義: 基本數據類型
- reactive用來定義: 對象(或數組)類型數據
- 備注: ref也可以用來定義對象(或數組)類型數據,它內部會自動通過reactive轉為代理對象
-
從原理角度對比:
- ref通過Object.defineProperty()的get和set來實現響應式(數據劫持)
- reactive通過Proxy來實現響應式(數據劫持),并通過Reflect操作源對象內部的數據
-
從使用角度對比:
- ref定義數據:操作數據需要 .value ,讀取數據時模板中直接讀取不需要 .value
- reactive 定義的數據: 操作數據和讀取數據均不需要 .value
5.setup的兩個注意點
- setup執行的時機
- 在beforeCreate之前執行一次,this是undefined
- setup的參數
- props:值為對象,包含: 組件外部傳遞過來,且組件內部聲明接收了屬性
- context:上下文對象
- attrs: 值為對象,包含:組件外部傳遞過來,但沒有在props配置中聲明的屬性,相當于 this.$attrs
- slots:收到插槽的內容,相當于$slots
- emit: 分發自定義事件的函數,相當于this.$emit
//父組件
<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import HelloWorld from './components/test3.vue';
const hello = (val) =>{console.log('傳遞的參數是:'+ val);
}
</script><template><img alt="Vue logo" src="./assets/logo.png" /><HelloWorld msg="傳遞吧" @hello="hello"><template v-slot:cacao><span>是插槽嗎</span></template><template v-slot:qwe><span>meiyou</span></template></HelloWorld>
</template>
//子組件
export default {name: 'test3',props: ['msg'],emits:['hello'],//這里setup接收兩個參數,一個是props,一個是上下文contextsetup(props,context){/*** props就是父組件傳來的值,但是他是Porxy類型的對象* >Proxy:{msg:'傳遞吧'}* 可以當作我們自定義的reactive定義的數據*//*** context是一個對象 包含以下內容:* 1.emit觸發自定義事件的 * 2.attrs 相當于vue2里面的 $attrs 包含:組件外部傳遞過來,但沒有在props配置中聲明的屬性* 3.slots 相當于vue2里面的 $slots* 3.expose 是一個回調函數*/console.log(context.slots);let person = reactive({name: '張三',age: 17,})function changeInfo(){context.emit('hello', 666)}//返回對象return {person,changeInfo}//返回渲染函數(了解) 這個h是個函數//return () => h('name','age')}
}
</script>
6.計算屬性與監視
(1)computed函數
- 與vue2.x中的寫法一致
- 需要引入computed
<template><h1>一個人的信息</h1><div>姓: <input type="text" v-model="person.firstName">名:<input type="text" v-model="person.lastName"><div><span>簡名:{{person.smallName}}</span> <br><span>全名:{{person.fullName}}</span></div></div>
</template>
<script>
import { computed,reactive } from 'vue'export default {name: 'test4',props: ['msg'],emits:['hello'],setup(){let person = reactive({firstName: '張',lastName: '三'})//簡寫形式person.smallName = computed(()=>{return person.firstName + '-' + person.lastName})//完全形態person.fullName = computed({get(){console.log('調用get');return person.firstName + '*' + person.lastName},set(value){console.log('調用set');const nameArr = value.split('*')person.firstName = nameArr[0]person.firstName = nameArr[1]},})return {person,}},}</script>
(2)watch函數
- 和computed一樣,需要引入api
- 有兩個小坑:
1.監視reactive定義的響應式數據的時候:oldValue無法獲取到正確的值,強制開啟了深度監視(deep配置無效)
2.監視reactive定義的響應式數據中某個屬性的時候:deep配置有效
具體請看下面代碼以及注釋
<template><h1>當前求和為: {{sum}}</h1><button @click="sum++">點我+1</button><hr><h1>當前信息為: {{msg}}</h1><button @click="msg+='!' ">修改信息</button><hr><h2>姓名: {{person.name}}</h2><h2>年齡: {{person.age}}</h2><button @click="person.name += '~' ">修改姓名</button> <button @click="person.age++">增長年齡</button>
</template><script>//使用setup的注意事項import { watch,ref,reactive } from 'vue'export default {name: 'test5',props: ['msg'],emits:['hello'],setup(){let sum = ref(0)let msg = ref('你好啊')let person = reactive({name: '張三',age: 18,job:{salary: '15k'},})//由于這里的this是指的是undefined,所以使用箭頭函數//情況一:監視ref所定義的一個響應式數據// watch(sum, (newValue,oldValue)=>{// console.log('新的值',newValue);// console.log('舊的值',oldValue);// })//情況二:監視ref所定義的多個響應式數據watch([sum,msg], (newValue,oldValue)=>{console.log('新的值',newValue); //['sum的newValue', 'msg的newValue']console.log('舊的值',oldValue); //['sum的oldValue', 'msg的oldValue']},{immediate: true,deep:true}) //這里vue3的deep是有點小問題的,可以不用deep,(隱式強制deep)//情況三:監視reactive定義的所有響應式數據,//1.此處無法獲取正確的oldValue(newValue與oldValue是一致值),且目前無法解決//2.強制開啟了深度監視(deep配置無效)/*** 受到碼友熱心評論解釋: 此處附上碼友的解釋供大家參考:* 1. 當你監聽一個響應式對象的時候,這里的newVal和oldVal是一樣的,因為他們是同一個對象【引用地址一樣】,* 即使里面的屬性值會發生變化,但主體對象引用地址不變。這不是一個bug。要想不一樣除非這里把對象都換了* * 2. 當你監聽一個響應式對象的時候,vue3會隱式的創建一個深層監聽,即對象里只要有變化就會被調用。* 這也解釋了你說的deep配置無效,這里是強制的。*/watch(person, (newValue,oldValue)=>{console.log('新的值',newValue); console.log('舊的值',oldValue);})//情況四:監視reactive對象中某一個屬性的值,//注意: 這里監視某一個屬性的時候可以監聽到oldValuewatch(()=>person.name, (newValue,oldValue)=>{console.log('新的值',newValue); console.log('舊的值',oldValue);})//情況五:監視reactive對象中某一些屬性的值watch([()=>person.name,()=>person.age], (newValue,oldValue)=>{console.log('新的值',newValue); console.log('舊的值',oldValue);})//特殊情況: 監視reactive響應式數據中深層次的對象,此時deep的配置奏效了watch(()=>person.job, (newValue,oldValue)=>{console.log('新的值',newValue); console.log('舊的值',oldValue);},{deep:true}) //此時deep有用return {sum,msg,person,}},}
</script>
(3)watchEffect函數
- watch的套路是:既要指明監視的屬性,也要指明監視的回調
- watchEffect的套路是:不用指明監視哪個屬性,監視的回調中用到哪個屬性,那就監視哪個屬性
- watchEffect有點像computed:
- 但computed注重的計算出來的值(回調函數的返回值),所以必須要寫返回值
- 而watchEffect更注重的是過程(回調函數的函數體),所以不用寫返回值
<script>//使用setup的注意事項import { ref,reactive,watchEffect } from 'vue'export default {name: 'test5',props: ['msg'],emits:['hello'],setup(){let sum = ref(0)let msg = ref('你好啊')let person = reactive({name: '張三',age: 18,job:{salary: '15k'},})//用處: 如果是比較復雜的業務,發票報銷等,那就不許需要去監聽其他依賴,只要發生變化,立馬重新回調//注重邏輯過程,你發生改變了我就重新執行回調,不用就不執行,只執行一次watchEffect(()=>{//這里面你用到了誰就監視誰,里面就發生回調const x1 = sum.valueconsole.log('我調用了');})return {sum,msg,person,}},}
</script>
7.生命周期函數
<template><h1>生命周期</h1><p>當前求和為: {{sum}}</p><button @click="sum++">加一</button>
</template><script>//使用setup的注意事項import { ref,reactive,onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted } from 'vue'export default {name: 'test7',setup(){let sum = ref(0)//通過組合式API的形式去使用生命周期鉤子/*** beforeCreate 和 created 這兩個生命周期鉤子就相當于 setup 所以,不需要這兩個* * beforeMount ===> onBeforeMount* mounted ===> onMounted* beforeUpdate ===> onBeforeUpdate* updated ===> onUpdated* beforeUnmount ===> onBeforeUnmount* unmounted ===> onUnmounted*/console.log('---setup---');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 {sum}},//這種是外層的寫法,如果想要使用組合式api的話需要放在setup中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---');}}
</script>
8.自定義hook函數
- 什么是hook函數: 本質是一個函數,把setup函數中使用的Composition API進行了封裝
- 類似于vue2.x中的 mixin
- 自定義hook的優勢: 復用代碼,讓setup中的邏輯更清楚易懂
- 使用hook實現鼠標打點”:
創建文件夾和usePoint.js文件
//usePoint.js
import {reactive,onMounted,onBeforeUnmount } from 'vue'
function savePoint(){//實現鼠標打點的數據let point = reactive({x: null,y: null})//實現鼠標點的方法const savePoint = (e)=>{point.x = e.pageXpoint.y = e.pageY} //實現鼠標打點的生命周期鉤子onMounted(()=>{window.addEventListener('click',savePoint)})onBeforeUnmount(()=>{window.removeEventListener('click',savePoint)})return point
}
export default savePoint
//組件test.vue<template><p>當前求和為: {{sum}} </p><button @click="sum++">加一</button><hr><h2>當前點擊時候的坐標: x: {{point.x}} y:{{point.y}}</h2></template><script>
import { ref } from 'vue'
import usePoint from '../hooks/usePoint'
export default {name: 'test8',setup(props,context){let sum = ref(0)let point = usePoint()return {sum,point}}
}
</script>
9.toRef
- 作用: 創建一個ref對象,其value值指向另一個對象中的某個屬性值
- 語法: const name = toRef(person, ‘name’)
- 應用:要將響應式對象中的某個屬性單獨提供給外部使用
- 擴展: toRefs與toRef功能一致,但是可以批量創建多個ref對象,語法: toRefs(person)
<template><h2>姓名: {{name2}}</h2><h2>年齡: {{person.age}}</h2><button @click="person.name += '~' ">修改姓名</button> <button @click="person.age++">增長年齡</button>
</template><script>//使用setup的注意事項import { reactive, toRef, toRefs } from 'vue'export default {name: 'test9',setup(){let person = reactive({name: '張三',age: 18,job:{salary: '15k'},})//toRefconst name2 = toRef(person,'name') //第一個參數是對象,第二個參數是鍵名console.log('toRef轉變的是',name2); //ref定義的對象//toRefs,批量處理對象的所有屬性//const x = toRefs(person)//console.log('toRefs轉變的是',x); //是一個對象return {person,name2,...toRefs(person)}},}
</script>
三、TypeScript 與組合式 API
1.為組件的 props 標注類型
//場景一: 使用<script setup>
<script setup lang="ts">
const props = defineProps({foo: { type: String, required: true },bar: Number
})props.foo // string
props.bar // number | undefined
</script>//也可以將 props 的類型移入一個單獨的接口中
<script setup lang="ts">
interface Props {foo: stringbar?: number
}
const props = defineProps<Props>()
</script>
//場景二: 不使用<script setup>
import { defineComponent } from 'vue'export default defineComponent({props: {message: String},setup(props) {props.message // <-- 類型:string}
})
- 注意點:為了生成正確的運行時代碼,傳給 defineProps() 的泛型參數必須是以下之一:
//1.一個類型字面量:
defineProps<{ /*... */ }>()//2.對同一個文件中的一個接口或對象類型字面量的引用
interface Props {/* ... */}
defineProps<Props>()//3.接口或對象字面類型可以包含從其他文件導入的類型引用,但是,傳遞給 defineProps 的泛型參數本身不能是一個導入的類型:
import { Props } from './other-file'// 不支持!
defineProps<Props>()
- Props 解構默認值
//當使用基于類型的聲明時,失去了對 props 定義默認值的能力。通過目前實驗性的響應性語法糖來解決:
<script setup lang="ts">
interface Props {foo: stringbar?: number
}// 對 defineProps() 的響應性解構
// 默認值會被編譯為等價的運行時選項
const { foo, bar = 100 } = defineProps<Props>()
</script>
2.為組件的 emits 標注類型
//場景一: 使用<script setup>
<script setup lang="ts">
const emit = defineEmits(['change', 'update'])
// 基于類型
const emit = defineEmits<{(e: 'change', id: number): void(e: 'update', value: string): void
}>()
</script>//場景二: 不使用<script setup>
import { defineComponent } from 'vue'export default defineComponent({emits: ['change'],setup(props, { emit }) {emit('change') // <-- 類型檢查 / 自動補全}
})
3.為 ref() 標注類型
import { ref } from 'vue'
import type { Ref } from 'vue'
//1.ref 會根據初始化時的值推導其類型:
// 推導出的類型:Ref<number>
const year = ref(2020)
// => TS Error: Type 'string' is not assignable to type 'number'.
year.value = '2020'//2.指定一個更復雜的類型,可以通過使用 Ref 這個類型:
const year: Ref<string | number> = ref('2020')
year.value = 2020 // 成功!//3.在調用 ref() 時傳入一個泛型參數,來覆蓋默認的推導行為:
// 得到的類型:Ref<string | number>
const year = ref<string | number>('2020')
year.value = 2020 // 成功!//4.如果你指定了一個泛型參數但沒有給出初始值,那么最后得到的就將是一個包含 undefined 的聯合類型:
// 推導得到的類型:Ref<number | undefined>
const n = ref<number>()
4.為reactive() 標注類型
import { reactive } from 'vue'
//1.reactive() 也會隱式地從它的參數中推導類型:
// 推導得到的類型:{ title: string }
const book = reactive({ title: 'Vue 3 指引' })//2.要顯式地標注一個 reactive 變量的類型,我們可以使用接口:
interface Book {title: stringyear?: number
}
const book: Book = reactive({ title: 'Vue 3 指引' })
5.為 computed() 標注類型
import { ref, computed } from 'vue'
//1.computed() 會自動從其計算函數的返回值上推導出類型:
const count = ref(0)// 推導得到的類型:ComputedRef<number>
const double = computed(() => count.value * 2)// => TS Error: Property 'split' does not exist on type 'number'
const result = double.value.split('')//2.通過泛型參數顯式指定類型:
const double = computed<number>(() => {// 若返回值不是 number 類型則會報錯
})
6.為事件處理函數標注類型
//在處理原生 DOM 事件時,應該為我們傳遞給事件處理函數的參數正確地標注類型
<script setup lang="ts">
function handleChange(event) {// 沒有類型標注時 `event` 隱式地標注為 `any` 類型,// 這也會在 tsconfig.json 中配置了 "strict": true 或 "noImplicitAny": true 時報出一個 TS 錯誤。console.log(event.target.value)
}
</script><template><input type="text" @change="handleChange" />
</template>//因此,建議顯式地為事件處理函數的參數標注類型,需要顯式地強制轉換 event 上的屬性:
function handleChange(event: Event) {console.log((event.target as HTMLInputElement).value)
}
7.為 provide / inject 標注類型
/*
provide 和 inject 通常會在不同的組件中運行。要正確地為注入的值標記類型,
Vue 提供了一個 InjectionKey 接口,它是一個繼承自 Symbol 的泛型類型,
可以用來在提供者和消費者之間同步注入值的類型:
*/
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'const key = Symbol() as InjectionKey<string>provide(key, 'foo') // 若提供的是非字符串值會導致錯誤const foo = inject(key) // foo 的類型:string | undefined//建議將注入 key 的類型放在一個單獨的文件中,這樣它就可以被多個組件導入。
//當使用字符串注入 key 時,注入值的類型是 unknown,需要通過泛型參數顯式聲明:
const foo = inject<string>('foo') // 類型:string | undefined//注意注入的值仍然可以是 undefined,因為無法保證提供者一定會在運行時 provide 這個值。
//當提供了一個默認值后,這個 undefined 類型就可以被移除:
const foo = inject<string>('foo', 'bar') // 類型:string//如果你確定該值將始終被提供,則還可以強制轉換該值:
const foo = inject('foo') as string
8.為模板引用標注類型
//模板引用需要通過一個顯式指定的泛型參數和一個初始值 null 來創建:
<script setup lang="ts">
import { ref, onMounted } from 'vue'const el = ref<HTMLInputElement | null>(null)onMounted(() => {el.value?.focus()
})
</script>
/**注意為了嚴格的類型安全,有必要在訪問 el.value 時使用可選鏈或類型守衛。這是因為直到組件被掛載前,這個 ref 的值都是初始的 null,并且在由于 v-if 的行為將引用的元素卸載時也可以被設置為 null。
*/
<template><input ref="el" />
</template>
9.為組件模板引用標注類型
//有時,你可能需要為一個子組件添加一個模板引用,以便調用它公開的方法。舉例來說,我們有一個 MyModal 子組件,它有一個打開模態框的方法
<!-- MyModal.vue -->
<script setup lang="ts">
import { ref } from 'vue'const isContentShown = ref(false)
const open = () => (isContentShown.value = true)defineExpose({open
})
</script>
//為了獲取 MyModal 的類型,我們首先需要通過 typeof 得到其類型,再使用 TypeScript 內置的 InstanceType 工具類型來獲取其實例類型:
<!-- App.vue -->
<script setup lang="ts">
import MyModal from './MyModal.vue'const modal = ref<InstanceType<typeof MyModal> | null>(null)const openModal = () => {modal.value?.open()
}
</script>
//注意,如果你想在 TypeScript 文件而不是在 Vue SFC 中使用這種技巧,需要開啟 Volar 的Takeover 模式。
四、Vuex與組合式API
- 組合式API 可以通過調用 useStore 函數,來在 setup 鉤子函數中訪問 store。這與在組件中使用選項式 API 訪問 this.$store 是等效的。
import { useStore } from 'vuex'export default {setup () {const store = useStore()}
}
1.訪問 state 和 getter
- 為了訪問 state 和 getter,需要創建 computed 引用以保留響應性,這與在選項式 API 中創建計算屬性等效。
import { computed } from 'vue'
import { useStore } from 'vuex'export default {setup () {const store = useStore()return {// 在 computed 函數中訪問 statecount: computed(() => store.state.count),// 在 computed 函數中訪問 getterdouble: computed(() => store.getters.double)}}
}
2.訪問 Mutation 和 Action
- 要使用 mutation 和 action 時,只需要在 setup 鉤子函數中調用 commit 和 dispatch 函數。
import { useStore } from 'vuex'export default {setup () {const store = useStore()return {// 使用 mutationincrement: () => store.commit('increment'),// 使用 actionasyncIncrement: () => store.dispatch('asyncIncrement')}}
}
五、 其他的Composition API
1.shallowReactive與shallowRef
- shallowReactive:只處理對象最外層屬性的響應式(淺響應式)只考慮第一層數據的響應式。
- shallowRef:只處理基本數據類型的響應式,不進行對象的響應式處理,傳遞基本數據類型的話跟ref沒有任何區別,ref是可以進行對象的響應式處理的
我們正常的ref創建的數據,里面的.value是一個proxy,而shallowRef創建的數據 .value里面是一個object數據類型,所以不會響應式數據
- 什么時候使用?:
- 如果有一個對象數據,結構比較深,但變化時只是外層屬性變化 ===> shallowReactive
- 如果有一個對象數據,后續功能不會修改對象中的屬性,而是生新的對象來替換 ===> shallowRef
2.readonly與shallowReadonly
- readonly:讓一個響應式的數據變成只讀的(深只讀)
- shallowReadonly: 讓一個響應式數據變成只讀的(淺只讀)
- 應用場景:不希望數據被修改的時候
<script>import { reactive,readonly,shallowReadonly } from 'vue'export default {name: 'test9',setup(){let person = reactive({name: '張三',job:{salary: '20k',}})person = readonly(person) //這個時候修改人的信息就不會改變了,所有的都不能改/*** 頁面不進行響應式的改變,一般存在兩種情況:* 1.setup里面定義的數據改變了,但是vue沒有檢測到,這個時候是不會改變的* 2.setup里面定義的數據壓根兒就不讓你改,這個時候也沒法響應式*/person = shallowReadonly(person) //只有最外層不能修改是只讀的,但是job還是可以改的return {person}},}
</script>
3.toRaw與markRaw
- toRaw
- 作用:將一個由reactive生成的響應式對象轉換為普通對象
- 使用場景:用于讀取響應式對象對應的普通對象,對這個普通對象的所有操作,不會引起頁面更新
- markRaw:
- 作用:標記一個對象,使其永遠不會再成為響應式對象
- 使用場景:
- 1.有些值不應被設置成響應式的,例如復雜的第三方類庫等
- 2.當渲染具有不可變數據的大列表時候,跳過響應式轉換可以提高性能
import {reactive,toRaw,markRaw} from 'vue'
setup(){let person = reactive({name: '張三',})function showRawPerson(){const p = toRaw(person)p.age++console.log(p)}function addCar(){let car = {name: '奔馳'}person.car = markRaw(car) //一旦這么做時候,他就永遠不能當成響應式數據去做了}
}
4.customRef
- 創建一個自定義的ref,并對其依賴項跟蹤和更新觸發進行顯示控制
- 實現防抖效果:
<template><input type="text" v-model="keyword"><h3>{{keyword}}</h3>
</template><script>import { customRef, ref } from 'vue'export default {name: 'test10',setup(){let timer;//自定義一個ref——名為: myReffunction myRef(value){return customRef((track,trigger)=>{return {get(){console.log(`有人讀取我的值了,要把${value}給他`); //兩次輸出: v-model讀取 h3里面的插值語法調了一次track() //追蹤一下改變的數據(提前跟get商量一下,讓他認為是有用的)return value},set(newValue){console.log(`有人把myRef這個容器中數據改了:${newValue}`);clearTimeout(timer)timer = setTimeout(()=>{value = newValuetrigger() //通知vue去重新解析模板,重新再一次調用get()},500)}}})}// let keyword = ref('hello') //使用內置提供的reflet keyword = myRef('hello') //使用自定義的refreturn {keyword,}},}
</script>
5.provide與inject
- 作用:實現祖孫組件間的通信
- 套路:父組件有一個provide選項提供數據,子組件有一個inject選項來開始使用這些數據
- 具體寫法:
//父組件
<script setup>import { ref,reactive,toRefs,provide } from 'vue';
import ChildVue from './components/Child.vue';let car = reactive({name: '奔馳',price: '40w'
})
provide('car',car) //給自己的后代組件傳遞數據
const {name, price} = toRefs(car)
</script>
<template><div class="app"><h3>我是父組件, {{name}}--{{price}}</h3><ChildVue></ChildVue></div>
</template>
<style>
.app{background-color: gray;padding: 10px;box-sizing: border-box;
}
</style>
//子組件
<script setup>
import { ref } from '@vue/reactivity';
import SonVue from './Son.vue';
</script><template><div class="app2"><h3>我是子組件</h3><SonVue></SonVue></div>
</template><style>
.app2{background-color: rgb(82, 150, 214);padding: 10px;box-sizing: border-box;
}
</style>
//孫組件
<script setup>import { ref,inject } from 'vue';
let car = inject('car') //拿到父組件的數據
const {name, price} = car
</script><template><div class="app3"><h3>我是孫組件</h3><p>{{name}}-{{price}}</p></div>
</template>
<style>
.app3{background-color: rgb(231, 184, 56);padding: 10px;box-sizing: border-box;
}
</style>
6.響應式數據的判斷
- isRef:檢查一個值是否為ref對象
- isReactivce:檢查一個對象是否是由reactive創建的響應式代理
- isReadonly:檢查一個對象是否由readonly創建的只讀代理
- isProxy:檢查一個對象是否由reactive或者readonly方法創建的代理
六、Composition API的優勢
1.傳統options API存在的問題
- 使用傳統的Options API中,新增或者修改一個需求,就需要分別在data,methods,computed里面修改
2.Composition API的優勢
- 我們可以更加優雅的組織我們的代碼,函數,讓相關功能的代碼更加有序的組織在一起
七、新的組件
1.Transition
- 會在一個元素或組件進入和離開 DOM 時應用動畫
- 它是一個內置組件,這意味著它在任意別的組件中都可以被使用,無需注冊。它可以將進入和離開動畫應用到通過默認插槽傳遞給它的元素或組件上。進入或離開可以由以下的條件之一觸發:
- 由 v-if 所觸發的切換
- 由 v-show 所觸發的切換
- 由特殊元素 切換的動態組件
<button @click="show = !show">切換</button>
<Transition><p v-if="show">HelloWord</p>
</Transition>
//當一個 <Transition> 組件中的元素被插入或移除時,會發生下面這些事情
/**
1.Vue 會自動檢測目標元素是否應用了 CSS 過渡或動畫。如果是,則一些 CSS 過渡 class 會在適當的時機被添加和移除
2.如果有作為監聽器的 JavaScript 鉤子,這些鉤子函數會在適當時機被調用
3.如果沒有探測到 CSS 過渡或動畫、也沒有提供 JavaScript 鉤子,那么 DOM 的插入、刪除操作將在瀏覽器的下一個動畫幀后執行
*///針對CSS 的過渡效果
/**
1.v-enter-from:進入動畫的起始狀態。在元素插入之前添加,在元素插入完成后的下一幀移除。
2.v-enter-active:進入動畫的生效狀態。應用于整個進入動畫階段。在元素被插入之前添加,在過渡或動畫完成之后移除。這個 class 可以被用來定義進入動畫的持續時間、延遲與速度曲線類型
3.v-enter-to:進入動畫的結束狀態。在元素插入完成后的下一幀被添加 (也就是 v-enter-from 被移除的同時),在過渡或動畫完成之后移除。
4.v-leave-from:離開動畫的起始狀態。在離開過渡效果被觸發時立即添加,在一幀后被移除
5.v-leave-active:離開動畫的生效狀態。應用于整個離開動畫階段。在離開過渡效果被觸發時立即添加,在過渡或動畫完成之后移除。這個 class 可以被用來定義離開動畫的持續時間、延遲與速度曲線類型。
6.v-leave-to:離開動畫的結束狀態。在一個離開動畫被觸發后的下一幀被添加 (也就是 v-leave-from 被移除的同時),在過渡或動畫完成之后移除。
*/
.v-enter-active,
.v-leave-active {transition: opacity 0.5s ease;
}.v-enter-from,
.v-leave-to {opacity: 0;
}
2.Fragment
- 在vue2中:組件必須有一個根標簽
- 在vue3中:組件可以沒有根標簽,內部會將多個標簽包含在一個Fragment虛擬元素中
- 好處:減少標簽層級,減少內存占用
3.Teleport
- 什么是Teleport? —— Teleport是一種能夠將我們組件html結構移動到指定位置的技術(開發的時候非常有用)
//彈窗實現
<script setup>
import { ref,inject } from 'vue';
let isShow = ref(false)
</script><template><div><button @click="isShow = true">點我彈窗</button><teleport to="body"> //定位到body<div class="mask" v-if="isShow"><div class="dialog"><h4>我是一個彈窗</h4><h5>內容</h5><h5>內容</h5><h5>內容</h5><button @click="isShow = false">關閉</button></div></div></teleport></div>
</template><style>
.dialog{width: 300px;height: 300px;text-align: center;position: absolute;top: 50%;left: 50%;transform: translate(-50%,-50%);background-color: blueviolet;margin: 0 auto;
}
.mask{position: absolute;top: 0;left: 0;bottom: 0;right: 0;background-color: rgba(0, 0, 0, 0.5);
}
</style>
4.Suspense
<script setup>import { defineAsyncComponent } from 'vue'; //引入異步組件
const ChildVue = defineAsyncComponent(()=> import('./components/Child.vue')) //這叫做動態引入
//這種引入叫做異步引入,如果app不出來的話,那么Child組件也不會引入進來,有一個先后順序// import ChildVue from './components/Child.vue'; //靜態引入
// 得等,等所有的組件加載完成之后app才會一起出現/*** Suspense這個標簽,底層就內置了插槽,就可以解決異步引入有時候刷新先后出來慢的問題* v-slot:default 表示默認的輸出組件* v-slot:fallback 表示如果頁面加載的慢了,會優先展示這個內容,有點像刷新頁面的時候數據回來的慢了,就加載一會兒
*/
</script><template><div class="app"><h3>我是父組件</h3><Suspense><template v-slot:default><ChildVue></ChildVue></template><template v-slot:fallback><h3>稍等,加載中....</h3></template></Suspense></div>
</template><style>
.app{background-color: gray;padding: 10px;box-sizing: border-box;
}
</style>
/**還有一種方法就是在子組件中,setup返回一個promise對象,這里之所以可以使用setup返回promise的原因
是: 我們引入的是異步組件且使用了<Suspense></Suspense>
*/
- 等待異步組件時渲染一些后備內容,獲得更好的用戶體驗
八: 新的生命周期鉤子
1.常見的生命周期鉤子
onMounted()
onUpdated()
onUnmounted()
onBeforeMount()
onBeforeUpdate()
onBeforeUnmount()
onActivated()
onDeactivated()
onServerPrefetch()
2.新的生命周期鉤子
//1.onErrorCaptured():注冊一個鉤子,在捕獲了后代組件傳遞的錯誤時調用。
function onErrorCaptured(callback: ErrorCapturedHook): voidtype ErrorCapturedHook = (err: unknown,instance: ComponentPublicInstance | null,info: string
) => boolean | void//2.onRenderTracked():注冊一個調試鉤子,當組件渲染過程中追蹤到響應式依賴時調用。
function onRenderTracked(callback: DebuggerHook): voidtype DebuggerHook = (e: DebuggerEvent) => voidtype DebuggerEvent = {effect: ReactiveEffecttarget: objecttype: TrackOpTypes /* 'get' | 'has' | 'iterate' */key: any
}
//3.onRenderTriggered():注冊一個調試鉤子,當響應式依賴的變更觸發了組件渲染時調用。
function onRenderTriggered(callback: DebuggerHook): voidtype DebuggerHook = (e: DebuggerEvent) => voidtype DebuggerEvent = {effect: ReactiveEffecttarget: objecttype: TriggerOpTypes /* 'set' | 'add' | 'delete' | 'clear' */key: anynewValue?: anyoldValue?: anyoldTarget?: Map<any, any> | Set<any>
}
//4.onServerPrefetch():注冊一個異步函數,在組件實例在服務器上被渲染之前調用。
function onServerPrefetch(callback: () => Promise<any>): void
/**
補充:1.如果這個鉤子返回了一個 Promise,服務端渲染會在渲染該組件前等待該 Promise 完成。2.這個鉤子僅會在服務端渲染中執行,可以用于執行一些僅存在于服務端的數據抓取過程
*///試例:
<script setup>
import { ref, onServerPrefetch, onMounted } from 'vue'const data = ref(null)onServerPrefetch(async () => {// 組件作為初始請求的一部分被渲染// 在服務器上預抓取數據,因為它比在客戶端上更快。data.value = await fetchOnServer(/* ... */)
})onMounted(async () => {if (!data.value) {// 如果數據在掛載時為空值,這意味著該組件// 是在客戶端動態渲染的。將轉而執行// 另一個客戶端側的抓取請求data.value = await fetchOnClient(/* ... */)}
})
</script>
九: 解決沒有this + 各種api的方法
- 在Vue2項目中可以使用this.$router.push等方法進行路由的跳轉,但是在Vue3的setup函數里,并沒有this這個概念,因此如何使用路由方法
// 在新的vue-router里面尤大加入了一些方法,比如這里代替this的useRouter,具體使用如下:
//引入路由函數
import { useRouter } from "vue-router";
//使用
setup() {//初始化路由const router = useRouter();router.push({path: "/"});return {};
}
- 在vue2中可以通過this來訪問到$refs,vue3中由于沒有this所以獲取不到了,但是官網中提供了方法來獲取:
<template><h2 ref="root">姓名</h2>
</template>
<script>//使用setup的注意事項import { onMounted, ref } from 'vue'export default {name: 'test9',setup(){const root = ref(null)onMounted(()=>{console.log(root.value);})return {root}},}
</script>//第二種方法,也可以通過getCurrentInstance來獲取
<template><h2 ref="root">姓名</h2>
</template><script>//使用setup的注意事項import { onMounted, ref, getCurrentInstance } from 'vue'export default {name: 'test9',setup(){)const {proxy} = getCurrentInstance()onMounted(()=>{console.log(proxy.$refs.root);})return {}},}
</script>
- 關于element在vue3的使用方法,沒有this.$message等方法解決方案
//關于element在vue3的使用方法,沒有this.$message等方法解決方案
<template><!-- 測試組件 --><button @click="doLogin">登錄</button>
</template><script>
import { getCurrentInstance } from 'vue'
export default {name: 'Test',setup () {const instance = getCurrentInstance() // vue3提供的方法,創建類似于this的實例const doLogin = () => {instance.proxy.$message({ type: 'error', text: '登錄失敗' }) // 類似于this.$message()}return { doLogin }},// 如果想試用this.$message,須在mounted鉤子函數中,setup中沒有this實例,//但vue3.0中還是建議在setup函數中進行邏輯操作mounted () {this.$message({ type: 'error', text: '登錄失敗' })}
}
</script>