Vue3丨从 5 个维度来讲 Vue3 变化

一些概念

Vue Composition API(VCA) 在实现上也其实只是把 Vue 本身就有的响应式系统更显式地暴露出来而已。

这不是函数式,只是 API 暴露为函数。

3.0 Template 编译出来的性能会比手写 jsx 快好几倍。

——尤雨溪

Vue2 传统的 data,computed,watch,methods 写法,我们称之为「选项式api(Options API )」
Vue3 使用 Composition API (VCA)可以根据逻辑功能来组织代码,一个功能相关的 api 会放在一起。

Vue 和 React 的逻辑复用手段

到目前为止,

Vue:Mixins(混入)、HOC(高阶组件)、作用域插槽、Vue Composition API(VCA/组合式API)。

React:Mixins、HOC、Render Props、Hook。

我们可以看到都是一段越来越好的成长史,这里就不再举例赘述,本文重心在 VCA,VCA 更偏向于「组合」的概念。

5个维度来讲 Vue3

1. 框架

一个例子先来了解 VCA

在 Vue 中,有了抽象封装组件的概念,解决了在页面上模块越多,越显臃肿的问题。但即使进行组件封装,在应用越来越大的时候,会发现页面的逻辑功能点越来越多, data/computed/watch/methods 中会被不断塞入逻辑功能,所以要将逻辑再进行抽离组合、复用,这就是 VCA。

举个简单的例子:

我们要实现 3 个逻辑

  1. 根据 id 获取表格的数据
  2. 可对表格数据进行搜索过滤
  3. 弹框新增数据到表格中

Vue2 options api 的处理

为了阅读质量,省略了部分代码,但不影响我们了解 VCA

// 逻辑功能(1)
const getTableDataApi = id => {
  const mockData = {
    1: [
      { id: 11, name: '张三1' },
      { id: 12, name: '李四1' },
      { id: 13, name: '王五1' }
    ],
    2: [
      { id: 21, name: '张三2' },
      { id: 22, name: '李四2' },
      { id: 23, name: '王五2' }
    ]
  };
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(mockData[id] || []);
    }, 1000);
  });
};

export default {
  name: 'VCADemo',
  components: { Modal },
  data() {
    return {
      // 逻辑功能(1)
      id: 1,
      table: [],
      // 逻辑功能(2)
      search: '',
      // 逻辑功能(3)
      modalShow: false,
      form: {
        id: '',
        name: ''
      }
    };
  },
  computed: {
    // 逻辑功能(2)
    getTableDataBySearch() {
      return this.table.filter(item => item.name.indexOf(this.search) !== -1);
    }
  },
  watch: {
    // 逻辑功能(1)
    id: 'getTableData'
  },
  mounted() {
    // 逻辑功能(1)
    this.getTableData();
  },
  methods: {
    // 逻辑功能(1)
    async getTableData() {
      const res = await getTableDataApi(this.id);
      this.table = res;
    },
    // 逻辑功能(3)
    handleAdd() {
      this.modalShow = true;
    },
    // 逻辑功能(3)
    handlePost() {
      const { id, name } = this.form;
      this.table.push({ id, name });
      this.modalShow = false;
    }
  }
};

这里只是举例简单的逻辑。如果项目复杂了,逻辑增多了。涉及到一个逻辑的改动,我们就可能需要修改分布在不同位置的相同功能点,提升了维护成本。

Vue3 composion api 的处理

让我们来关注逻辑,抽离逻辑,先看主体的代码结构

import useTable from './composables/useTable';
import useSearch from './composables/useSearch';
import useAdd from './composables/useAdd';

export default defineComponent({
  name: 'VCADemo',
  components: { Modal },
  setup() {
    // 逻辑功能(1)
    const { id, table, getTable } = useTable(id);
    // 逻辑功能(2)
    const { search, getTableBySearch } = useSearch(table);
    // 逻辑功能(3)
    const { modalShow, form, handleAdd, handlePost } = useAdd(table);
    return {
      id,
      table,
      getTable,

      search,
      getTableBySearch,

      modalShow,
      form,
      handleAdd,
      handlePost
    };
  }
});

