美文网首页react & vue & angular
Vue3.0新特性以及使用经验总结

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

作者: 没名字的某某人 | 来源:发表于2022-02-11 16:46 被阅读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对象的普通对象。具体使用方法如下:
    <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>
    

    生命周期钩子

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


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

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

    teleport

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

    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>
    ...
    
    watermark7.jpg
    可以看到,我们使用teleport 组件,通过 to 属性,指定该组件渲染的位置与 <div id="app"></div>同级,也就是在 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 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.x中将slotslot-scope进行了合并统一使用。Vue3.x中 v-slot

    <!--父组件中使用-->
    <template v-slot:content="scoped">
      <div v-for="item in scope.data">{{item}}</div>
    </template>
    
    <!--也可以简写成:-->
    <template #content="{data}">
      <div v-for="item in data">{{item}}</div>
    </template>
    

    自定义指令

    首先回顾下 Vue2 中实现一个自定义指令:

    // 注册一个全局自定义指令 v-focus
    Vue.directive('focus', {
      inserted: function (el){
        // 聚焦
        el.focus()
      }
    })
    

    在vue2.x中,自定义指令通过以下几个可选钩子创建:

    • bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    • inserted: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
    • update: 所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
    • componentUpdated: 指令所在组件的VNode 及其子VNode 全部更新后调用
    • unbind: 只调用一次,指令与元素解绑时调用
      在Vue3 中对自定义的API进行了更加语义化的修改。


      watermark8.jpg

      所以在 Vue3 中, 可以这样来自定义指令:

    const { createApp } from "vue"
    
    const app = createApp({})
    app.directive('focus', {
        mounted(el) {
            el.focus()
        }
    })
    

    然后可以在模板中任何元素上使用新的v-focus指令, 如下:

    <input v-focus />
    

    v-model 升级

    在使用Vue3 之前就了解到 v-model 发生了很大的变化,下面来了解下发生了哪些变化:

    • 变更:在自定义组件上使用v-model时,属性以及事件的默认名称变了
    • 变更: v-bind.sync修饰符在Vue3中去掉,合并到v-model
    • 新增: 同一组件可以同时设置多个v-model
    • 新增: 开发者可以自定义v-model修饰符

    在vue2中,在组件上使用 v-model 其实就相当于传递了 value属性,并触发了input事件:

    <search-input v-model="searchValue"></search-input>
    <!-- 相当于 -->
    <search-input :value="searchValue" @input="searchValue=$event"></search-input>
    

    这时 v-model只能绑定在组件的value属性上,如果我们想给自己组件用一个别的属性,并且不想通过input来更新值,在.sync出来之前,Vue2是这样实现的:

    // 子组件:searchInput.vue
    export default {
        model:{
            prop: 'search',
            event:'change'
        }
    }
    

    修改后, searchInput 组件使用v-model就相当于这样:

    <search-input v-model="searchValue"><search-input>
    <!-- 相当于 -->
    <search-input :search="searchValue" @change="searchValue=$event"><search-input>
    

    但是在实际开发中,有些场景我们可能需要对一个 prop 进行 “双向绑定”, 这里以最常见的 modal 为例子:modal 挺合适属性双向绑定的,外部可以控制组件的visible显示或者隐藏,组件内部关闭可以控制 visible属性隐藏,同时visible 属性同步传输到外部。组件内部, 当我们关闭modal时, 在子组件中以 update:PropName 模式触发事件:

    this.$emit('update:visible', false)
    

    然后在父组件中可以监听这个事件进行数据更新:

    <modal :visible="isVisible" @update:visible="isVisible = $event"></modal>
    

    此时我们也可以使用v-bind.sync来简化实现:

    <modal :visible.sync="isVisible"></modal>
    

    上面回顾了 Vue2 中v-model实现以及组件属性的双向绑定,那么在 Vue 3 中应该怎样实现的呢?在vue3中,在自定义组件上使用v-model,相当于传递一个modelValue属性,同时触发一个update:modelValue事件:

    <modal v-model="isVisible"></modal>
    <!--相当于-->
    <modal :modelValue="isVisible" @update:modelValue="isVisible = $event"></modal>
    

    如果要绑定属性名,只需要给v-model传递一个参数就行,同时可以绑定多个v-model

    <modal v-model:visible="isVisible" v-model:content="content"></modal>
    
    <!-- 相当于 -->
    <modal
        :visible="isVisible"
        :content="content"
        @update:visible="isVisible"
        @update:content="content"
    />
    

    不知道你有没有发现,这个写法完全没有.sync什么事儿了, 所以啊,Vue 3 中又抛弃了.sync写法, 统一使用v-model

    异步组件

    Vue3 中使用defineAsyncComponent定义异步组件,配置选项component 替换为loader,Loader函数本身不在接收resolve 和 reject 参数,且必须返回一个Promise ,用法如下

    <template>
      <!--异步组件的使用-->
      <AsyncPage/>
    </template>
    <script>
    import {defineAsyncComponent} from 'vue';
    export default{
       components:{
        // 无配置项异步组件
        AsyncPage: defineAsyncComponent(()=>import('./NextPage.vue')),
        // 有配置项异步组件
        AsyncPage: defineAsyncComponent(()=>{
          loader:()=>import('./NextPage.vue'),
          delay:200,
          timeout:3000,
          errorComponent:()=>import("./ErrorComponent.vue"),
          loadingComponent:()=>import("./LoadingComponent.vue")
         })
      },
    }
    </script>
    

    相关文章

      网友评论

        本文标题:Vue3.0新特性以及使用经验总结

        本文链接:https://www.haomeiwen.com/subject/ycsakrtx.html