vue3.0 Composition API 翻译版(超长)

Composition API 一组基于功能的附加API,允许灵活地组成组件逻辑。

观看Vue Mastery的Vue 3基础课程。下载Vue 3备忘单

基本示例

<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非常容易上手,并使中小型应用程序的构建变得轻而易举。但是如今,随着Vue的采用率增长,许多用户也正在使用Vue来构建大型项目,这些项目是由多个开发人员组成的团队在很长的时间内进行迭代和维护的。多年来,我们目睹了其中一些项目遇到了Vue当前API所带来的编程模型的限制。这些问题可以概括为两类:

  1. 随着功能的增长,复杂组件的代码变得越来越难以推理。这种情况尤其发生在开发人员正在阅读自己未编写的代码时。根本原因是Vue的现有API通过选项强制执行代码组织,但是在某些情况下,通过逻辑考虑来组织代码更有意义。

  2. 缺乏用于在多个组件之间提取和重用逻辑的干净且免费的机制。(有关逻辑提取和重用的更多详细信息)

该RFC中提出的API在组织组件代码时为用户提供了更大的灵活性。现在可以将代码组织为每个函数都处理特定功能的函数,而不必总是通过选项来组织代码。API还使在组件之间甚至外部组件之间提取和重用逻辑变得更加简单。我们将在“ 详细设计”部分中说明如何实现这些目标

更好的类型推断

开发人员在大型项目上的另一个常见功能要求是更好的TypeScript支持。Vue当前的API在与TypeScript集成时提出了一些挑战,这主要是因为Vue依赖单个this上下文来公开属性,并且this在Vue组件中使用比普通JavaScript更具魔力(例如this嵌套在methods指向组件实例而不是methods对象的点下方的内部函数)。换句话说,Vue现有的API在设计时就没有考虑类型推断,并且在尝试使其与TypeScript完美配合时会产生很多复杂性。

今天vue-class-component,大多数将Vue与TypeScript一起使用的用户正在使用,该库允许将组件编写为TypeScript类(在装饰器的帮助下)。在设计3.0时,我们试图提供一个内置的Class API,以更好地解决以前的RFC(已删除)中的键入问题。但是,当我们在设计上进行讨论和迭代时,我们注意到,要使Class API解决类型问题,它必须依赖装饰器-这是一个非常不稳定的第2阶段提案,在实现细节方面存在很多不确定性。这使其成为一个相当危险的基础。(有关类API类型问题的更多详细信息,请点击此处)

相比之下,此RFC中提议的API大多使用普通的变量和函数,它们自然是类型友好的。用建议的API编写的代码可以享受完整的类型推断,几乎不需要手动类型提示。这也意味着用提议的API编写的代码在TypeScript和普通JavaScript中看起来几乎相同,因此,即使非TypeScript用户也可以从键入中受益,以获得更好的IDE支持。

API简介

这里提出的API并没有引入新的概念,而是更多地将Vue的核心功能(例如创建和观察响应状态)公开为独立功能。在这里,我们将介绍一些最基本的API,以及如何使用它们代替2.x选项来表达组件内逻辑。请注意,本节重点介绍基本概念,因此不会详细介绍每个API。完整的API规范可在“ API参考”部分中找到。

#反应状态和副作用

让我们从一个简单的任务开始:声明一些反应状态。

import { reactive } from 'vue'// reactive stateconst state = reactive({
  count: 0})

reactive与Vue.observable()2.x 中的当前API 等效,已重命名以避免与RxJS observables混淆。在这里,返回的state是所有Vue用户都应该熟悉的反应性对象。

Vue中反应性状态的基本用例是我们可以在渲染期间使用它。由于依赖关系跟踪,当反应性状态更改时,视图会自动更新。在DOM中渲染某些内容被视为“副作用”:我们的程序正在修改程序本身(DOM)外部的状态。要应用并根据反应状态自动重新应用副作用,我们可以使用watchEffectAPI:

import { reactive, watchEffect } from 'vue'const state = reactive({
  count: 0})watchEffect(() => {
  document.body.innerHTML = `count is ${state.count}`})

watchEffect期望具有可实现所需副作用的功能(在这种情况下,请设置innerHTML)。它立即执行该函数,并跟踪其在执行期间用作依赖项的所有反应状态属性。在此,state.count在初始执行后,将作为此监视程序的依赖项进行跟踪。当state.count在将来的某个时间发生突变时,内部函数将再次执行。

