Vue3笔记

136 阅读3分钟

注意: 本文是自己参考别人的博客汇总便于自己学习记录使用(开头已经备注参考来源,若有侵权,可私信删文,谢谢噢)

参考

1.Vue3中文文档

2.让你30分钟快速掌握vue3

3.Vue3.0 新特性以及使用经验总结

4.vue3新特性及对比 2.x 的重要变更

1、Vue3对比Vue2的特点

3.0 对比 2.x 的重要变更有 6 个方面:

  • Performance(性能): 优化了虚拟 DOM,有了更加优化的编译,实现了更加高效的组件初始化

    • Rewritten virtual dom implementation (重写了虚拟 DOM)
    • Compiler-informed fast paths (优化编译)
    • More efficient component initialization (更高效的组件初始化)
    • 1.3-2x better update performance (1.3~2 倍的更新性能)
    • 2-3x faster SSR (2~3 倍的 SSR 速度)
  • Tree-shaking support (支持 Tree-shaking): Vue3 中,为了满足体积更小的需求,支持 Tree-shaking,也就意味着我们可以按需求引用的内置的指令和方法。

    • All runtime features included: 22.5kb. More features but still lighter than Vue 2。大多数可选功能(如 v-model)现在都是支持 Tree-shaking 的
    • Bare-bone HelloWorld size: 13.5kb. 11.75kb with only Composition API support
    • All runtime features included: 22.5kb. More features but still lighter than Vue 2
  • Composition API: 主要是提高了代码逻辑的可复用性,并且将 Reactivity 模块独立出来,这也使得 vue 3 变得更加灵活地与其他框架组合使用。

    • Usable alongside existing Options API (可与现有选项 API 一起使用)
    • Flexible logic composition and reuse (灵活的逻辑组成和重用)
    • Reactivity module can be used as a standalone library (Reactivity 模块可以作为独立的库使用)
  • Fragment, Teleport, Suspense

    • Fragment: vue2时,由于组件必须只有一个根节点,很多时候会添加一些没有意义的节点用于包裹。Fragment组件就是用于解决这个问题的
    • Teleport其实就是React中的Portal,提供一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案
    • 和React中的Supense是一样的。Suspense 让你的组件在渲染之前进行“等待”,并在等待时显示 fallback 的内容
  • Better TypeScript support (更好的 TypeScript 支持度)

  • Custom Renderer API (自定义的 Renderer API)

2、compositionAPI、setup()

什么是组合式API
Vue3.x 推出了Composition API主要将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。

image.png

2.1 setup

setup() 函数是 vue3 中,专门为组件提供的新属性。它为我们使用 vue3Composition API 新特性提供了统一的入口, setup 函数会在 beforeCreatecreated 之前执行, vue3也是取消了这两个钩子,统一用setup代替, 该函数相当于一个生命周期函数,vue中过去的datamethodswatch等全部都用对应的新增api写在setup()函数中

setup(props, context) {
    // Attribute (非响应式对象,等同于 $attrs)
    context.attrs
    // 插槽 (非响应式对象,等同于 $slots)
    context.slots
    // 触发事件 (方法,等同于 $emit)
    context.emit
    // 暴露公共 property (函数)
    context.expose
    
    return {}
  }
  • props: 用来接收 props 数据, props 是响应式的,当传入新的 props 时,它将被更新。
  • context 用来定义上下文, 上下文对象中包含了一些有用的属性,这些属性在 vue 2.x 中需要通过 this 才能访问到, 在 setup() 函数中无法访问到 this,是个 undefined
  • context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。
  • 返回值: return {}, 返回响应式数据, 模版中需要使用的函数

注意: 因为 props 是响应式的, 你不能使用 ES6 解构,它会消除 prop 的响应性。不过你可以使用如下的方式去处理

<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue';
export default defineComponent({
  setup(props, context) {
  
    const { title } = toRefs(props)
    
    console.log(title.value)
    
    return {}
  }
});
</script>

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:

<script lang="ts">
import { defineComponent, reactive, toRef, toRefs } from 'vue';
export default defineComponent({
  setup(props, context) {
  
    const { title } = toRef(props, 'title')
    
    console.log(title.value)
    
    return {}
  }
});
</script>

