4

一些概念

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 !

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


前端精
536 声望20 粉丝