Option API 的写法会碰到的问题?
明明是相同操作逻辑,却散落在元件的个地方,导致对应属性的程式码有所增长,这样时间久了不仅不利于程式码维护,而之后想再使用某功能,却因为资料状态依赖关系 也无法重复使用了
在当时 Vue 有提供几种方式来解决像是 自定义指令 directive,minxin,但是仍然难以解决Option API 难以被重复使用的缺点
Composition API 程式能依功能分类来使用,可增加阅读性之外同时还可以 跨元件使用来增加复用性
<script setup>
- script setup中会自动注册引入的元件,也就是说,它会以档名为主作为元件,这样就不用再写
name
属性来定义了 - 所有的变数、函式都可以直接给模板(template)使用,不需要再
return
defineProps 和 defineEmits
defineProps
和 defineEmits
主要是用来在<script setup>
定义 props 与 emits,而其中这两个接受的值与我们过去定义 props 与 emits 的值相同。
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
// setup code
</script>
useSlots 和 useAttrs
针对 $slots
与$attrs
,官方文件有提到通常会直接通过 模板
来使用它,不过当你需要在<script setup>
中使用它们时,可以引入 useSlots
和useAttrs
来读取。
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
Setup()
setup 函式 为 Composition API启动元件的初始入口位置,而在其函式内通常会包含 生命周期 hook、状态资料 计算属性 等。最后再将模板需要使用的状态及功能回传出来给 template 就可以了。
其中在元件内的 功能逻辑与资料状态其实不一定要全写在setup
内,可以依照情境需求适时地抽出来,当需要使用时再 import
进来使用即可, 而这样做的好处主要是可以让元件内的 setup
函式利于 程式码阅读及维护。
setup 中的参数 props/context
当元件启动时,在 setup 函式中会分别带入 props
与 context
两个参数。
props
export default {
props: {
name: String
},
setup(props,{attrs,slots,emit}) {
//const { name } = props 不可以这样喔,需透过toRefs!
console.log(props.name)
.... 略
}
}
从上我们可以看到,因为没有 this 可以使用,所以 setup 透过参数传递 props 物件
来供内部函式使用。
而这边注意的是由于 props 中的资料是响应式的,所以要取出 props 内的资料不能直接使用 ES6 解构, 这样会造成 props 的响应消失,如需要解构使用,我们可以透过 Vue3 新增的 APItoRefs
方法来完成此目的。
context
而 setup 函数传递的第二个参数就是 context
物件,而在 Context 物件内含有attrs
、slots
、emit
API,而与 props 不同地方的是,因为 context 只是一个普通的 JS 物件,不是响应式的,所以针对其 可以 ES6 解构来进行使用
生命周期 Hook
Option API | Composition API |
---|---|
beforeCreate | setup |
created | setup |
beforeMount | onbeforeMount |
mounted | onMounted |
beforeUpdated | onBeforeUpdated |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmount | onUnmount |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
从上生命周期 hook 对照图我们可以发现,在过去的 created 与 beforeCreate 被 setup 所取代了,这是为什么呢?
针对这个疑问,文件是这样说明的:
在新式setup函数执行时机主要是 介于 beforeCreate、created 两者之间,因此就不需要再特地来定义他们,直接由 setup 来取代即可。而这也表示,在 created 之后的生命周期钩子 (onMounted、onUpdated 等) 都应该在 setup () 中来编写。
import { onMounted, onUpdated, onUnmounted } from 'vue'
export default {
setup() {
onMounted(() => { // 使用的方式改为函数式的方式来使用
console.log('mounted!')
})
onUpdated(() => {
console.log('updated!')
})
onUnmounted(() => {
console.log('unmounted!')
})
}
}
资料定义 ref 与 reactive
过去会透过 Vue Instance 中的data
的属性,来 定义资料状态或着方法,而 data 本会身是一个 回传 object 的 function,接着我们就可以在模板 (template) 或着其他属性中来使用。
而针对 data 中内部 资料状态 的响应,Vue 是采去 object.definePropertity 的方式,透过创建 getter 与 setter 来追踪资料状态的变化。
透过宣告一个 ref (null) 来回传给模板 (template) 进行绑定
在 ref () 函式中可以接受一个任何基本型别的参数,且会回传一个响应式物件 (更正 ref 也是被 ES6 proxy 所代理
)。而在这物件之中会提供一个.value
属性来更新或读取资料内的状态。
如果是针对 v-for 所渲染的多个动态元素我们要如何抓到各个 Dom 元素呢?
首先,我们可以先宣告一个 ref 包装的阵列 ref([])
回传给模板使用,同时模板这边透过 :ref
来绑定 function 每一个 Dom 元素
我们就可以透过 divs [x] 取得对应的 Dom 节点了 (自行带入对应 index)
<template>
<div v-for ="(num,i) in nums "
:ref="(el)=>{ divs[i] = el }">{{num}}</div>
<template/>
<script>
import { ref, reactive} form 'vue'
export default{
setup(){
const nums = reactive([1,2,3])
const divs = ref([]);
//其中需要注意的地方,因为 Dom 节点是动态产生的,有可能因为 nums 内容顺序更动而改变,所以在每次更动前我们需要在 onBeforeUpdate 重置一下,确保拿到的元素是正确的。
onBeforeUpdate(()=>{
divs.value =[]
})
return{ nums,divs }
}
}
</script>
其中需要注意的地方,因为 Dom 节点是动态产生的,有可能因为 nums 内容顺序更动而改变,所以在每次更动前我们需要在
onBeforeUpdate
重置一下,确保拿到的元素是正确的。
reactive
reactive 为 Composition API 定义资料状态的另一个函式,其中 reactive () 只接受 物件
及 阵列
型别的参数,最后会回传一个被 ES6 Proxy 所代理过的物件
,达到响应式的更新,而其中要读取或更新其资料的方式就不需透过.value 来取值了
toRef & toRefs
props 与 reactive无法使用直接透过 ES6 解构语法来自动解构,因为这样会导致 资料失去原本的响应功能,所以针对这个问题, Vue 提供了 toRef 与 toRefs两个函式来进行 解构后同时保有响应功能
toRef
toRef 会将原本响应式物件内的一个属性抽离出来包装成一个 ref 的值,因为与原本对象是连接的,所以修改时,原本的值也会跟着变动。
<template>
<div class="wrap">
<div class="container">
<h1>使用 toRef 测试</h1>
{{userName}}
<input type="text" v-model="userName">
</div>
</div>
</template>
<script>
import { reactive,toRef,onUpdated } from 'vue'
export default{
setup(){
const userInfo= reactive({
name:'Winnie',
age: 24
})
const userName = toRef(userInfo, 'name') //将响应物件里的属性抽离出来 使用
onUpdated(()=>{
console.log('toRef:',userName.value)
console.log('元物件:',userInfo.name) // 元物件内的name
})
return {
userName //回传 name属性 给 template使用
}
//从上我们可以看到 userName 是从 userInfo 中的 name 属性抽离出来的,
//当传出去给 template 使用并更动它时,除了 userName 的值更新了从 console.log 也可以看 userInfo.name 的值也跟着跟新了。
}
}
</script>
toRefs
toRefs 会将原本 响应式物件 转为 普通响应式物件。其中在 toRefs 后的物件与值是引用关系且每个值都转为 ref 来达到内部值保有响应性。
...略
setup(){
const setCount= reactive({
count:1,
add: ()=> setCount.count ++
})
// const { count, add } = toRefs(info) 也可以这样来解构,可以选择要用的就好了。
return {
...toRefs(setCount)
}
}
计算属性 Computed
123 木头人范例:
- 我们可以看到在 Composition API 中computed () 参数为一个 getter 函式,同时会回传一个ref 物件来进行响应,所以当我们今天要取
computed中的值
时,一样也需要透过.value
进行读取。
//Character.js
<template>
<div v-show='HP < 0'> Game Over</div> // 当血量少于0 显示Game over
<div v-show='HP === 10'>Success</div>
// 角色资讯
<div>Character :{{characterNum}}</div>
<div>HP: {{HP}}</div>
<div>step : {{step}}</div>
<div>HP:{{HP}}</div>
// 动作执行
<button @click="goForward">逃命啊</button>
<button @click="goDie">啊~死了</button>
</template>
<script>
import { ref, computed, onUpdated } from 'vue'
export default{
setup(){
const characterNum = '456'
const initHP = ref(1)
const step = ref(0)
const HP = computed(()=> initHP.value += step.value)//computed 参数为一个getter 函式
const goForward = ()=> step.value ++
const goDie = ()=> step.value - 10
onUpdated(()=>{
console.log(HP.value)// computed会回传一个ref物件,所以读取内部值时需要加上.value
})
return {
characterNum, //将资料传出去给模板使用
HP,
step,
goForward,
goDie
}
}
}
- 另外如果今天我们要指定 set 的话,也可以透过 传入物件 来指定get与set,(
这边就不用木头人了,因为太长了)
<script>
import { ref, computed, onUpdated } from 'vue'
export default{
setup(){
const count = ref(1)
const setCount = computed({
get:()=> count.value +1,
set:(val)=> count.value =val-1
})
}
}
watch()& watchEffect()
watch () 用法上,需改为一个 函式
的语法来使用
从下方我们可以看到,当我们监听一个资料状态时,watch () 中第一个参数为观察ref物件
,第二个参数为一个 callBack
,当状态更新,就会针对其来执行 callback。
<template>
<div>
{{name}} <button @click="setName">隐姓埋名</button>
</div>
</template>
<script>
import {ref, reactive ,watch } from 'vue';
export default({
setup() {
const name =ref('winnie')
const setName = () => {
name.value = '大侠爱吃汉堡饱'
}
//观察一个ref物件
watch(name, (val,oldVa)=>{
console.log('newName:'${val},'oldName:'${oldVal});
})
return {
name,
setName,
}
}
});
</script>
watch()三个小问题解答
- 如果是 reactive 中的其中一属性呢?
我们可以将 watch () 第一个参数改为 有回传值的 getter 函式,
//略...
<script>
import {reactive,watch } from 'vue';
//略..
const name = reactive({
initName : 'winnie',
setName: name.initName = '大侠爱吃汉堡饱'
})
watch(
()=> name.initName, // 观察一个getter
(name,oldName)=>{
console.log('newName:'${val},'oldName:'${oldVal});
})
//略..
- 如果今天有多笔资料需要被观察?
我们可以透过 阵列
的方式来带入第一个参数之中,同时被观察。
这边需注意 我们可以从 console.log 中看到,因为是多笔资料写在一个 watch () 之中,所以 callback 是
共用
的,如要执行不同动作,可以分开来写。
//略...
<script>
import {reactive,watch } from 'vue';
//略..
const name = reactive({
initName : 'winnie',
setName: name.initName = '大侠爱吃汉堡饱',
fakeName: 'Banana',
setFake : name.fakeName='你不是大侠吃香蕉'
})
watch(
[()=> name.initName,()=> name.fakeName], // 使用阵列侦听多笔
([name,fakeName],[oldName,oldFakeName])=>{
console.log('Name:'+name,'oldName:'+oldName);
console.log('fakeName:'+fakeName,'oldName:'+oldFakeName);
})
//略..
- 如要针对 物件中的物件 做深层监听?
我们可以在 watch () 第三个参数中 加上 {deep:true}
// 略..
watch(state.obj,(val,oldVal)=>{}, {deep:true});
//略..
watchEffect()
控制台我们可以看到 ,watchEffect () 不用指定要监听的目标,只要在 callback 函式中对应响应式资料更新后就会依照对应资料来执行了,而与 watch 不同的是,watchEffect () 在初始 setup () 的时候,就会先执行一次了。
export default{
setup(){
const name =ref('winnie')
const setName = ()=> name.value = '大侠爱吃汉堡饱'
watchEffect(()=>{
console.log(name.value)
})
}
}
解除 watchEffect 监听
在每一个 watchEffect () 中都会回传一个专属的 WachStopHandle 函式
。如果想要停止监听,可以直接呼叫其来停止监听。
const stop = watchEffect(()=>{
console.log(name.value)
})
stop();
provide/inject 实现跨阶层元件之间的状态沟通
从以上祖孙失联中的传话方式就好比我们在 Vue 之中使用传统 props
来做跨阶层元件之间的状态沟通,透过一层一层元件来传递资料,而这样不仅会很麻烦,同时也会增加元件之间的耦合度。
我们需在提供资料状态的上层元件加上provide () 函式来传递,而其主要接受两个参数,来定义要传递的资料名称与值,再透过 Inject()使用刚才所定义的名称来取值
// grandpa.vue---------------------------------------------
<script>
import { ref, provide } from 'vue';
export default ({
setup() {
//传递资料也可以是任何形式,如ref物件 、字串等
provide('figure', "爷爷");
provide('message', "小明 你吃饱了吗");
}
});
</script>
// grandson.vue--------------------------------------------
<script>
import { inject } from 'vue';
export default ({
setup() {
//透过 定义的名称名称来取值
const messenger= inject('figure')
const content = inject('message')
return {
messenger,
} content
}
});
</script>
由于 provide/inject 主要作用是跨元件之间资料状态沟通,而其绑定的值预设并不是响应式的
提问: 跨元件响应式资料的处理我使用在 provide 时使用 ref () 或 reactive () 把资料包装起来,在inject.vue文件中修改状态可以吗?
因为 provide/inject 不像 Vuex 可以追踪各个 state 的变动,如果资料状态操作分散在多个子元件中,被更改出现问题时,要解决就会变得困难,同时程式码也会变得很难维护。
官方文件这边就提出建议,当使用 provide/inject 值时,尽可能将对响应式 property 的所有修改及操作,限制在 provide () 的元件内部中,来更新响应式的资料状态。
//provide.vue
import { ref, provide, readonly }
export default{
setup(){
const count = ref(0);
const setCount = ()={
count.vlaue++
}
provide('count', readonly(count)) // 使用readonly把保护起来,只能透过setCount来更动它。
provide('setCount',setCount)
}
}
//inject.vue--------------------------------------------
<template>
<div>{{count}}</div>
<button @click="updateCount">+1</button>
</template>
//略...
setup(){
const count =inject('count') //因为被 readonly保护了,所以只可读,不可直接修改他
const updateCount = inject('setCount')
return{
count,
updateCount
}
}
//略...
VueUse
VueUse 作者为 Anthony Fu ,是一个主要以 Composition API 为基底的 函式工具库,其中 VueUse 主要特色可以分为以下几点:
-
Vue 2 和 Vue 3 都支援
-
采取Tree Shaking结构,只会打包引入的程式码
-
支持各种插件
-
可配置事件过滤器和目标 VueUse 中封装了很多常用的功能, 像是 追踪 ref 状态的更改,键盘 / 滑鼠输入事件等,主要可分为以下九大类型:
-
Animation (动画)
-
Browser (浏览器)
-
Component (元件)
-
Formatters (格式)
-
Misc (各式各样的)
-
Sensors (传感器)
-
State (状态)
-
Utility (实用方法)
-
Watch (观察) 稍后会在实作中其中一个功能出来介绍,剩下的就不一一介绍了,有兴趣的捧油们可以到官方文件细细品尝 VueUse(vueuse.org/guide/index…
npm i @vueuse/core
以 复制文字 为例:
<script setup> //setup语法糖
import { useClipboard } from '@vueuse/core'; //只需要引入所使用到的 API
import { ref } from 'vue'
setup() {
const input = ref('我是 Winnie')
const {
text, //复制的值
isSupported, //浏览器有无支援
copy, //方法
copied
} = useClipboard()
</script>
小声说
点赞是不要钱的,但作者会开心好几天~
其实还想写一些坑,算是composition-api的番外篇,大概会写
- 用 reactive 还是 ref
- v-if & v-for 他俩不能在一起啊
- 生命周期 hooks 与 async/await
- Suspense 非同步元件 有兴趣的到时候可以来看看~