setup 接收两个参数:props,context。可以返回一个对象,对象的各个属性都是被 proxy 的,进行监听追踪,将在模板上进行响应式渲染。

我们来关注其中一个逻辑,useTable,一般来说我们会用 use 开头进行命名,有那味了~

// VCADemo/composables/useTable.ts
// 逻辑功能(1)相关
import { ref, onMounted, watch, Ref } from 'vue';
import { ITable } from '../index.type';

const getTableApi = (id: number): Promise<ITable[]> => {
  const mockData: { [key: number]: ITable[] } = {
    1: [
      { id: '11', name: '张三1' },
      { id: '12', name: '李四1' },
      { id: '13', name: '王五1' }
    ],
    2: [
      { id: '21', name: '张三2' },
      { id: '22', name: '李四2' },
      { id: '23', name: '王五2' }
    ]
  };
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(mockData[id] || []);
    }, 1000);
  });
};
export default function useTable() {
  const id = ref<number>(1);
  const table = ref<ITable[]>([]);
  const getTable = async () => {
    table.value = await getTableApi(id.value);
  };
  onMounted(getTable);
  watch(id, getTable);
  return {
    id,
    table,
    getTable
  };
}

我们把相关逻辑独立抽离,并「组合」在一起了,可以看到在 vue 包暴露很多独立函数提供我们使用,已经不再 OO 了,嗅到了一股 FP 的气息~

上面这个例子先说明了 VCA 的带来的好处,Vue3 的核心当然是 VCA,Vue3 不仅仅是 VCA,让我们带着好奇往下看~

生命周期,Vue2 vs Vue3

选项式 API(Vue2) Hook inside setup(Vue3)
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

Hook inside setup,顾名思义,VCA 建议在 setup 这个大方法里面写我们的各种逻辑功能点。

Teleport 组件

传送,将组件的 DOM 元素挂载在任意指定的一个 DOM 元素,与 React Portals 的概念是一致的。

一个典型的例子,我们在组件调用了 Modal 弹框组件,我们希望的弹框是这样子的,绝对居中,层级最高,如:

组件的结构是这样子的

<Home>
  <Modal />
</Home>

但是如果在父组件 Home 有类似这样的样式,如 transform

就会影响到 Modal 的位置,即使 Modal 用了 position:fixed 来定位,如:

这就是为什么我们需要用 Teleport 组件来帮助我们 “跳出” 容器,避免受到父组件的一些约束控制,把组件的 DOM 元素挂载到 body 下,如:

<Teleport to="body">
  <div v-if="show">
    ...Modal 组件的 DOM 结构...
  </div>
</Teleport>

注意:即使 Modal 跳出了容器,也保持 “父子组件关系”,只是 DOM 元素的位置被移动了而已 。

异步组件(defineAsyncComponent)

我们都知道在 Vue2 也有异步组件的概念,但整体上来说不算完整~,Vue3 提供了 defineAsyncComponent 方法与 Suspense 内置组件,我们可以用它们来做一个优雅的异步组件加载方案。

直接看代码:

HOCLazy/index.tsx

import { defineAsyncComponent, defineComponent } from 'vue';
import MySuspense from './MySuspense.vue';
export default function HOCLazy(chunk: any, isComponent: boolean = false) {
  const wrappedComponent = defineAsyncComponent(chunk);
  return defineComponent({
    name: 'HOCLazy',
    setup() {
      const props = { isComponent, wrappedComponent };
      return () => <MySuspense {...props} />;
    }
  });
}

解释:HOCLazy 接收了两个参数,chunk 就是我们经常采用的组件异步加载方式如:chunk=()=>import(xxx.vue)isComponent 表示当前的“组件”是一个 组件级 or 页面级,通过判断 isComponent 来分别对应不同的 “loading” 操作。

HOCLazy/MySuspense.vue

<template>
  <Suspense>
    <template #default>
      <component :is="wrappedComponent"
                 v-bind="$attrs" />
    </template>
    <template #fallback>
      <div>
        <Teleport to="body"
                  :disabled="isComponent">
          <div v-if="delayShow"
               class="loading"
               :class="{component:isComponent}">
            <!-- 组件和页面有两种不一样的loading方式,这里不再详细封装 -->
            <div> {{isComponent?'组件级':'页面级'}}Loading ...</div>
          </div>
        </Teleport>
      </div>
    </template>
  </Suspense>
