Vue Composition API 陷阱

前端桃园

共 6323字,需浏览 13分钟

 · 2020-09-05

编者按:本文转载自XxjzZ的掘金文章,快乐来一起学习吧!

前言

自从React Hooks出现之后,批评的声音不断,很多人说它带来了心智负担,因为相比传统的Class写法,useState/useEffect的依赖于执行顺序的特点让人捉摸不透。与此相对的,在Vue3 Composition API RFC 中,我们看到Vue3官方描述CompositionAPI是一个基于已有的"响应式"心智模型的更好方案,这让我们觉得好像不需要任何心智模型的切换就可以迅速投入到Compositoin API的开发中去。但在我尝试了一段时间后,发现事实并非如此,我们依然需要一些思维上的变化来适应新的Compsition API。

Setup陷阱

简单陷阱

先看一个Vue2简单例子:

<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,我们就容易写出这样的代码:

<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被取出来,它就是一个单纯的简单类型数据,响应式就丢了。

复杂陷阱

数据结构越复杂,我们就越容易落入陷阱,在这里我们把一段业务逻辑抽离到自定义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

}

复制代码

然后像往常一样,我们在业务组件中去使用:

// 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 引用类型),都会导致响应式断掉,进而无法更新视图。

所有这些问题的根源都是:setup只会执行一次。

迁移到新的心智模型

  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:解构可能有风险,优先使用引用本身,而不是解构它

// 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大佬,大佬给出了一个”约定”,那就是尽量少使用“解构”。这我也很无奈。其实我是希望官方能够给出一个工具,让我们减少在自定义hooks中犯错误的可能性。(toRefs其实就是这样的一个工具,但是它并不能解决所有问题)



推荐阅读




我的公众号能带来什么价值?(文末有送书规则,一定要看)

每个前端工程师都应该了解的图片知识(长文建议收藏)

为什么现在面试总是面试造火箭?

浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报