2.2 ref、reactive、 toRefs

在 vue2.x 中, 定义数据都是在data中, 但是 Vue3.x 可以使用reactiveref来进行数据定义。 那么refreactive他们有什么区别呢?分别什么时候使用呢?说到这里,我又不得不提一下,看到很多网上不少文章说 (reactive用于处理对象的双向绑定,ref则处理 js 基础类型的双向绑定)。我其实不太赞同这样的说法,这样很容易初学者认为ref就能处理 js 基本类型, 比如ref也是可以定义对象的双向绑定的啊, 上段代码:

 setup() {
    const obj = ref({count:1, name:"张三"})
    setTimeout(() =>{
        obj.value.count = obj.value.count + 1
        obj.value.name = "李四"
    }, 1000)
    return{
        obj
    }
  }

我们将obj.countobj.name绑定到页面上也是可以的;但是reactive函数确实可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean 等。 接下来使用代码展示一下refreactive的使用:
运行效果:

上面的代码中,我们绑定到页面是通过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>

2.3 watch、watchEffect

watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。

watch(source, callback, [options])

参数说明:

  • source: 可以支持 string,Object,Function,Array; 用于指定要侦听的响应式变量
  • callback: 执行的回调函数
  • options:支持 deep、immediate 和 flush 选项。

接下来我会分别介绍这个三个参数都是如何使用的, 如果你对 watch 的使用不明白的请往下看:

2.3.1 侦听 reactive 定义的数据

import { defineComponent, ref, reactive, toRefs, watch } from "vue";
export default defineComponent({
  setup() {
    const state = reactive({ nickname: "xiaofan", age: 20 });

    setTimeout(() => {
      state.age++;
    }, 1000);

    // 修改age值时会触发 watch的回调
    watch(
      () => state.age,
      (curAge, preAge) => {
        console.log("新值:", curAge, "老值:", preAge);
      }
    );

    return {
      ...toRefs(state),
    };
  },
});

2.3.2 侦听 ref 定义的数据

const year = ref(0);

setTimeout(() => {
  year.value++;
}, 1000);

watch(year, (newVal, oldVal) => {
  console.log("新值:", newVal, "老值:", oldVal);
});

2.3.3 侦听多个数据

上面两个例子中,我们分别使用了两个 watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据:

watch([() => state.age, year], ([curAge, newVal], [preAge, oldVal]) => {
console.log("新值:", curAge, "老值:", preAge); console.log("新值:", newVal,
"老值:", oldVal); });

2.3.4 侦听复杂的嵌套对象

我们实际开发中,复杂数据随处可见, 比如:

const state = reactive({
  room: {
    id: 100,
    attrs: {
      size: "140平方米",
      type: "三室两厅",
    },
  },
});
watch(
  () => state.room,
  (newType, oldType) => {
    console.log("新值:", newType, "老值:", oldType);
  },
  { deep: true }
);

如果不使用第三个参数deep:true, 是无法监听到数据变化的。 前面我们提到,默认情况下,watch 是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢?其实使用也很简单, 给第三个参数中设置immediate: true即可。关于flush配置,还在学习,后期会补充

2.3.5 stop 停止监听

我们在组件中创建的watch监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()函数的返回值,操作如下:

const stopWatchRoom = watch(() => state.room, (newType, oldType) => {
  console.log("新值:", newType, "老值:", oldType);
},
{ deep:true }
);

setTimeout(()=>{
    // 停止监听
    stop()
}, 3000)

2.3.6 watchEffect

还有一个监听函数watchEffect, 在我看来watch已经能满足监听的需求,为什么还要有watchEffect呢?虽然我没有 get 到它的必要性,但是还是要介绍一下watchEffect,首先看看它的使用和watch究竟有何不同。

import { defineComponent, ref, reactive, toRefs, watchEffect } from "vue";
export default defineComponent({
  setup() {
    const state = reactive({ nickname: "xiaofan", age: 20 });
    let year = ref(0)

    setInterval(() =>{
        state.age++
        year.value++
    },1000)

    watchEffect(() => {
        console.log(state);
        console.log(year);
      }
    );

    return {
        ...toRefs(state)
    }
  },
});