</template>

<script lang="ts">
import { defineComponent, defineAsyncComponent, ref, onMounted } from 'vue';
export default defineComponent({
  name: 'HOCLazy',
  props: ['isComponent', 'wrappedComponent'],
  setup(props) {
    const delayShow = ref<boolean>(false);
    onMounted(() => {
      setTimeout(() => {
        delayShow.value = true;
        // delay 自己拿捏,也可以以 props 的方式传入
      }, 300);
    });
    return { ...props, delayShow };
  }
});
</script>

<style lang="less" scoped>
.loading {
  // 组件级样式
  &.component {
  }
  // 页面级样式
}
</style>

解释:

  1. Suspense 组件有两个插槽,具名插槽 fallback 我们这里可以理解成一个 loading 的占位符,在异步组件还没显示之前的后备内容。
  2. 这里还用了 Vue 的动态组件 component 来灵活的传入一个异步组件,v-bind="$attrs" 来保证我们传递给目标组件的 props 不会消失。
  3. fallback 中我们利用了判断 isComponent 来展示不同的 loading ,因为我们希望页面级的 loading 是“全局”的,组件级是在原来的文档流,这里用了 Teleport :disabled="isComponent" 来控制是否跳出。
  4. 细心的小伙伴会发现这里做了一个延迟显示 delayShow,如果我们没有这个延迟,在网络环境良好的情况下,loading 每次都会一闪而过,会有一种“反优化”的感觉。

调用 HOCLazy:
为了更好的看出效果,我们封装了 slow 方法来延迟组件加载:

utils/slow.ts

const slow = (comp: any, delay: number = 1000): Promise<any> => {
  return new Promise(resolve => {
    setTimeout(() => resolve(comp), delay);
  });
};
export default slow;

调用(组件级)

<template>
  <LazyComp1 str="hello~" />
</template>
const LazyComp1 = HOCLazy(
  () => slow(import('@/components/LazyComp1.vue'), 1000),
  true
);
// ...
components: {
  LazyComp1
},
// ...

看个效果:

其实这与 React 中的 React.lazy + React.Suspense 的概念是一致的,之前写过的一篇文章 《React丨用户体验丨hook版 lazy loading》,小伙伴可以看看做下对比~

ref,reactive,toRef,toRefs 的区别使用

ref(reference)

ref 和 reactive 的存在都是了追踪值变化(响应式),ref 有个「包装」的概念,它用来包装原始值类型,如 string 和 number ,我们都知道不是引用类型是无法追踪后续的变化的。ref 返回的是一个包含 .value 属性的对象。

setup(props, context) {
  const count = ref<number>(1);
  // 赋值
  count.value = 2;
  // 读取
  console.log('count.value :>> ', count.value);
  return { count };
}

在 template 中 ref 包装对象会被自动展开(Ref Unwrapping),也就是我们在模板里不用再 .value

<template>  
  {{count}}
</template>

reactive

与 Vue2 中的 Vue.observable() 是一个概念。
用来返回一个响应式对象,如:

const obj = reactive({
  count: 0
})
// 改变
obj.count++

注意:它用来返回一个响应式对象,本身就是对象,所以不需要包装。我们使用它的属性,不需要加 .value 来获取。

toRefs

官网:因为 props 是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。

让我们关注 setup 方法的 props 的相关操作:

<template>
  {{name}}
  <button @click="handleClick">点我</button>
</template>
// ...
props: {
  name: { type: String, default: ' ' }
},
setup(props) {
  const { name } = props;
  const handleClick = () => {
    console.log('name :>> ', name);
  };
  return { handleClick };
}
// ...

注意:props 无需通过 setup 函数 return,也可以在 template 进行绑定对应的值

我们都知道解构是 es6 一种便捷的手段,编译成 es5 ,如:

// es6 syntax
const { name } = props;
// to es5 syntax
var name = props.name;

