Vue3.0的设计目标是什么?做了哪些优化

image.png

一、设计目标

不以解决实际业务痛点的更新都是耍流氓,下面我们来列举一下Vue3之前我们或许会面临的问题

  • 随着功能的增长,复杂组件的代码变得越来越难以维护
  • 缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机制
  • 类型推断不够友好
  • bundle的时间太久了

而 Vue3 经过长达两三年时间的筹备,做了哪些事情?
我们从结果反推

  • 更小
  • 更快
  • TypeScript支持
  • API设计一致性
  • 提高自身可维护性
  • 开放更多底层功能

一句话概述,就是更小更快更友好了

更小

Vue3移除一些不常用的 API
引入tree-shaking,可以将无用模块“剪辑”,仅打包需要的,使打包的整体体积变小了

更快

主要体现在编译方面:

  • diff算法优化
  • 静态提升
  • 事件监听缓存
  • SSR优化

下篇文章我们会进一步介绍

更友好

vue3在兼顾vue2的options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力
这里代码简单演示下:
存在一个获取鼠标位置的函数

  1. import { toRefs, reactive } from 'vue';
  2. function useMouse(){
  3. const state = reactive({x:0,y:0});
  4. const update = e=>{
  5. state.x = e.pageX;
  6. state.y = e.pageY;
  7. }
  8. onMounted(()=>{
  9. window.addEventListener('mousemove',update);
  10. })
  11. onUnmounted(()=>{
  12. window.removeEventListener('mousemove',update);
  13. })
  14. return toRefs(state);
  15. }

我们只需要调用这个函数,即可获取x、y的坐标,完全不用关注实现过程
试想一下,如果很多类似的第三方库,我们只需要调用即可,不必关注实现过程,开发效率大大提高
同时,VUE3是基于typescipt编写的,可以享受到自动的类型定义提示

二、优化方案

vue3从很多层面都做了优化,可以分成三个方面:

  • 源码
  • 性能
  • 语法 API

    源码

    源码可以从两个层面展开:

  • 源码管理

  • TypeScript

    源码管理

    vue3整个源码是通过 monorepo的方式维护的,根据功能将不同的模块拆分到packages目录下面不同的子目录中
    image.png
    这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性
    另外一些 package(比如 reactivity 响应式库)是可以独立于 Vue 使用的,这样用户如果只想使用 Vue3的响应式能力,可以单独依赖这个响应式库而不用去依赖整个 Vue

    TypeScript

    Vue3是基于typeScript编写的,提供了更好的类型检查,能支持复杂的类型推导

    性能

    vue3是从什么哪些方面对性能进行进一步优化呢?

  • 体积优化

  • 编译优化
  • 数据劫持优化

这里讲述数据劫持:
在vue2中,数据劫持是通过Object.defineProperty,这个 API 有一些缺陷,并不能检测对象属性的添加和删除

  1. Object.defineProperty(data, 'a',{
  2. get(){
  3. // track
  4. },
  5. set(){
  6. // trigger
  7. }
  8. })

尽管Vue为了解决这个问题提供了 set和delete实例方法,但是对于用户来说,还是增加了一定的心智负担
同时在面对嵌套层级比较深的情况下,就存在性能问题

  1. default {
  2. data: {
  3. a: {
  4. b: {
  5. c: {
  6. d: 1
  7. }
  8. }
  9. }
  10. }
  11. }

相比之下,vue3是通过proxy监听整个对象,那么对于删除还是监听当然也能监听到
同时Proxy 并不能监听到内部深层次的对象变化,而 Vue3 的处理方式是在getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归

语法 API