这是Vue反应系统的本质。当您从data()组件中返回对象时,它会在内部使之具有反应性reactive()。模板被编译为innerHTML使用这些反应特性的渲染函数(认为效率更高)。

watchEffect与2.x watch选项类似,但是它不需要分离监视的数据源和副作用回调。Composition API还提供了watch与2.x选项完全相同的功能。

继续上面的示例,这是我们处理用户输入的方式:

function increment() {
  state.count++}

document.body.addEventListener('click', increment)

但是,借助Vue的模板系统,我们无需纠缠innerHTML或手动附加事件侦听器。让我们用一种假设的renderTemplate方法简化该示例,以便我们专注于反应性方面:

import { reactive, watchEffect } from 'vue'const state = reactive({
  count: 0})function increment() {
  state.count++}const renderContext = {
  state,
  increment
}watchEffect(() => {
  // hypothetical internal code, NOT actual API
  renderTemplate(
    `<button @click="increment">{{ state.count }}</button>`,
    renderContext
  )})

计算状态和引用

有时我们需要依赖于其他状态的状态-在Vue中,这是通过计算属性来处理的。要直接创建一个计算值,我们可以使用computedAPI:

import { reactive, computed } from 'vue'const state = reactive({
  count: 0})const double = computed(() => state.count * 2)

computed这里返回什么?如果我们猜测如何computed在内部实现,我们可能会想到以下内容:

// simplified pseudo codefunction computed(getter) {
  let value
  watchEffect(() => {
    value = getter()
  })
  return value
}

但是我们知道这value是行不通的:如果是类似的原始类型number,computed则一旦返回,它与内部更新逻辑的连接将丢失。这是因为JavaScript基本类型是通过值而不是通过引用传递的

将值分配给对象作为属性时,也会发生相同的问题。如果一个反应性值在分配为属性或从函数返回时不能保持其反应性,那么它将不是很有用。为了确保我们始终可以读取计算的最新值,我们需要将实际值包装在一个对象中,然后返回该对象:

// simplified pseudo codefunction computed(getter) {
  const ref = {
    value: null
  }
  watchEffect(() => {
    ref.value = getter()
  })
  return ref
}

另外,我们还需要拦截对对象.value属性的读/写操作,以执行依赖关系跟踪和更改通知(为简单起见,此处省略了代码)。现在,我们可以按引用传递计算所得的值,而不必担心失去反应性。折衷是为了获取最新值,我们现在需要通过.value以下方式访问它:

const double = computed(() => state.count * 2)watchEffect(() => {
  console.log(double.value)}) // -> 0

state.count++ // -> 2

这double是一个我们称为“ ref”的对象,因为它用作对其持有的内部值的反应性引用。

您可能会意识到Vue已经有了“引用”的概念,但是仅用于引用模板(“模板引用”)中的DOM元素或组件实例。看看这个,看看新裁判系统如何能够同时用于逻辑状态和模板裁判。

除了计算的引用外,我们还可以使用refAPI 直接创建普通的可变引用:

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

参考展开

我们可以将ref公开为渲染上下文的属性。在内部,Vue将对ref进行特殊处理,以便在渲染上下文中遇到ref时,该上下文直接公开其内部值。这意味着在模板中,我们可以直接编写{{ count }}而不是{{ count.value }}。

这是相同计数器示例的版本,使用ref代替reactive:

import { ref, watch } from 'vue'const count = ref(0)function increment() {
  count.value++}const renderContext = {
  count,
  increment
}watchEffect(() => {
  renderTemplate(
    `<button @click="increment">{{ count }}</button>`,
    renderContext
  )})

另外,当引用作为属性嵌套在反应对象下时,它也将在访问时自动展开:

const state = reactive({
  count: 0,
  double: computed(() => state.count * 2)})// no need to use `state.double.value`
console.log(state.double)

组件中的用法

到目前为止,我们的代码已经提供了可以根据用户输入进行更新的工作UI,但是该代码仅运行一次且不可重用。如果我们想重用逻辑,那么合理的下一步似乎是将其重构为一个函数:

import { reactive, 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
  )})

