Vue-30-前瞻体验-Vue-Function-API

12次阅读

共计 4700 个字符,预计需要花费 12 分钟才能阅读完成。

最近 Vue 官方公布了 Vue 3.0 最重要的 RFC:Function-based component API,并发布了兼容 Vue 2.0 版本的 plugin:vue-function-api,可用于提前体验 Vue 3.0 版本的 Function-based component API。笔者出于学习的目的,提前在项目中尝试了 vue-function-api。

笔者计划写两篇文章,本文为笔者计划的第一篇,主要为笔者在体验 Vue Function API 的学习心得。第二篇计划写阅读 vue-function-api 的核心部分代码原理,包括setupobservablelifecycle

本文阅读时间约为 15~20 分钟。

概述

Vue 2.x 及以前的高阶组件的组织形式或多或少都会面临一些问题,特别是在需要处理重复逻辑的项目中,一旦开发者组织项目结构组织得不好,组件代码极有可能被人诟病为“胶水代码”。而在 Vue 2.x 及之前的版本,解决此类问题的办法大致是下面的方案:

  • mixin
  • 函数式组件
  • slots

笔者维护的项目也需要处理大量复用逻辑,在这之前,笔者一直尝试使用 mixin 的方式来实现组件的复用。有些问题也一直会对开发者和维护者造成困惑,如一个组件同时 mixin 多个组件,很难分清对应的属性或方法写在哪个 mixin 里。其次,mixin的命名空间冲突也可能造成问题。难以保证不同的 mixin 不用到同一个属性名。为此,官方团队提出函数式写法的意见征求稿,也就是 RFC:Function-based component API。使用函数式的写法,可以做到更灵活地复用组件,开发者在组织高阶组件时,不必在组件组织上考虑复用,可以更好地把精力集中在功能本身的开发上。

注:本文只是笔者使用 vue-function-api 提前体验 Vue Function API,而这个 API 只是 Vue 3.0 的 RFC,而并非与最终 Vue 3.x API 一致。发布后可能有不一致的地方。

在 Vue 2.x 中使用

要想提前在 Vue 2.x 中体验 Vue Function API,需要引入 vue-function-api,基本引入方式如下:

import Vue from 'vue';
import {plugin as VueFunctionApiPlugin} from 'vue-function-api';

Vue.use(VueFunctionApiPlugin);

基本组件示例

先来看一个基本的例子:

<template>
    <div>
        <span>count is {{count}}</span>
        <span>plusOne is {{plusOne}}</span>
        <button @click="increment">count++</button>
    </div>
</template>

<script>
import Vue from 'vue';
import {value, computed, watch, onMounted} from 'vue-function-api';

export default {setup(props, context) {
        // reactive state
        const count = value(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,
        };
    },
};
</script>

详解

setup

setup函数是 Vue Function API 构建的函数式写法的主逻辑,当组件被创建时,就会被调用,函数接受两个参数,分别是父级组件传入的 props 和当前组件的上下文 context。看下面这个例子,可以知道在context 中可以获取到下列属性值:

const MyComponent = {
    props: {name: String},
    setup(props, context) {console.log(props.name);
        // context.attrs
        // context.slots
        // context.refs
        // context.emit
        // context.parent
        // context.root
    }
}

value & state

value函数创建一个包装对象,它包含一个响应式属性value

那么为何要使用 value 呢,因为在 JavaScript 中,基本类型并没有引用,为了保证属性是响应式的,只能借助包装对象来实现,这样做的好处是组件状态会以引用的方式保存下来,从而可以被在 setup 中调用的不同的模块的函数以参数的形式传递,既能复用逻辑,又能方便地实现响应式。

直接获取包装对象的值必须使用 .value,但是,如果包装对象作为另一个响应式对象的属性,Vue 内部会通过 proxy 来自动展开包装对象。同时,在模板渲染的上下文中,也会被自动展开。

import {state, value} from 'vue-function-api';
const MyComponent = {setup() {const count = value(0);
        const obj = state({count,});
        console.log(obj.count) // 作为另一个响应式对象的属性,会被自动展开

        obj.count++ // 作为另一个响应式对象的属性,会被自动展开
        count.value++ // 直接获取响应式对象,必须使用.value

        return {count,};
    },
    template: `<button @click="count++">{{count}}</button>`,
};