假设父组件更改了 props.name 值,当我们再点击了 button 输出的 name 就还是之前的值,不会跟着变化,这其实是一个基础的 js 的知识点。

为了方便我们对它进行包装,toRefs 可以理解成批量包装 props 对象,如:

const { name } = toRefs(props);
const handleClick = () => {
  // 因为是包装对象,所以读取的时候要用.value
  console.log('name :>> ', name.value);
};

可以理解这一切都是因为我们要用解构,toRefs 所采取的解决方案。

toRef

toRef 的用法,就是多了一个参数,允许我们针对一个 key 进行包装,如:

const name = toRef(props,'name');
console.log('name :>> ', name.value);

watchEffect vs watch

Vue3 的 watch 方法与 Vue2 的概念类似,watchEffect 会让我们有些疑惑。其实 watchEffect 与 watch 大体类似,区别在于:

watch 可以做到的

  • 懒执行副作用
  • 更具体地说明什么状态应该触发侦听器重新运行
  • 访问侦听状态变化前后的值

对于 Vue2 的 watch 方法,Vue3 的 "watch" 多了一个「清除副作用」 的概念,我们着重关注这点。

这里拿 watchEffect 来举例:

watchEffect:它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

watchEffect 方法简单结构

watchEffect(onInvalidate => {
  // 执行副作用
  // do something...
  onInvalidate(() => {
    // 执行/清理失效回调
    // do something...
  })
})

执行失效回调,有两个时机

  • 副作用即将重新执行时,也就是监听的数据发生改变时
  • 组件卸载时

一个例子:我们要通过 id 发起请求获取「水果」的详情,我们监听 id,当 id 切换过于频繁(还没等上个异步数据返回成功)。可能会导致最后 id=1 的数据覆盖了id=2 的数据,这并不是我们希望的。

我们来模拟并解决这个场景:

模拟接口 getFruitsById

interface IFruit {
  id: number;
  name: string;
  imgs: string;
}
const list: { [key: number]: IFruit } = {
  1: { id: 1, name: '苹果', imgs: 'https://xxx.apple.jpg' },
  2: { id: 2, name: '香蕉', imgs: 'https://xxx.banana.jpg' }
};
const getFruitsById = (
  id: number,
  delay: number = 3000
): [Promise<IFruit>, () => void] => {
  let _reject: (reason?: any) => void;
  const _promise: Promise<IFruit> = new Promise((resolve, reject) => {
    _reject = reject;
    setTimeout(() => {
      resolve(list[id]);
    }, delay);
  });
  return [
    _promise,
    () =>
      _reject({
        message: 'abort~'
      })
  ];
};

这里封装了“取消请求”的方法,利用 reject 来完成这一动作。

在 setup 方法

setup() {
  const id = ref<number>(1);
  const detail = ref<IFruit | {}>({});

  watchEffect(async onInvalidate => {
    onInvalidate(() => {
      cancel && cancel();
    });
    // 模拟id=2的时候请求时间 1s,id=1的时候请求时间 2s
    const [p, cancel] = getFruitsById(id.value, id.value === 2 ? 1000 : 2000);
    const res = await p;
    detail.value = res;
  });
  // 模拟频繁切换id,获取香蕉的时候,获取苹果的结果还没有回来,取消苹果的请求,保证数据不会被覆盖
  id.value = 2;
  // 最后 detail 值为 { "id": 2, "name": "香蕉", "imgs": "https://xxx.banana.jpg" }
}

如果没有执行 cancel() ,那么 detail 的数据将会是 { "id": 1, "name": "苹果", "imgs": "https://xxx.apple.jpg" },因为 id=1 数据比较“晚接收到”。

这就是在异步场景下常见的例子,清理失效的回调,保证当前副作用有效,不会被覆盖。感兴趣的小伙伴可以继续深究。

fragment(片段)

我们都知道在封装组件的时候,只能有一个 root 。在 Vue3 允许我们有多个 root ,也就是片段,但是在一些操作值得我们注意。

inheritAttrs=true[默认] 时,组件会自动在 root 继承合并 class ,如:

子组件

<template>
  <div class="fragment">
    <div>div1</div>
    <div>div2</div>
  </div>
</template>

父组件调用,新增了一个 class