注意上面的代码如何不依赖于组件实例的存在。实际上,到目前为止引入的API都可以在组件上下文之外使用,从而使我们能够在更广泛的场景中利用Vue的反应系统。

现在,如果我们离开了调用setup(),创建观察者并将模板呈现到框架的任务,我们可以仅使用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>

这是我们熟悉的单文件组件格式,只有逻辑部分(<script>)用不同的格式表示。模板语法保持完全相同。<style>被省略,但也将完全相同。

#生命周期挂钩

到目前为止,我们已经涵盖了组件的纯状态方面:用户输入上的反应状态,计算状态和变异状态。但是组件可能还需要执行一些副作用-例如,登录到控制台,发送ajax请求或在上设置事件监听器window。这些副作用通常在以下时间执行:

  • 当某些状态改变时;

  • 安装,更新或卸载组件时(生命周期挂钩)。

我们知道我们可以使用watchEffect和watchAPI根据状态变化来应用副作用。至于在不同的生命周期挂钩中执行副作用,我们可以使用专用的onXXXAPI(直接反映现有的生命周期选项):

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

这些生命周期注册方法只能在setup钩子调用期间使用。它会自动找出setup使用内部全局状态调用钩子的当前实例。有意设计这种方式来减少将逻辑提取到外部功能时的摩擦。

有关这些API的更多详细信息,请参见《API参考》。但是,我们建议在深入研究设计细节之前先完成以下几节

代码组织

至此,我们已经使用导入的函数复制了组件API,但是该做什么呢?用选项定义组件似乎要比将所有功能混合在一起来使功能更有组织性!

这是可以理解的第一印象。但是,正如动机部分所述,我们认为Composition API实际上可以带来更好的组织代码,尤其是在复杂的组件中。在这里,我们将尝试解释原因。

#什么是“组织机构代码”?

让我们退后一步,考虑当我们谈论“组织代码”时的真正含义。保持代码井井有条的最终目的应该是使代码更易于阅读和理解。“理解”代码是什么意思?我们真的可以仅仅因为知道组件包含哪些选项而声称自己“了解”了组件吗?您是否遇到过由另一位开发人员创作的大型组件(例如this),并且很难将其包裹住?

想一想我们将如何引导同一个开发人员通过一个大型组件,如上面链接的组件。您很可能从“此组件正在处理X,Y和Z”开始,而不是“此组件具有这些数据属性,这些计算的属性和这些方法”。在理解组件时,我们更关心“组件正在尝试做什么”(即代码背后的意图),而不是“组件碰巧使用了哪些选项”。虽然使用基于选项的API编写的代码自然可以回答后者,但在表达前者方面做得相当差。

#逻辑问题与选项类型

让我们将组件要处理的“ X,Y和Z”定义为逻辑关注点。小型单一用途的组件通常不存在可读性问题,因为整个组件只处理一个逻辑问题。但是,在高级用例中,这个问题变得更加突出。以Vue CLI UI文件浏览器为例。该组件必须处理许多不同的逻辑问题:

  • 跟踪当前文件夹状态并显示其内容

  • 处理文件夹导航(打开,关闭,刷新...)

  • 处理新文件夹的创建

  • 仅切换显示收藏夹

  • 切换显示隐藏文件夹

  • 处理当前工作目录更改

您是否可以通过阅读基于选项的代码立即识别并区分这些逻辑问题?这肯定是困难的。您会注意到,与特定逻辑问题相关的代码通常会分散在各处。例如,“创建新文件夹”功能使用了两个数据属性,一个计算属性和一个方法 -其中在距数据属性一百行的位置定义了该方法。

如果我们对这些逻辑问题中的每一个进行彩色编码,我们会注意到在使用组件选项表示它们时有多分散:

正是这种碎片化使得难以理解和维护复杂的组件。通过选项的强制分隔使基本的逻辑问题变得模糊。另外,当处理单个逻辑关注点时,我们必须不断地“跳动”选项块,以查找与该关注点相关的部分。

注意:原始代码可能会在几个地方进行改进,但是我们正在展示最新提交(在撰写本文时),而没有进行修改,以提供我们自己编写的实际生产代码示例。

如果我们可以并置与同一逻辑问题相关的代码,那就更好了。这正是Composition API使我们能够执行的操作。可以通过以下方式编写“创建新文件夹”功能:

function useCreateFolder (openFolder) {
  // originally data properties
  const showNewFolder = ref(false)
  const newFolderName = ref('')

  // originally computed property
  const newFolderValid = computed(() => isValidMultiName(newFolderName.value))

  // originally a method
  async function createFolder () {
    if (!newFolderValid.value) return
    const result = await mutate({
      mutation: FOLDER_CREATE,
      variables: {
        name: newFolderName.value
      }
    })
    openFolder(result.data.folderCreate.path)
    newFolderName.value = ''
    showNewFolder.value = false
  }

  return {
    showNewFolder,
    newFolderName,
    newFolderValid,
    createFolder
  }}

请注意,现在如何将与“创建新文件夹”功能相关的所有逻辑并置并封装在一个函数中。由于其描述性名称,该功能在某种程度上也是自记录的。这就是我们所说的合成函数。建议使用约定以函数名称开头,use以表明它是组合函数。这种模式可以应用于组件中的所有其他逻辑问题,从而产生了许多很好的解耦功能:

此比较不包括导入语句和setup()函数。使用Composition API重新实现的完整组件可以在此处找到。

现在,每个逻辑关注点的代码在组合函数中并置在一起。当在大型组件上工作时,这大大减少了对恒定“跳跃”的需求。合成功能也可以在编辑器中折叠,以使组件更易于扫描

export default {
  setup() { // ...
  }}function useCurrentFolderData(networkState) { // ...}function useFolderNavigation({ networkState, currentFolderData }) { // ...}function useFavoriteFolder(currentFolderData) { // ...}function useHiddenFolders() { // ...}function useCreateFolder(openFolder) { // ...}

setup()现在,该函数主要用作调用所有组合函数的入口点:

export default {
  setup () {
    // Network
    const { networkState } = useNetworkState()

    // Folder
    const { folders, currentFolderData } = useCurrentFolderData(networkState)
    const folderNavigation = useFolderNavigation({ networkState, currentFolderData })
    const { favoriteFolders, toggleFavorite } = useFavoriteFolders(currentFolderData)
    const { showHiddenFolders } = useHiddenFolders()
    const createFolder = useCreateFolder(folderNavigation.openFolder)

    // Current working directory
    resetCwdOnLeave()
    const { updateOnCwdChanged } = useCwdUtils()

    // Utils
    const { slicePath } = usePathUtils()

    return {
      networkState,
      folders,
      currentFolderData,
      folderNavigation,
      favoriteFolders,
      toggleFavorite,
      showHiddenFolders,
      createFolder,
      updateOnCwdChanged,
      slicePath
    }
  }}

当然,这是我们使用options API时无需编写的代码。但是请注意,该setup功能几乎读起来像是对该组件要执行的操作的口头描述-这是基于选项的版本中完全缺少的信息。您还可以根据传递的参数清楚地看到组合函数之间的依赖关系流。最后,return语句是检查模板暴露内容的唯一位置。

给定相同的功能,通过选项定义的组件和通过组合函数定义的组件会表现出两种表达同一基本逻辑的不同方式。基于选项的API迫使我们根据选项类型组织代码,而Composition API使我们能够基于逻辑关注点组织代码。

#逻辑提取和重用

当涉及跨组件提取和重用逻辑时,Composition API非常灵活。this合成函数不依赖魔术上下文,而仅依赖于其参数和全局导入的Vue API。您可以通过简单地将其导出为函数来重用组件逻辑的任何部分。您甚至可以extends通过导出setup组件的全部功能来达到等效的效果。

让我们看一个例子:跟踪鼠标的位置。

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 logic...
    return { x, y }
  }}

在文件资源管理器示例的Composition API版本中,我们将一些实用程序代码(例如usePathUtils和useCwdUtils)提取到了外部文件中,因为我们发现它们对其他组件很有用。

使用现有模式(例如混合,高阶组件或无渲染组件)(通过作用域插槽),也可以实现类似的逻辑重用。互联网上有大量信息解释这些模式,因此在此我们将不再重复详细说明。高层次的想法是,与组合函数相比,这些模式中的每一个都有各自的缺点:

  • 渲染上下文中公开的属性的来源不清楚。例如,当使用多个mixin读取组件的模板时,可能很难确定从哪个mixin注入了特定的属性。

  • 命名空间冲突。Mixins可能会在属性和方法名称上发生冲突,而HOC可能会在预期的prop名称上发生冲突。

  • 性能。HOC和无渲染组件需要额外的有状态组件实例,这会降低性能。

