Vue Composition API 陷阱

前言

自從React Hooks出現以後,批評的聲音不斷,不少人說它帶來了心智負擔,由於相比傳統的Class寫法,useState/useEffect的依賴於執行順序的特色讓人捉摸不透。與此相對的,在Vue3 Composition API RFC 中,咱們看到Vue3官方描述CompositionAPI是一個基於已有的"響應式"心智模型的更好方案,這讓咱們以爲好像不須要任何心智模型的切換就能夠迅速投入到Compositoin API的開發中去。但在我嘗試了一段時間後,發現事實並不是如此,咱們依然須要一些思惟上的變化來適應新的Compsition API。javascript

Setup陷阱

簡單陷阱

先看一個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

遷移到新的心智模型

  1. 時刻記住setup只會執行一次
  2. 永遠不要直接使用簡單類型
  3. 解構可能有風險,優先使用引用自己,而不是解構它
  4. 能夠經過一些手段讓解構變得安全

使用新心智模型來解決問題

簡單陷阱:永遠不要直接使用簡單類型安全


<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
}
複製代碼

使用JSX或h

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其實就是這樣的一個工具,可是它並不能解決全部問題)

相關文章
相關標籤/搜索