vue3新特性及使用感受

VUE3新特性

1. Composition API

  • setup

    使用setup时,它接受两个参数:

    1. props: 组件传入的属性
    2. context:提供三个属性

    setup 中接受的props是响应式的, 当传入新的 props 时,会及时被更新。由于是响应式的, 所以不可以使用 ES6 解构,解构会消除它的响应式。

    const { xxx } = toRefs(props) // 可用toRefs解决
    复制代码

    setup中不能访问 Vue2 中最常用的this对象,所以context中就提供了this中最常用的三个属性:attrsslotemit,分别对应 Vue2.x 中的 $attr属性、slot插槽 和$emit发射事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。

    语法糖 script setup

    直接给script标签添加setup属性,不要像旧的语法那样在底部return一堆属性出去

    组件import后直接在template使用,不需要注册

    <template>
      <div>
     		<span>{{ num }}</span>
    	  <panel></panel>
      </div>
    </template>
    
    <script setup>
      import { ref } from 'vue'
      import Panel from '@/components/Panel.vue'
      
      const num = ref(0)
    </srcipt>
    复制代码

    script setup 相关文档

  • reactive

    <template>
      <div class="about">
        <div>
          <span>count 点击次数: </span>
          <span>{{ count }}</span>
          <button @click="addCount">点击增加</button>
        </div>
      </div>
    </template>
    <script>
    import { reactive, toRefs } from 'vue'
    export default {
      setup () {
        const state = reactive({
          count: 0
        })
        const addCount = function () {
          state.count++
        }
        return {
          // 这样展开后state property会失去响应式,因为是取值返回,不是引用
          // ...state,
          ...toRefs(state),
          addCount,
        }
      },
    }
    </script>
    
    复制代码
  • ref

    ref函数将一个普通对象转化为响应式包装对象,将一个 ref 值暴露给渲染上下文,在渲染过程中,Vue 会直接使用其内部的值,在模板中可以把 {{ num.value }} 直接写为 {{ num }} ,但是在js中还是需要通过 num.value取值和赋值

    <template>
      <div class="about">
        <div>
          <span>num 点击次数: </span>
          <span>{{ num }}</span>
          <button @click="addNum">点击增加</button>
        </div>
      </div>
    </template>
    <script>
    import { ref } from 'vue'
    export default {
      setup () {
        const num = ref(0)
    
        const addNum = function () {
          num.value++
        }
        return {
          num,
          addNum
        }
      }
    }
    </script>
    复制代码
  • toRefs

    toRefsreactive对象转换为普通对象,其中结果对象上的每个属性都是指向原始对象中相应属性的ref引用对象,这在组合函数返回响应式状态时非常有用,这样保证了开发者使用对象解构或拓展运算符不会丢失原有响应式对象的响应

  • readonly

    对于不允许写的对象,不管是普通object对象、reactive对象、ref对象,都可以通过readonly方法返回一个只读对象

    直接修改readonly对象,控制台会打印告警信息,不会报错

    const state = reactive({
      count: 0
    })
    const readonlyState = readonly(state)
    
    // 监听只读属性,state.count修改后依然会触发readonlyState.count更新
    watch(() => readonlyState.count, (newVal, oldVal) => {
      console.log('readonly state is changed!')
      setTimeout(() => {
        // 修改只读属性会打印告警信息,但是不会报错
        readonlyState.count = 666
      }, 1000)
    })
    复制代码
  • 生命周期

setup 执行时机是在 beforeCreate 之前执行

相对应的生命周期,3版本的执行时机总比2版本的早

img

  • Computed

    接受一个 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象。

    const count = ref(1)
    const plusOne = computed(() => count.value + 1)
    
    console.log(plusOne.value) // 2
    复制代码

    或者,它也可以使用具有 getset 函数的对象来创建可写的 ref 对象。

    const count = ref(1)
    const plusOne = computed({
      get: () => count.value + 1,
      set: val => {
        count.value = val - 1
      }
    })
    
    plusOne.value = 1
    console.log(count.value) // 0
    复制代码
  • watch

	watch(source, callback, [options])
复制代码