这里当然说的就是composition API,其两大显著的优化:

  • 优化逻辑组织
  • 优化逻辑复用

    逻辑组织

    一张图,我们可以很直观地感受到 Composition API在逻辑组织方面的优势
    image.png
    相同功能的代码编写在一块,而不像options API那样,各个功能的代码混成一块

    逻辑复用

    在vue2中,我们是通过mixin实现功能混合,如果多个mixin混合,会存在两个非常明显的问题:命名冲突和数据来源不清晰
    而通过composition这种形式,可以将一些复用的代码抽离出来作为一个函数,只要的使用的地方直接进行调用即可
    同样是上文的获取鼠标位置的例子

    1. import { toRefs, reactive, onUnmounted, onMounted } from 'vue';
    2. function useMouse(){
    3. const state = reactive({x:0,y:0});
    4. const update = e=>{
    5. state.x = e.pageX;
    6. state.y = e.pageY;
    7. }
    8. onMounted(()=>{
    9. window.addEventListener('mousemove',update);
    10. })
    11. onUnmounted(()=>{
    12. window.removeEventListener('mousemove',update);
    13. })
    14. return toRefs(state);
    15. }

    组件使用

    1. import useMousePosition from './mouse'
    2. export default {
    3. setup() {
    4. const { x, y } = useMousePosition()
    5. return { x, y }
    6. }
    7. }

    可以看到,整个数据来源清晰了,即使去编写更多的hook函数,也不会出现命名冲突的问题

    参考文献

  • https://juejin.cn/post/6850418112878575629#heading-5

  • https://vue3js.cn/docs/zh

Vue3.0性能提升主要是通过哪几方面体现的?

image.png

一、编译阶段

回顾Vue2,我们知道每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把用到的数据property记录为依赖,当依赖发生改变,触发setter,则会通知watcher,从而使关联的组件重新渲染
image.png
试想一下,一个组件结构如下图

  1. <template>
  2. <div id="content">
  3. <p class="text">静态文本</p>
  4. <p class="text">静态文本</p>
  5. <p class="text">{{ message }}</p>
  6. <p class="text">静态文本</p>
  7. ...
  8. <p class="text">静态文本</p>
  9. </div>
  10. </template>

可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff 和遍历其实都是不需要的,造成性能浪费
因此,Vue3在编译阶段,做了进一步优化。主要有如下:

  • diff算法优化
  • 静态提升
  • 事件监听缓存
  • SSR优化

    diff算法优化

    vue3在diff算法中相比vue2增加了静态标记
    关于这个静态标记,其作用是为了会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较
    下图这里,已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高
    image.png
    关于静态类型枚举如下
    1. export const enum PatchFlags {
    2. TEXT = 1,// 动态的文本节点
    3. CLASS = 1 << 1, // 2 动态的 class
    4. STYLE = 1 << 2, // 4 动态的 style
    5. PROPS = 1 << 3, // 8 动态属性,不包括类名和样式
    6. FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
    7. HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点
    8. STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 Fragment
    9. KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
    10. UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
    11. NEED_PATCH = 1 << 9, // 512
    12. DYNAMIC_SLOTS = 1 << 10, // 动态 solt
    13. HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff
    14. BAIL = -2 // 一个特殊的标志,指代差异算法
    15. }

    静态提升

    Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用
    这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用 ```javascript 你好
{{ message }}
没有做静态提升之前javascript export function render(ctx, cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock(_Fragment, null, [ _createVNode(“span”, null, “你好”), _createVNode(“div”, null, _toDisplayString(_ctx.message), 1 / TEXT /) ], 64 / STABLE_FRAGMENT /)) } 做了静态提升之后javascript const _hoisted_1 = /*#__PURE/_createVNode(“span”, null, “你好”, -1 / HOISTED /) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock(_Fragment, null, [ _hoisted_1, _createVNode(“div”, null, _toDisplayString(_ctx.message), 1 / TEXT /) ], 64 / STABLE_FRAGMENT */)) } // Check the console for the AST 静态内容_hoisted_1被放置在render 函数外,每次渲染的时候只要取 _hoisted_1 即可<br />同时 _hoisted_1 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff <a name="fW3wR"></a> ### 事件监听缓存 默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化html
  1. 没开启事件监听器缓存
  2. ```javascript
  3. export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
  4. return (_openBlock(), _createBlock("div", null, [
  5. _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
  6. // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
  7. ]))
  8. })

