自從React Hooks出現以後,批評的聲音不斷,不少人說它帶來了心智負擔,由於相比傳統的Class寫法,useState/useEffect的依賴於執行順序的特色讓人捉摸不透。與此相對的,在Vue3 Composition API RFC 中,咱們看到Vue3官方描述CompositionAPI是一個基於已有的"響應式"心智模型的更好方案,這讓咱們以爲好像不須要任何心智模型的切換就能夠迅速投入到Compositoin API的開發中去。但在我嘗試了一段時間後,發現事實並不是如此,咱們依然須要一些思惟上的變化來適應新的Compsition API。javascript
先看一個Vue2簡單例子:html
<template>
<div id="app">
{{count}}
<button @click="addCount"></button>
</div>
</template>
<script> export default { data() { return { count: 0 } }, methods: { addCount() { this.count += 1 } } }; </script>
複製代碼
在Vue2的心智模型中,咱們總會在data中返回一個對象,咱們並不關心對象的值是簡單類型仍是引用類型,由於它們都能很好的被響應式系統處理,就像上面這個例子同樣。可是,若是咱們不做任何心智模型的變化,就開始使用Composition API,咱們就容易寫出這樣的代碼:vue
<template>
<div id="app">
{{count}}
<button @click="addCount"></button>
</div>
</template>
<script> import { reactive } from '@vue/runtime-dom' export default { setup() { const data = reactive({ count: 0 }) function addCount() { data.count += 1 } return { count: data.count, addCount } } }; </script>
複製代碼
實際上,這段代碼不能正常運做,當你點擊button時,視圖不會響應數據變化。緣由是,咱們先將data中的count取了出來,再合併到this.$data中,可是一旦count被取出來,它就是一個單純的簡單類型數據,響應式就丟了。java
數據結構越複雜,咱們就越容易落入陷阱,在這裏咱們把一段業務邏輯抽離到自定義hooks裏,以下:react
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_name',
role: 'default_role'
},
projectList: []
})
onMounted(() => {
// 異步獲取數據
fetch(...).then(result => {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
return data
}
複製代碼
而後像往常同樣,咱們在業務組件中去使用:git
// App.vue
<template>
<div>
{{name}}
{{role}}
{{list}}
</div>
</template>
<script> import useSomeData from './useSomeData' export default { setup() { const { userInfo, projectList } = useSomeData() return { name: userInfo.name // 響應式斷掉 role: userInfo.role, // 響應式斷掉 list: projectList // 響應式仍是斷掉 } } } </script>
複製代碼
咱們看到,無論咱們從響應式數據裏取出什麼(簡單類型 or 引用類型),都會致使響應式斷掉,進而沒法更新視圖。github
全部這些問題的根源都是:setup只會執行一次。api
簡單陷阱:永遠不要直接使用簡單類型安全
<template>
<div id="app">
{{count}}
<button @click="addCount"></button>
</div>
</template>
<script> import { reactive, ref } from '@vue/runtime-dom' export default { setup() { const count = ref(0) // 在這裏使用ref包裹一層引用容器 function addCount() { count.value += 1 } return { count, addCount } } }; </script>
複製代碼
複雜陷阱-方案1:解構可能有風險,優先使用引用自己,而不是解構它markdown
// useSomeData.js
...
// App.vue
<template>
<div>
{{someData.userInfo.name}}
{{someData.userInfo.role}}
{{someData.projectList}}
</div>
</template>
<script> import useSomeData from './useSomeData' export default { setup() { const someData = useSomeData() return { someData } } } </script>
複製代碼
複雜陷阱-方案2:能夠經過computed讓解構變得安全
// useSomeData.js
import { reactive, onMounted, computed } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_user',
role: 'default_role'
},
projectList: []
})
onMounted(() => {
// 異步獲取數據
fetch(...).then(result => {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
const userName = computed(() => data.userInfo.name)
const userRole = computed(() => data.userinfo.role)
const projectList = computed(() => data.projectList)
return {
userName,
userRole,
projectList
}
}
複製代碼
// App.vue
export default {
setup() {
const { userName, userRole, projectList } = useSomeData()
return {
name: userName // 是計算屬性,響應式不會斷掉
role: userRole, // 是計算屬性,響應式不會斷掉
list: projectList // 是計算屬性,響應式不會斷掉
}
}
}
複製代碼
複雜陷阱-方案3:方案2須要額外寫一些computed屬性,比較麻煩,咱們還能夠經過toRefs讓解構變得安全
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_user',
role: 'default_role'
},
projectList: []
})
onMounted(() => {
// 異步獲取數據
fetch(...).then(result => {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
// 使用toRefs
return toRefs(data)
}
複製代碼
// App.vue
export default {
setup() {
// 如今userInfo和projectList都已經被ref包裹了一層
// 這層包裹會在template中自動解開
const { userInfo, projectList } = useSomeData()
return {
name: userInfo.value.name, // ???好了嗎
role: userInfo.value.role, // ???好了嗎
list: projectList // ???好了嗎
}
}
}
複製代碼
你覺得這樣就行了嗎?其實這裏有一個陷阱中的陷阱:projectList好了,可是name和role依然是響應式斷開的狀態,由於toRefs只會」淺「包裹,實際上useSomeData返回的結果是這樣的:
const someData = useSomeData()
↓
{
userInfo: {
value: {
name: '...', // 依然是簡單類型,沒有被包裹
role: '...' // 依然是簡單類型,沒有被包裹
}
},
projectList: {
value: [...]
}
}
複製代碼
所以,咱們的useSomeData若是想要經過toRefs實現真正的解構安全,須要這樣寫:
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {
...
// 讓每一層級都套一層ref
return toRefs({
projectList: data.projectList,
userInfo: toRefs(data.userInfo)
})
}
複製代碼
建議:使用自定義hooks返回數據的時候,若是數據的層級比較簡單,能夠直接使用toRefs包裹;若是數據的層級比較複雜,建議使用computed。
上述操做實際上是Vue官方使用CompositionAPI的標準方式,由於CompositionAPI徹底就是按照setup只會執行一次進行設計的。可是不能否認的是,這的確帶來了許多心智負擔,由於咱們不得不時刻關注響應式數據到底能不能解構,否則一不當心就容易調到坑裏。
其實全部這些問題都出在setup只會執行一次,那麼有沒有辦法解決呢?有的,可使用JSX或h的寫法,繞過setup只會執行一次的問題:
仍是這個存在安全隱患的自定義hooks:
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_name',
role: 'default_role'
},
projectList: []
})
onMounted(() => {
// 異步獲取數據
fetch(...).then(result => {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
return data
}
複製代碼
import useSomeData from './useSomeData'
export default {
setup() {
const someData = useSomeData()
return () => {
const { userInfo: { name, role }, projectList } = someData
return (
<div> { name } { role } { projectList } </div>
)
}
}
}
複製代碼
在使用JSX或h的時候,setup須要返回一個函數,這個函數其實就是render函數,它在數據變化時會從新執行,因此咱們只須要把解構的邏輯放到render函數裏,那麼就解決了setup只會執行一次的問題。
咱們可能須要一些約定,來約束自定義hooks的使用方式。可是官方並無給出,這將致使咱們hooks會寫的五花八門,而且漏洞百出。目前來看,」不要解構「是最安全的方式。
我專門就這個問題請教了yyx大佬(#1739),大佬給出了一個」約定」,那就是儘可能少使用「解構」。這我也很無奈。其實我是但願官方可以給出一個工具,讓咱們減小在自定義hooks中犯錯誤的可能性。(toRefs其實就是這樣的一個工具,可是它並不能解決全部問題)