美文网首页Web前端之路
Vue composition API 吃螃蟹指南

Vue composition API 吃螃蟹指南

作者: kitssang | 来源:发表于2019-10-25 10:12 被阅读0次

    本文是Vue中3新的Composition API简单介绍及使用指南,具体内容可以自行阅读官方提供的RFC

    Motivation

    所有API都不是偶然设计出来的,所以作者的设计的时候必定有他的含义,我们先看看文档中提到的两个设计动机。

    • 逻辑复用及代码整理
    • 更好的Typescript支持

    像Vue这种现代框架已经很好解决了组件复用这一问题了,但是随着时间推移,你会发现组件里面的状态和逻辑也越来越多,越来越难维护,以一个普通的搜索组件为例,使用普通的语法就像下面的代码那样,用data定义组件的state,在methods里面定义组件的方法,看起来就像下面

    export default {
      name: 'SearchComponent',
      data() {
        return {
          🔍
        }
      },
      methods: {
        🔍
      }
    }
    

    我们可能还需要一个过滤搜索条件的方法

    export default {
      name: 'SearchComponent',
      data() {
        return {
          🔍
          📑
        }
      },
      methods: {
        🔍
        📑
      }
    }
    

    可以看到,处理同样逻辑的方法被划分在datamethods里面,我们可以回想一下除此之外定义一个组件有多少个组件选项。

    • components
    • props
    • data
    • computed
    • methods
    • lifecycle methods

    处理一个逻辑,状态及方法就可能分离在这6个地方,维护时间一长了,很有可能看到这种千行代码的组件了。(Put some demo)

    使用 Composition API 的话会变成什么样子呢,还是以上方搜索组件举例

    function 🔍() { // Search state & methods }
    function 📑() { // Sort state & methods }
    
    export default {
      name: 'SearchComponent',
      setup() {
        🔍
        📑
      }
    }
    

    组件状态及方法单独分离出来了!下面我们接着看一下如何用setup代替以前的方法。

    How

    我们还是以上面搜索组件为例,我们先转化成较为真实的代码

    <template>
      <div class="search">
        <form>
          <input v-model="searchValue" type="text" />
          <button @click="search"></button>
        </form>
    
        <ul class="result">
          <li class="result__item" v-for="item in filterData" :key="item.id">
            {{ item.name }}
          </li>
        </ul>
    
        <span v-if="loading">loading</span>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        searchValue: '',
        loading: true,
        searchResult: []
      },
      computed: {
        filterData() {
          return searchResult.filter(i => i.id > 3)
        }
      },
      methods: {
        async search() {
          this.loading = true
          const { data } = await fetch('/search', body: this.searchValue.trim())
          this.searchResult = data.toJSON()
          this.loading = false
        }
      },
      mounted() {
        // Get initial data
        this.search()
      }
    }
    </script>
    

    逻辑不复杂,只有一个简单的搜索方法和一个过滤数据的computed方法,组件挂载的时候执行下搜索方法初始化列表数据。我们看一下这部分代码在 Composition API下是怎么工作的。

    <template>
      <div class="search">
        <form>
          <input v-model="state.searchValue" type="text" />
          <button @click="search"></button>
        </form>
    
        <ul class="result">
          <li class="result__item" v-for="item in filterData" :key="item.id">
            {{ item.name }}
          </li>
        </ul>
    
        <span v-if="state.loading">loading</span>
      </div>
    </template>
    
    <script>
    import { reactive, computed, onMounted, toRef } from 'vue'
    
    export default {
      setup() {
        // Initial State
        const state = reactive({
          searchValue: '',
          loading: true,
          searchResult: []
        })
    
        // Computed
        const filterData = computed(() => state.searchResult.filter(i => i.id > 3))
    
        // Methods
        async function search() {
          state.loading = true;
          const { data } = await fetch('/search', body: this.searchValue.trim())
          state.searchResult = data.toJSON()
          state.loading = false;
        }
    
        // Lifecircle hook
        onMounted(() => {
          search()
        })
    
        return { ...toRef(state), filterData, search }
      }
    }
    </script>
    

    从这里我们可以看到两个明显的变化:

    1. 组件从原本的选项配置变成了函数定义
    2. 由于1中的改变,组件也不需要使用this去指定当前组件执行的上下文

    这是组件逻辑复用重要的一步。

    逻辑分离和复用

    上方简单介绍了如何使用新的API去重构我们的组件,但是还没真正的感受到它的魅力,我们继续拿上方重写好的组件继续改造,把逻辑抽离出来进行复用。我们一开始就知道组件中有搜索过滤搜索条件两部分重要功能,所以我们目标就是把这两部分逻辑抽离出来。

    // Search logic
    function useSearch() {
      const searchValue = ref('')
      const searchResult = ref([])
      const loading = ref(true)
    
      function search() {
        loading.value = true;
        const { data } = await fetch('/search', body: this.searchValue.trim())
        searchResult.value = data.toJSON()
        loading.value = false;
      }
    
      onMounted(() => search());
    
      return { searchValue, searchResult, loading, search }
    }
    
    // Filter logic
    function useFilterSearchResult(searchResult) {
      const filterData = computed(() => searchResult.value.filter(i => i.id > 3))
      return { filterData }
    }
    
    // Component Instance
    export default {
      setup() {
        const search = useSearch()
        return { ...search, ...useFilterSearchResult(search.searchResult) }
      }
    }
    

    从这里我们已经完全将组件中的状态都独立出来了,到此已经大概将Composition API运行方式及使用方式大概介绍完毕了,我们知道了如何把组件的状态单独分开维护,以及使用这些如何使用这些API去组合所有状态及方法。

    缺点

    聊完 Composition API 的优点,那么我们来聊下它的缺点。Vue中的是使用依赖追踪的方式去通知变更的,所以在开发Vue组件的时候我们只需要用this.x = y重新赋值某个状态,DOM Tree就会自动响应变更。这是Vue的优势,但在Composition API中就变了一个需要重点关注的点,如果使用不好,甚至会变成缺点。

    我是可以从试着从Vue的实现原理去查找此原因。Vue2中是使用Object.defineProperty,而Vue3则使用了Proxy,但无论使用哪种方法,我们在访问和设置某个state的时候实际是调用了他们getter/setter的方法,但是这个state作为一个返回值或者参数的时候,它实际是作为一个传递到了另外要给方法中,所以他的getter/setter将会丢失,数据无法响应。这是什么意思呢,我们可以看到下面的例子

    我们先建立一个获取鼠标坐标的hook

    function useMousePostion() {
      const pos = reactive({
        x: 0,
        y: 0
      })
      
      onMount(() => {
        // Add Event Listener
      })
    
      return pos
    }
    
    // comsuming component
    export default {
      setup() {
        const { x, y } = useMousePosition()
    
        // 响应丢失
        return { x, y }
    
        // 响应丢失
        return { ...useMousePositon() }
    
        // It's work
        return {
          pos: useMousePosition()
        }
      }
    }
    

    这是为什么呢?这涉及到Javascript里面的一些基础知识 引用传递及值传递

    引用传递及值传递

    思考下面的代码

    function changeStuff(a, b, c) {
      a = a * 10
      b.item = 'changed'
      c = { item: 'changed' }
    }
    
    var num = 10
    var obj1 = { item: 'unchanged' }
    var obj2 = { item: 'unchanged' }
    
    changeStuff(num, obje1, obj2)
    console.log(num)
    console.log(obj1.item)
    console.log(obj2.item)
    
    10
    changed
    unchanged
    

    这说明Javascript中参数传递是以值传递的方式进行传递的,而传递的内容是它自己本身的引用值

    用官方提供的图表示可能更为直观(左为引用传递,右为值传递)

    image

    所以上面响应丢失的问题就可以解释了

    export default {
      setup() {
        // 这里只取了useMousePosition返回值的引用值
        // 而值里面的getter/setter丢失
        const { x, y } = useMousePosition()
    
        // 响应丢失
        return { x, y }
    
        // 响应丢失
        // 同上
        return { ...useMousePositon() }
    
        // It's work
        // 这里的pos实际是useMousePositon里面的pos的值
        // 所以pos.x与pos.y的属性依旧存在
        return {
          pos: useMousePosition()
        }
      }
    }
    

    怎么解决这些问题

    使用官方提供的API

    看上面的代码,官方有提供有两种方式解决这个问题,我们修改一下useMousePosition,首先是toRefs

    function useMousePostion() {
      const pos = reactive({
        x: 0,
        y: 0
      })
      
      onMount(() => {
        // Add Event Listener
      })
    
      return toRefs(pos)
    }
    
    // comsuming component
    export default {
      setup() {
        // Work!
        const { x, y } = useMousePosition()
        return { x, y }
    
        // Work
        return { ...useMousePositon() }
      }
    }
    

    另外一个是使用ref去初始化state

    function useMousePostion() {
      const x = ref(0)
      const y = ref(0)
      
      onMount(() => {
        document.body.addEventListener('mousemove', (e) => {
          x.value = e.x
          y.value = e.y
        })
        // Add Event Listener
      })
    
      return { x, y }
    }
    
    export default {
      setup() {
        // Work!
        const { x, y } = useMosuePosition()
        return { x, y }
    
        // Work!
        return { ...useMousePostion() }
      }
    }
    

    使用Typesciprt

    使用Typescript能严格控制函数参数的类及以及确定函数返回值,结合 Composition 会发生怎样的化学反应呢,我们看以下代码,还是刚才的useMousePosition例子

    function useMosuePosition1():  {
      const pos = reactive({
        x: 0,
        y: 0
      })
    
      return pos
    }
    
    function useMousePostion2() {
      const x = ref(0)
      const y = ref(9)
    
      return { x, y }
    }
    
    export default {
      setup() {
        // Code IntelliSense
        // x is number
        // y is number
        const { x, y } = useMousePosition1()
    
        // x is Ref<number>
        // y is Ref<number>
        const { x, y } = useMousePosition2()
      }
    }
    

    而且 Compotion API 的出现的另外一个原因就是为了解决在2中一直被诟病的typescript支持问题,使用函数代替配置的形式能更好地支持类型推断,不会出现像以前拓展了Vue的prototype而推断不出里面的实例方法。

    Summary 总结

    新的 Composition API 让我们有了更好的方式去组织我们的组件状及方法,但是也像官方介绍文档说的一样 More Flexibility Requires More Discipline,获得更多灵活性同时需要更多的条件去约束。

    不管是React Hooks还是Vue Componsition API,我们都可以知道前端工程会变得越来越复杂,前端的架构设计也在不断的进步,现代框架对前端Javascript基本知识是越来严格,各种软件设计模式在前端工程中也会越来越重要。

    希望此文对大家吃螃蟹有所启发,共勉之 🙆‍♀️

    Vue Composition API RFC —— Vue Composition API介绍
    Vue Compostion Repository —— Vue Composition API Github仓库
    求值策略 —— 引用传递、值传递介绍

    相关文章

      网友评论

        本文标题:Vue composition API 吃螃蟹指南

        本文链接:https://www.haomeiwen.com/subject/ykxsvctx.html