zoukankan      html  css  js  c++  java
  • 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>
    
  • 相关阅读:
    dfs介绍
    UVA11149 矩阵快速幂
    UVA1476 三分法
    漂亮的表达式!(不断补充)
    BC Round#33 B(10的18次方,快速乘法+快速幂取余)
    UVA 1639(组合数学)
    UVA 10612(数论找规律)
    小模板
    1589象棋(大模拟)
    bnuoj 29368 Check the Identity(栈)
  • 原文地址:https://www.cnblogs.com/zgh-blog/p/composition_api.html
Copyright © 2011-2022 走看看