​ 参数说明:

​ source: 可以支持 string,Object,Function,Array; 用于指定要侦听的响应式变量

​ callback: 执行的回调函数

​ options:支持 deep、immediate 和 flush 选项。

侦听 reactive 定义的数据

   const state = reactive({ nickname: "lilei", age: 20 });
    // 修改age值时会触发 watch的回调
    watch(
      () => state.age,             
      (curAge, preAge) => {
        console.log("新值:", curAge, "老值:", preAge);
      }
    );
复制代码

侦听 ref 定义的数据

const year = ref(0);
watch(year, (newVal, oldVal) => {
  console.log("新值:", newVal, "老值:", oldVal);
});
复制代码

侦听多个数据

watch(
  [() => state.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 }
);

复制代码

stop 停止监听

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

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

setTimeout(()=>{
    // 停止监听
    stopWatchRoom()
}, 3000)
复制代码

2. Fragment

  1. 可以直接写多个节点,根节点不是必要的,无需创建了,减少了节点数。

  2. Fragment节点是虚拟的,不会DOM树中呈现。

<template>
    <div></div>
    <div></div>
</template>
复制代码

3. Teleport

例子:在子组件Header中使用到Dialog组件,此时Dialog就被渲染到一层层子组件内部,处理嵌套组件的定位、z-index和样式都变得困难。 Dialog从用户感知的层面,应该是一个独立的组件,从 dom 结构应该完全剥离 Vue 顶层组件挂载的 DOM;同时还可以使用到 Vue 组件内的状态(data或者props)的值。

即希望继续在组件内部使用Dialog, 又希望渲染的 DOM 结构不嵌套在组件的 DOM 中

若希望 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>
  </teleport>
</template>
复制代码

在一个子组件Header.vue中使用Dialog组件。header组件

<div class="header">
    ...
    <navbar />
    <Dialog v-if="dialogVisible"></Dialog>
</div>
...
复制代码

Dom 渲染效果如下:

img

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

4. Suspense

image-20210629141155408

在正确渲染组件之前进行一些异步请求是很常见的事。组件通常会在本地处理这种逻辑,绝大多数情况下这是非常完美的做法。

<suspense> 组件提供了另一个方案,允许等待整个组件树处理完毕而不是单个组件。

<template>
  <suspense>
     <template #default>
       <async-component></async-component>
     </template>
     <template #fallback>
       <div> 
         Loading...
       </div>
     </template>
  </suspense>
</template> 
复制代码

这个 <suspense> 组件有两个插槽。它们都只接收一个子节点。default 插槽里的节点会尽可能展示出来。如果不能,则展示 fallback 插槽里的节点。

重要的是,异步组件不需要作为 <suspense> 的最近子节点。它可以在组件树任意深度的位置且不需要出现在和 <suspense> 自身相同的模板中。只有所有的后代组件都准备就绪,该内容才会被认为解析完毕。

5. vue-router

vue2.x使用路由选项redirect设置路由自动调整,vue3.x中移除了这个选项,将在子路由中添加一个空路径路由来匹配跳转

// vue2.x router
[
  {
    path: '/',
    component: Layout,
    name: 'WebHome',
    redirect: '/dashboard', // 这里写跳转
    children: [
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('../views/dashboard/index.vue')
      }
    ]
  }
]

// vue3.x router
[
  {
    path: '/',
    component: Layout,
    name: 'WebHome',
    children: [
      { path: '', redirect: 'dashboard' }, // 这里写跳转
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('../views/dashboard/index.vue')
      }
    ]
  }
]
复制代码
import { useRouter, useRoute } from 'vue-router'

export default {
  setup () {
    const router = useRouter()
    const route = useRoute()
    return {
      
    }
  }
}
复制代码

6. Vuex

import { useStore } from 'vuex'

export default {
  setup () {
    const store = userStore()

    const userId = computed(() => store.state.app.userId)
    const getUserInfo = () => store.dispatch('xxxx')
		const setUserInfo = () => store.commit('xxx')
    return {
      userId
    }
  }
}
复制代码

7. getCurrentInstance

获取当前的组件实例