<MyFragment class="extend-class" />

子组件会被渲染成

<div class="fragment extend-class">
  <div> div1 </div>
  <div> div2 </div>
</div>

如果我们使用了 片段 ,就需要显式的去指定绑定 attrs ,如子组件:

<template>
  <div v-bind="$attrs">div1</div>
  <div>div2</div>
</template>

emits

在 Vue2 我们会对 props 里的数据进行规定类型,默认值,非空等一些验证,可以理解 emits 做了类似的事情,把 emit 规范起来,如:

// 也可以直接用数组,不做验证
// emits: ['on-update', 'on-other'],
emits: {
  // 赋值 null 不验证
  'on-other': null,
  // 验证
  'on-update'(val: number) {
    if (val === 1) {
      return true;
    }
    // 自定义报错
    console.error('val must be 1');
    return false;
  }
},
setup(props, ctx) {
  const handleEmitUpdate = () => {
    // 验证 val 不为 1,控制台报错
    ctx.emit('on-update', 2);
  };
  const handleEmitOther = () => {
    ctx.emit('on-other');
  };
  return { handleEmitUpdate, handleEmitOther };
}

在 setup 中,emit 已经不再用 this.$emit 了,而是 setup 的第二个参数 context 上下文来获取 emit 。

v-model

个人还是挺喜欢 v-model 的更新的,可以提升封装组件的体验感~

在Vue2,假设我需要封装一个弹框组件 Modal,用 show 变量来控制弹框的显示隐藏,这肯定是一个父子组件都要维护的值。因为单向数据流,所以需要在 Modal 组件 emit 一个事件,父组件监听事件接收并修改这个 show 值。
为了方便我们会有一些语法糖,如 v-model,但是在 Vue2 一个组件上只能有一个 v-model ,因为语法糖的背后是 value@input 的组成, 如果还有多个类似这样的 “双向修改数据”,我们就需要用语法糖 .sync 同步修饰符。

Vue3 把这两个语法糖统一了,所以我们现在可以在一个组件上使用 多个 v-model 语法糖,举个例子:

先从父组件看

<VModel v-model="show"
        v-model:model1="check"
        v-model:model2.hello="textVal" />

hello为自定义修饰符

我们在一个组件上用了 3 个 v-model 语法糖,分别是

v-model 语法糖 对应的 prop 对应的 event 自定义修饰符对应的 prop
v-model(default) modelValue update:modelValue
v-model:model1 model1 update:model1
v-model:model2 model2 update:model2 model2Modifiers

这样子我们就更清晰的在子组件我们要进行一些什么封装了,如:

VModel.vue

// ...
props: {
  modelValue: { type: Boolean, default: false },
  model1: { type: Boolean, default: false },
  model2: { type: String, default: '' },
  model2Modifiers: {
    type: Object,
    default: () => ({})
  }
},
emits: ['update:modelValue', 'update:model1', 'update:model2'],
// ...

key attribute

<template>
  <input type="text"
         placeholder="请输入账号"
         v-if="show" />
  <input type="text"
         placeholder="请输入邮箱"
         v-else />
  <button @click="show=!show">Toggle</button>
</template>

类似这样的 v-if/v-else,在 Vue2 中,会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染,所以当我们在第一个 input 中输入,然后切换第二个
input 。第一个 input 的值将会被保留复用。

有些场景下我们不要复用它们,需要添加一个唯一的 key ,如:

<template>
  <input type="text"
         placeholder="请输入账号"
         v-if="show"
         key="account" />
  <input type="text"
         placeholder="请输入邮箱"
         v-else
         key="email" />
  <button @click="show=!show">Toggle</button>
</template>

但是在 Vue3 我们不用显式的去添加 key ,这两个 input 元素也是完全独立的,因为 Vue3 会对 v-if/v-else 自动生成唯一的 key。

全局 API

在 Vue2 我们对于一些全局的配置可能是这样子的,例如我们使用了一个插件

Vue.use({
  /* ... */
});
const app1 = new Vue({ el: '#app-1' });
const app2 = new Vue({ el: '#app-2' });

但是这样子这会影响两个根实例,也就是说,会变得不可控。

