Vue3知识点

简介

  • 2020年9月18日发布Vue3正式版本V3.0.0,命名为One Piece。
  • Vue 的组件可以按两种不同的风格书写:选项式 API 和组合式 API。

该选哪一个?

  • 两种 API 风格都能够覆盖大部分的应用场景。它们只是同一个底层系统所提供的两套不同的接口。实际上,选项式 API 是在组合式 API 的基础上实现的!关于 Vue 的基础概念和知识在它们之间都是通用的。

  • 选项式 API 以“组件实例”的概念为中心 (即上述例子中的 this),对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致。同时,它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好。

  • 组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要你对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。

[图片上传失败...(image-a9c136-1660454836197)]

Vue3带来的变化

1. 性能提升1.3~2.x

  • 核心代码 + Composition API :13.5kb,最小可以到11.75kb
  • 所有的Runtime:22.5kb(Vue2是32kb)
    为什么会有这么大的性能提升呢? 这里就要说到 Compiler 的原理:
  • 静态Node不再做更新处理
  • 静态绑定的class和id不再做更新处理
  • vue在mount的过程中会编译成ast语法树, 会给动态的内容打上一个标记PatchFlag,进行更新分析(动态绑定),会区分哪些是静态内容哪些是动态内容,然后对动态内容去做更新处理
  • 事件监听器Cache缓存处理(cacheHandlers),组件创建的过程中不会去重复的多次实例化,对内存的优化是非常好的,减少创建对象的数量,从而减少内存占用提高性能
  • hoistStatich自动针对多静态节点进行优化,输出字符串
    测试地址:https://vue-next-template-explorer.netlify.app

2. Ts支持,新增:Fragment、Teleport、Suspense

  • Fragment不受根节点限制,渲染函数可接收Array。意思就是我们在temeplate中不再受根节点限制,可以任意的插入多个文本,字符串或者图片
  • Teleport--类似Portal,随用随取比如:弹窗、Actions,比如我们有可能需要在app节点之外比如body中控制一个弹窗的显示或者隐藏,可以用到它
  • Suspense 从框架层面的一个异步组件,可以支持嵌套加载的一个场景,比如树形组件我们要加载多层加的组织架构,我们希望把下面所有的组织架构都加载完成以后再显示整个组件,这个时候就需要用到它。例如:async setup()

3. 按需加载(配合vite)& 组合Api

Vue2和Vue3的比较

1. 为什么要用 Composition API?

(1) Vue2对于复杂逻辑的组件,后期变得无法维护
下面是vue2实现加减的代码:

<template>
  <div class="homePage">
    <p>count: {{ count }}</p>   
    <p>倍数: {{ multiple }}</p>        
    <div>
      <button style="margin-right: 10px" @click="increase">加1</button>
      <button @click="decrease">减一</button>    
    </div>      
  </div>
</template>
<script>
export default {
  data() {
    return { count: 0 };
  },
  computed: {
    multiple() {
      return 2 * this.count;
    },
  },
  methods: {
    increase() {
      this.count++;
    },
    decrease() {
      this.count--;
    },
  },
};
</script>

上面代码只是实现了对 count 的加减以及显示倍数, 就需要分别在data、methods、computed中进行操作,当我们增加一个需求,就会出现下图的情况:

[图片上传失败...(image-7fc1e4-1660454836197)]
当我们业务复杂了就会大量出现上面的情况, 随着复杂度上升,就会出现这样一张图:

[图片上传失败...(image-43cb6f-1660454836197)]
当这个组件的代码超过几百行时,这时增加或者修改某个需求, 就要在data、methods、computed以及mounted中反复的跳转
如果可以按照逻辑进行分割,将上面这张图变成下边这张图,是不是就清晰很多了呢, 这样的代码可读性和可维护性都更高:

[图片上传失败...(image-6d6040-1660454836197)]

那么vue2.x版本给出的解决方案就是Mixin, 但是使用Mixin会有缺陷:

  • 命名空间冲突
  • 不清楚暴露出来的变量的作用
  • 逻辑重用到其他 component 经常遇到问题(不易复用)
    Vue3.x就推出了 Composition API 主要就是为了解决上面的问题,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。

(2)scoped slot作用域插槽(配置项多、代码分裂、性能差)
(3) Vue2对Ts支持不充分
(4)Vue3使复杂组件逻辑进行分离,组件间的逻辑共享
(5)Vue3组合式API + 函数式编程

组合式 API (Composition API)

  • 通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。
    [图片上传失败...(image-fcfc2-1660454836197)]

1. setup

  • setup是 Vue3.x 新增的一个选项, 他是组件内使用 Composition API的入口。
export default defineComponent({
  beforeCreate() {
    console.log("----beforeCreate----");
  },
  created() {
    console.log("----created----");
  },
  setup() {
    console.log("----setup----");
  },
})

[图片上传失败...(image-cf7909-1660454836197)]

  • 通过代码打印结果,setup 执行时机是在 beforeCreate 之前执行
