Vue 3.0 Composition API - 中文翻译

Composition API

github地址 https://github.com/ch-zgh-1993/know-vue3/blob/master/docs/Composition API直译.md
发布转载请附原文链接 https://www.cnblogs.com/zgh-blog/articles/composition_api.html

这两天初步了解了下 vue 3.0 相关的一些内容,对于 Composition API 的指导文档过了一遍 (当然根据经验,有些重要的地方可能需要在实践过程中反复思考)。依据自己的理解,对 Composition API 做了一下中文的说明,这里放一下,方便大家在学习的时候能更直接的阅读和理解。如果有误或者有更好的理解,欢迎指出评论~

一套新增的, 基于函数的 api 让组件逻辑更加灵活。

原因,动机

逻辑复用和代码组织

过去使用 vue 的原因是快速,简单的构建中小型项目。随着 vue 使用者的发展,被用来构建大型项目,这会使团队对项目进行迭代需要耗费更长的时间。过去的时间我们见证了这些项目被 vue 当前的 api 的编程模型所限制,大概有两类问题:

  • 复杂组件在不断扩展中,变得难以理解。特别在我们读别人写的代码时,根本原因是 现有 API 强制按 options 组织代码,没有逻辑关系,实际上,按逻辑关系组织代码更有意义。
  • 缺少一个干净,无成本的机制,在多个组件之间提取和重用逻辑。

新增 API 的目的是提供更灵活的组件组织代码方式。现在不使用 options,而是使用 function 组织代码。使多个组件复用逻辑的提取更直接,甚至是外部的组件。

更好的类型检查机制

另一个来自大型项目开发者的声音是对 TS 的支持。当前 的 api 和 TS 整合使用时带来了一些挑战,大部分原因是 vue 依赖 this 上下文来暴露属性,在组件中 this 使用更多比简单的 js。(例如: 在方法中我们用 this 指向 组件本身,而不是 methods 对象。) 现有 API 设计时没有考虑类型推断,这使得当你使用 TS 造成了大量的复杂性。

大多人在 vue 中使用 TS 用 vue-class-component. 允许组件被命名为 TS class. 在 3.0, 我们提供了 built-in Class API 更好的解决之前的类型问题。在设计方面的讨论和迭代中,我们发现 Class API 解决类型问题,依赖装饰器,这在实现细节中增加了很多不确定的提议,这是一个危险的基础。

相比较,本 API 大多使用简单的变量和函数,对类型更友好。新的 API 可以享受完整的类型推断,使用新的 api 编写的代码在 TS 和 JS 看起来几乎相同,即使你不用 TS , 也可以获得更好的 IDE 支持。

详细设计

API 介绍

新的 API 没有新的概念,而是将 VUE 的核心功能暴露为独立的函数。比如 创建状态响应机制。这里将介绍一些最基本的 API,如何用这些 API 代替 2.x 选项来表示组件逻辑。这里介绍几本思想,不是所有的 API.

状态响应和一些作用

声明响应状态: reactive 等同于 2.x 的 Vue.observable(), 重命名避免和 RxJS observates 混淆。 监测数据变化: watchEffect 类似于 effect, 不需要分离监控数据源。 Composition API 提供 watch function 类似于 2.x.

我们之前返回的 data ,内部使用 reactive 创建响应状态,模版编译在一个渲染函数 watchEffect,使用了这些响应状态。

我们不需要关注 innerHTML 或者 eventListeners. 我们用 假定的API 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
    )
})


computed: 一个状态依赖另一个状态。 那么 computed 是如何实现的呢?如果直接监控原始数据类型,或者对象属性的值,直接监测时不起作用的。那么很自然的,我们会把他包装为一个对象,使用对象替代。此外,我们还需要拦截对象的属性读写操作,便于我们执行跟踪和执行数据渲染变更。现在我们使用引用类型,不用担心丢失响应性。 类似于 dobule 我们称为 ref,是对内部的参考, 你已经意识到之前内部属性 refs 引用 dom element/component, 现在还可以包括 逻辑状态。

除了 computed , 我们还可以直接创建 ref

import { reactive, computed, watchEffect, ref } from 'vue'

const state = reactive({
    count: 0
})

const double = computed(() => state.count * 2)

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