在 Vue3 引入一个新的 API createApp 方法,返回一个实例:

import { createApp } from 'vue';
const app = createApp({ /* ... */ });

然后我们就可以在这个实例上挂载全局相关方法,并只对当前实例生效,如:

app
  .component(/* ... */)
  .directive(/* ... */ )
  .mixin(/* ... */ )
  .use(/* ... */ )
  .mount('#app');

需要注意的是,在 Vue2 我们用了 Vue.prototype.$http=()=>{} 这样的写法,来对 “根Vue” 的 prototype 进行挂载方法,使得我们在子组件,可以通过原型链的方式找到 $http 方法,即 this.$http

而在 Vue3 我们类似这样的挂载需要用一个新的属性 globalProperties

app.config.globalProperties.$http = () => {}

在 setup 内部使用 $http

setup() {
  const {
    ctx: { $http }
  } = getCurrentInstance();
}

2. 底层优化

Proxy 代理

Vue2 响应式的基本原理,就是通过 Object.defineProperty,但这个方式存在缺陷。使得 Vue 不得不通过一些手段来 hack,如:

  • Vue.$set() 动态添加新的响应式属性
  • 无法监听数组变化,Vue 底层需要对数组的一些操作方法,进行再封装。如 pushpop 等方法。

而在 Vue3 中优先使用了 Proxy 来处理,它代理的是整个对象而不是对象的属性,可对于整个对象进行操作。不仅提升了性能,也没有上面所说的缺陷。

简单举两个例子:

  1. 动态添加响应式属性
