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其实就是这样的一个工具,可是它并不能解决全部问题)

相关文章
相关标签/搜索