2022-10-24

一、Vue3.0 環境搭建

使用 vite 創建 Vue(3.2.30)項目

npm install yarn -gyarn create vite vue3-project --template vuecdvue3-project // 進入項目根目錄yarn // 安裝依賴包yarn dev // 啟動本地服務

安裝 vue-router、vuex全家桶

yarn add vue-router@latest // v4.0.14

yarn add vuex@latest // v4.0.2

安裝 UI 組件庫:在Vue3環境中,一定找支持 Vue3的組件庫,那些 Vue2的組件庫是無法使用的。

yarn add ant-design-vue@next // v2.2.8

yarn add vite-plugin-components --dev // 支持ant-design-vue按需引入

支持 ant-design-vue 組件按需引入

#vite.config.tsimport{defineConfig}from'vite'importvuefrom'@vitejs/plugin-vue'importViteComponents,{AntDesignVueResolver}from'vite-plugin-components'// https://vitejs.dev/config/exportdefaultdefineConfig({plugins:[vue(),ViteComponents({customComponentResolvers:[AntDesignVueResolver()],})]})

支持 Sass 樣式語法

yarn add sass // v1.49.9

1、入口文件 main.js

import{createApp}from'vue'importrouterfrom'./router.ts'importstorefrom'./store'importAppfrom'./App.vue'// 導入UI樣式表import"ant-design-vue/dist/antd.css"constapp=createApp(App)// 配置全局屬性(這里不能再使用Vue.prototype了)app.config.globalProperties.$http=''app.use(router)// 注冊路由系統app.use(store)// 注冊狀態管理// 全局指令app.directive('highlight',{beforeMount(el,binding,vnode){el.style.background=binding.value}})app.mount('#app')// 掛載

2、Vue-Router (v4) 詳解

注意:在vue3環境中,必須要使用vue-router(v4)

創建router,使用createRouter()

指定路由模式,使用history屬性:createWebHashHistory/createWebHistory()

路由注冊,在mian.js中 app.use(router)

如果當前項目嚴格使用組合式API進行開發,必須使用 useRoute、userRouter等Hooks API進行開發。

<router-link>已經沒有tag屬性的,可以用custom和插槽實現自定義。

<router-view>新增了"插槽"功能,極其強大,參見路由中的偽代碼,它在實現<keep-alive>和<transition>動畫將變得更簡單,還可以Suspense實現Loading。

新增了幾個組合API:useRoute/useRouter/useLink。

查詢vue-router(v3)和vue-router(v4)的變化:https://next.router.vuejs.org/zh/guide/migration/index.html

在Vue3環境中編寫路由配置,參考代碼如下:

import{createRouter,createWebHashHistory}from'vue-router'constHome=()=>import('@/pages/study/Home.vue')constFind=()=>import('@/pages/study/Find.vue')constUser=()=>import('@/pages/study/User.vue')constCnode=()=>import('@/pages/cnode/index.vue')exportdefaultcreateRouter({history:createWebHashHistory(),// Hash路由routes:[{path:'/home',component:Home,meta:{transition:'fade',isAlive:true}},{path:'/find',component:Find},{path:'/user',component:User},{path:'/cnode',component:Cnode}]})

3、Vuex 根store 代碼示例

版本:在vue3環境中,必須要使用 vuex(4)

注意:在組件中使用 vuex數據時,哪怕是在setup中,也要使用 computed 來訪問狀態管理中的數據,否則數據不響應。

在Vue3環境中編寫 Vuex代碼示例如下:

#src/store/index.tsimport{createStore}from'vuex'importcnodefrom'./modules/cnode'exportdefaultcreateStore({getters:{},modules:{cnode}})

4、Vuex 子store 代碼示例

#src/store/modules/cnode.tsimport{fetchList}from'@/utils/api'exportdefault{namespaced:true,state:{msg:'cnode',list:[],cates:[{id:1,tab:'',label:'全部'},{id:2,tab:'good',label:'精華'},{id:3,tab:'share',label:'分享'},{id:4,tab:'ask',label:'問答'},{id:5,tab:'job',label:'招聘'}]},mutations:{updateList(state,payload){state.list=payload}},actions:{getList({commit},params){fetchList(params).then(list=>{console.log('文章列表',list)commit('updateList',list)})}}}

5、App 根組件代碼示例

<template><!-- 路由菜單 --><router-linkto='/home'>首頁</router-link><router-linkto='/find'>發現</router-link><router-linkto='/user'>我們</router-link><!-- 視圖容器 --><router-view/></template><scriptsetup></script><stylelang='scss'>html,body{padding:0;margin:0;}</style><stylelang='scss'scoped>a{display:inline-block;padding:5px15px;}</style>

二、組合API 詳解

為什么要使用setup組合?

Vue3 中新增的 setup,目的是為了解決 Vue2 中“數據和業務邏輯不分離”的問題。

Vue3中使用 setup 是如何解決這一問題的呢?

第1步: 用setup組合API 替換 vue2 中的data/computed/watch/methods等選項;

第2步: 把setup中相關聯的功能封裝成一個個可獨立可維護的hooks。

1、ref

作用:一般用于定義基本數據類型數據,比如 String / Boolean / Number等。

背后:ref 的背后是使用 reactive 來實現的響應式.

語法:const x = ref(100)

訪問:在 setup 中使用 .value 來訪問。

<template><h1v-text='num'></h1><!-- 在視圖模板中,無須.value來訪問 --><button@click='num--'>自減</button><!-- 在setup中,要使用.value來訪問 --><button@click='add'>自增</button></template><scriptsetup>import{ref}from'vue'constnum=ref(100)constadd=()=>num.value++</script>

2、isRef

作用:判斷一個變量是否為一個 ref 對象。

語法:const bol = isRef(x)

<template><h1v-text='hello'></h1></template><scriptsetup>import{ref,isRef,reactive}from'vue'consthello=ref('Hello')constworld=reactive('World')console.log(isRef(hello))// trueconsole.log(isRef(world))// false</script>

3、unref

作用:用于返回一個值,如果訪問的是 ref變量,就返回其 .value值;如果不是 ref變量,就直接返回。

語法:const x = unref(y)

<template><h1v-text='hello'></h1></template><scriptsetup>import{ref,unref}from'vue'consthello=ref('Hello')constworld='World'console.log(unref(hello))// 'Hello'console.log(unref(world))// 'World'</script>

4、customRef

作用:自定義ref對象,把ref對象改寫成get/set,進一步可以為它們添加 track/trigger。

<template><h1v-text='num'></h1><button@click='num++'>自增</button></template><scriptsetup>import{customRef,isRef}from'vue'constnum=customRef((track,trigger)=>{letvalue=100return{get(){track()returnvalue},set(newVal){value=newValtrigger()}}})console.log(isRef(num))// true</script>

5、toRef

作用:把一個 reactive對象中的某個屬性變成 ref 變量。

語法:const x = toRef(reactive(obj), 'key') // x.value

<template><h1v-text='age'></h1></template><scriptsetup>import{toRef,reactive,isRef}from'vue'letuser={name:'張三',age:10}letage=toRef(reactive(user),'age')console.log(isRef(age))// true</script>

6、toRefs

作用:把一個reactive響應式對象變成ref變量。

語法:const obj1 = toRefs(reactive(obj))

應用:在子組件中接收父組件傳遞過來的 props時,使用 toRefs把它變成響應式的。

<template><h1v-text='info.age'></h1></template><scriptsetup>import{toRefs,reactive,isRef}from'vue'letuser={name:'張三',age:10}letinfo=toRefs(reactive(user))console.log(isRef(info.age))// trueconsole.log(isRef(info.name))// trueconsole.log(isRef(info))// true</script>

7、shallowRef

作用:對復雜層級的對象,只將其第一層變成 ref 響應。 (性能優化)

語法:const x = shallowRef({a:{b:{c:1}}, d:2}) 如此a、b、c、d變化都不會自動更新,需要借助 triggerRef 來強制更新。

<template><h1v-text='info.a.b.c'></h1><button@click='changeC'>更新[c]屬性</button><h1v-text='info.d'></h1><button@click='changeD'>更新[d]屬性</button></template><scriptsetup>import{shallowRef,triggerRef,isRef}from'vue'letinfo=shallowRef({a:{b:{c:1}},d:2})console.log(isRef(info.value.a.b.c))// falseconsole.log(isRef(info))// trueconsole.log(isRef(info.a))// falseconsole.log(isRef(info.d))// falseconstchangeC=()=>{info.value.a.b.c++triggerRef(info)// 強制渲染更新}constchangeD=()=>{info.value.d++triggerRef(info)// 強制渲染更新}</script>

8、triggerRef

作用:強制更新一個 shallowRef對象的渲染。

語法:triggerRef(shallowRef對象)

參考代碼:見上例。

9、reactive

作用:定義響應式變量,一般用于定義引用數據類型。如果是基本數據類型,建議使用ref來定義。

語法:const info = reactive([] | {})

<template><divv-for='(item,idx) in list'><spanv-text='idx'></span>-<spanv-text='item.id'></span>-<spanv-text='item.label'></span>-<spanv-text='item.tab'></span></div><button@click='addRow'>添加一行</button></template><scriptsetup>import{reactive}from'vue'constlist=reactive([{id:1,tab:'good',label:'精華'},{id:2,tab:'ask',label:'問答'},{id:3,tab:'job',label:'招聘'},{id:4,tab:'share',label:'分享'}])constaddRow=()=>{list.push({id:Date.now(),tab:'test',label:'測試'})}</script>

10、readonly

作用:把一個對象,變成只讀的。

語法:const rs = readonly(ref對象 | reactive對象 | 普通對象)

<template><h1v-text='info.foo'></h1><button@click='change'>改變</button></template><scriptsetup>import{reactive,readonly}from'vue'constinfo=readonly(reactive({bar:1,foo:2}))constchange=()=>{info.foo++// target is readonly}</script>

11、isReadonly

作用: 判斷一個變量是不是只讀的。

語法:const bol = isReadonly(變量)

<scriptsetup>import{reactive,readonly,isReadonly}from'vue'constinfo=readonly(reactive({bar:1,foo:2}))console.log(isReadonly(info))// trueconstuser=readonly({name:'張三',age:10})console.log(isReadonly(user))// true</script>

12、isReactive

作用:判斷一變量是不是 reactive的。

注意:被 readonly代理過的 reactive變量,調用 isReactive 也是返回 true的。

<scriptsetup>import{reactive,readonly,isReactive}from'vue'constuser=reactive({name:'張三',age:10})constinfo=readonly(reactive({bar:1,foo:2}))console.log(isReactive(info))// trueconsole.log(isReactive(user))// true</script>

13、isProxy

作用:判斷一個變量是不是 readonly 或 reactive的。

<scriptsetup>import{reactive,readonly,ref,isProxy}from'vue'constuser=readonly({name:'張三',age:10})constinfo=reactive({bar:1,foo:2})constnum=ref(100)console.log(isProxy(info))// trueconsole.log(isProxy(user))// trueconsole.log(isProxy(num))// false</script>

14、toRaw

作用:得到返回 reactive變量或 readonly變量的"原始對象"。

語法::const raw = toRaw(reactive變量或readonly變量)

說明:reactive(obj)、readonly(obj) 和 obj 之間是一種代理關系,并且它們之間是一種淺拷貝的關系。obj 變化,會導致reactive(obj) 同步變化,反之一樣。

<scriptsetup>import{reactive,readonly,toRaw}from'vue'constuu={name:'張三',age:10}constuser=readonly(uu)console.log(uu===user)// falseconsole.log(uu===toRaw(user))// trueconstii={bar:1,foo:2}constinfo=reactive(ii)console.log(ii===info)// falseconsole.log(ii===toRaw(info))// true</script>

15、markRaw

作用:把一個普通對象標記成"永久原始",從此將無法再變成proxy了。

語法:const raw = markRaw({a,b})

<scriptsetup>import{reactive,readonly,markRaw,isProxy}from'vue'constuser=markRaw({name:'張三',age:10})constu1=readonly(user)// 無法再代理了constu2=reactive(user)// 無法再代理了console.log(isProxy(u1))// falseconsole.log(isProxy(u2))// false</script>

16、shallowReactive

作用:定義一個reactive變量,只對它的第一層進行Proxy,,所以只有第一層變化時視圖才更新。

語法:const obj = shallowReactive({a:{b:9}})

<template><h1v-text='info.a.b.c'></h1><h1v-text='info.d'></h1><button@click='change'>改變</button></template><scriptsetup>import{shallowReactive,isProxy}from'vue'constinfo=shallowReactive({a:{b:{c:1}},d:2})constchange=()=>{info.d++// 只改變d,視圖自動更新info.a.b.c++// 只改變c,視圖不會更新// 同時改變c和d,二者都更新}console.log(isProxy(info))// trueconsole.log(isProxy(info.d))// false</script>

17、shallowReadonly

作用:定義一個reactive變量,只有第一層是只讀的。

語法:const obj = shallowReadonly({a:{b:9}})

<template><h1v-text='info.a.b.c'></h1><h1v-text='info.d'></h1><button@click='change'>改變</button></template><scriptsetup>import{reactive,shallowReadonly,isReadonly}from'vue'constinfo=shallowReadonly(reactive({a:{b:{c:1}},d:2}))constchange=()=>{info.d++// d是讀的,改不了info.a.b.c++// 可以正常修改,視圖自動更新}console.log(isReadonly(info))// trueconsole.log(isReadonly(info.d))// false</script>

18、computed

作用:對響應式變量進行緩存計算。

語法:const c = computed(fn / {get,set})

<template><divclass='page'><spanv-for='p in pageArr'v-text='p'@click='page=p':class='{"on":p===page}'></span></div><!-- 在v-model上使用computed計算屬性 --><inputv-model.trim='text'/><br>你的名字是:<spanv-text='name'></span></template><scriptsetup>import{ref,computed}from'vue'constpage=ref(1)constpageArr=computed(()=>{constp=page.valuereturnp>3?[p-2,p-1,p,p+1,p+2]:[1,2,3,4,5]})constname=ref('')consttext=computed({get(){returnname.value.split('-').join('')},// 支持計算屬性的setter功能set(val){name.value=val.split('').join('-')}})</script><stylelang='scss'scoped>.page{&>span{display:inline-block;padding:5px15px;border:1pxsolid#eee;cursor:pointer;}&>span.on{color:red;}}</style>

19、watch

作用:用于監聽響應式變量的變化,組件初始化時,它不執行。

語法:const stop = watch(x, (new,old)=>{}),調用stop() 可以停止監聽。

語法:const stop = watch([x,y], ([newX,newY],[oldX,oldY])=>{}),調用stop()可以停止監聽。

<template><h1v-text='num'></h1><h1v-text='usr.age'></h1><button@click='change'>改變</button><button@click='stopAll'>停止監聽</button></template><scriptsetup>import{ref,reactive,watch,computed}from'vue'// watch監聽ref變量、reactive變量的變化constnum=ref(1)constusr=reactive({name:'張三',age:1})constchange=()=>{num.value++usr.age++}conststop1=watch([num,usr],([newNum,newUsr],[oldNum,oldUsr])=>{// 對ref變量,newNum是新值,oldNum是舊值console.log('num',newNum===oldNum)// false// 對reactive變量,newUsr和oldUsr相等,都是新值console.log('usr',newUsr===oldUsr)// true})// watch還可以監聽計算屬性的變化consttotal=computed(()=>num.value*100)conststop2=watch(total,(newTotal,oldTotal)=>{console.log('total',newTotal===oldTotal)// false})// 停止watch監聽conststopAll=()=>{stop1();stop2()}</script>

20、watchEffect

作用:相當于是 react中的 useEffect(),用于執行各種副作用。

語法:const stop = watchEffect(fn),默認其 flush:'pre',前置執行的副作用。

watchPostEffect,等價于 watchEffect(fn, {flush:'post'}),后置執行的副作用。

watchSyncEffect,等價于 watchEffect(fn, {flush:'sync'}),同步執行的副作用。

特點:watchEffect 會自動收集其內部響應式依賴,當響應式依賴發變化時,這個watchEffect將再次執行,直到你手動 stop() 掉它。

<template><h1v-text='num'></h1><button@click='stopAll'>停止掉所有的副作用</button></template><scriptsetup>import{ref,watchEffect}from'vue'letnum=ref(0)// 等價于 watchPostEffectconststop1=watchEffect(()=>{// 在這里你用到了 num.value// 那么當num變化時,當前副作用將再次執行// 直到stop1()被調用后,當前副作用才死掉console.log('---effect post',num.value)},{flush:'post'})// 等價于 watchSyncEffectconststop2=watchEffect(()=>{// 在這里你用到了 num.value// 那么當num變化時,當前副作用將再次執行// 直到stop2()被調用后,當前副作用才死掉console.log('---effect sync',num.value)},{flush:'sync'})conststop3=watchEffect(()=>{// 如果在這里用到了 num.value// 你必須在定時器中stop3(),否則定時器會越跑越快!// console.log('---effect pre', num.value)setInterval(()=>{num.value++// stop3()},1000)})conststopAll=()=>{stop1()stop2()stop3()}</script>

21、生命周期鉤子

選項式的 beforeCreate、created,被setup替代了。setup表示組件被創建之前、props被解析之后執行,它是組合式 API 的入口。

選項式的 beforeDestroy、destroyed 被更名為 beforeUnmount、unmounted。

新增了兩個選項式的生命周期 renderTracked、renderTriggered,它們只在開發環境有用,常用于調試。

在使用 setup組合時,不建議使用選項式的生命周期,建議使用 on* 系列 hooks生命周期。

<template><h1v-text='num'></h1><button@click='num++'>自增</button></template><scriptsetup>import{ref,onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted,onRenderTracked,onRenderTriggered,onActivated,onDeactivated,onErrorCaptured}from'vue'console.log('---setup')constnum=ref(100)// 掛載階段onBeforeMount(()=>console.log('---開始掛載'))onRenderTracked(()=>console.log('---跟蹤'))onMounted(()=>console.log('---掛載完成'))// 更新階段onRenderTriggered(()=>console.log('---觸發'))onBeforeUpdate(()=>console.log('---開始更新'))onUpdated(()=>console.log('---更新完成'))// 銷毀階段onBeforeUnmount(()=>console.log('---開始銷毀'))onUnmounted(()=>console.log('---銷毀完成'))// 與動態組件有關onActivated(()=>console.log('---激活'))onDeactivated(()=>console.log('---休眠'))// 異常捕獲onErrorCaptured(()=>console.log('---錯誤捕獲'))</script>

22、provide / inject

作用:在組件樹中自上而下地傳遞數據.

語法:provide('key', value)

語法:const value = inject('key', '默認值')

# App.vue<scriptsetup>import{ref,provide}from'vue'constmsg=ref('Hello World')// 向組件樹中注入數據provide('msg',msg)</script># Home.vue<template><h1v-text='msg'></h1></template><scriptsetup>import{inject}from'vue'// 消費組件樹中的數據,第二參數為默認值constmsg=inject('msg','Hello Vue')</script>

23、getCurrentInstance

作用:用于訪問內部組件實例。請不要把它當作在組合式 API 中獲取 this 的替代方案來使用。

語法:const app = getCurrentInstance()

場景:常用于訪問 app.config.globalProperties 上的全局數據。

<scriptsetup>import{getCurrentInstance}from'vue'constapp=getCurrentInstance()// 全局數據,是不具備響應式的。constglobal=app.appContext.config.globalPropertiesconsole.log('app',app)console.log('全局數據',global)</script>

24、關于setup代碼范式(最佳實踐)

只使用 setup 及組合API,不要再使用vue選項了。

有必要封裝 hooks時,建議把功能封裝成hooks,以便于代碼的可維護性。

能用 vite就盡量使用vite,能用ts 就盡量使用ts。

三、Vue3 組件通信

1、第一個組件(品類組件)

使用 setup 及組件API ,自定義封裝 Vue3 組件。

defineProps 用于接收父組件傳遞過來的自定義屬性。

defineEmits 用于聲明父組件傳遞過來的自定義事件。

useStore,配合 computed 實現訪問 Vuex中的狀態數據。

# 文件名 CnCate.vue<template><divclass='cates'><spanv-for='item in cates'v-text='item.label':class='{"on": tab===item.tab}'@click='change(item.tab)'></span></div></template><scriptsetup>import{defineProps,defineEmits,computed}from'vue'import{useStore}from'vuex'// 接收自定義屬性constprops=defineProps({tab:{type:String,default:''}})constemit=defineEmits(['update:tab'])// 從vuex中訪問cates數據conststore=useStore()constcates=computed(()=>store.state.cnode.cates)constchange=(tab)=>{emit('update:tab',tab)// 向父組件回傳數據}</script><stylelang="scss"scoped>.cates{padding:5px20px;background-color:rgb(246,246,246);}.catesspan{display:inline-block;height:24px;line-height:24px;margin-right:25px;color:rgb(128,189,1);font-size:14px;padding:010px;cursor:pointer;}.catesspan.on{background-color:rgb(128,189,1);color:white;border-radius:3px;}</style>

2、第二個組件(分頁組件)

使用 toRefs 把 props 變成響應式的。在Vue3中,默認情況下 props是不具備響應式的,即父組件中的數據更新了,在子組件中卻是不更新的。

使用 computed 實現動態頁碼結構的變化。

defineProps、defineEmits,分別用于接收父組件傳遞過來的自定義屬性、自定義事件。

# 文件名 CnPage.vue<template><divclass='pages'><span@click='prev'>&lt;&lt;</span><spanv-if='page>3'>...</span><spanv-for='i in pages'v-text='i':class='{"on":i===page}'@click='emit("update:page", i)'></span><span>...</span><span@click='emit("update:page", page+1)'>>></span></div></template><scriptsetup>import{defineProps,defineEmits,computed,toRefs}from'vue'letprops=defineProps({page:{type:Number,default:1}})const{page}=toRefs(props)constemit=defineEmits(['update:page'])constpages=computed(()=>{// 1? 1 2 3 4 5 ...// 2? 1 2 3 4 5 ...// 3? 1 2 3 4 5 ...// 4? ... 2 3 4 5 6 ...// n? ... n-2 n-1 n n+1 n+2 ...constv=page.valuereturnv<=3?[1,2,3,4,5]:[v-2,v-1,v,v+1,v+2]})constprev=()=>{if(page.value===1)alert('已經是第一頁了')elseemit('update:page',page.value-1)}</script><stylelang="scss"scoped>.pages{line-height:50px;text-align:right;}.pages>span{cursor:pointer;display:inline-block;width:34px;height:30px;margin:0;line-height:30px;text-align:center;font-size:12px;border:1pxsolid#ccc;}.pages>span.on{background:rgb(128,189,1);color:white;}</style>

3、在父級組件中使用 自定義組件

v-model:tab='tab' 是 :tab 和 @update:tab 的語法糖簡寫;

v-model:page='page' 是 :page 和 @update:page 的語法糖簡寫;

使用 watch 監聽品類和頁面的變化,然后觸發調接口獲取新數據。

# 文件名 Cnode.vue<template><divclass='app'><!-- <CnCate :tab='tab' @update:tab='tab=$event' /> --><CnCatev-model:tab='tab'/><!-- <CnPage :page='page' @update:page='page=$event' /> --><CnPagev-model:page='page'/></div></template><scriptsetup>import{ref,watch}from'vue'importCnCatefrom'./components/CnCate.vue'importCnPagefrom'./components/CnPage.vue'consttab=ref('')constpage=ref(1)conststop=watch([tab,page],()=>{console.log('當品類或頁碼變化時,調接口')})</script>

四、Hooks 封裝

1、為什么要封裝 Hooks ?

我們都知道,在Vue2中,在同一個.vue組件中,當 data、methods、computed、watch 的體量較大時,代碼將變得臃腫。為了解決代碼臃腫問題,我們除了拆分組件外,別無它法。

在Vue3中,同樣存在這樣的問題:當我們的組件開始變得更大時,邏輯關注點將越來越多,這會導致組件難以閱讀和理解。但是,在Vue3中,我們除了可以拆分組件,還可以使用 Hooks封裝來解決這一問題。

所謂 Hooks封裝,就是把不同的邏輯關注點抽離出來,以達到業務邏輯的獨立性。這一思路,也是Vue3 對比Vue2的最大亮點之一。

2、如何封裝 Hooks 呢?

在 setup 組合的開發模式下,把具體某個業務功能所用到的 ref、reactive、watch、computed、watchEffect 等,提取到一個以 use* 開頭的自定義函數中去。

封裝成 use* 開頭的Hooks函數,不僅可以享受到封裝帶來的便利性,還有利于代碼邏輯的復用。Hooks函數的另一個特點是,被復用時可以保持作用域的獨立性,即,同一個Hooks函數被多次復用,彼此是不干擾的。

3、在哪些情況下需要封裝 Hooks呢?

我總結了兩種場景:一種是功能類Hooks,即為了邏輯復用的封裝;另一種是業務類Hooks,即為了邏輯解耦的封裝。下面我給兩組代碼,說明這兩種使用場景。

4、示例:功能類 Hooks封裝

import{computed}from'vue'import{useRoute}from'vue-router'import{useStore}from'vuex'// 返回路由常用信息exportfunctionuseLocation(){constroute=useRoute()constpathname=route.fullPathreturn{pathname}}// 用于方便地訪問Vuex數據exportfunctionuseSelector(fn){conststore=useStore()// Vuex數據要用computed包裹,處理響應式問題returncomputed(()=>fn(store.state))}// 用于派發actions的exportfunctionuseDispatch(){conststore=useStore()returnstore.dispatch}

5、示例:業務類 Hooks封裝

import{ref,computed,watch,watchEffect}from'vue'import{useDispatch,useSelector}from'@/hooks'// 業務邏輯封裝exportfunctionuseCnode(){lettab=ref('')// tab.valueletpage=ref(1)// page.valueconstdispatch=useDispatch()// 使用 store數據constcates=useSelector(state=>state.cnode.cates)constlist=useSelector(state=>state.cnode.list)// 用于處理list列表數據constnewList=computed(()=>{constresult=[]list.value.forEach(ele1=>{constcate=cates.value.find(ele2=>ele2.tab===ele1.tab)ele1['label']=ele1.top?'置頂':(ele1.good?'精華':(cate?.label||'問答'))ele1['first']=tab.value===''result.push(ele1)})returnresult})// 相當于react中useEffect(fn, [])// watchEffect,它可以自動收集依賴項watchEffect(()=>{dispatch('cnode/getList',{tab:tab.value,limit:5,page:page.value})})// 當品類發生變化時,頁碼重置為第一頁watch(tab,()=>page.value=1)return[tab,page,newList]}

最后想說的是,不能為了封裝Hooks而封裝。要看具備場景:是否有復用的價值?是否有利于邏輯的分離?是否有助提升代碼的可閱讀性和可維護性?

五、Vue3 新語法細節

1、在Vue2中,v-for 和 ref 同時使用,這會自動收集 $refs。當存在嵌套的v-for時,這種行為會變得不明確且效率低下。在Vue3中,v-for 和 ref 同時使用,這不再自動收集$refs。我們可以手動封裝收集 ref 對象的方法,將其綁定在 ref 屬性上。

<template><divclass='grid'v-for="i in 5":ref='setRef'><spanv-for='j in 5'v-text='(i-1)*5+j':ref='setRef'></span></div></template><scriptsetup>import{onMounted}from'vue'// 用于收集ref對象的數組constrefs=[]// 定義手動收集ref的方法constsetRef=el=>{if(el)refs.push(el)}onMounted(()=>console.log('refs',refs))</script><stylelang='scss'scoped>.grid{width:250px;height:50px;display:flex;text-align:center;line-height:50px;&>span{flex:1;border:1pxsolid#ccc;}}</style>

2、在Vue3中,使用 defineAsyncComponent 可以異步地加載組件。需要注意的是,這種異步組件是不能用在Vue-Router的路由懶加載中。

<scriptsetup>import{defineAsyncComponent}from'vue'// 異步加載組件constAsyncChild=defineAsyncComponent({loader:()=>import('./components/Child.vue'),delay:200,timeout:3000})</script>

3、Vue3.0中的 $attrs,包含了父組件傳遞過來的所有屬性,包括 class 和 style 。在Vue2中,$attrs 是接到不到 class 和 style 的。在 setup 組件中,使用 useAttrs() 訪問;在非 setup組件中,使用 this.$attrs /setupCtx.attrs 來訪問。

<scriptsetup>// 在非setup組件中,使用this.$attrs/setupCtx.attrsimport{useAttrs}from'vue'constattrs=useAttrs()// 能夠成功訪問到class和styleconsole.log('attrs',attrs)</script>

4、Vue3中,移除了 $children 屬性,要想訪問子組件只能使用 ref 來實現了。在Vue2中,我們使用 $children 可以方便地訪問到子組件,在組件樹中“肆意”穿梭。

5、Vue3中,使用 app.directive() 來定義全局指令,并且定義指令時的鉤子函數們也發生了若干變化。

app.directive('highlight',{// v3中新增的created(){},// 相當于v2中的 bind()beforeMount(el,binding,vnode,prevVnode){el.style.background=binding.value},// 相當于v2中的 inserted()mounted(){},// v3中新增的beforeUpdate(){},// 相當于v2中的 update()+componentUpdated()updated(){},// v3中新增的beforeUnmount(){},// 相當于v2中的 unbind()unmounted(){}})

6、data 選項,只支持工廠函數的寫法,不再支持對象的寫法了。在Vue2中,創建 new Vue({ data }) 時,是可以寫成對象語法的。

<script>import{createApp}from'vue'createApp({data(){return{msg:'Hello World'}}}).mount('#app')</script>

7、Vue3中新增了 emits 選項。在非<script setup>寫法中,使用 emits選項 接收父組件傳遞過來的自定義,使用 ctx.emit() / this.$emit() 來觸發事件。在<script setup>中,使用 defineEmits 來接收自定義事件,使用 defineProps 來接收自定義事件。

<template><h1v-text='count'@click='emit("update:count", count+1)'></h1></template><scriptsetup>import{defineProps,defineEmits}from'vue'// 接收父組件傳遞過來的自定義屬性constprops=defineProps({count:{type:Number,default:100}})// 接收父組件傳遞過來的自定義事件// emit 相當于 vue2中的 this.$emit()constemit=defineEmits(['change','update:count'])</script>

8、Vue3中 移除了 $on / $off / $once 這三個事件 API,只保留了 $emit 。

9、Vue3中,移除了全局過濾器(Vue.filter)、移除了局部過濾器 filters選項。取而代之,你可以封裝自定義函數或使用 computed 計算屬性來處理數據。

10、Vue3 現在正式支持了多根節點的組件,也就是片段,類似 React 中的 Fragment。使用片段的好處是,當我們要在 template 中添加多個節點時,沒必要在外層套一個 div 了,套一層 div 這會導致多了一層 DOM結構??梢?,片段 可以減少沒有必要的 DOM 嵌套。

<template><header>...</header><main>...</main><footer>...</footer></template>

11、函數式組件的變化:在Vue2中,要使用 functional 選項來支持函數式組件的封裝。在Vue3中,函數式組件可以直接用普通函數進行創建。如果你在 vite 環境中安裝了 `@vitejs/plugin-vue-jsx` 插件來支持 JSX語法,那么定義函數式組件就更加方便了。

#Counter.tsxexportdefault(props,ctx)=>{// props是父組件傳遞過來的屬性// ctx 中有 attrs, emit, slotsconst{value,onChange}=propsreturn(<><h1>函數式組件</h1><h1onClick={()=>onChange(value+1)}>{value}</h1></>)}

12、Vue2中的Vue構造函數,在Vue3中已經不能再使用了。所以Vue構造函數上的靜態方法、靜態屬性,比如 Vue.use/Vue.mixin/Vue.prototype 等都不能使用了。在Vue3中新增了一套實例方法來代替,比如 app.use()等。

import{createApp}from'vue'importrouterfrom'./router'importstorefrom'./store'importAppfrom'./App.vue'constapp=createApp(App)// 相當于 v2中的 Vue.prototypeapp.config.globalProperties.$http=''// 等價于 v2中的 Vue.useapp.use(router)// 注冊路由系統app.use(store)// 注冊狀態管理

13、在Vue3中,使用 getCurrentInstance 訪問內部組件實例,進而可以獲取到 app.config 上的全局數據,比如 $route、$router、$store 和自定義數據等。這個 API 只能在?setup?或?生命周期鉤子?中調用。

<script>// 把使用全局數據的功能封裝成Hooksimport{getCurrentInstance}from'vue'functionuseGlobal(key){returngetCurrentInstance().appContext.config.globalProperties[key]}</script><scriptsetup>import{getCurrentInstance}from'vue'constglobal=getCurrentInstance().appContext.config.globalProperties// 得到 $route、$router、$store、$http ...使用自定義Hooks方法訪問全局數據const$store=useGlobal('$store')console.log('store',$store)</script>

14、我們已經知道,使用 provide 和 inject 這兩個組合 API 可以組件樹中傳遞數據。除此之外,我們還可以應用級別的 app.provide() 來注入全局數據。在編寫插件時使用 app.provide() 尤其有用,可以替代app.config.globalProperties。

#main.tsconstapp=createApp(App)app.provide('global',{msg:'Hello World',foo:[1,2,3,4]})#在組件中使用<scriptsetup>import{inject}from'vue'constglobal=inject('global')</script>

15、在Vue2中,Vue.nextTick() / this.$nextTick 不能支持 Webpack 的 Tree-Shaking 功能的。在 Vue3 中的 nextTick ,考慮到了對 Tree-Shaking 的支持。

<template><divv-html='content'></div></template><scriptsetup>import{ref,watchPostEffect,nextTick}from'vue'constcontent=ref('')watchPostEffect(()=>{content.value=`<div id='box'>動態HTML字符串</div>`// 在nextTick中訪問并操作DOMnextTick(()=>{constbox=document.getElementById('box')box.style.color='red'box.style.textAlign='center'})})</script>

16、Vue3中,對于?v-if/v-else/v-else-if的各分支項,無須再手動綁定 key了, Vue3會自動生成唯一的key。因此,在使用過渡動畫 對多個節點進行顯示隱藏時,也無須手動加 key了。

<template><!-- 使用<teleport>組件,把animate.css樣式插入到head標簽中去 --><teleportto='head'><linkrel="stylesheet"/></teleport><!-- 使用<transition>過渡動畫,無須加key了 --><transitionenter-active-class='animate__animated animate__zoomIn'leave-active-class='animate__animated animate__zoomOutUp'mode='out-in'><h1v-if='bol'>不負當下</h1><h1v-else>不畏未來</h1></transition><button@click='bol=!bol'>Toggle</button></template><scriptsetup>import{ref}from'vue'constbol=ref(true)</script>

17、在Vue2中,使用 Vue.config.keyCodes 可以修改鍵盤碼,這在Vue3 中已經淘汰了。

18、Vue3中,$listeners 被移除了。因此我們無法再使用 $listeners 來訪問、調用父組件給的自定義事件了。

19、在Vue2中,根組件掛載 DOM時,可以使用 el 選項、也可以使用 $mount()。但,在 Vue3中只能使用 $mount() 來掛載了。并且,在 Vue 3中,被渲染的應用會作為子元素插入到 <div id='app'> 中,進而替換掉它的innerHTML。

20、在Vue2中,使用 propsData 選項,可以實現在 new Vue() 時向根組件傳遞 props 數據。在Vue3中,propsData 選項 被淘汰了。替代方案是:使用createApp的第二個參數,在 app實例創建時向根組件傳入 props數據。

#main.tsimport{createApp}from'vue'importAppfrom'./App.vue'// 使用第二參數,向App傳遞自定義屬性constapp=createApp(App,{name:'vue3'})app.mount('#app')// 掛載#App.vue<scriptsetup>import{defineProps}from'vue'// 接收 createApp() 傳遞過來的自定義屬性constprops=defineProps({name:{type:String,default:''}})console.log('app props',props)</script>

21、在Vue2中,組件有一個 render 選項(它本質上是一個渲染函數,這個渲染函數的形參是 h 函數),h 函數相當于 React 中的 createElement()。在Vue3中,render 函數選項發生了變化:它的形參不再是 h 函數了。h 函數變成了一個全局 API,須導入后才能使用。

import{createApp,h}from'vue'importAppfrom'./App.vue'constapp=createApp({render(){returnh(App)}},{name:'vue3'})app.$mount('#app')

22、Vue3中新增了實驗性的內置組件 <suspense>,它類似 React.Suspense 一樣,用于給異步組件加載時,指定 Loading指示器。需要注意的是,這個新特征尚未正式發布,其 API 可能隨時會發生變動。

<template><suspense><!-- 用name='default'默認插槽加載異步組件 --><AsyncChild/><!-- 異步加載成功前的loading 交互效果 --><template#fallback><div>Loading...</div></template></suspense></template><scriptsetup>import{defineAsyncComponent}from'vue'constAsyncChild=defineAsyncComponent({loader:()=>import('./components/Child.vue'),delay:200,timeout:3000})</script>

23、Vue3中,過渡動畫<transition>發生了一系列變化。之前的 v-enter 變成了現在的 v-enter-from , 之前的 v-leave 變成了現在的 v-leave-from 。另一個變化是:當使用<transition>作為根結點的組件,從外部被切換時將不再觸發過渡效果。

<template><transitionname='fade'><h1v-if='bol'>但使龍城飛將在,不教胡馬度陰山!</h1></transition><button@click='bol=!bol'>切換</button></template><scriptsetup>import{ref}from'vue'constbol=ref(true)</script><stylelang='scss'scoped>.fade-enter-from{opacity:0;color:red;}.fade-enter-active{transition:all1sease;}.fade-enter-to{opacity:1;color:black;}.fade-leave-from{opacity:1;color:black;}.fade-leave-active{transition:all1.5sease;}.fade-leave-to{opacity:0;color:blue;}</style>

24、在Vue3中,v-on的.native修飾符已被移除。

25、同一節點上使用 v-for 和 v-if ,在Vue2中不推薦這么用,且v-for優先級更高。在Vue3中,這種寫法是允許的,但 v-if 的優秀級更高。

26、在Vue2中,靜態屬性和動態屬性同時使用時,不確定最終哪個起作用。在Vue3中,這是可以確定的,當動態屬性使用 :title 方式綁定時,誰在前面誰起作用;當動態屬性使用 v-bind='object'方式綁定時,誰在后面誰起作用。

<template><!-- 這種寫法,同時綁定靜態和動態屬性時,誰在前面誰生效! --><divid='red':id='("blue")'>不負當下</div><div:title='("hello")'title='world'>不畏未來</div><hr><!-- 這種寫法,同時綁定靜態和動態屬性時,誰在后面誰生效! --><divid='red'v-bind='{id:"blue"}'>不負當下</div><divv-bind='{title:"hello"}'title='world'>不畏未來</div></template>

27、當使用watch選項偵聽數組時,只有在數組被替換時才會觸發回調。換句話說,在數組被改變時偵聽回調將不再被觸發。要想在數組被改變時觸發偵聽回調,必須指定deep選項。

<template><divv-for='t in list'v-text='t.task'></div><button@click.once='addTask'>添加任務</button></template><scriptsetup>import{reactive,watch}from'vue'constlist=reactive([{id:1,task:'讀書',value:'book'},{id:2,task:'跑步',value:'running'}])constaddTask=()=>{list.push({id:3,task:'學習',value:'study'})}// 當無法監聽一個引用類型的變量時// 添加第三個選項參數 { deep:true }? watch(list,()=>{console.log('list changed',list)},{deep:true})</script>

28、在Vue2中接收 props時,如果 prop的默認值是工廠函數,那么在這個工廠函數里是有 this的。在Vue3中,生成 prop 默認值的工廠函數不再能訪問this了。

<template><!-- v-for循環一個對象 --><divv-for='(v,k,i) in info'><spanv-text='i'></span>-<spanv-text='k'></span>-<spanv-text='v'></span></div><!-- v-for循環一個數組 --><divv-for='n in list'v-text='n'></div></template><scriptsetup>import{defineProps,inject}from'vue'// 為該 prop 指定一個 default 默認值時,// 如果是對象或數組類型,默認值必須從一個工廠函數返回。constprops=defineProps({info:{type:Object,default(){// 在Vue3中,這里是沒有this的,但可以訪問injectconsole.log('this',this)// nullreturninject('info',{name:'張三',age:10})}},list:{type:Array,default(){returninject('list',[1,2,3,4])}}})</script>

29、Vue3中,新增了 <teleport>組件,這相當于 ReactDOM.createPortal(),它的作用是把指定的元素或組件渲染到任意父級作用域的其它DOM節點上。上面第 16個知識點中,用到了 <teleport> 加載 animate.css 樣式表,這算是一種應用場景。

除此之外,<teleport>還常用于封裝 Modal 彈框組件,示例代碼如下:

# Modal.vue<template><!-- 當Modal彈框顯示時,將其插入到<body>標簽中去 --><teleportto='body'><divclass='layer'v-if='visibled'@click.self='cancel'><divclass='modal'><header></header><main><slot></slot></main><footer></footer></div></div></teleport></template><scriptsetup>import{defineProps,defineEmits,toRefs}from'vue'constprops=defineProps({visibled:{type:Boolean,default:false}})constemit=defineEmits(['cancel'])constcancel=()=>emit('cancel')</script><stylelang="scss">.layer{position:fixed;bottom:0;top:0;right:0;left:0;background-color:rgba(0,0,0,0.7);.modal{width:520px;position:absolute;top:100px;left:50%;margin-left:-260px;box-sizing:border-box;padding:20px;border-radius:3px;background-color:white;}}</style>

在業務頁面組件中使用自定義封裝的 Modal 彈框組件:

<template><Modal:visibled='show'@cancel='show=!show'><div>彈框主體內容</div></Modal><button@click='show=!show'>打開彈框</button></template><scriptsetup>import{ref,watch}from'vue'importModalfrom'./components/Modal.vue'constshow=ref(false)</script>

30、在Vue3中,移除了 model 選項,移除了 v-bind 指令的 .sync 修飾符。在Vue2中,v-model 等價于 :value + @input ;在Vue3中,v-model 等價于 :modelValue + @update:modelValue 。在Vue3中,同一個組件上可以同時使用多個 v-model。在Vue3中,還可以自定義 v-model 的修飾符。

封裝帶有多個 v-model的自定義組件:

# GoodFilter.vue<template><span>請選擇商家(多選):</span><spanv-for='s in shopArr'><inputtype='checkbox':value='s.value':checked='shop.includes(s.value)'@change='shopChange'/><spanv-text='s.label'></span></span><br><span>請選擇價格(單選):</span><spanv-for='p in priceArr'><inputtype='radio':value='p.value':checked='p.value===price'@change='priceChange'/><spanv-text='p.label'></span></span></template><scriptsetup>import{reactive,defineProps,defineEmits,toRefs}from'vue'constprops=defineProps({shop:{type:Array,default:[]},// 接收v-model:shop的自定義修飾符shopModifiers:{default:()=>({})},price:{type:Number,default:500},// 接收v-model:price的自定義修飾符priceModifiers:{default:()=>({})}})const{shop,price}=toRefs(props)// 接收v-model的自定義事件constemit=defineEmits(['update:shop','update:price'])constshopArr=reactive([{id:1,label:'華為',value:'huawei'},{id:2,label:'小米',value:'mi'},{id:3,label:'魅族',value:'meizu'},{id:4,label:'三星',value:'samsung'}])constpriceArr=reactive([{id:1,label:'1000以下',value:500},{id:2,label:'1000~2000',value:1500},{id:3,label:'2000~3000',value:2500},{id:4,label:'3000以上',value:3500}])// 多選框constshopChange=ev=>{const{checked,value}=ev.target// 使用v-model:shop的自定義修飾符const{sort}=props.shopModifiersletnewShop=(checked?[...shop.value,value]:shop.value.filter(e=>e!==value))if(sort)newShop=newShop.sort()emit('update:shop',newShop)}// 單選框constpriceChange=ev=>{emit('update:price',Number(ev.target.value))}</script><stylelang='scss'scoped>.nav{&>span{display:inline-block;padding:5px15px;}&>span.on{color:red;}}</style>

使用帶有多個 v-model 的自定義組件:

<template><GoodFilterv-model:shop.sort='shop'v-model:price='price'/></template><scriptsetup>import{ref,reactive,watch}from'vue'importGoodFilterfrom'./components/GoodFilter.vue'constshop=ref([])constprice=ref(500)watch([shop,price],()=>{console.log('changed',shop.value,price.value)})</script>

六、寫在最后

后續繼續分享?Vue3響應式原理、Vite構建工具、Pinia(2)、ElementPlus、Vant(3)?等的使用。Vue3全家桶值得深入學習與關注,為Vue開發者帶來全新的開發體驗。


轉發備份摘自于 https://zhuanlan.zhihu.com/p/482851017? 感謝知識分享

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容