开启事件侦听器缓存后

  1. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  2. return (_openBlock(), _createBlock("div", null, [
  3. _createVNode("button", {
  4. onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
  5. }, "点我")
  6. ]))
  7. }

上述发现开启了缓存后,没有了静态标记。也就是说下次diff算法的时候直接使用

SSR优化

当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染

  1. <div>
  2. <div>
  3. <span>你好</span>
  4. </div>
  5. ... // 很多个静态属性
  6. <div>
  7. <span>{{ message }}</span>
  8. </div>
  9. </div>

编译后

  1. import { mergeProps as _mergeProps } from "vue"
  2. import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"
  3. export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  4. const _cssVars = { style: { color: _ctx.color }}
  5. _push(`<div${
  6. _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
  7. }><div><span>你好</span>...<div><span>你好</span><div><span>${
  8. _ssrInterpolate(_ctx.message)
  9. }</span></div></div>`)
  10. }

二、源码体积

相比Vue2,Vue3整体体积变小了,除了移出一些不常用的API,重要的是Tree shanking(Tree-shaking原始的本意通过工具”摇”我们的JS文件,将其中用不到的代码”摇”掉,是一个性能优化的范畴)
任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小

三、响应式系统

vue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter和setter,实现响应式
vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历

Vue3.0里为什么要用 Proxy API 替代 defineProperty API

image.png

一、Object.defineProperty

定义:Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

为什么能实现响应式

通过defineProperty 两个属性,get及set

  • get

属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值

  • set

属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined
下面通过代码展示:
定义一个响应式函数defineReactive

  1. function update() {
  2. app.innerText = obj.foo
  3. }
  4. function defineReactive(obj, key, val) {
  5. Object.defineProperty(obj, key, {
  6. get() {
  7. console.log(`get ${key}:${val}`);
  8. return val
  9. },
  10. set(newVal) {
  11. if (newVal !== val) {
  12. val = newVal
  13. update()
  14. }
  15. }
  16. })
  17. }

调用defineReactive,数据发生变化触发update方法,实现数据响应式

  1. const obj = {}
  2. defineReactive(obj, 'foo', '')
  3. setTimeout(()=>{
  4. obj.foo = new Date().toLocaleTimeString()
  5. },1000)

在对象存在多个key情况下,需要进行遍历

  1. function observe(obj) {
  2. if (typeof obj !== 'object' || obj == null) {
  3. return
  4. }
  5. Object.keys(obj).forEach(key => {
  6. defineReactive(obj, key, obj[key])
  7. })
  8. }

如果存在嵌套对象的情况,还需要在defineReactive中进行递归

  1. function defineReactive(obj, key, val) {
  2. observe(val)
  3. Object.defineProperty(obj, key, {
  4. get() {
  5. console.log(`get ${key}:${val}`);
  6. return val
  7. },
  8. set(newVal) {
  9. if (newVal !== val) {
  10. val = newVal
  11. update()
  12. }
  13. }
  14. })
  15. }

当给key赋值为对象的时候,还需要在set属性中进行递归

  1. set(newVal) {
  2. if (newVal !== val) {
  3. observe(newVal) // 新值是对象的情况
  4. notifyUpdate()
  5. }
  6. }

上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题
现在对一个对象进行删除与添加属性操作,无法劫持到

  1. const obj = {
  2. foo: "foo",
  3. bar: "bar"
  4. }
  5. observe(obj)
  6. delete obj.foo // no ok
  7. obj.jar = 'xxx' // no ok

当我们对一个数组进行监听的时候,并不那么好使了

  1. const arrData = [1,2,3,4,5];
  2. arrData.forEach((val,index)=>{
  3. defineProperty(arrData,index,val)
  4. })
  5. arrData.push() // no ok
  6. arrData.pop() // no ok
  7. arrDate[0] = 99 // ok

可以看到数据的api无法劫持到,从而无法实现数据响应式,
所以在Vue2中,增加了set、delete API,并且对数组api方法进行一个重写
还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题