setup 参数
  • 使用setup时,它接受两个参数:
  1. props: 组件传入的属性
  2. context
  • setup 中接受的props是响应式的, 当传入新的 props 时,会及时被更新。由于是响应式的, 所以不可以使用 ES6 解构,解构会消除它的响应式。
  • 错误代码示例, 这段代码会让 props 不再支持响应式:
export default defineComponent ({
    setup(props, context) {
        const { name } = props
        console.log(name)
    },
})
  • 开发中我们想要使用解构,还能保持props的响应式,需要用到后面的toRefs来解决
  • setup中不能访问 Vue2 中最常用的this对象,所以context中就提供了this中最常用的三个属性:attrsslotemit分别对应 Vue2.x 中的 $attr属性、slot插槽 和$emit发射事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。

2. reactive、ref 与 toRefs

  • 在 vue2.x 中, 定义数据都是在data中, 但是 Vue3.x 可以使用reactive和ref来进行数据定义。
import { ref } from 'vue'

const count = ref(0)

reactive函数可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean 等

<template>
  <div class="homePage">
    <p>第 {{ year }} 年</p>
    <p>姓名: {{ nickname }}</p>
    <p>年龄: {{ age }}</p>
  </div>
</template>

<script>
import { defineComponent, reactive, ref } from "vue";
export default defineComponent({
  setup() {
    const year = ref(0);
    const user = reactive({ nickname: "xiaofan", age: 26, gender: "女" });
    setInterval(() => {
      year.value++;
      user.age++;
    }, 1000);
    return {
      year,
      user
    };
  },
});
</script>

上面的代码中,我们绑定到页面是通过user.name, user.age;这样写感觉很繁琐,我们能不能直接将user中的属性解构出来使用呢? 答案是不能直接对user进行结构, 这样会消除它的响应式, 这里就和上面我们说props不能使用 ES6 直接解构就呼应上了。那我们就想使用解构后的数据怎么办,解决办法就是使用toRefs
toRefs 用于将一个 reactive 对象转化为属性全部为 ref 对象的普通对象。具体使用方式如下:

<template>
  <div class="homePage">
    <p>第 {{ year }} 年</p>
    <p>姓名: {{ nickname }}</p>
    <p>年龄: {{ age }}</p>
  </div>
</template>

<script>
import { defineComponent, reactive, ref, toRefs } from "vue";
export default defineComponent({
  setup() {
    const year = ref(0);
    const user = reactive({ nickname: "xiaofan", age: 26, gender: "女" });
    setInterval(() => {
      year.value++;
      user.age++;
    }, 1000);
    return {
      year,
      // 使用reRefs
      ...toRefs(user),
    };
  },
});
</script>

生命周期钩子

[图片上传失败...(image-e57ed6-1660454836197)]

从图中我们可以看到 Vue3.0 新增了setup,这个在前面我们也详细说了, 然后是将 Vue2.x 中的beforeDestroy名称变更成beforeUnmount; destroyed 表更为 unmounted,作者说这么变更纯粹是为了更加语义化,因为一个组件是一个mountunmount的过程。其他 Vue2 中的生命周期仍然保留。
上边 生命周期图 中并没包含全部的生命周期钩子, 还有其他的几个, 全部生命周期钩子如图所示:
[图片上传失败...(image-e1eadd-1660454836197)]

我们可以看到 beforeCreatecreatedsetup 替换了(但是Vue3中你仍然可以使用, 因为Vue3是向下兼容的, 也就是你实际使用的是vue2的)。其次,钩子命名都增加了 on ; Vue3.x还新增用于调试的钩子函数 onRenderTriggeredonRenderTricked

自定义 Hooks

开篇的时候我们使用 Vue2.x 写了一个实现加减的例子, 这里可以将其封装成一个 hook, 我们约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分。 useCount.ts 实现:

import { ref, Ref, computed } from "vue";

type CountResultProps = {
  count: Ref<number>;
  multiple: Ref<number>;
  increase: (delta?: number) => void;
  decrease: (delta?: number) => void;
};

export default function useCount(initValue = 1): CountResultProps {
  const count = ref(initValue);

  const increase = (delta?: number): void => {
    if (typeof delta !== "undefined") {
      count.value += delta;
    } else {
      count.value += 1;
    }
  };
  const multiple = computed(() => count.value * 2);

  const decrease = (delta?: number): void => {
    if (typeof delta !== "undefined") {
      count.value -= delta;
    } else {
      count.value -= 1;
    }
  };

  return {
    count,
    multiple,
    increase,
    decrease,
  };
}

接下来看一下在组件中使用useCount这个 hook:

<template>
  <p>count: {{ count }}</p>
  <p>倍数: {{ multiple }}</p>
  <div>
    <button @click="increase()">加1</button>
    <button @click="decrease()">减一</button>
  </div>
</template>

<script lang="ts">
import useCount from "../hooks/useCount";
 setup() {
    const { count, multiple, increase, decrease } = useCount(10);
        return {
            count,
            multiple,
            increase,
            decrease,
        };
    },
</script>

