# CompositionAPI RFC (opens new window)

# 概要

将 2.x 中与组件逻辑相关的选项以 API 函数的形式重新设计。

# 基本例子

import { ref, computed, watch, onMounted } from 'vue'
const App = {
  template: `
    <div>
      <span>count is {{ count }}</span>
      <span>plusOne is {{ plusOne }}</span>
      <button @click="increment">count++</button>
    </div>
  `,
  setup() {
    // reactive state
    const count = ref(0)
    // computed state
    const plusOne = computed(() => count.value + 1)
    // method
    const increment = () => { count.value++ }
    // watch
    watch(() => count.value * 2, val => {
      console.log(`count * 2 is ${val}`)
    })
    // lifecycle
    onMounted(() => {
      console.log(`mounted`)
    })
    // expose bindings on render context
    return {
      count,
      plusOne,
      increment
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 逻辑组合与复用

2.x中如何组织逻辑,以及如何在多个组件中抽取和复用逻辑存在很多的问题,常用的方式有下列三种

  1. Mixins
  • 模版中的数据来源不清晰
  • 命名空间冲突
  1. 高阶组件(heigher-order Components ,HOCS)
  • 模版中的数据来源不清晰
  • HOC 在注入的 props 命名空间冲突
  • 需要额外的组件实例嵌套来封装逻辑
  1. 无渲染组件 Renderless Components (基于 scoped slots / 作用域插槽封装逻辑的组件)
  • 需要额外的组件实例嵌套来封装逻辑

3.0中以上问题都不会存在。使用基于函数的 API, 我们可以将相关联的代码抽取到一个 "composition function"(组合函数)中 —— 该函数封装了相关联的逻辑, 并将需要暴露给组件的状态以响应式的数据源的方式返回出来。

例如:用组合函数来封装鼠标位置侦听逻辑的例子

import { ref, onMounted, onUnmounted } from 'vue'
function useMouse() {
  const x = ref(0)
  const y = ref(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

// 在组件中使用该函数
const Component = {
  setup() {
    const { x, y } = useMouse()
    // 与其它函数配合使用
    const { z } = useOtherLogic()
    return { x, y, z }
  },
  template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

3.0 优势:

  • 暴露给模版的属性来源清晰(从函数返回)
  • 返回值可以被任意重命名,所以不存在命名空间冲突;
  • 没有创建额外的组件实例所带来的性能损耗

# 类型推导

增强对 TypeScript 的支持 更好的tree-shaking

# 设计细节

  • setup() 函数

    • 是我们组织逻辑的地方。它会在一个组件实例被创建时,初始化了 props 之后调用,接受props作为参数
  • 组件状态

    • 类似 data()函数一样,setup() 函数也可以返回一个对象,返回的对象可以在模块中使用
  • 包装对象

    • ref() 返回的是一个 value reference (包装对象)。一个包装对象只有一个属性:.value ,该属性指向内部被包装的值。
      为什么需要包装对象?

      始值类型如 string 和 number 是只有值,没有引用的。如果在一个函数中返回一个字符串变量,接收到这个字符串的代码只会获得一个值, 是无法追踪原始变量后续的变化的。

      因此,包装对象的意义就在于提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器, Vue 的包装对象是响应式的数据源。有了这样的容器,我们就可以在封装了逻辑的组合函数中将状态以引用的方式传回给组件。 组件负责展示(追踪依赖),组合函数负责管理状态(触发更新):

      setup() {
        const valueA = useLogicA() // valueA 可能被 useLogicA() 内部的代码修改从而触发更新
        const valueB = useLogicB()
        return {
          valueA,
          valueB
        }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8

      包装对象也可以包装非原始值类型的数据,被包装的对象中嵌套的属性都会被响应式地追踪。 用包装对象去包装对象或是数组并不是没有意义的:它让我们可以对整个对象的值进行替换 —— 比如用一个 filter 过的数组去替代原数组:

        const numbers = ref([1, 2, 3])
        // 替代原数组,但引用不变
        numbers.value = numbers.value.filter(n => n > 1)
      
      1
      2
      3
  • Ref Unwrapping(包装对象的自动展开)

    • 包装对象在模板中使用是自动展开,不需要 .value
    • 当一个包装对象被作为另一个响应式对象的属性引用的时候也会被自动展开
      代码示例
      const MyComponent = {
        setup() {
          const count = ref(0)
          const obj = reactive({
            count
          })
          // 当一个包装对象被作为另一个响应式对象的属性引用的时候也会被自动展开
          obj.count // 0
          obj.count ++ // 1
          return {
            count: ref(0)
          }
        },
        template: `<button @click="count++">{{ count }} 不需要.value 来取值</button>`
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
  • render function

    • 和 2.x 一样的 render 选项也可以使用,但如果用了 setup(),就应该尽量使用内联返回的渲染函数,因为这样可以避免先返回一堆绑定然后再在另一个函数里解构出来,同时类型推导也会更简单直接一些
  • Computed Value (计算值)

  • 通过 computed 来包装一个值,只有当依赖变化的时候它才会被重新计算。

DETAILS
 import { ref, computed } from 'vue'
 const count = ref(0)
 const countPlusOne = computed(() => count.value + 1)
 console.log(countPlusOne.value) // 1
 count.value++
 console.log(countPlusOne.value) // 2
1
2
3
4
5
6
  • 在setup函数中返回给模板使用,并且在模板中会自动展开,

  • 返回的是只读属性

  • Watchers

watch() API 提供了基于观察状态的变化来执行副作用的能力

watch() 接收的第一个参数被称作 “数据源”,它可以是, 下列三种值的任意一种, 第二个参数是回调函数。回调函数只有当数据源发生变动时才会被触发。

  • 一个返回任意值的函数
  • 一个包装对象
  • 一个包含上述两种数据源的数组
DETAILS
```js
  watch(
    // getter
    () => count.value + 1,
    // callback
    (value, oldValue) => {
      console.log('count + 1 is: ', value)
    }
  )
  // -> count + 1 is: 1

  count.value++
  // -> count + 1 is: 2


  第三个是配置项,包括下列属性
  lazy与 2.x 的 immediate 正好相反
  deep与 2.x 行为一致
  onTrack 和 onTrigger 是两个用于 debug 的钩子,分别在 watcher 追踪到依赖和依赖发生变化的时候被调用,
  获得的参数是一个包含了依赖细节的 debugger event。

  interface WatchOptions {
    lazy?: boolean
    deep?: boolean
    flush?: 'pre' | 'post' | 'sync'
    onTrack?: (e: DebuggerEvent) => void
    onTrigger?: (e: DebuggerEvent) => void
  }

  interface DebuggerEvent {
    effect: ReactiveEffect
    target: any
    key: string | symbol | undefined
    type: 'set' | 'add' | 'delete' | 'clear' | 'get' | 'has' | 'iterate'
  }
```
  1. 观察 props

props 对象是一个可观测的响应式对象,可以用 watch()函数观测 props的所有属性

  1. 观察包装对象

watch()可以观测用 ref(1),value(1),computed(()=>count.value * 2)等函数包装的对象

  // double 是一个计算包装对象
  const double = computed(() => count.value * 2)

  watch(double, value => {
    console.log('double the count is: ', value)
  }) // -> double the count is: 0

  count.value++ // -> double the count is: 2
1
2
3
4
5
6
7
8
  1. 观察多个数据源

watch() 也可以观察一个包含多个数据源的数组,任意一个数据源的变化都会触发回调,同时回调会接收到包含对应值的数组作为参数:

watch(
  [refA, () => refB.value],
  ([a, b], [prevA, prevB]) => {
    console.log(`a is: ${a}`)
    console.log(`b is: ${b}`)
  }
)
1
2
3
4
5
6
7
  1. 停止观察

watch() 返回一个停止观察的函数:

const stop = watch(...)
// stop watching
stop()
1
2
3
  1. 清理副作用

有时候当观察的数据源变化后,我们可能需要对之前所执行的副作用进行清理。举例来说,一个异步操作在完成之前数据就产生了变化,我们可能要撤销还在等待的前一个操作。为了处理这种情况,watcher 的回调会接收到的第三个参数是一个用来注册清理操作的函数。调用这个函数可以注册一个清理函数。清理函数会在下属情况下被调用

  • 在回调被下一次调用前
  • 在 watcher 被停止前
import { watch, performAsyncOperation, onCleanup }
watch(idValue, (id, oldId, onCleanup) => {
  const token = performAsyncOperation(id)
  onCleanup(() => {
    // id 发生了变化,或是 watcher 即将被停止.
    // 取消还未完成的异步操作。
    token.cancel()
  })
})

1
2
3
4
5
6
7
8
9
10
  1. Watcher 回调的调用时机 默认情况下,所有的 watcher 回调都会在当前的 renderer flush 之后被调用。这确保了在回调中 DOM 永远都已经被更新完毕。如果你想要让回调在 DOM 更新之前或是被同步触发,可以使用 flush 选项:

watch(
  () => count.value + 1,
  () => console.log(`count changed`),
  {
    flush: 'post', // default, fire after renderer flush
    flush: 'pre', // fire right before renderer flush
    flush: 'sync' // fire synchronously
  }
)
1
2
3
4
5
6
7
8
9
10
  1. 生命周期函数

所有现有的生命周期钩子都会有对应的 onXXX 函数(只能在 setup() 中使用):

import { onMounted, onUpdated, onUnmounted } from 'vue'
const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    // destroyed 调整为 unmounted
    onUnmounted(() => {
      console.log('unmounted!')
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. 依赖注入

如果注入的是一个包装对象,则该注入绑定会是响应式的(也就是说,如果 Ancestor 修改了 count,会触发 Descendent 的更新)。

import { provide, inject } from 'vue'
// 依赖注入的 inject 方法是唯一必须手动声明类型的 API:
const CountSymbol = Symbol()
const Ancestor = {
  setup() {
    // providing a ref can make it reactive
    const count = ref(0)
    provide(CountSymbol, count)
  }
}

const Descendent = {
  setup() {
    const count = inject(CountSymbol)
    //  const count: Ref<number> = inject(CountSymbol) TS中
    return {
      count
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  1. 类型推导

为了能够在 TypeScript 中提供正确的类型推导,我们需要通过一个函数来定义组件:

import { defineComponent, ref } from 'vue'
const MyComponent = defineComponent({
  // props declarations are used to infer prop types
  props: {
    msg: String
  },
  setup(props) {
    props.msg // string | undefined

    // bindings returned from setup() can be used for type inference
    // in templates
    const count = ref(0)
    return {
      count
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 纯 TypeScript 的 Props 类型声明

3.0 的 props 选项不是必须的,如果你不需要运行时的 props 类型检查,你也可以选择完全在 TypeScript 的类型层面声明 props 的类型:

import { defineComponent, h } from 'vue'
interface Props {
  msg: string
}
const MyComponent = defineComponent({
  setup(props: Props) {
    return () => h('div', props.msg)
  }
})
1
2
3
4
5
6
7
8
9
  1. Required Props

Props 默认都是可选的,也就是说它们的类型都可能是 undefined。非可选的 props 需要声明 required: true :

  1. 复杂 Props 类型

Vue 提供的 PropType 类型可以用来声明任意复杂度的 props 类型,但需要用 as any 进行一次强制类型转换:

import { defineComponent, PropType } from 'vue'
defineComponent({
  props: {
    options: (null as any) as PropType<{ msg: string }>
  },
  setup(props) {
    props.options // { msg: string } | undefined
  }
}) 
1
2
3
4
5
6
7
8
9
  1. 新的render函数
 import { h } from 'vue'
//  一
  render() {
   return h('div', {
      id: 'foo',
      onClick:this.onClick
    },'hello')
  }
  // 二 v-if 

  render() {
    const ok = true
    return ok ? 
        : h('div', {id: 'foo',onClick:this.onClick},'hello')
        ? h('p', {id: 'bar',onClick:this.onClick},'wolrd')
  }
  // 三 列表循环 v-for

    render() {
        const list = [{name:1,age:1},{name:2,age:2}]
        return list.map(item => {
         return h('div', {id: 'foo',onClick:this.onClick},item.name)
        })
      }
  // slot处理
    render() {
        const slot = this.$slot.default && this.$slot.default() || []
        // return slot
        return h('div', slot)
      }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 与 React Hooks 的对比

这里提出的 API 和 React Hooks 有一定的相似性,具有同等的基于函数抽取和复用逻辑的能力,但也有很本质的区别。React Hooks 在每次组件渲染时都会调用,通过隐式地将状态挂载在当前的内部组件节点上,在下一次渲染时根据调用顺序取出。而 Vue 的 setup() 每个组件实例只会在初始化时调用一次 ,状态通过引用储存在 setup() 的闭包内

  • 整体上更符合 JavaScript 的直觉;
  • 不受调用顺序的限制,可以有条件地被调用;
  • 不会在后续更新时不断产生大量的内联函数而影响引擎优化或是导致 GC 压力;
  • 不需要总是使用 useCallback 来缓存传给子组件的回调以防止过度更新;
  • 不需要担心传了错误的依赖数组给 useEffect/useMemo/useCallback 从而导致回调中使用了过期的值 —— Vue 的依赖追踪是全自动的。