小结

  • 检测不到对象属性的添加和删除
  • 数组API方法无法监听到
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

    二、proxy

    Proxy的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了
    在ES6系列中,我们详细讲解过Proxy的使用,就不再述说了
    下面通过代码进行展示:
    定义一个响应式方法reactive
    1. function reactive(obj) {
    2. if (typeof obj !== 'object' && obj != null) {
    3. return obj
    4. }
    5. // Proxy相当于在对象外层加拦截
    6. const observed = new Proxy(obj, {
    7. get(target, key, receiver) {
    8. const res = Reflect.get(target, key, receiver)
    9. console.log(`获取${key}:${res}`)
    10. return res
    11. },
    12. set(target, key, value, receiver) {
    13. const res = Reflect.set(target, key, value, receiver)
    14. console.log(`设置${key}:${value}`)
    15. return res
    16. },
    17. deleteProperty(target, key) {
    18. const res = Reflect.deleteProperty(target, key)
    19. console.log(`删除${key}:${res}`)
    20. return res
    21. }
    22. })
    23. return observed
    24. }
    测试一下简单数据的操作,发现都能劫持
    1. const state = reactive({
    2. foo: 'foo'
    3. })
    4. // 1.获取
    5. state.foo // ok
    6. // 2.设置已存在属性
    7. state.foo = 'fooooooo' // ok
    8. // 3.设置不存在属性
    9. state.dong = 'dong' // ok
    10. // 4.删除属性
    11. delete state.dong // ok
    再测试嵌套对象情况,这时候发现就不那么 OK 了 ```javascript const state = reactive({ bar: { a: 1 } })

// 设置嵌套对象属性 state.bar.a = 10 // no ok

  1. 如果要解决,需要在get之上再进行一层代理
  2. ```javascript
  3. function reactive(obj) {
  4. if (typeof obj !== 'object' && obj != null) {
  5. return obj
  6. }
  7. // Proxy相当于在对象外层加拦截
  8. const observed = new Proxy(obj, {
  9. get(target, key, receiver) {
  10. const res = Reflect.get(target, key, receiver)
  11. console.log(`获取${key}:${res}`)
  12. return isObject(res) ? reactive(res) : res
  13. },
  14. return observed
  15. }

三、总结

Object.defineProperty只能遍历对象属性进行劫持

  1. function observe(obj) {
  2. if (typeof obj !== 'object' || obj == null) {
  3. return
  4. }
  5. Object.keys(obj).forEach(key => {
  6. defineReactive(obj, key, obj[key])
  7. })
  8. }

Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的

Proxy可以直接监听数组的变化(push、shift、splice)

  1. const obj = [1,2,3]
  2. const proxtObj = reactive(obj)
  3. obj.psuh(4) // ok

Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等,这是Object.defineProperty不具备的
正因为defineProperty自身的缺陷,导致Vue2在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外set、delete方法)

  1. / 数组重写
  2. const originalProto = Array.prototype
  3. const arrayProto = Object.create(originalProto)
  4. ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
  5. arrayProto[method] = function () {
  6. originalProto[method].apply(this.arguments)
  7. dep.notice()
  8. }
  9. });
  10. // set、delete
  11. Vue.set(obj,'bar','newbar')
  12. Vue.delete(obj),'bar')

Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9

参考文献

Vue3.0 所采用的 Composition Api 与 Vue2.x 使用的 Options Api 有什么不同

image.png

开始之前

Composition API 可以说是Vue3的最大特点,那么为什么要推出Composition Api,解决了什么问题?
通常使用Vue2开发的项目,普遍会存在以下问题:

  • 代码的可读性随着组件变大而变差
  • 每一种代码复用的方式,都存在缺点
  • TypeScript支持有限

以上通过使用Composition Api都能迎刃而解

正文

一、Options Api

Options API,即大家常说的选项API,即以vue为后缀的文件,通过定义methods,computed,watch,data等属性与方法,共同处理页面逻辑
如下图:
image.png
可以看到Options代码编写方式,如果是组件状态,则写在data属性上,如果是方法,则写在methods属性上…
用组件的选项 (data、computed、methods、watch) 组织逻辑在大多数情况下都有效
然而,当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解