如果某一个状态不需要在不同函数中被响应式修改,可以通过 state 创建响应式对象,这个 state 创建的响应式对象并不是包装对象,不需要使用 .value 来取值。

watch & computed

watchcomputed 的基本概念与 Vue 2.x 的 watchcomputed一致,watch可以用于追踪状态变化来执行一些后续操作,computed用于计算属性,用于依赖属性发生变化进行重新计算。

computed返回一个只读的包装对象,和普通包装对象一样可以被 setup 函数返回,这样就可以在模板上下文中使用 computed 属性。可以接受两个参数,第一个参数返回当前的计算属性值,当传递第二个参数时,computed是可写的。

import {value, computed} from 'vue-function-api';

const count = value(0);
const countPlusOne = computed(() => count.value + 1);

console.log(countPlusOne.value); // 1

count.value++;
console.log(countPlusOne.value); // 2

// 可写的计算属性值
const writableComputed = computed(
    // read
    () => count.value + 1,
    // write
    val => {count.value = val - 1;},
);

watch第一个参数和 computed 类似,返回被监听的包装对象属性值,不过另外需要传递两个参数:第二个参数是回调函数,当数据源发生变化时触发回调函数,第三个参数是options。其默认行为与 Vue 2.x 有所不同:

  • lazy:是否会在组件创建时就调用一次回调函数,与 Vue 2.x 相反,lazy 默认是 false,默认会在组件创建时调用一次。
  • deep:与 Vue 2.x 的 deep 一致
  • flush:有三个可选值,分别为 ‘post’(在渲染后,即 nextTick 后才调用回调函数),’pre’(在渲染前,即 nextTick 前调用回调函数),’sync’(同步触发)。默认值为 ’post’。
// 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

watch 多个被包装对象属性时,参数均可以通过数组的方式进行传递,同时,与 Vue 2.x 的 vm.$watch 一样,watch返回取消监听的函数:

const stop = watch([valueA, () => valueB.value],
    ([a, b], [prevA, prevB]) => {console.log(`a is: ${a}`);
        console.log(`b is: ${b}`);
    }
);

stop();

注意:在 RFC:Function-based component API 初稿中,有提到 effect-cleanup,是用于清理一些特殊情况的副作用的,目前已经在提案中被取消了。

生命周期

所有现有的生命周期都有对应的钩子函数,通过 onXXX 的形式创建,但有一点不同的是,destoryed钩子函数需要使用 unmounted 代替:

import {onMounted, onUpdated, onUnmounted} from 'vue-function-api';

const MyComponent = {setup() {onMounted(() => {console.log('mounted!');
        });
        onUpdated(() => {console.log('updated!');
        });
        // destroyed 调整为 unmounted
        onUnmounted(() => {console.log('unmounted!');
        });
    },
};

一些思考

上面的详解部分,主要抽取的是 Vue Function API 的常见部分,并非 RFC:Function-based component API 的全部,例如其中的依赖注入,TypeScript类型推导等优势,在这里,由于篇幅有限,想要了解更多的朋友,可以点开 RFC:Function-based component API 查看。个人也在 Function-based component API 讨论区看到了更多地一些意见:

  • 由于底层设计,在 setup 取不到组件实例 this 的问题。
  • 包装对象的问题:在 RFC 讨论区,为了同时保证 TypeScript 类型推导、复用性和保留 Vue 的数据监听,包装属性必须使用 .value 来取值是讨论最激烈的,可见 Amendment proposal to Function-based Component API,在这个讨论中,toBindings提供了保留数据侦听下的多函数间的共享状态方案。

下一篇文章中,笔者将阅读 vue-function-api 的核心部分代码原理,包括 setupobservablelifecycle 等,从内部探索 Vue Function API 可能带给我们的改变。

当然,目前 Vue Function API 还处在讨论阶段,Vue 3.0 还处在开发阶段,还是期待下半年 Vue 3.0 的初版问世吧,希望能给我们带来更多的惊喜。

正文完
 0