# Vue Composition API RFC

记录有关VueComposition API的一些RFC(Request For Comments)

# Summary

与组件逻辑相关的选项API函数的形式重新设计。

# 基本例子

import { value, 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() {
        const count = value(0)

        const plusOne = computed(() => count.value + 1)

        const increment = () => { count.value++ }

        watch(() => count.value * 2, val => {
            console.log(`count * 2 is ${val}`)
        })

        onMounted(() => console.log(`mounted`))

        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

# 设计动机

# 逻辑组合与复用

在多个组件之间抽取、复用逻辑时更灵活清晰

TIP

  • Mixins
  • HOC
  • Renderless Components(基于scoped slots封装逻辑的组件)

以上逻辑复用模式存在的缺点:

1、模板中的数据来源不清晰。(光看模板很难区分出一个属性是来自哪个mixin,HOC也有类似问题)

2、命名空间冲突。(属性名、方法名可能会冲突,HOC也有类似问题)

3、性能损耗。 HOC和Renderless Components都需要额外的组件实例来嵌套逻辑。

针对“逻辑组合与复用”这个问题,可以使用Function-based API

# 类型推导

  • 想在3.0增强对TS的支持,但Class API不是很好的方案
  • 基于函数的API对类型推导很友好(TS对函数的参数、返回值和泛型的支持非常完备)

# 打包尺寸

  • 每个函数都可以作为 named ES export 被单独引入,对tree-shaking友好(最终打包时会移除那些未使用的代码)
  • 所有函数名、setup函数内部的变量名可以被压缩,class的属性/方法名不行

# 设计细节

# setup()

调用时机:在组件实例被创建、且初始化props之后

一个可以接收props参数、新的组件选项。

const MyComponent = {
    props: {
        name: String
    },
    setup(props) {
        // 注意:这个props也是响应式的,可以当作数据源去观测
        console.log(props.name)
    }
}
1
2
3
4
5
6
7
8
9

可以把setup当成是2.x里的data(),因为它们返回的对象里的属性都会被暴露给模板的渲染上下文:

const MyComponent = {
    props: {
        name: String
    },
    setup(props) {
        return {
            msg: `My name is ${props.name}`
        }
    },
    template: `<div>{{ msg }}</div>`
}
1
2
3
4
5
6
7
8
9
10
11

# setup内部管理的值:value()

通过value()创建,返回的是一个包装对象:

import { value } from 'vue';

const MyComponent = {
    setup() {
        // 1、通过value()创建一个setup内部管理的值,返回的是一个包装对象msg
        // 2、这个包装对象只有一个属性(value),这个value属性指向的是内部被包装的那个值(也就是字符串'hello')
        const msg = value('hello')

        // 3、声明了一个方法,可以通过它对包装对象msg进行修改
        const changeMsg = newMsg => {
            msg.value = newMsg
        }
        return {
            msg,
            changeMsg
        }
    },
    template: `<div @click="() => changeMsg('heshiyu')">{{ msg }}</div>`
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 包装对象的原因

提供了一个能够在函数之间,以引用的方式传递任意类型值的容器。

与React Hooks中的useRef的不同:Vue的包装对象同时也是响应式的数据源

有了包装对象,我们就可以在封装了逻辑的组合函数中,将状态以引用的方式传回给组件。

组件负责展示(依赖跟踪),组合函数负责状态管理(触发更新)

# 包装对象的好处

对整个对象/数组的值进行替换,但引用不变

const numbers = value([1, 2, 3])

numbers.value = numbers.value.filter(n => n > 1)
1
2
3
setup() {
    const valueA = useLogicA() // valueA可以被useLogicA()内部的代码修改,从而触发更新
    return {
        valueA
    }
}
1
2
3
4
5
6

# 创建一个没有包装的响应式对象

可以使用state()

import { state } from 'vue'

const obj = state({ count: 0 })

object.count++
1
2
3
4
5

# 包装对象的自动展开(Value Unwrapping)

在以下两种情况时,包装对象会自动展开:

  • 1、被暴露给模板的渲染上下文时
const MyComponent = {
    setup() {
        return {
            count: value(0)
        }
    },
    template: `<button @click="count++">{{ count }}</button>`
}
1
2
3
4
5
6
7
8
  • 2、被嵌套在另一个响应式对象中
const count = value(0)
const obj = state({
    count // 嵌套在了另一个响应式对象中
})

console.log(obj.count) // 0

obj.count++ // 当obj.count加1时
console.log(count.value) // 1,包装对象count的value为1,可以理解
console.log(obj.count) // 1。可见,两处值相同,是同一个count

count.value++ // 当包装对象count的value加1时
console.log(count.value) // 2
console.log(obj.count) // 2。值相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14

包装对象的一个基本规则:只有需要以变量的形式(即:在setup()里的函数中,而不是在模板中)去引用一个包装对象时,才会需要用到.value去取它内部的值

在模板中,不需要知道.value的存在

// 如,在setup()里需要改变包装对象中的值时:
import { value } from 'vue'

const MyComponent = () => {
    setup(props) {
        const msg = value('hello')
        const appendName = () => {
            msg.value = `heshiyu` // 需要以变量的形式去引用一个包装对象时
        }

        return {
            msg,
            appendName
        }
    },
    // 在模板中不需要知道.value的存在
    template: `<div @click="appendName">{{ msg }}</div>`
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

以上等同于

// 手写Render函数的写法
import { value } from 'vue'

const MyComponent = () => {
    setup(props) {
        const msg = value('hello')
        const appendName = () => {
            msg.value = `heshiyu` // 需要以变量的形式去引用一个包装对象时
        }

        return (props, slots, attrs, vnode) => (
            h('div', {
                onClick: appendName
            }, msg.value)
        )
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 计算值:Computed Value

value()可以包装一个可变的值,那computed()就是可以包装一个计算值

import { value, computed } from 'vue'

const count = value(0)
const countPlusOne = computed(() => count.value + 1) // 依赖项为count.value【因为直接以(函数内)变量的形式引用count,所以需要声明.value】

console.log(countPlusOne.value) // 1。可见countPlusOne也是个包装对象

count.value++ // 当包装对象count的value加1
console.log(countPlusOne.value) // 2。可见countPlusOne的依赖是count.value,所以countPlusOne里的value会重新计算
1
2
3
4
5
6
7
8
9

computed()返回的是一个只读的包装对象,它:

  • 可以在setup()中被返回
  • 可以在模板的渲染上下文中自动展开,(和.value()返回的包装对象一样)

TIP

在computed()中声明setter函数:

const count = value(0)
const writableComputed = computed(
    // read
    () => count.value + 1,
    // write
    val => {
        count.value = val - 1
    }
)
1
2
3
4
5
6
7
8
9

# Watchers

提供了一个基于观察状态的变化,从而来执行副作用的能力。

watch(数据源, 回调函数 [, Watch选项])

返回值:一个停止观察的函数。const stop = watch(...)

watcher也会在当前组件被销毁时,自动停止

  • 第一个参数:数据源

    • 一个返回任意值的函数
    • 一个包装对象
    • 一个包含以上两种类型的数组
  • 第二个参数:回调函数

    • 2.x的不同:默认自动immediate: true
    • 默认在当前DOM更新后才被调用
    • 参数分别为:(val, prevVal, onCleanup)
// 第三个参数是清理函数
// 触发时机:1、在回调被下次调用前;2、在watcher被停止前
// 适用情况:当一个异步操作在完成之前,数据就已经发生了变化,需要撤销**正在等待的异步结果**
watch(valueA, (val, oldVal, onCleanup) => {
    const token = performAsyncOperation(val)

    onCleanup(() => {
        // 1、当val发生了变化,下次回调准备调用前
        // 2、或是在watcher被停止前
        // 可取消未完成的异步操作:
        token.cancel()
    })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 第三个参数:Watch选项
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
2
3
4
5
6
7
8
9
10
11
12
13
14
  • lazy:和immediate相反
  • deep:监听的深度
  • onTrack:在watcher跟踪到依赖,被调用赋值的函数
  • onTrigger:依赖发生变化时,被调用赋值的函数

TIP

当要观察多个数据源时,可以:

watch(
    // 对于valueA是个包装对象
    // 对于valueB是属于在setup()里的函数内直接使用,所以得声明`.value`
    [valueA, () => valueB.value], 

    // 回调函数里的参数,也是**按照依赖项的顺序**,各自放到(val, oldValue)的数组中
    // (回调函数最多只有两个参数)
    ([a, b], [prevA, prevB]) => {
        console.log(`a is: ${a}`)
        console.log(`b is: ${b}`)
    }
)
1
2
3
4
5
6
7
8
9
10
11
12

# Function-basedAPI例子

针对“逻辑组合与复用”这个问题,可以使用基于函数的API(Function-based API),把相关代码抽取到一个composition function(组合函数,use开头)里,并将需要暴露给组件的状态以响应式数据源的方式return出来。

// 1、将逻辑抽取到一个composition function里
// 作用:封装鼠标位置侦听逻辑
function useMouse() {
    const x = value(0)
    const y = value(0)
    const update = e => {
        x.value = e.pageX
        y.value = e.pageY
    }

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

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

    // 2、将需要暴露给组件的状态,以响应式数据源的方式返回出来
    return { x, y }
}

// 3、在组件中使用composition function:useMouse、useOtherLogic
const Component = {
    setup() {
        const { x, y } = useMouse()

        const { z } = useOtherLogic()

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

好处:

  • 暴露给模板的属性来源清晰(因为是从函数返回值里取的)
  • 没有命名冲突(因为可以通过解构赋值来重命名)
  • 没有额外性能损耗(因为没有创建额外的组件)

# 链接

Vue Composition API RFC(英文) (opens new window)

Vue Composition API RFC(中文) (opens new window)

更新时间: 11/21/2021, 2:45:24 AM