简单对比 vue2.x 与 vue3.x 响应式

Vue3.x 将使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty
这里就简单对比一下:

  • Object.defineProperty只能劫持对象的属性, 而 Proxy 是直接代理对象

由于Object.defineProperty只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是 Proxy 直接代理对象, 不需要遍历操作

  • Object.defineProperty对新增属性需要手动进行Observe

因为Object.defineProperty劫持的是对象的属性,所以新增属性时,需要重新遍历对象, 对其新增属性再次使用Object.defineProperty进行劫持。也就是 Vue2.x 中给数组和对象新增属性时,需要使用set才能保证新增的属性也是响应式的,set内部也是通过调用Object.defineProperty去处理的

Teleport

Teleport 就像是哆啦 A 梦中的「任意门」,任意门的作用就是可以将人瞬间传送到另一个地方。有了这个认识,我们再来看一下为什么需要用到 Teleport 的特性呢,看一个小例子:

  • 在子组件Header中使用到Dialog组件,我们实际开发中经常会在类似的情形下使用到 Dialog ,此时Dialog就被渲染到一层层子组件内部,处理嵌套组件的定位、z-index和样式都变得困难。
  • Dialog从用户感知的层面,应该是一个独立的组件,从 dom 结构应该完全剥离 Vue 顶层组件挂载的 DOM;同时还可以使用到 Vue 组件内的状态(data或者props)的值。简单来说就是,即希望继续在组件内部使用Dialog, 又希望渲染的 DOM 结构不嵌套在组件的 DOM 中。
  • 此时就需要 Teleport 上场,我们可以用<Teleport>包裹Dialog, 此时就建立了一个传送门,可以将Dialog渲染的内容传送到任何指定的地方。
  • 接下来就举个小例子,看看 Teleport 的使用方式:

我们希望 Dialog 渲染的 dom 和顶层组件是兄弟节点关系, 在index.html文件中定义一个供挂载的元素:

<body>
  <div id="app"></div>
  <div id="dialog"></div>
</body>

定义一个Dialog组件Dialog.vue, 留意 to 属性, 与上面的id选择器一致:

<template>
  <teleport to="#dialog">
    <div class="dialog">
      <div class="dialog_wrapper">
        <div class="dialog_header" v-if="title">
          <slot name="header">
            <span>{{ title }}</span>
          </slot>
        </div>
      </div>
      <div class="dialog_content">
        <slot></slot>
      </div>
      <div class="dialog_footer">
        <slot name="footer"></slot>
      </div>
    </div>
  </teleport>
</template>

最后在一个子组件Header.vue中使用Dialog组件

<div class="header">
    ...
    <navbar />
    <Dialog v-if="dialogVisible"></Dialog>
</div>

[图片上传失败...(image-8ff413-1660454836197)]

可以看到,我们使用 teleport 组件,通过 to 属性,指定该组件渲染的位置与 <div id="app"></div> 同级,也就是在 body 下,但是 Dialog 的状态 dialogVisible 又是完全由内部 Vue 组件控制.

Suspense

Suspense是 Vue3.x 中新增的特性, 那它有什么用呢?我们通过 Vue2.x 中的一些场景来认识它的作用。 Vue2.x 中应该经常遇到这样的场景:

<template>
<div>
    <div v-if="!loading">
        ...
    </div>
    <div v-if="loading">
        加载中...
    </div>
</div>
</template>

在前后端交互获取数据时, 是一个异步过程,一般我们都会提供一个加载中的动画,当数据返回时配合v-if来控制数据显示。
如果你使用过vue-async-manager这个插件来完成上面的需求, 你对Suspense可能不会陌生,Vue3.x 感觉就是参考了vue-async-manager.
Vue3.x 新出的内置组件Suspense, 它提供两个template slot, 刚开始会渲染一个 fallback 状态下的内容, 直到到达某个条件后才会渲染 default 状态的正式内容, 通过使用Suspense组件进行展示异步渲染就更加的简单。注意如果使用 Suspense, 要返回一个 promise
使用:

 <Suspense>
        <template #default>
            <async-component></async-component>
        </template>
        <template #fallback>
            <div>
                Loading...
            </div>
        </template>
  </Suspense>

asyncComponent.vue:

<template>
<div>
    <h4>这个是一个异步加载数据</h4>
    <p>用户名:{{user.nickname}}</p>
    <p>年龄:{{user.age}}</p>
</div>
</template>

<script>
import { defineComponent } from "vue"
import axios from "axios"
export default defineComponent({
    setup(){
        const rawData = await axios.get("http://xxx.xinp.cn/user")
        return {
            user: rawData.data
        }
    }
})
</script>

从上面代码来看,Suspense 只是一个带插槽的组件,只是它的插槽指定了defaultfallback 两种状态。

片段(Fragment)

在 Vue2.x 中, template中只允许有一个根节点:

<template>
    <div>
        <span></span>
        <span></span>
    </div>
</template>

但是在 Vue3.x 中,你可以直接写多个根节点

<template>
    <span></span>
    <span></span>
</template>
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容