作者:Mingle
轉發鏈接:https://mp.weixin.qq.com/s/iOq-eeyToDXJ6lvwnC12DQ
前言
背景:2019年2月6號,React 發布 「16.8.0」 版本,vue緊隨其后,發布了「vue3.0 RFC」
Vue3.0受React16.0 推出的hook抄襲啟發(咳咳...),提供了一個全新的邏輯復用方案。使用基于函數的 API,我們可以將相關聯的代碼抽取到一個 "composition function"(組合函數)中 —— 該函數封裝了相關聯的邏輯,并將需要暴露給組件的狀態以相應式的數據源的方式返回出來。
本文目的
本文會介紹Vue3.0「組合api的用法和注意點」。最后會用一個 Todolist 的項目實戰,向大家介紹「Vue3.0的邏輯復用寫法以及借用provide和inject的新型狀態管理方式」
本文提綱:
- 如何新建一個使用vue3.0的項目
- conposition api
- 邏輯復用(hook)和狀態管理(provide+inject)
- 結合項目實戰,做一個todo list
正文
如何新建一個使用vue3.0的項目
接下來向大家簡單介紹下如何嘗鮮 -- 自己創建一個vue3.0的項目。
- 安裝vue0-cli
我這邊使用的是最新版本的vue-cli 4.4.0
npm install -g @vue/cli
# OR
yarn global add @vue/cli
- 將vue升級到bata版本
vue add vue-next
ok了。就這么簡單!
conposition api
#### 目錄
- 基本例子
- setup()
- reactive
- ref
- computed
- watchEffect
- watch
- 生命周期
- 依賴注入
基本例子
<template>
<div>
<div>count is {{ count.count }}</div>
<div>plusOne is {{ plusOne }}</div>
<button @click="increment">count++</button>
</div>
</template>
<script>
// eslint-disable-next-line no-unused-vars
import { reactive, computed, watch, onMounted } from 'vue'
export default {
name: 'HelloWorld',
props: {
msg: String
},
setup () {
// reactive state
const count = reactive({ count: 0 })
console.log("setup -> count", count.count)
// computed state
const plusOne = computed(() => count.count + 1)
// method
const increment = () => { count.count++ }
// watch
watch(() => count.count * 2, val => {
console.log(`count * 2 is ${val}`)
})
// lifecycle
onMounted(() => {
console.log(`mounted`)
})
// expose bindings on render context
return {
count,
plusOne,
increment
}
}
}
</script>
setup
?
該setup功能是新的組件選項。它是組件內部暴露出所有的屬性和方法的統一API。
?
調用時機
創建組件實例,然后初始化 props ,緊接著就調用setup 函數。從生命周期鉤子的視角來看,它會在 beforeCreate 鉤子之前被調用
模板中使用
如果 setup 返回一個對象,則對象的屬性將會被合并到組件模板的渲染上下文
<template>
<div>{{ count }} {{ object.foo }}</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const object = reactive({ foo: 'bar' })
// 暴露給模板
return {
count,
object,
}
},
}
</script>
setup 參數
- 「props」第一個參數接受一個響應式的props,這個props指向的是外部的props。如果你沒有定義props選項,setup中的第一個參數將為undifined。props和vue2.x并無什么不同,仍然遵循以前的原則;
- 不要在子組件中修改props;如果你嘗試修改,將會給你警告甚至報錯。
- 不要結構props。結構的props會失去響應性。
2.「上下文對象」第二個參數提供了一個上下文對象,從原來 2.x 中 this 選擇性地暴露了一些 property。
const MyComponent = {
setup(props, context) {
context.attrs
context.slots
context.emit
},
}
Tip:
由于vue3.x向下兼容vue2.x,所以我在嘗試之后發現,一個vue文件中你可以同時寫兩個版本的東西。
import { reactive, computed, watch, onMounted } from 'vue'
export default {
name: 'HelloWorld',
props: {
count: Number,
},
data () {
return {
msg: "我是vue2.x中的this"
}
},
methods: {
test () {
console.log(this.msg)
}
},
mounted () {
console.log('vue2.x mounted')
},
// eslint-disable-next-line no-unused-vars
setup (props, val) {
console.log(this, 'this') // undefined
onMounted(() => {
console.log('vue3.x mounted')
})
return {
...props
}
}
}
當然這邊不推薦你在項目中這么用,但是抱著嘗鮮和探究的態度,我們勢必要弄清如果這么寫要注意哪些?
- 如果我寫了mounted(2.x),在setup函數中又寫了onMounted(3.x),誰先執行?
setup中的先執行。因為setup() 在解析 2.x 選項前被調用;
- 我在vue2.x選項中中定義在this上的變量,在setup上可以通過this訪問嗎?可以重復定義嗎?可以return嗎?
首先在setup中的this將不再指向vue,而是undefined;所以在setup函數內部自然無法訪問到vue實例上的this。
setup內部定義的變量和外表的變量并無沖突;
但是如果你要將其return 暴露給template,那么就會產生沖突。
reactive
?
接收一個普通對象然后返回該普通對象的響應式代理。等同于 2.x 的 Vue.observable()
?
const obj = reactive({ count: 0 })
ref
?
接受一個參數值并返回一個響應式且可改變的 ref 對象。ref 對象擁有一個指向內部值的單一屬性 value。
?
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
tip:
- ref常用于基本類型,reactive用于引用類型。如果ref傳入對象,其實內部會自動變為reactive.
- 當 ref 作為渲染上下文的屬性返回(即在setup() 返回的對象中)并在模板中使用時,它會自動解套,無需在模板內額外書寫 .value;
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
setup() {
return {
const count = ref(0)
count: count, // 而不是 count.value
}
},
}
</script>
- 當 ref 作為 reactive 對象的 property 被訪問或修改時,也將自動解套 value 值,其行為類似普通屬性。
const count = ref(0)
const state = reactive({
count,
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
- 注意當嵌套在 reactive Object 中時,ref 才會解套。從 Array 或者 Map 等原生集合類中訪問 ref 時,不會自動解套:
const arr = reactive([ref(0)])
// 這里需要 .value
console.log(arr[0].value)
const map = reactive(new Map([['foo', ref(0)]]))
// 這里需要 .value
console.log(map.get('foo').value)
computed
computed和vue2.x版本保持一致,支持getter和setter
- 傳入一個 getter 函數,返回一個默認不可手動修改的 ref 對象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 錯誤!
- 或者傳入一個擁有 get 和 set 函數的對象,創建一個可手動修改的計算狀態。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
},
})
plusOne.value = 1
console.log(count.value) // 0
watchEffect
?
傳入的一個函數,并且立即執行,響應式追蹤其依賴,并在其依賴變更時重新運行該函數。
?
注冊監聽
import {watchEffect}from 'vue' // 導入api
const count = ref(0) // 定義響應數據
watchEffect(() => console.log(count.value)) // 注冊監聽函數
// -> 打印出 0
setTimeout(() => {
count.value++
// -> 打印出 1
}, 100)
注銷監聽
- 默認情況下是在**組件卸載**的時候停止監聽;- 也可以顯示**調用返回值**以停止偵聽;
const stop = watchEffect(() => {
/* ... */
})
// 之后
stop()
清除副作用
> 有時副作用函數會執行一些異步的副作用, 這些響應需要在其失效時清除(即完成之前狀態已改變了)。所以偵聽副作用傳入的函數可以接收一個 onInvalidate 函數作入參, 用來注冊清理失效時的回調。
當以下情況發生時,這個失效回調會被觸發:
- 副作用即將重新執行時
- 偵聽器被停止
const count = ref(0)
watchEffect(
(onInvalidate) => {
console.log(count.value, '副作用')
const token = setTimeout(() => {
console.log(count.value, '副作用')
}, 4000)
onInvalidate(() => {
// id 改變時 或 停止偵聽時
// 取消之前的異步操作
token.cancel()
})
}
)
副作用刷新時機
> Vue 的響應式系統會緩存副作用函數,并異步地刷新它們,這樣可以避免同一個 tick 中多個狀態改變導致的不必要的重復調用。在核心的具體實現中, 組件的更新函數也是一個被偵聽的副作用。當一個用戶定義的副作用函數進入隊列時, 會在所有的組件更新后執行:
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
setup() {
const count = ref(0)
watchEffect(() => {
console.log(count.value)
})
return {
count,
}
},
}
</script>
在這個例子中:
- count 會在初始運行時同步打印出來
- 更改 count 時,將在組件更新后執行副作用。
如果副作用需要同步或在組件更新之前重新運行,我們可以傳遞一個擁有 flush 屬性的對象作為選項(默認為 'post'):
// 同步運行
watchEffect(
() => {
/* ... */
},
{
flush: 'sync',
}
)
// 組件更新前執行
watchEffect(
() => {
/* ... */
},
{
flush: 'pre',
}
)
watch
> watch API 完全等效于 2.x this.$watch (以及 watch 中相應的選項)。watch 需要偵聽特定的數據源,并在回調函數中執行副作用。默認情況是懶執行的,也就是說僅在偵聽的源變更時才執行回調。
- 對比 watchEffect,watch 允許我們:
- 懶執行副作用;
- 更明確哪些狀態的改變會觸發偵聽器重新運行副作用;
- 訪問偵聽狀態變化前后的值。
- 偵聽單個數據源
偵聽器的數據源可以是一個擁有返回值的 getter 函數,也可以是 ref:
// 偵聽一個 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接偵聽一個 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
- 偵聽多個數據源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
- 與 watchEffect 共享的行為
watch 和 watchEffect 在停止偵聽, 清除副作用 (相應地 onInvalidate 會作為回調的第三個參數傳入),副作用刷新時機 和 助聽器調試 等方面行為一致.
生命周期鉤子函數
?
可以直接導入 onXXX 一組的函數來注冊生命周期鉤子,這些生命周期鉤子注冊函數只能在 setup() 期間同步使用,在卸載組件時,在生命周期鉤子內部同步創建的偵聽器和計算狀態也將自動刪除。
?
- 「與 2.x 版本生命周期相對應的組合式 API」
- beforeCreate -> 使用 setup()
- created -> 使用 setup()
- beforeMount -> onBeforeMount
- mounted -> onMounted
- beforeUpdate -> onBeforeUpdate
- updated -> onUpdated
- beforeDestroy -> onBeforeUnmount
- destroyed -> onUnmounted
- errorCaptured -> onErrorCaptured
- 新增的鉤子函數
- onRenderTracked
- onRenderTriggered
兩個鉤子函數都接收一個DebuggerEvent,與 watchEffect 參數選項中的 onTrack 和 onTrigger 類似:
export default {
onRenderTriggered(e) {
debugger
// 檢查哪個依賴性導致組件重新渲染
},
}
依賴注入
?
provide 和 inject 提供依賴注入,功能類似 2.x 的 provide/inject。兩者都只能在當前活動組件實例的 setup() 中調用。
?
這是本篇文章的重點。結合項目實戰以此來探索一下未來的 Vue 狀態管理模式和邏輯復用模式。
「用法」
?
provide 和 inject 提供依賴注入,功能類似 2.x 的 provide/inject。兩者都只能在當前活動組件實例的 setup() 中調用。
?
import { provide, inject } from 'vue'
const ThemeSymbol = Symbol()
const Ancestor = {
setup() {
provide(ThemeSymbol, 'dark')
},
}
const Descendent = {
setup() {
const theme = inject(ThemeSymbol, 'light' /* optional default value */)
return {
theme,
}
},
}
inject 接受一個可選的的默認值作為第二個參數。如果未提供默認值,并且在 provide 上下文中未找到該屬性,則 inject 返回 undefined。
- 「注入的響應性」
可以使用 ref 來保證 provided 和 injected 時間值的響應:
// 提供者:
const themeRef = ref('dark')
provide(ThemeSymbol, themeRef)
// 使用者:
const theme = inject(ThemeSymbol, ref('light'))
watchEffect(() => {
console.log(`theme set to: ${theme.value}`)
})
如果注入一個響應式對象,則它的狀態變化也可以被偵聽。
邏輯組合與復用
引出問題:
我們通常會基于一堆相同的數據進行花樣呈現,有列表展示、有餅圖占比、有折線圖趨勢、有熱力圖說明頻次等等,這些組件使用的是相同的一些數據和數據處理邏輯。對于數據處理邏輯,目前vue有
- Mixins
- 高階組件 (Higher-order Components, aka HOCs)
- Renderless Components (基于 scoped slots / 作用于插槽封裝邏輯的組件)
但是上面的方案是存在一些弊端:
- 模版中的數據來源不清晰
- 命名空間沖突。
- 需要額外的組件實例嵌套來封裝邏輯(性能問題);
##### 基于組合api 的解決方案
function useMouse() {
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
// 在組件中使用該函數
const Component = {
setup() {
const { x, y } = useMouse()
// 與其它函數配合使用
const { z } = useotherLogic()
return { x, y, z }
},
template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}
項目預覽
源碼:https://github.com/961998264/todolist-vue-3.0
項目介紹
- 已完成事件列表
- 未完成事件列表
- 查看事件詳情
- 修改事件完成狀態和事件詳情
項目src目錄
hooks文件夾是專門放hook的
context文件夾以模塊劃分
先來看下context編寫(我這邊是用的ts)
import { provide, ref, Ref, inject, computed, } from 'vue' //vue api
import { getListApi } from 'api/home' // mock的api
// 以下為定義的ts類型,你也可以單獨建一個專門定義類型的文件。
type list = listItem[]
interface listItem {
title: string,
context: string,
id: number,
status: number,
}
interface ListContext {
list: Ref<list>,
getList: () => {},
changeStatus: (id: number, status: number) => void,
addList: (item: listItem) => void,
delList: (id: number) => void,
finished: Ref<list>,
unFinish: Ref<list>,
setContext: (id: number, context: string) => void,
setActiveItem: () => void,
}
provide名稱,推薦用Symbol
const listymbol = Symbol()
提供provide的函數
export const useListProvide = () => {
// 全部事件
const list = ref<list>([]);
// 當前查看的事件id
const activeId = ref<number | null>(null)
// 當前查看的事件
const activeItem = computed(() => {
if (activeId.value || activeId.value === 0) {
const item = list.value.filter((item: listItem) => item.id === activeId.value)
return item[0]
} else {
return null
}
})
// 獲取list
const getList = async function () {
const res: any = await getListApi()
console.log("useListProvide -> res", res)
if (res.code === 0) {
list.value = res.data
}
}
// 新增list
const addList = (item: listItem) => {
list.value.push(item)
}
//修改狀態
const changeStatus = (id: number, status: number) => {
console.log('status', status)
const removeIndex = list.value.findIndex((listItem: listItem) => listItem.id === id)
if (removeIndex !== -1) {
list.value[removeIndex].status = status
}
};
// 修改事件信息
const setContext = (id: number, context: string) => {
const Index = list.value.findIndex((listItem: listItem) => listItem.id === id)
if (Index !== -1) {
list.value[Index].context = context
}
}
// 刪除事件
const delList = (id: number) => {
console.log("delList -> id", id)
for (let i = 0; i < list.value.length; i++) {
if (list.value[i].id === id) {
list.value.splice(i, 1)
break
}
}
}
// 未完成事件列表
const unFinish = computed(() => {
return list.value.filter(item => item.status === 0)
})
// 已完成事件列表
const finished = computed(() => {
return list.value.filter(item => item.status === 1)
})
provide(listymbol, {
list,
unFinish,
finished,
changeStatus,
getList,
addList,
delList,
setContext,
activeItem,
activeId
})
}
在這個函數中定義 待辦事件,并且定義一系列增刪改查函數,通過provide暴露出去。
提供inject的函數
export const useListInject = () => {
const listContext = inject<ListContext>(listymbol);
if (!listContext) {
throw new Error(`useListInject must be used after useListProvide`);
}
return listContext
};
全局狀態肯定不止一個模塊,所以在 context/index.ts 下做統一的導出
import { useListProvide, useListInject } from './home/index'
console.log("useListInject", useListInject)
export { useListInject }
export const useProvider = () => {
useListProvide()
}
然后在 App.vue 的根組件里使用 provide,在最上層的組件中注入全局狀態。
import { useProvider } from './context/index'
export default {
name: 'App',
setup () {
useProvider()
return {
}
}
}
在組件中獲取數據:
import { useListInject } from '../../context/home/index'
setup () {
const { list, changeStatus, getList, unFinish, finished, addList, a ctiveItem, setContext } = useListInject()
}
不管是父子組件還是兄弟組件,或者是比關系套更深的組件,我們都可以通過useListInject來獲取到相應式的數據。
- 「邏輯聚合」 同一份數據的相關邏輯我們可以寫在一個usexxxx的函數中,不再像以前,按照選擇將邏輯分開。在methods,computed,watch,created,mounted中來回跳轉。
- 「取代vuex」 在比較小的項目中,你可以用這種狀態管理的方式取代vuex。(反正我用react基本不用redux,不管項目大小)。
作者:Mingle
轉發鏈接:https://mp.weixin.qq.com/s/iOq-eeyToDXJ6lvwnC12DQ