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

写在最前:本文转自掘金

Composition API

Composition API 主要解决了,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。


watermark3.jpg

setup

setup 是 Vue3.x新增的一个选项,他是组件内使用 Compsition API的入口。

setup 执行时机

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)
  },
})

toRefs学习的地方为大家解答。接下来我们来说一下setup接受的第二个参数context,我们前面说了setup中不能访问Vue2中最常用的this对象,所以context中提供了this中最常用的是三个属性:attrsslotemit,分别对应Vue2.x中的$attr属性、slot插槽和$emit触发事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新的值。

reactive、 ref 与toRefs

在Vue3.x中,定义数据可以使用reactiveref来进行数据定义。那么refreactive他们有什么区别呢?分别什么时候使用呢?ref可以处理js基础类型的双向绑定,也可以定义对象类型双向绑定。但是reactive函数只能代理对象类型。

watermark4.jpg

我们在模板中使用user.name,user.age这样写感觉很繁琐,如果需要解构出来,就需要toRefstoRefs用于将一个reactive对象转化为属性全部为ref对象的普通对象。具体使用方法如下:




生命周期钩子

我们可以直接看生命周期图来认识有哪些生命周期钩子


watermark5.jpg

从图中我们可以看到Vue3.0新增了setup,然后是将Vue2.x中的beforeDestroy名称变更为beforeUnmountdestroyed名称变更为unmounted。其它Vue2中的生命周期仍然保留。上边生命周期图并没有包含全部的生命周期钩子,还有其它几个,全部声明周期钩子如下图所示:

watermark6.jpg

我们可以看到beforeCreatecreatedsetup替换了。其次,钩子函数都增加了on前缀;Vue3.x还新增了用于调试的钩子函数onRenderTriggeredonRenderTricked下面我们就简单使用几个钩子,方便大家学习如何使用,Vue3.x中的钩子函数是需要从vue中导入的

import {defineComponent, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmout, onUnmounted, onErrorCaptured, onRenderTracked, onRenderTriggered} from 'vue';
export default defineComponent({
  beforeCreate(){
    console.log("beforeCreated")
  },
  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)
    });
  }
})

watch 与 watchEffect 的用法

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

watch(source, callback, [options])

参数说明

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

侦听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),
    }
  }
})

侦听ref定义的数据

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

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

侦听多个数据

   watch([()=>test.age,year],([curAge,newVal],[preAge, oldVal])=>{console.log(curAge,preAge);console.log(newVal,oldVal)})

侦听复杂的嵌套对象

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配置,还在学习,后期补充。

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

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

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

watchEffect 的用法

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 无法获取到变化前的值, 只能获取变化后的值

computed 计算属性

setup() {
    let name = ref('xiaofan')
    let age = ref(21)
    
    //计算属性
    let getInfo = computed(() => {
        return `我的名字:${name.value},今年${age.value},请多多指教`
    })
    return {
        name,
        age,
        getInfo,
    }
}

自定义 Hooks

我们来写了一个实现加减的例子,这里将其封装成hook,我们约定这些[自定义的hook]以use作为前缀,和普通的函数加以区分。useCount.ts实现:

import { ref, Ref, computed } from 'vue'

interface CountResultProps {
  count: Ref;
  multiple: Ref;
  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




teleport

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

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


  

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


最后在一个子组件Header.vue中使用Dialog组件, 这里主要演示 Teleport 的使用,不相关的代码就省略了。header组件

...
...

watermark7.jpg

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

Suspense

暂未学习

片段(Fragment)

在 Vue2.x 中, template中只允许有一个根节点,
但是在 Vue3.x 中,你可以直接写多个根节点, 是不是很爽。

更好的 Tree - Shaking

Vue3.x 在考虑到tree-shaking的基础上重构了全局和内部API,表现结果就是现在的全局API需要通过ESMoudle的引用方式进行具名引用,比如在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(()=>{
  ...
})

受影响的API

  • Vue.nextTick
  • Vue.observable(用 Vue.reactive 替换)
  • Vue.version
  • Vue.compile(仅限完整版本时可用)
  • Vue.set(仅在 2.x 兼容版本中可用)
  • Vue.delete(与上同)

变更

slot 具名插槽语法

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



在父组件中使用:


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

// 子组件

export default {
data(){
        return{
            data:["走过来人来人往","不喜欢也得欣赏","陪伴是最长情的告白"]
        }
    }
}