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 会放在一块儿。html

Vue 和 React 的逻辑复用手段

到目前为止,前端

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

React:Mixins、HOC、Render Props、Hook。react

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

5个维度来说 Vue3

1. 框架

一个例子先来了解 VCA

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

举个简单的例子:es6

咱们要实现 3 个逻辑web

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

Vue2 options api 的处理

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

// 逻辑功能(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;
    }
  }
};

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

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 !

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

相关文章
相关标签/搜索