二、Composition Api

在 Vue3 Composition API 中,组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(更加的高内聚,低耦合)
即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所有 API
image.png

三、对比

下面对Composition Api与Options Api进行两大方面的比较

  • 逻辑组织
  • 逻辑复用

    逻辑组织

    Options API

    假设一个组件是一个大型组件,其内部有很多处理逻辑关注点(对应下图不用颜色)
    image.png
    可以看到,这种碎片化使得理解和维护复杂组件变得困难
    选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块

    Compostion API

    而Compositon API正是解决上述问题,将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去
    下面举个简单例子,将处理count属性相关的代码放在同一个函数了

    1. function useCount() {
    2. let count = ref(10);
    3. let double = computed(() => {
    4. return count.value * 2;
    5. });
    6. const handleConut = () => {
    7. count.value = count.value * 2;
    8. };
    9. console.log(count);
    10. return {
    11. count,
    12. double,
    13. handleConut,
    14. };
    15. }

    组件上中使用count

    1. export default defineComponent({
    2. setup() {
    3. const { count, double, handleConut } = useCount();
    4. return {
    5. count,
    6. double,
    7. handleConut
    8. }
    9. },
    10. });

    再来一张图进行对比,可以很直观地感受到 Composition API在逻辑组织方面的优势,以后修改一个属性功能的时候,只需要跳到控制该属性的方法中即可
    image.png

    逻辑复用

    在Vue2中,我们是用过mixin去复用相同的逻辑
    下面举个例子,我们会另起一个mixin.js文件

    1. export const MoveMixin = {
    2. data() {
    3. return {
    4. x: 0,
    5. y: 0,
    6. };
    7. },
    8. methods: {
    9. handleKeyup(e) {
    10. console.log(e.code);
    11. // 上下左右 x y
    12. switch (e.code) {
    13. case "ArrowUp":
    14. this.y--;
    15. break;
    16. case "ArrowDown":
    17. this.y++;
    18. break;
    19. case "ArrowLeft":
    20. this.x--;
    21. break;
    22. case "ArrowRight":
    23. this.x++;
    24. break;
    25. }
    26. },
    27. },
    28. mounted() {
    29. window.addEventListener("keyup", this.handleKeyup);
    30. },
    31. unmounted() {
    32. window.removeEventListener("keyup", this.handleKeyup);
    33. },
    34. };

    然后在组件中使用

    1. <template>
    2. <div>
    3. Mouse position: x {{ x }} / y {{ y }}
    4. </div>
    5. </template>
    6. <script>
    7. import mousePositionMixin from './mouse'
    8. export default {
    9. mixins: [mousePositionMixin]
    10. }
    11. </script>

使用单个mixin似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时候

  1. mixins: [mousePositionMixin, fooMixin, barMixin, otherMixin]

会存在两个非常明显的问题:

  • 命名冲突
  • 数据来源不清晰

现在通过Compositon API这种方式改写上面的代码

  1. import { onMounted, onUnmounted, reactive } from "vue";
  2. export function useMove() {
  3. const position = reactive({
  4. x: 0,
  5. y: 0,
  6. });
  7. const handleKeyup = (e) => {
  8. console.log(e.code);
  9. // 上下左右 x y
  10. switch (e.code) {
  11. case "ArrowUp":
  12. // y.value--;
  13. position.y--;
  14. break;
  15. case "ArrowDown":
  16. // y.value++;
  17. position.y++;
  18. break;
  19. case "ArrowLeft":
  20. // x.value--;
  21. position.x--;
  22. break;
  23. case "ArrowRight":
  24. // x.value++;
  25. position.x++;
  26. break;
  27. }
  28. };
  29. onMounted(() => {
  30. window.addEventListener("keyup", handleKeyup);
  31. });
  32. onUnmounted(() => {
  33. window.removeEventListener("keyup", handleKeyup);
  34. });
  35. return { position };
  36. }

