一组低侵入的、函数式的API,使得我们能够更灵活地组合组件的逻辑

基本范例


<template>
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
</template>

<script>
  import { reactive, computed } from 'vue'

  export default {
    setup() {
      const state = reactive({
        count: 0,
        double: computed(() => state.count * 2),
      })

      function increment() {
        state.count++
      }

      return {
        state,
        increment,
      }
    },
  }
</script>

动机与目的


更好的逻辑复用与代码组织

  • Vue简单易学,构建中小型APP、WEB很简单
  • Vue用于构建更大型的项目,就会遇到Vue当前API所带来的编程模型的限制。问题归纳为:
    • 1.项目功能增长,复杂组件的代码变得难以阅读和理解。根本原因就是Vue现有的API迫使我们通过选项组织代码,但有时候通过逻辑关系组织代码更有意义。
    • 2.目前缺少一种简洁且低成本的机制来提取和重用多个组件之间的逻辑。
  • RFC中提出的API为组件代码的组织提供了更大的灵活性。因此我们不需要总是通过选项来组织代码,而是可以将代码组织为处理特定功能的函数。这些API还使得在这些组件之间甚至组件之外逻辑的提取和重用变得更加简单。

更好的类型推导

  • 大型项目开发者的常见需求是更好的TypeScript支持。Vue当前的API在集成TypeScript时,由于Vue依赖一个简单的this上下文来暴露property,导致API集成Typescript时遇到了不少麻烦。目前使用this的方式是比较微妙的(methods选项下的函数的this是指向组件实例的,而不是指向methods对象的)

  • Vue 现有的 API 在设计之初没有照顾到类型推导,这使适配 TypeScript 变得复杂。

  • 当前大部分使用Typescript的Vue开发者都在通过vue-class-component这个库将组件撰写为TypeScript class(借助decorator)。

  • 本 RFC 中提出的方案更多地利用了天然对类型友好的普通变量与函数。用该提案中的 API 撰写的代码会完美享用类型推导,并且也不用做太多额外的类型标注。

  • 这意味着你写的JavaScript代码几乎就是TypeScript的代码

设计细节


API介绍

  • 该提案中API更像是暴露Vue的核心功能————比如用独立的函数来创建和监听响应式的状态
  • 该提案只介绍这些API的基本思路,不展开完整的细节
响应式状态与副作用
  • 创建一个响应式的状态:
import { reactive } from 'vue'

// state 现在是一个响应式的状态
const state = reactive({
  count: 0,
})
  • reactive几乎等价于2.x中现有的Vue.observable()API,state是响应式对象
  • 在Vue中,响应式状态的基本用例就是在渲染时使用它。因为存在依赖追踪,视图会在响应式状态发生改变时自动更新。在DOM当中渲染内容会被视为一种“副作用”:程序会在外部修改其本身(也就是这个DOM)的状态。我们可以使用watchEffectAPI应用基于响应式状态的副作用,并自动进行重应用。
  import { reactive, watchEffect } from 'vue'

  const state = reactive({
    count: 0,
  })

  watchEffecct(() => {
    document.body.innerHTML = `count is ${state.count}`
  })
  • watchEffect接受一个应用预期副作用(这里就是innerHTML)的函数。会立即执行,并将该执行过程中用到的所有响应式状态的property作为依赖进行追踪。

  • 当开发者在组件中从data()返回一个对象,内部实质上通过调用reactive()使其变为响应式。

  • 模板会被编译为渲染函数(可理解为一种更高效的innerHTML),因而·可以使用这些响应式的property。

watchEffect 和 2.x 中的 watch 选项类似,但是它不需要把被依赖的数据源和副作用回调分开。组合式 API 同样提供了一个 watch 函数,其行为和 2.x 的选项完全一致。

  • 继续展示如何处理用户输入:
  function increment() {
    state.count++
  }

  document.body.addEventListeber('click', increment)
  • 在Vue的模板系统中,我们不需要纠结用innerHTML还是手动挂载事件监听器。
  • 让我们将例子简化为一个假设的 renderTemplate 方法,以专注在响应性这方面:
import { reactive, watchEffect } from 'vue

const state = reavtive({
  count: 0,
})

function increment() {
  state.count++
}

function renderContext = {
  state,
  increment,
}

watchEffect(() => {
  // 假设的方法,并不是真实的API
  renderTemplate(
    `<button @click="increment">{{state.count}}</button>`,
    renderContext
  )
})
计算状态与Ref
  • 当需要一个依赖于其他状态的状态时,Vue通过计算属性来处理,使用computedAPI直接创建一个计算值:
  import { reactive, computed } from 'vue'

  const state = reactive({
    count : 0,
  })

  const double = computed(() => state.count * 2)
  • now,可以通过引用来传递计算值,且不用担心其响应式特性会消失。代价是:为了获取最新的值,每次都需要写.value
  const double = computed(() => state.count * 2)

  watchEffect(() => {
    console.log(double.value)
  }) // -> 0

  state.count++ // -> 2
  • 除了计算值的 ref,我们还可以使用 ref API 直接创建一个可变更的普通的 ref
解开Ref
  • 我们可以将一个ref值暴露给渲染上下文,在渲染过程中,Vue会直接使用其内部的值,也就是说在模板中可以把count.value直接写成count

  • 当一个 ref 值嵌套于响应式对象之中时,访问时会自动解开:

  const state = reactive({
    count: 0,
    double: computed(() => state.count * 2),
  })console.log()
  // 无需再使用state.double.value
  console.log(state.double)
