一、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'><<</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? 感謝知識分享