执行结果首先打印一次stateyear值;然后每隔一秒,打印stateyear值。 从上面的代码可以看出, 并没有像watch一样需要先传入依赖,watchEffect会自动收集依赖, 只要指定一个回调函数。在组件初始化时, 会先执行一次来收集依赖, 然后当收集到的依赖中数据发生变化时, 就会再次执行回调函数。所以总结对比如下:

  1. watchEffect 不需要手动传入依赖
  2. watchEffect 会先执行一次用来自动收集依赖
  3. watchEffect 无法获取到变化前的值, 只能获取变化后的值

2.4 computed

该函数用来创造计算属性,和过去一样,它返回的值是一个ref对象。 里面可以传方法,或者一个对象,对象中包含set()get()方法

2.4.1 创建只读的计算属性

import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
  setup(props, context) {
    const age = ref(18)

    // 根据 age 的值,创建一个响应式的计算属性 readOnlyAge,它会根据依赖的 ref 自动计算并返回一个新的 ref
    const readOnlyAge = computed(() => age.value++) // 19

    return {
      age,
      readOnlyAge
    }
  }
});
</script>

2.4.2 通过set()get()方法创建一个可读可写的计算属性

<script lang="ts">
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
  setup(props, context) {
    const age = ref<number>(18)

    const computedAge = computed({
      get: () => age.value + 1,
      set: value => age.value = value
    })
    // 为计算属性赋值的操作,会触发 set 函数, 触发 set 函数后,age 的值会被更新
    age.value = 100
    return {
      age,
      computedAge
    }
  }
});
</script>

2.5 生命周期钩子

我们可以直接看生命周期图来认识都有哪些生命周期钩子 (图片是根据官网翻译后绘制的): 从图中我们可以看到 Vue3.0 新增了setup,这个在前面我们也详细说了, 然后是将 Vue2.x 中的beforeDestroy名称变更成beforeUnmount; destroyed 表更为 unmounted,作者说这么变更纯粹是为了更加语义化,因为一个组件是一个mountunmount的过程。其他 Vue2 中的生命周期仍然保留。 上边生命周期图中并没包含全部的生命周期钩子, 还有其他的几个, 全部生命周期钩子如图所示:
我们可以看到beforeCreatecreatedsetup替换了(但是 Vue3 中你仍然可以使用, 因为 Vue3 是向下兼容的, 也就是你实际使用的是 vue2 的)。其次,钩子命名都增加了on; Vue3.x 还新增用于调试的钩子函数onRenderTriggeredonRenderTricked 下面我们简单使用几个钩子, 方便大家学习如何使用,Vue3.x 中的钩子是需要从 vue 中导入的:

import { defineComponent, onBeforeMount, onMounted, onBeforeUpdate,onUpdated,onBeforeUnmount, onUnmounted, onErrorCaptured, onRenderTracked,onRenderTriggered } from "vue"; 
export default defineComponent({ 
    // beforeCreate和created是vue2的
    beforeCreate() {
        console.log("------beforeCreate-----");
    },
    created() {
        console.log("------created-----");
    },
    setup() {
        console.log("------setup-----");
        // vue3.x生命周期写在setup中
        onBeforeMount(() => {
            console.log("------onBeforeMount-----");
        });
        onMounted(() => { 
            console.log("------onMounted-----");
        }); // 调试哪些数据发生了变化
        onRenderTriggered((event) => {
            console.log("------onRenderTriggered-----",event);
        })
    },
});

onRenderTracked 状态跟踪

onRenderTracked直译过来就是状态跟踪,它会跟踪页面上所有响应式变量和方法的状态,也就是我们用return返回去的值,他都会跟踪。只要页面有update的情况,他就会跟踪,然后生成一个event对象,我们通过event对象来查找程序的问题所在。

使用onRenderTracked同样要使用import进行引入。

import { .... ,onRenderTracked,} from "vue";