相比之下,使用Composition API:

  • 暴露给模板的属性具有明确的来源,因为它们是从合成函数返回的值。

  • 合成函数返回的值可以任意命名,因此不会发生名称空间冲突。

  • 没有创建仅用于逻辑重用的不必要的组件实例。

#与现有API一起使用

Composition API可以与现有的基于选项的API一起使用。

  • Composition API在2.x选项(data,computed&methods)之前已解决,并且无法访问由这些选项定义的属性。

  • 返回的属性setup()将this在2.x选项中公开并可以访问。

#插件开发

如今,许多Vue插件都将属性注入this。例如,Vue Router注入this.$route和this.$router,而Vuex注入this.$store。由于每个插件都要求用户增加注入属性的Vue类型,这使得类型推断变得棘手。

使用Composition API时,没有this。相反,插件将充分利用="https://composition-api.vuejs.org/api.html#dependency-injection">provide并inject在内部和公开的组成功能。以下是插件的假设代码:

const StoreSymbol = Symbol()export function provideStore(store) {
  provide(StoreSymbol, store)}export function useStore() {
  const store = inject(StoreSymbol)
  if (!store) {
    // throw error, no store provided
  }
  return store
}

并在使用代码中:

// provide store at component root//const App = {
  setup() {
    provideStore(store)
  }}const Child = {
  setup() {
    const store = useStore()
    // use the store
  }}

请注意,也可以通过全局API更改RFC中建议的应用程序级提供来提供商店,但是useStore使用组件中的样式API相同。

#缺点

#介绍引用的开销

从技术上讲,Ref是此提案中引入的唯一“新”概念。引入它是为了将反应性值作为变量传递,而无需依赖对的访问this。缺点是:

  1. 使用Composition API时,我们将需要不断将ref与纯值和对象区分开来,从而增加了使用API时的精神负担。通过使用命名约定(例如,将所有ref变量后缀为xxxRef)或使用类型系统,可以大大减轻精神负担。另一方面,由于提高了代码组织的灵活性,因此组件逻辑将更多地被隔离为一些小的函数,这些函数的局部上下文很简单,引用的开销很容易管理。

  2. 由于需要,读取和变异refs比使用普通值更冗长.value。一些人建议使用编译时语法糖(类似于Svelte 3)来解决此问题。尽管从技术上讲这是可行的,但我们认为将其作为Vue的默认值是不合理的(如在与Svelte的比较中所讨论的)。就是说,这在用户领域作为Babel插件在技术上是可行的。

我们已经讨论了是否有可能完全避免使用Ref概念并仅使用反应性对象,但是:

  • 计算的获取器可以返回原始类型,因此不可避免地要使用类似Ref的容器。

  • 仅出于反应性的考虑,仅期望或返回原始类型的组合函数也需要将值包装在对象中。如果框架没有提供标准的实现,那么用户很有可能最终会发明自己的Ref like模式(并导致生态系统碎片化)。

#参考与反应

可以理解,用户可能会对ref和之间使用哪个感到困惑reactive。首先要知道的是,您将需要了解两者才能有效地使用Composition API。独家使用一个极有可能导致神秘的解决方法或重新发明轮子。

使用ref和之间的区别reactive可以与您编写标准JavaScript逻辑的方式进行比较:

// style 1: separate variableslet x = 0let y = 0function updatePosition(e) {
  x = e.pageX
  y = e.pageY
}// --- compared to ---// style 2: single objectconst pos = {
  x: 0,
  y: 0}function updatePosition(e) {
  pos.x = e.pageX
  pos.y = e.pageY
}
  • 如果使用ref,则使用ref在很大程度上将样式(1)转换为更冗长的等效项(以使原始值具有反应性)。

  • 使用reactive几乎与样式(2)相同。我们只需要使用创建对象即可reactive。

但是,reactive仅运行的问题在于,复合函数的使用者必须始终保持对返回对象的引用,以保持反应性。该对象不能被破坏或散布:

// composition functionfunction useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0
  })

  // ...
  return pos
}// consuming componentexport default {
  setup() {
    // reactivity lost!
    const { x, y } = useMousePosition()
    return {
      x,
      y
    }

    // reactivity lost!
    return {
      ...useMousePosition()
    }

    // this is the only way to retain reactivity.
    // you must return `pos` as-is and reference x and y as `pos.x` and `pos.y`
    // in the template.
    return {
      pos: useMousePosition()
    }
  }}

该toRefsAPI被提供给处理该约束-它的每个属性转换反应性对象到对应的REF上:

function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0
  })

  // ...
  return toRefs(pos)}// x & y are now refs!const { x, y } = useMousePosition()

总结起来,有两种可行的样式:

  1. 使用ref和reactive随便怎么样你在正常的JavaScript声明基本类型变量和对象变量。使用这种样式时,建议使用具有IDE支持的类型系统。

  2. reactive尽可能使用,记住toRefs从组合函数返回反应对象时使用。这减少了裁判的精神开销,但并没有消除对这个概念熟悉的需要。

在现阶段,我们认为在refvs. 上强制采用最佳做法为时尚早reactive。我们建议您从上面的两个选项中选择与您的心理模型相符的样式。我们将收集现实世界中的用户反馈,并最终提供有关此主题的更多确定性指导。

#返回声明的详细程度

一些用户对于return语句的setup()冗长和仿佛样板感到担忧。

我们认为明确的退货声明有利于可维护性。它使我们能够显式控制暴露给模板的内容,并且可以作为跟踪在组件中定义模板属性的起点。

有人建议自动公开在中setup()声明的变量,从而使return语句成为可选的。同样,我们不认为这应该是默认设置,因为它违背了标准JavaScript的直觉。但是,有一些方法可以减少用户空间中的琐事:

  • IDE扩展,该扩展基于在中声明的变量自动生成return语句 setup()

  • Babel插件隐式生成并插入return语句。

#更多的灵活性需要更多的纪律

许多用户指出,尽管Composition API在代码组织方面提供了更大的灵活性,但它也需要开发人员更多的纪律才能“正确执行”。有些人担心该API会导致经验不足的意大利面条式代码。换句话说,尽管Composition API提高了代码质量的上限,但同时也降低了代码质量的下限。

我们在一定程度上同意这一点。但是,我们认为:

  1. 上限的收益远大于下限的损失。

  2. 通过适当的文档和社区指导,我们可以有效地解决代码组织问题。

一些用户使用Angular 1控制器作为设计可能导致编写不良代码的示例。Composition API和Angular 1控制器之间的最大区别是,它不依赖于共享范围上下文。这使得将逻辑分成单独的功能变得非常容易,这是JavaScript代码组织的核心机制。

任何JavaScript程序都以入口文件开头(将其视为setup()程序的)。我们根据逻辑关注点将程序分为功能和模块来组织程序。Composition API使我们能够对Vue组件代码执行相同的操作。换句话说,使用Composition API时,编写井井有条的JavaScript代码的技能会直接转化为编写井井有条的Vue代码的技能。

#采用策略

Composition API纯粹是添加的,不会影响/弃用任何现有的2.x API。它已通过@vue/composition库作为2.x插件提供。该库的主要目标是提供一种试验API并收集反馈的方法。当前的实现是此提案的最新版本,但是由于作为插件的技术限制,可能包含一些不一致性。随着该提案的更新,它可能还会收到制动变化,因此我们不建议在此阶段在生产中使用它。

我们打算将API内置在3.0中。它将与现有的2.x选项一起使用。

对于选择仅在应用程序中使用Composition API的用户,可以提供编译时标志,以删除仅用于2.x选项的代码并减小库的大小。但是,这是完全可选的。

该API将被定位为高级功能,因为它旨在解决的问题主要出现在大规模应用程序中。我们无意修改文档以将其用作默认文档。相反,它将在文档中有其自己的专用部分。

#附录

#类API的类型问题

引入类API的主要目的是提供一种具有更好TypeScript推理支持的替代API。但是,this即使使用基于类的API ,Vue组件也需要将从多个源声明的属性合并到单个上下文中,这一事实带来了一些挑战。

一个例子是道具的打字。为了将props合并到this,我们必须对组件类使用通用参数,或使用装饰器。

这是使用通用参数的示例:

interface Props {
  message: string
}class App extends Component<Props> {
  static props = {
    message: String
  }}