state.count++ // -> 2

// 内部简单代码
function computed (getter) {
    const ref = {
        value: null
    }
    watchEffect(() => {
        ref.value = getter()
    })
    return ref
}

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

ref 内部: 在渲染上下文暴露 ref 属性, 在内部, vue 对 ref 进行特殊处理,在遇到渲染上下文时,直接公开 ref 的内部值。也就是在 template 中,直接使用 count 而不是 count.value.

此外,ref 在作为属性在一个响应对象中,也会自动展开。

import { ref, watch, reactive, computed } from 'vue'

const state = reactive({
    count: 0,
    dobule: computed(() => state.count * 2)
})
console.log(state.dobule) // 不是 state.dobule.value

const count = ref(0)

function increment() {
    count.value++
}

const renderContext = {
    count,
    increment
}

watchEffect(() => {
    renderTemplate(
        `<button @click="increment">{{ count }}</button>`,
        renderContext
    )
})

components 重用: 如果想要重用逻辑,下一步是将其重构为一个函数。

生命周期钩子: 除了这些,我们还需打印,发送 ajax, 增加事件在 window.这些在一下的时间完成:

  • 当状态改变时。 watch, watchEffect.
  • 当组件 mounted, updated, unmounted. (on XXXAPI)

lifecycle methods 将总是在 setup 钩子中调用,将自动计算出调用 setup 钩子的当前实例。这个设计使得将逻辑提取到外部函数时,减少分歧。

代码组织

现在我们已经通过导入的函数复制了组件 API, 那用选项定义似乎比函数中混合更有条理? 回到动机,我们试着去理解其中的原因。

什么是代码组织

组织代码的目标是使我们更容易的阅读和理解代码,我们知道他的选项就理解了吗?你是否遇到过别人写的大型组件,你很困难去清晰的理解?

我们向别人描述这个组件,会说 这个组件处理什么事, A,B,C. 而不会说这个组件包含 这些 data, computed, methods. options 在告诉组件做了什么上,处理的不好。

逻辑问题 vs options

我们更多的时候定义组件使用逻辑问题,可读性问题通常不在小型,单一功能的组件中。在大型的组件中问题更加突出,比如 Vue CLI UI file explorer 处理更多不同的逻辑问题:

  • 跟踪当前文件状态,显示内容
  • 处理文件状态,打开,关闭,刷新
  • 创建新的文件
  • 弹出最喜欢的文件
  • 显示隐藏的文件
  • 当前工作文件夹改变

你去查看时很难识别这些逻辑问题部分通过选项, 这些逻辑问题通常是零散并且散落在各处,比如创建新的文件用到了两个 properties, 一个 computed prop, 一个 method. 这些距离相差甚远。这使得复杂的组件维护和理解变得更加困难。 通过选项强制分割掩盖了逻辑问题,当我们关注一个逻辑时,不得不在 options 中来回跳转。确实是。

如果我们能把同一个逻辑问题的代码收集在一块就好了。这刚好就是我们的 Composition API 做的。创建新的文件可以被重写: 将逻辑收集到一个 function 中,他的命名有些自文档化,我们称它是 composition function, 建议在函数名开头使用 use 表明它是一个结构函数。

每个逻辑问题都收集在一起,减少了我们来回跳,在一个大的组件中。 Composition functions 在编辑器中折叠,更容易被扫描查找。

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
    }
}

setup 作为调用所有函数的入口。
setup 函数 读起来像是对组件做的内容的口头描述,这是基于 options 版本无法做到的。还可以根据参数看到 Composition function 之间的依赖关系流。 return返回的内容可以用来检查 template 中使用的内容。

给定相同的功能, options 和 composition function 定义的组件表达了相同底层逻辑的两种不同方式。 一个基于 option type, 一个基于 logic concerns.

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
        }
    }
}
function useCurrentFolderData(networkState) { // ...
}

function useFolderNavigation({ networkState, currentFolderData }) { // ...
}

function useFavoriteFolder(currentFolderData) { // ...
}

function useHiddenFolders() { // ...
}

function useCreateFolder(openFolder) { // ...
}

逻辑提取和重用