在组件中使用

  1. <template>
  2. <div>
  3. Mouse position: x {{ x }} / y {{ y }}
  4. </div>
  5. </template>
  6. <script>
  7. import { useMove } from "./useMove";
  8. import { toRefs } from "vue";
  9. export default {
  10. setup() {
  11. const { position } = useMove();
  12. const { x, y } = toRefs(position);
  13. return {
  14. x,
  15. y,
  16. };
  17. },
  18. };
  19. </script>

可以看到,整个数据来源清晰了,即使去编写更多的 hook 函数,也不会出现命名冲突的问题

小结

  • 在逻辑组织和逻辑复用方面,Composition API是优于Options API
  • 因为Composition API几乎是函数,会有更好的类型推断。
  • Composition API对 tree-shaking 友好,代码也更容易压缩
  • Composition API中见不到this的使用,减少了this指向不明的情况
  • 如果是小型组件,可以继续使用Options API,也是十分友好的

说说Vue 3.0中Treeshaking特性?举例说明一下?

image.png

一、是什么

Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination
简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码
如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去
而treeshaking则是一开始就把有用的蛋白蛋黄(import)放入搅拌,最后直接作出蛋糕
也就是说 ,tree shaking 其实是找出使用的代码
在Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到

  1. import Vue from 'vue'
  2. Vue.nextTick(() => {})

而Vue3源码引入tree shaking特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中

  1. import { nextTick, observable } from 'vue'
  2. nextTick(() => {})

1

二、如何做