引用后就可以在setup()函数中进行引用了。

onRenderTracked((event) => {
  console.log("状态跟踪组件----------->");
  console.log(event);
});

写完后可以到终端中启动测试服务yarn serve,然后看一下效果,在组件没有更新的时候onRenderTracked是不会执行的,组件更新时,他会跟组里边每个值和方法的变化。

onRenderTriggered 状态触发

onRenderTriggered直译过来是状态触发,它不会跟踪每一个值,而是给你变化值的信息,并且新值和旧值都会给你明确的展示出来。

如果把onRenderTracked比喻成散弹枪,每个值都进行跟踪,那onRenderTriggered就是狙击枪,只精确跟踪发生变化的值,进行针对性调试。

使用它同样要先用import进行引入

import { .... ,onRenderTriggered,} from "vue";

在使用onRenderTriggered前,记得注释相应的onRenderTracked代码,这样看起来会直观很多。 然后把onRenderTriggered()函数,写在setup()函数里边,

onRenderTriggered((event) => {
  console.log("状态触发组件--------------->");
  console.log(event);
});

对 event 对象属性的详细介绍:

- key 那边变量发生了变化
- newValue 更新后变量的值
- oldValue 更新前变量的值
- target 目前页面中的响应变量和函数

3、模块化

将相关的逻辑聚合在一起,还可以扩展更多的功能

4、Teleport瞬移组件

4.1 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 的使用方式

4.2 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组件, 这里主要演示 Teleport 的使用,不相关的代码就省略了。header组件

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

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

5、Suspende异步请求组件

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组件进行展示异步渲染就更加的简单。:::warning 如果使用 Suspense, 要返回一个 promise :::Suspense 组件的使用:

  <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>

6、片段(Fragment)

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

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

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

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

7、更好的Tree-Shaking

Vue3.x 在考虑到 tree-shaking的基础上重构了全局和内部 API, 表现结果就是现在的全局 API 需要通过 ES Module的引用方式进行具名引用, 比如在 Vue2.x 中,我们要使用 nextTick:

// vue2.x
import Vue from "vue"

Vue.nextTick(()=>{
    ...
})

Vue.nextTick() 是一个从 Vue 对象直接暴露出来的全局 API,其实 $nextTick() 只是 Vue.nextTick() 的一个简易包装,只是为了方便而把后者的回调函数的 this 绑定到了当前的实例。虽然我们借助webpacktree-shaking, 但是不管我们实际上是否使用Vue.nextTick(), 最终都会进入我们的生产代码, 因为 Vue 实例是作为单个对象导出的, 打包器无法坚持出代码总使用了对象的哪些属性。 在 Vue3.x 中改写成这样:

import { nextTick } from "vue"

nextTick(() =>{
    ...
})

8、Vue2.x与Vue3.x响应式对比

9、变更

9.1 slot具名插槽语法

在 Vue2.x 中, 具名插槽的写法:

<!--  子组件中:-->
<slot name="title"></slot>

在父组件中使用:

<template slot="title">
    <h1>歌曲:成都</h1>
<template>

如果我们要在 slot 上面绑定数据,可以使用作用域插槽,实现如下:

// 子组件
<slot name="content" :data="data"></slot>
export default {
    data(){
        return{
            data:["走过来人来人往","不喜欢也得欣赏","陪伴是最长情的告白"]
        }
    }
}
<!-- 父组件中使用 -->
<template slot="content" slot-scope="scoped">
    <div v-for="item in scoped.data">{{item}}</div>
<template>

在 Vue2.x 中具名插槽和作用域插槽分别使用slotslot-scope来实现, 在 Vue3.0 中将slotslot-scope进行了合并同意使用。 Vue3.0 中v-slot

<!-- 父组件中使用 -->
 <template v-slot:content="scoped">
   <div v-for="item in scoped.data">{{item}}</div>
</template>

<!-- 也可以简写成: -->
<template #content="{data}">
    <div v-for="item in data">{{item}}</div>
</template>

9.2 自定义指令

见参考博客链接

9.3 v-model升级

见参考博客链接

9.4 异步组件

见参考博客链接