Composition API 在提取和重用逻辑时,更加的灵活。比起依赖 magical this context , composition function 依赖参数和 vue 全局 api. 你能用组件的任何一部分通过导入 function. 甚至可以导出扩展 setup 来实现同样的组件。

类似逻辑重用,我们可以通过现有的 mixin, 高阶组件(higher-order components), renderless components 无渲染组件(作用域插槽)。 这些和 composition function 相比,都有缺点:

  • context prop 来源不明确。比如使用多个 mixin, 那么很难判断一个属性从哪注入。
  • 命名冲突。 混入可能在属性和方法名上发生冲突,高阶组件可能在属性名发生冲突。
  • 性能。 高阶组件和无渲染组件需要额外的 状态组件实例,这就浪费性能。

相比较之下,使用 composition api:

  • 公开到模版的属性有明确的来源, returns。
  • composition function 返回的值可以任意命名,因此不存在命名冲突。
  • 不需要创建额外的组件实例。
// example
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 }
    }
}

和现有 API 一起使用

Composition API 可以和 options-based API 一起使用。

  • composition api 在 2.x options(data, computed, methods) 之前解析,并且不能访问 options 定义的属性。
  • setup 返回的 prop 将被挂在 this 上,在 2.x options 中时可用的。

插件开发

一些插件今天注入属性在 this, 例如 Vue Router this.$route/this.$router Vuex this.$store. 这使得类型推断变得困难,因为每个插件都需要用户为注入的属性增加 vue 类型。在使用 composition api 时, 没有 this, 插件将相应的 provide/inject 暴露在 composition function.

在任何组件中,通过 inject 使用 app provide 的任何依赖。

const StoreSymbol = Symbol()
export function provideStore(store) {
    provide(StoreSymbol, store)
}

export function useStore() {
    const store = inject(StoreSymbol)
    if(!store) {
        throw new Error('no store provide')
    }
    return store
}


// 使用

// 根组件中提供 store
const App = {
    setup () {
        provideStore(store)
    }
}

// 这里使用 store
const Child = {
    setup () {
        const store = useStore()
    }
}

缺点

引入 refs 的开销

从技术上来说, ref 是 composition api 引入的唯一的新概念。 引入 ref 是为了将 响应值作为变量传递,不必依赖 this.这里的缺点是:

  • 我们需要始终区分 refs 来自简单值和对象。增加了我们要考虑的事情。
    • 使用命名约定可以大大减少我们的负担(比如所有的 ref 变量命名为 xxxRef),或者通过类型系统。另一方面,由于提高代码组织的灵活性,组件逻辑通常会被分割成一些小的函数,在这些函数中,上下文比较简单, ref 的开销也容易管理。
  • 现在由于需要使用 .value, 使用 refs 变得更加繁琐,冗长。
    • 有人建议使用编译时提供语法糖去解决这个问题,就像 Svelte 3。 虽然在技术上可行,但我们认为作为 vue 的默认值,这是没有意义的。就是说,通过 Babel 插件在用户方面,技术上是可行的。

我们已经讨论了,是不是可以避开使用 ref 概念,只使用 响应对象,然而:

  • 计算属性可以返原始类型,因此使用对象,类似于 ref 的容器是不能避免的。
  • composition function 期望返回原始类型总是需要被包装在 object 中为了响应的目的。如果没有框架提供标准的实现,用户很可能最终会发明自己的 (ref-like) 模式(导致生态系统崩溃).

ref vs reactive

正常滴,用户可能会对使用 ref 和 reactive 之间的使用感到困惑。首先,你要理解这两个内容,去更高效的使用 composition api. 只使用一个将很可能导致难以理解的代码,或者重复造轮子。

使用 ref 和 reactive 之间的一些区别可以与如何写标准的 js 逻辑进行比较。

  • 如果使用 ref, 我们在很大程度上,使用 refs 将样式 1 转换为更加详细,繁琐的等效的值(为了使原始类型具有响应性)
  • 使用 reactive 更接近样式 2, 我们仅需要使用 reactive 创建对象。

然而,当仅使用 reactive 问题是 composition function 必须保持返回对象的引用 为了保持响应性,这个对象不能进行解构和扩展。

提供 toRefs API 用来处理约束 - 它将把每一个属性在可响应对象上转换成一个对应的 ref.