Tree shaking是基于ES6模板语法(import与exports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量
Tree shaking无非就是做了两件事:

  • 编译阶段利用ES6 Module判断哪些模块已经加载
  • 判断那些模块和变量未被使用或者引用,进而删除对应代码

下面就来举个例子:
通过脚手架vue-cli安装Vue2与Vue3项目
vue create vue-demo
1

Vue2 项目

组件中使用data属性

  1. <script>
  2. export default {
  3. data: () => ({
  4. count: 1,
  5. }),
  6. };
  7. </script>

对项目进行打包,体积如下图
image.png
为组件设置其他属性(compted、watch)

  1. import { reactive, defineComponent, computed, watch } from "vue";
  2. export default defineComponent({
  3. setup() {
  4. const state = reactive({
  5. count: 1,
  6. });
  7. const double = computed(() => {
  8. return state.count * 2;
  9. });
  10. watch(
  11. () => state.count,
  12. (count, preCount) => {
  13. console.log(count);
  14. console.log(preCount);
  15. }
  16. );
  17. return {
  18. state,
  19. double,
  20. };
  21. },
  22. });

再一次打包,发现打包出来的体积并没有变化
image.png

Vue3 项目

组件中简单使用

  1. import { reactive, defineComponent } from "vue";
  2. export default defineComponent({
  3. setup() {
  4. const state = reactive({
  5. count: 1,
  6. });
  7. return {
  8. state,
  9. };
  10. },
  11. });

将项目进行打包
image.png
在组件中引入computed和watch

  1. import { reactive, defineComponent, computed, watch } from "vue";
  2. export default defineComponent({
  3. setup() {
  4. const state = reactive({
  5. count: 1,
  6. });
  7. const double = computed(() => {
  8. return state.count * 2;
  9. });
  10. watch(
  11. () => state.count,
  12. (count, preCount) => {
  13. console.log(count);
  14. console.log(preCount);
  15. }
  16. );
  17. return {
  18. state,
  19. double,
  20. };
  21. },
  22. });

再次对项目进行打包,可以看到在引入computer和watch之后,项目整体体积变大了
image.png

三、作用

通过Tree shaking,Vue3给我们带来的好处是:

用Vue3.0 写过组件吗?如果想实现一个 Modal你会怎么设计?

image.png

一、组件设计

组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式
现在有一个场景,点击新增与编辑都弹框出来进行填写,功能上大同小异,可能只是标题内容或者是显示的主体内容稍微不同
这时候就没必要写两个组件,只需要根据传入的参数不同,组件显示不同内容即可
这样,下次开发相同界面程序时就可以写更少的代码,意义着更高的开发效率,更少的 Bug和更少的程序体积

二、需求分析

实现一个Modal组件,首先确定需要完成的内容:

  • 遮罩层
  • 标题内容
  • 主体内容
  • 确定和取消按钮

主体内容需要灵活,所以可以是字符串,也可以是一段 html 代码
特点是它们在当前vue实例之外独立存在,通常挂载于body之上
除了通过引入import的形式,我们还可通过API的形式进行组件的调用
还可以包括配置全局样式、国际化、与typeScript结合

三、实现流程

首先看看大致流程:

  • 目录结构
  • 组件内容
  • 实现 API 形式
  • 事件处理
  • 其他完善

    目录结构

    Modal组件相关的目录结构
    1. ├── plugins
    2. └── modal
    3. ├── Content.tsx // 维护 Modal 的内容,用于 h 函数和 jsx 语法
    4. ├── Modal.vue // 基础组件
    5. ├── config.ts // 全局默认配置
    6. ├── index.ts // 入口
    7. ├── locale // 国际化相关
    8. ├── index.ts
    9. └── lang
    10. ├── en-US.ts
    11. ├── zh-CN.ts
    12. └── zh-TW.ts
    13. └── modal.type.ts // ts类型声明相关

因为 Modal 会被 app.use(Modal) 调用作为一个插件,所以都放在plugins目录下

组件内容

首先实现modal.vue的主体显示内容大致如下

  1. <Teleport to="body" :disabled="!isTeleport">
  2. <div v-if="modelValue" class="modal">
  3. <div
  4. class="mask"
  5. :style="style"
  6. @click="maskClose && !loading && handleCancel()"
  7. ></div>
  8. <div class="modal__main">
  9. <div class="modal__title line line--b">
  10. <span>{{ title || t("r.title") }}</span>
  11. <span
  12. v-if="close"
  13. :title="t('r.close')"
  14. class="close"
  15. @click="!loading && handleCancel()"
  16. ></span
  17. >
  18. </div>
  19. <div class="modal__content">
  20. <Content v-if="typeof content === 'function'" :render="content" />
  21. <slot v-else>
  22. {{ content }}
  23. </slot>
  24. </div>
  25. <div class="modal__btns line line--t">
  26. <button :disabled="loading" @click="handleConfirm">
  27. <span class="loading" v-if="loading"></span>{{ t("r.confirm") }}
  28. </button>
  29. <button @click="!loading && handleCancel()">
  30. {{ t("r.cancel") }}
  31. </button>
  32. </div>
  33. </div>
  34. </div>
  35. </Teleport>

最外层上通过Vue3 Teleport 内置组件进行包裹,其相当于传送门,将里面的内容传送至body之上
并且从DOM结构上来看,把modal该有的内容(遮罩层、标题、内容、底部按钮)都实现了
关于主体内容

  1. <div class="modal__content">
  2. <Content v-if="typeof content==='function'"
  3. :render="content" />
  4. <slot v-else>
  5. {{content}}
  6. </slot>
  7. </div>

可以看到根据传入content的类型不同,对应显示不同得到内容
最常见的则是通过调用字符串和默认插槽的形式

  1. // 默认插槽
  2. <Modal v-model="show"
  3. title="演示 slot">
  4. <div>hello world~</div>
  5. </Modal>
  6. // 字符串
  7. <Modal v-model="show"
  8. title="演示 content"
  9. content="hello world~" />

通过 API 形式调用Modal组件的时候,content可以使用下面两种

  • h 函数

    1. $modal.show({
    2. title: '演示 h 函数',
    3. content(h) {
    4. return h(
    5. 'div',
    6. {
    7. style: 'color:red;',
    8. onClick: ($event: Event) => console.log('clicked', $event.target)
    9. },
    10. 'hello world ~'
    11. );
    12. }
    13. });
  • JSX

    1. $modal.show({
    2. title: '演示 jsx 语法',
    3. content() {
    4. return (
    5. <div
    6. onClick={($event: Event) => console.log('clicked', $event.target)}
    7. >
    8. hello world ~
    9. </div>
    10. );
    11. }
    12. });

    实现 API 形式

    那么组件如何实现API形式调用Modal组件呢?
    在Vue2中,我们可以借助Vue实例以及Vue.extend的方式获得组件实例,然后挂载到body上

    1. import Modal from './Modal.vue';
    2. const ComponentClass = Vue.extend(Modal);
    3. const instance = new ComponentClass({ el: document.createElement("div") });
    4. document.body.appendChild(instance.$el);

    虽然Vue3移除了Vue.extend方法,但可以通过createVNode实现

    1. import Modal from './Modal.vue';
    2. const container = document.createElement('div');
    3. const vnode = createVNode(Modal);
    4. render(vnode, container);
    5. const instance = vnode.component;
    6. document.body.appendChild(container);

    在Vue2中,可以通过this的形式调用全局 API

    1. export default {
    2. install(vue) {
    3. vue.prototype.$create = create
    4. }
    5. }

    而在 Vue3 的 setup 中已经没有 this概念了,需要调用app.config.globalProperties挂载到全局

    1. export default {
    2. install(app) {
    3. app.config.globalProperties.$create = create
    4. }
    5. }

    事件处理

    下面再看看Modal组件内部是如何处理「确定」「取消」事件的,既然是Vue3,当然采用Compositon API 形式

    1. // Modal.vue
    2. setup(props, ctx) {
    3. let instance = getCurrentInstance(); // 获得当前组件实例
    4. onBeforeMount(() => {
    5. instance._hub = {
    6. 'on-cancel': () => {},
    7. 'on-confirm': () => {}
    8. };
    9. });
    10. const handleConfirm = () => {
    11. ctx.emit('on-confirm');
    12. instance._hub['on-confirm']();
    13. };
    14. const handleCancel = () => {
    15. ctx.emit('on-cancel');
    16. ctx.emit('update:modelValue', false);
    17. instance._hub['on-cancel']();
    18. };
    19. return {
    20. handleConfirm,
    21. handleCancel
    22. };
    23. }

    在上面代码中,可以看得到除了使用传统emit的形式使父组件监听,还可通过_hub属性中添加 on-cancel,on-confirm方法实现在API中进行监听

    1. app.config.globalProperties.$modal = {
    2. show({}) {
    3. /* 监听 确定、取消 事件 */
    4. }
    5. }

    下面再来目睹下_hub是如何实现

    1. // index.ts
    2. app.config.globalProperties.$modal = {
    3. show({
    4. /* 其他选项 */
    5. onConfirm,
    6. onCancel
    7. }) {
    8. /* ... */
    9. const { props, _hub } = instance;
    10. const _closeModal = () => {
    11. props.modelValue = false;
    12. container.parentNode!.removeChild(container);
    13. };
    14. // 往 _hub 新增事件的具体实现
    15. Object.assign(_hub, {
    16. async 'on-confirm'() {
    17. if (onConfirm) {
    18. const fn = onConfirm();
    19. // 当方法返回为 Promise
    20. if (fn && fn.then) {
    21. try {
    22. props.loading = true;
    23. await fn;
    24. props.loading = false;
    25. _closeModal();
    26. } catch (err) {
    27. // 发生错误时,不关闭弹框
    28. console.error(err);
    29. props.loading = false;
    30. }
    31. } else {
    32. _closeModal();
    33. }
    34. } else {
    35. _closeModal();
    36. }
    37. },
    38. 'on-cancel'() {
    39. onCancel && onCancel();
    40. _closeModal();
    41. }
    42. });
    43. }
    44. };

    #其他完善

    关于组件实现国际化、与typsScript结合,大家可以根据自身情况在此基础上进行更改

    #参考文献

  • https://segmentfault.com/a/1190000038928664