import { getCurrentInstance } from 'vue'

export default {
  setup () {
    // const { ctx } = getCurrentInstance()          ctx 只在开发环境生效,生产环境无效
    const { proxy: ctx } = getCurrentInstance()
    console.log('ccccc', ctx)
    return {
     
    }
  }
}
复制代码

了解更多可查看官方文档

使用总结

  1. 在2版本时候,当代码行数很多时,data,计算属性,watch都分布在不同区域,要来回切换,开发体验不好,新版本可以按业务逻辑放到一块

  2. 组合式Api开发起来更加灵活,逻辑复用较好

  3. 业务逻辑集中写在setup中,可能会导致代码臃肿较难维护

​ 组件抽离、要统一开发规范

  1. 利用 ES6 模块系统import/export,按需编译,体积比Vue2.x更小 (Tree shaking)

    import { computed, watch } from "vue";

问题:

watch监听reactive的数据为什么需要用函数返回?

1.官网,data源要是返回值的getter函数或ref

image-20210702170541469

watch源码

export function watch<T = any>(
  source: WatchSource<T> | WatchSource<T>[],  /* getter方法  */
  cb: WatchCallback<T>,                       /* hander回调函数 */
  options?: WatchOptions                      /* watchOptions */
): StopHandle { 
  return doWatch(source, cb, options)
}
复制代码

watch接受三个参数,分别是getter方法,回调函数,和options配置项。

doWatch核心方法

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): StopHandle {
  /* 此时的 instance 是当前正在初始化操作的 instance  */
  const instance = currentInstance
  let getter: () => any
  if (isArray(source)) { /*  判断source 为数组 ,此时是watch情况 */
    getter = () =>
      source.map(
        s =>
          isRef(s)
            ? s.value
            : callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
      )
  /* 判断ref情况 ,此时watch api情况*/
  } else if (isRef(source)) {
    getter = () => source.value
   /* 正常watch情况,处理getter () => state.count */
  } else if (cb) { 
    getter = () =>
      callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
  } else {
    /*  watchEffect 情况 */
    getter = () => {
      if (instance && instance.isUnmounted) {
        return
      }
      if (cleanup) {
        cleanup()
      }
      return callWithErrorHandling(
        source,
        instance,
        ErrorCodes.WATCH_CALLBACK,
        [onInvalidate]
      )
    }
  }
   /* 处理深度监听逻辑 */
  if (cb && deep) {
    const baseGetter = getter
    /* 将当前 */
    getter = () => traverse(baseGetter())
  }

  let cleanup: () => void
  /* 清除当前watchEffect */
  const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
    cleanup = runner.options.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
    }
  }
  
  let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE

  const applyCb = cb
    ? () => {
        if (instance && instance.isUnmounted) {
          return
        }
        const newValue = runner()
        if (deep || hasChanged(newValue, oldValue)) {
          if (cleanup) {
            cleanup()
          }
          callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
            newValue,
            oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
            onInvalidate
          ])
          oldValue = newValue
        }
      }
    : void 0
  /* TODO:  scheduler事件调度*/
  let scheduler: (job: () => any) => void
  if (flush === 'sync') { /* 同步执行 */
    scheduler = invoke
  } else if (flush === 'pre') { /* 在组件更新之前执行 */
    scheduler = job => {
      if (!instance || instance.isMounted) {
        queueJob(job)
      } else {
        job()
      }
    }
  } else {  /* 正常情况 */
    scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
  }
  const runner = effect(getter, {
    lazy: true, /* 此时 lazy 为true ,当前watchEffect不会立即执行 */
    computed: true,
    onTrack,
    onTrigger,
    scheduler: applyCb ? () => scheduler(applyCb) : scheduler
  })

  recordInstanceBoundEffect(runner)
  /* 执行watcherEffect函数 */
  if (applyCb) {
    if (immediate) {
      applyCb()
    } else {
      oldValue = runner()
    }
  } else {
    runner()
  }
  /* 返回函数 ,用终止当前的watchEffect */
  return () => {
    stop(runner)
    if (instance) {
      remove(instance.effects!, runner)
    }
  }
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享