由于传递给泛型参数的接口仅处于类型区域,因此用户仍然需要为上的props代理行为提供运行时props声明this。该双重声明是多余且笨拙的。

我们已经考虑过使用装饰器作为替代:

class App extends Component<Props> {
  @prop message: string
}

使用装饰器会产生对第二阶段规范的依赖,存在很多不确定性,尤其是当TypeScript的当前实现与TC39提案完全不同步时。此外,无法公开使用装饰器声明的道具类型this.$props,这会破坏TSX的支持。用户还可以假定他们可以@prop message: string = 'foo'在技术上无法按预期方式使用时声明道具的默认值。

另外,当前没有办法利用上下文类型作为类方法的参数-这意味着传递给Class render函数的参数不能具有基于Class其他属性的推断类型。

#与React Hooks的比较

基于函数的API提供了与React Hooks相同级别的逻辑组合功能,但有一些重要的区别。与React钩子不同,该setup()函数仅被调用一次。这意味着使用Vue的Composition API的代码为:

  • 总的来说,它更符合惯用的JavaScript代码的直觉;

  • 对呼叫顺序不敏感,可以有条件;

  • 在每次提炼中不反复调用,并产生较小的GC压力;

  • 无需考虑useCallback几乎总是需要在哪儿防止内联处理程序导致子组件的过度渲染;

  • 不受地方的问题useEffect,并useMemo可以捕捉陈旧的变量,如果用户忘记传递正确的依赖阵列。Vue的自动依赖关系跟踪确保观察者和计算值始终正确无效。

我们认可React Hooks的创造力,这是该建议的主要灵感来源。但是,上面提到的问题确实存在于设计中,我们注意到Vue的反应性模型提供了解决这些问题的方法。

#与Svelte的比较

尽管采用的路线截然不同,但是Composition API和Svelte 3的基于编译器的方法实际上在概念上有很多共通之处。这是一个并行的示例:

<script>import { ref, watchEffect, onMounted } from 'vue'export default {
  setup() {
    const count = ref(0)

    function increment() {
      count.value++
    }

    watchEffect(() => console.log(count.value))

    onMounted(() => console.log('mounted!'))

    return {
      count,
      increment
    }
  }}</script>
<script>import { onMount } from 'svelte'let count = 0function increment() {
  count++}

$: console.log(count)onMount(() => console.log('mounted!'))</script>

velte代码看起来更简洁,因为它在编译时执行以下操作:

  • 隐式地将整个<script>块(import语句除外)包装到为每个组件实例调用的函数中(而不是仅执行一次)

  • 隐式注册对可变突变的反应性

  • 隐式地将所有作用域内的变量暴露给渲染上下文

  • 将$语句编译成重新执行的代码

从技术上讲,我们可以在Vue中做同样的事情(可以通过userland Babel插件来完成)。我们不这样做的主要原因是与标准JavaScript保持一致。如果您从<script>Vue文件的块中提取代码,我们希望它与标准ES模块完全一样地工作。<script>另一方面,Svelte 块中的代码在技术上不再是标准的JavaScript。这种基于编译器的方法存在很多问题:

  1. 无论是否编译,代码的工作方式都不同。作为一个渐进式框架,许多Vue用户可能希望/需要/必须在没有构建设置的情况下使用它,因此,编译后的版本不能成为默认版本。另一方面,Svelte将自身定位为编译器,并且只能与构建步骤一起使用。这是两个框架在有意识地做出的折衷。

  2. 代码在内部/外部组件中的工作方式不同。当尝试从Svelte组件中提取逻辑并将其提取到标准JavaScript文件中时,我们将失去神奇的简洁语法,而不得不使用更为冗长的低级API。

  3. Svelte的反应性编译仅适用于顶级变量-它不涉及在函数内部声明的变量,因此我们无法在组件内部声明的函数中封装反应性状态。这对具有功能的代码组织施加了不小的限制-正如我们在RFC中所展示的那样,这对于保持大型组件的可维护性非常重要。

  4. 非标准语义使与TypeScript集成成为问题。

这绝不是说Svelte 3是一个坏主意-实际上,这是一种非常创新的方法,我们非常尊重Rich的工作。但是基于Vue的设计约束和目标,我们必须做出不同的权衡。

05-08 1911
05-06 2123
实付 79.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值