组件中的使用方式
  • 如果我们希望可以重用根据用户输入来进行更新的代码,则可以重构一个函数:
  import { react, computed, watchEffect } from 'vue'

  function setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2),
    })

    function increment() {
      state.count++
    }

    return {
      state,
      increment,
    }
  }

  const renderContext = setup()

  watchEffect(() => {
    renderTemplate(
      `<button @click="increment">
      Count is: {{ state.count }}, double is  {{ state.double }}
      </button>`,
      renderContext
    )
  })
  • 现在把调用setup()、创建侦听器和渲染模板的逻辑组合在一起交给框架:
  <template>
    <button @click="increment">
      Count is: {{ state.count }}, double is: {{ state.double }}
    </button>
  </template>

  <script>
    import { reactive, computed } from 'vue'

    export default {
      setup() {
        const state = reactive({
          count: 0,
          double: computed(() => state.count * 2),
        })

        function increment() {
          state.count++
        }

        return {
          state,
          increment,
        }
      },
    }
  </script>
生命周期钩子函数
  • 到此为止,我们覆盖了组件的纯状态层面:

    • 响应式状态
    • 计算状态
    • 用户输入状态更新
  • 一个组件可能还会产生其他副作用:

    • 在控制台打印信息
    • 发送AJAX请求、axios
    • 在全局window对象上设置事件监听器
  • 这些副作用大都发生在

    • 状态变化时、
    • 组件挂载完成、内容更新或者解除挂载时(这里就对应生命周期钩子)
  • 状态发生变化时,可以使用watchEffectwatchAPI应用副作用。

  • 为了在生命周期钩子中产生副作用,我们就可以使用形如onXXX的API

  import { onMounted } from 'vue'

  export default {
    setup() {
      onMounted(() => {
        console.log('component is mounted!')
      })
    }
  }

这些生命周期注册方法只能用在 setup 钩子中。它会通过内部的全局状态自动找到调用此 setup 钩子的实例。有意如此设计是为了减少将逻辑提取到外部函数时的冲突。

代码组织

  • 该意见稿认为组合式API能够为开发者的代码带来更好的组织结构!
什么是有组织的代码
  • 有组织的代码的最终目标应该是:代码更可读、更易于被理解。
逻辑关注点 VS. 选项类型
  • 选项的强行分离为展示背后的逻辑关注点设置了障碍。此外,在处理单个逻辑关注点时,我们必须不断地在选项代码块之间“跳转”。

  • 当处理一个功能的相关代码都被合并并封装在一个函数中,最终成为一些好解耦的函数。建议使用use作为函数名的开头,以表示它是一个组合函数。

  • 同样的功能、两套组件定义呈现出对内在逻辑的不同的表达方式。基于选项的 API 促使我们通过 选项类型 组织代码,而组合式 API 让我们可以基于逻辑关注点组织代码。

逻辑提取与复用

  • 当我们在组件间提取并复用逻辑时,组合式API是十分灵活的。一个组合函数仅依赖它的参数和Vue全局导出的API,而不是依赖其微妙的this上下文。可以将组件内的任何一段逻辑导出为函数以复用它。甚至可以通过导出整个setup函数达到和extends等价的效果。

  • 一个例子:

  import { ref, onMounted, onUnmounted } from 'vue

  export function useMousePosition() {
    const x = ref(0)
    const y = ref(0)

    function update(e) {
      x.value = e.pageX
      y.value = e.pageY
    }

    onMounted(() => {
      window.addEventListener('mousemove', update)
    })

    onUnmounted(() => {
      window.removeEventListener('mousemove', update)
    })

    return {x, y}
  }
  • 一下是一个组件如何利用该函数的展示:
  import { useMousePosition } from './mouse'

  export default {
    setup() {
      const { x, y } = useMousePosition()
      // other codes
      return { x, y }
    }
  }
  • 类似的逻辑复用也可以通过诸如 mixins、高阶组件或是 (通过作用域插槽实现的) 无渲染组件的模式达成。

  • 相比于组合函数,这些模式都有各自的弊端:

    • 渲染上下文中暴露的property来源不清晰。例如在阅读一个运用了多个 mixin 的模板时,很难看出某个 property 是从哪一个 mixin 中注入的。
    • 命名空间冲突。Mixin之间的property和方法可能有冲突,同时高阶组件也可能和预期的prop有命名冲突。
    • 性能方面,高阶组件和无渲染组件需要额外的有状态的组件实例,从而使得性能有所损耗。
  • 相比而言,组合式API:

    • 暴露给模板的 property 来源十分清晰,因为它们都是被组合逻辑函数返回的值。
    • 不存在命名空间冲突,可以通过解构任意命名
    • 不再需要仅为逻辑复用而创建的组件实例

与现有API配合

组合式API完全可以和现有的基于选项的API配合使用。

  • 组合式API会在2.x的选项(datacomputedmethods)之前解析,并且不能提前访问这些选项中定义的property。
  • setup()函数返回的property将会暴露给this。它们在2.x的选项中可以访问到。

插件开发

  • 当下许多Vue的插件都向this注入property。

    • 例如 Vue Router 注入 this.$routethis.$router,而 Vuex 注入 this.$store.
  • 当使用组合式API时,我们不再使用this,取而代之的是,插件将在内部利用provideinject并暴露一个组合函数。

弊端

引入Ref的心智负担

  • Ref 可以说是本提案中唯一的“新”概念。引入它是为了以变量形式传递响应式的值而不再依赖访问 this

  • 弊端如下:

    • 1.当使用组合式API时,我们需要一直区别[响应式值引用]