总结: 有两种可行的方式:

  • 使用 ref 和 reactive 就像在 js 中声明原始数据类型和对象变量一样。推荐使用支持类型的 IDE 系统。
  • 尽可能的使用 reactive, 在 composition function 返回 响应对象时使用 toRefs. 这会减少使用 refs 的心理负担,但并不能消除这个概念的必要性。

在这个阶段,我们认为想要一个最佳实践关于 ref 和 reactive 之间言之尚早,我们建议从上面的两个选项中选取更符合你的心理模型的一种。我们将收集用户的真实反馈,在对这一话题提供更加明确的指导。

// style 1:  分离变量 
let x = 0
let y = 0
function updatePosition(e) {
    x = e.pageX
    y = e.pageY
}

// --- compared to ---

// style2: 单一对象
const pos = {
    x: 0,
    y: 0
}

function updatePosition(e) {
    pos.x = e.pageX
    pos.y = e.pageY
}
// composition function 
function useMousePosition () {
    consst pos = reactive({
        x: 0,
        y: 0
    })
    return pos
}

// consuming component
export default {
    setup () {
        // 丢失响应性
        const {x, y} = useMousePosition()
        return {
            x,
            y
        }
        // 丢失响应性
        return {
            ...useMousePosition()
        }

        // 这是唯一保持响应性的方式,返回 pos,在 template 中使用 pos.x, pos.y
        return {
            pos: useMousePosition()
        }
    }
}

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

    return toRefs(pos)
}

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

返回语句的复杂,冗长程度

一些用户已经注意到 setup 的返回语句将称为冗长的就像范例一样。我们认为简洁的 return 语句 将有利于维护,他让我们能明确控制暴露在 template 中的内容,并跟踪组件中定义的模版属性时充当起点。

有人建议自动暴露 setup 中声明的变量,使 return 语句可选,但我们不认为这应该被默认,因为违背了 js 的直观。这有一些方法可以让他不那么麻烦:

  • 使用 IDE 扩展,根据 setup 中声明的变量自动生成返回语句。
  • 隐式生成并插入返回语句的 babel plugin.

更灵活的 需要 更多纪律性,要求

许多用户指出,虽然 composition api 提供了更多的灵活性,但他也需要开发人员更多的遵守,如何正确的做。一些人担心会导致缺乏经验的人使用意大利面代码,换句话说,当 composition api 提供代码质量的同时,也降低了下限。

我们在一定程度上同意,但是,我们也相信:

  • 上界的收益远远大于下界的损失。
  • 通过恰当的文档和社区指导,我们能有效的解决代码组织问题。

一些用户使用 angular1 controller 作为怎么设计会导致不良的代码的实例,composition api 和 angular1 controller 之间最大的区别是不依赖共享作用域上下文,这使得几那个逻辑分解为单独的函数非常容易,这也是 js 组织代码的核心机制。

任何 js 项目开始于一个入口文件 (就好像一个项目的 program), 我们组织项目通过基于逻辑问题,分离函数/模块。 composition api 使我们可以做类似的处理在 vue component code. 换句话说,编写组织良好的 js 代码能直接转换为使用 composition api 编写组织良好的 vue 代码的技能。

选用策略

composition api 是纯粹的添加,不影响/不支持 任何现有的 2.x api. 已经通过 @vue/composition 库作为 2.x 的插件提供。 这个库的主要目标是提供一种方式来测试 api 和收集用户的反馈。 当前实现和建议是最新的,但由于差劲啊技术的限制,可能包含一些细微的不一致。可能会接受一些改变就像这个建议更新一样,因此我们不建议在现阶段生产中使用。

我们打算在 3.0 内置此 API. 将和 2.x 的 options 一起存在。

对于在 app 中只使用 composition api, 可以提供编译时的选项去删除 2.x 的 options 以减少库的大小。当然,这完全可选。

本 api 将被定义为一个高级特性,因为他要解决的问题出现在大规模的应用程序中。我们不打算修改文档将它用作默认值,相反,他在文档中将有自己的专用部分。

附录

class api 的类型问题

引入 class api 的主要目标是提供可选的 api 处理更好的 TS 推断支持。然而, vue 组件需要从多个源声明的属性合并到单一的 this 上下文中,即使使用基于 class 的 api, 也存在一些挑战。

