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"href="https://cdn.jsdelivr.net/npm/animate.css@4.1.1/animate.css"/></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阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容