const targetObj = { id: '1', name: 'zhagnsan' };
const proxyObj = new Proxy(targetObj, {
  get: function (target, propKey, receiver) {
    console.log(`getting key:${propKey}`);
    return Reflect.get(...arguments);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting key:${propKey},value:${value}`);
    return Reflect.set(...arguments);
  }
});
proxyObj.age = 18;
// setting key:age,value:18

如上,用 Proxy 我们对 proxyObj 对象动态添加的属性也会被拦截到。

Reflect 对象是ES6 为了操作对象而提供的新 API。它有几个内置的方法,就如上面的 get / set,这里可以理解成我们用 Reflect 更加方便,否则我们需要如:

get: function (target, propKey, receiver) {
  console.log(`getting ${propKey}!`);
  return target[propKey];
},
  1. 对数组的操作进行拦截
const targetArr = [1, 2];
const proxyArr = new Proxy(targetArr, {
  set: function (target, propKey, value, receiver) {
    console.log(`setting key:${propKey},value:${value}`);
    return Reflect.set(...arguments);
  }
});
proxyArr.push('3');
// setting key:2,value:3
// setting key:length,value:3

静态提升(hoistStatic) vdom

我们都知道 Vue 有虚拟dom的概念,它能为我们在数据改变时高效的渲染页面。

Vue3 优化了 vdom 的更新性能,简单举个例子

Template

<div class="div">
  <div>content</div>
  <div>{{message}}</div>
</div>

Compiler 后,没有静态提升

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", { class: "div" }, [
    _createVNode("div", null, "content"),
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ]))
}

Compiler 后,有静态提升

const _hoisted_1 = { class: "div" }
const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, "content", -1 /* HOISTED */)

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", _hoisted_1, [
    _hoisted_2,
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ]))
}

静态提升包含「静态节点」和「静态属性」的提升,也就是说,我们把一些静态的不会变的节点用变量缓存起来,提供下次 re-render 直接调用。
如果没有做这个动作,当 render 重新执行时,即使标签是静态的,也会被重新创建,这就会产生性能消耗。

3. 与 TS

3.0 的一个主要设计目标是增强对 TypeScript 的支持。原本我们期望通过 Class API 来达成这个目标,但是经过讨论和原型开发,我们认为 Class 并不是解决这个问题的正确路线,基于 Class 的 API 依然存在类型问题。——尤雨溪

基于函数的 API 天然 与 TS 完美结合。

defineComponent

在 TS 下,我们需要用 Vue 暴露的方法 defineComponent,它单纯为了类型推导而存在的。

props 推导

import { defineComponent } from 'vue';
export default defineComponent({
  props: {
    val1: String,
    val2: { type: String, default: '' },
  },
  setup(props, context) {
    props.val1;
  }
})

当我们在 setup 方法访问 props 时候,我们可以看到被推导后的类型,

  • val1 我们没有设置默认值,所以它为 string | undefined
  • 而 val2 的值有值,所以是 string,如图:

PropType

我们关注一下 props 定义的类型,如果是一个复杂对象,我们就要用 PropType 来进行强转声明,如:

interface IObj {
  id: number;
  name: string;
}

obj: {
  type: Object as PropType<IObj>,
  default: (): IObj => ({ id: 1, name: '张三' })
},

或 联合类型

type: {
  type: String as PropType<'success' | 'error' | 'warning'>,
  default: 'warning'
},

4. build丨更好的 tree-sharking(摇树优化)

tree-sharking 即在构建工具构建后消除程序中无用的代码,来减少包的体积。

基于函数的 API 每一个函数都可以用 import { method1,method2 } from "xxx";,这就对 tree-sharking 非常友好,而且函数名同变量名都可以被压缩,对象去不可以。举个例子,我们封装了一个工具,工具提供了两个方法,用 method1method2 来代替。

我们把它们封装成一个对象,并且暴露出去,如:

// utils
const obj = {
  method1() {},
  method2() {}
};
export default obj;
// 调用
import util from '@/utils';
util.method1();

经过webpack打包压缩之后为:

a={method1:function(){},method2:function(){}};a.method1();

我们不用对象的形式,而用函数的形式来看看:

// utils
export function method1() {}
export function method2() {}
// 调用
import { method1 } from '@/utils';
method1();

经过webpack打包压缩之后为:

function a(){}a();

用这个例子我们就可以了解 Vue3 为什么能更好的 tree-sharking ,因为它用的是基于函数形式的API,如:

import {
  defineComponent,
  reactive,
  ref,
  watchEffect,
  watch,
  onMounted,
  toRefs,
  toRef
} from 'vue';

5. options api 与 composition api 取舍

我们上面的代码都是在 setup 内部实现,但是目前 Vue3 还保留了 Vue2 的 options api 写法,就是可以“并存”,如:

// ...
setup() {
  const val = ref<string>('');
  const fn = () => {};
  return {
    val,
    fn
  };
},
mounted() {
  // 在 mounted 生命周期可以访问到 setup return 出来的对象
  console.log(this.val);
  this.fn();
},
// ...

结合 react ,我们知道 “函数式”,hook 是未来的一个趋势。

所以个人建议还是采用都在 setup 内部写逻辑的方式,因为 Vue3 可以完全提供 Vue2 的全部能力。

总结

个人觉得不管是 React Hook 还是 Vue3 的 VCA,我们都可以看到现在的前端框架趋势,“更函数式”,让逻辑复用更灵活。hook 的模式新增了 React / Vue 的抽象层级,「组件级 + 函数级」,可以让我们处理逻辑时分的更细,更好维护。

Vue3 One Piece,nice !

最后,前端精本精祝您圣诞快乐🎄~ (听说公众号关注「前端精」会更快乐哦~

推荐阅读更多精彩内容

  • 我们一直都有关注和阅读很多关于Vue3的新特性和功能即将到来。但是我们没有一个具体的概念在开发中会有如何的改变和不...
    bayi_lzp阅读 412评论 0 4
  • Vue3.0的优势 性能比Vue2.x快1.2~2倍 按需编译,体积比Vue2.x更小 组合API(类似React...
    强某某阅读 528评论 0 5
  • 我们一直都有关注和阅读很多关于Vue3的新特性和功能即将到来。但是我们没有一个具体的概念在开发中会有如何的改变和不...
    三钻阅读 6,918评论 2 17
  • 因为这个月的月初给自己定了个小目标,学完Vue3的基本使用,并使用Vue3亲手做一个小项目(稍微透露一下,我制作的...
    1kesou阅读 1,329评论 0 3
  • Vue 3 的 Template 支持多个根标签,Vue 2 不支持 Vue 3 有 createApp(),而 ...
    sweetBoy_9126阅读 11,438评论 0 12