一个例子是属性类型, 为了合并属性到 this,我们需要泛型参数到组件的 class, 或者使用 decorator.

使用泛型参数: 由于传递泛型参数的接口是 in type-land only, 用户仍然需要提供一个 runtime 属性声明为 this 上的属性代理, 这两次声明是多余的,尴尬的。

装饰器 decorators: 使用装饰器会产生对 stage-2 的依赖,这种依赖有很多的不确定性,特别是当 TS 当前的实现和 TC39 协议完全不同步时。此外,没有方法使用装饰器在 this.$props 公开声明属性的类型, 这会 破坏 TSX 的支持。用户还可能设置默认值,比如 @prop message: string = 'foo', 但技术上无法按照预期工作。

此外,目前没有方法利用上下文类型来处理 class 方法的参数,这意味着传递给类的 render 函数的参数,不能基于类的其他属性做类型推断。

// 泛型参数
interface Props {
    message: string
}

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


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

和 react hooks 比较

基于函数的 api 提供了相同级别的逻辑组合功能类似于 react hooks,但是有一些重要的不同点,不像 react hooks, setup 函数仅仅被调用一次,这意味着代码使用 composition api:

  • 一般来说,更符合 js 代码的表达方式。
  • 对于调用顺序不敏感,可以是有条件的。
  • 不会重复调用在每次渲染时,减少垃圾处理的压力。
  • 不被影响: 为了防止内联处理程序导致子组件的过度重新渲染问题,几乎总是需要使用 useCallback.
  • 不受影响: 如果用户忘记传递正确的依赖数组,那么 useEffect 和 useMemo 或许会捕获过时的变量。 vue 的自动依赖跟踪确保 watchers 和 computed 值将总是正确的。

我们承认 React Hooks 的创造性,他是这个提议的主要灵感来源。然而,上面提到的这些问题确实存在于他的设计中, Vue 的响应模型提供了一种解决这些问题的方法。

和 Svelte 比较

尽管走的路线不同,但是 composition api 和 svelte3 的 compiler-based 方法实际上有大量的共同点,下面是一个例子:

Svelte 代码看起来更加的简洁,因为他在编译时做了一下内容:

  • 隐式的将 script block (除了 import 语句) 包装到函数中,这个函数被每个组件实例调用,而不是只执行一次。
  • 隐式注册响应属性在变量改变时
  • 隐式向渲染上下文暴露作用域变量。
  • 将 $ 语句编译为重新执行的代码。

技术上来说,这些我们也可以在 vue 中做(通过 babel plugin 实现),我们没有做的主要原因是和标准的 js 对齐。如果从 vue file 提取 script block 的代码,我们希望他的工作方式和标准的 ES 模块相同。另一方面,在 svelte script block 中,技术上不再是标准的 js, 这种基于编译的方法有很多问题:

  • 代码在 编译/没有编译时 的工作方式不同。 作为一个渐进式框架,许多 vue 用户可能 希望/需要/必须 在没有构建设置的情况下使用它, 所以编译版本不能被默认。 另一方面,Svelte 是一个编译器,这是两个框架的本质区别。
  • 代码在 内部/外部 组件工作方式不同。 当从 Svelte 组件中提取逻辑到标准的 js 文件中,我们将丢失 magical 简洁的语法,必须后退到一个更详细的低级 api。
  • Svelte 的响应编译仅仅工作在顶级变量,不包括函数内的变量声明,因此我们不能在组件内部声明的函数中封装响应状态。这就给函数的代码组织设置了约束,正如我们在 RFC 中演示的,这对于保持大型组件的可维护性非常重要。
  • 不标准的语义使得和 TS 集成存在问题。

这里没有任何说 Svelte3 不好,事实上他是一个非常创新的方法,我们高度尊重 Rich 的工作,但是基于 vue 的设计约束和目标,我们必须做出对应的权衡。

// vue
<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>


// svelte
<script type="text/javascript">
import {onMount} from 'svelte'

let count = 0
function increment() {
    count++
}

$: console.log(count)

onMount(() => console.log('mounted'))
</script>
posted @ 2020-04-27 11:14  铁柱成针  阅读(2975)  评论(0编辑  收藏  举报