Vue3新特性

1、Composition API

1.1 什么是 Composition API

  • 函数式编程,逻辑复用途经–函数组合(组合式 Composition API)
  • 面对对象编程,逻辑复用的途径–继承(选项式 选项式 API )

函数式编程思想将组件 UI逻辑 和状态逻辑解耦,让组件得到更大的复用性

vue2 选项式API

export default {
  // data() 返回的属性将会成为响应式的状态
  // 并且暴露在 `this` 上
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
}
</script>

Vue3 组合式(更拥抱这种写法)

<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)//或者const count = reactive({value:0})
// 用来修改状态、触发更新的函数
function increment() {
  count.value++
}
</script>

1.2 常用 Composition API

Q1: 说说你对 setup 的理解

  1. 组合式 API 的入口

Q2: ref 和 reactive 有什么区别?

  1. ref 内部使用了 reactive
  2. ref(obj) === reactive({ value: obj })

Q3: ref 和 shallowRef 的区别,以及 reactive 和 shallowReactive 的区别?

  1. shallow 表示浅层,这里均是指的响应值作用在第一层,即 .value,不过我们可以使用 triggerRef(xxx) 来在深层内容变更后,手动触发更新,需要注意的是 shallowReactive 没有对应方法

Q4: watchEffect 与 watch 的区别

  1. watch 是懒执行,属性改变的时候执行,而 watchEffect 是默认会执行一次,然后属性改变也会执行。
  2. watch 是需要传入侦听的数据源,而 watchEffect 是自动收集数据源作为依赖;
  3. watch 可以访问侦听状态变化前后的值,而 watchEffect 没有。

1.2.1 setup

setup() 钩子是在组件中使用组合式 API 的入口

<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)//或者const count = reactive({value:0})
// 用来修改状态、触发更新的函数
function increment() {
  count.value++
}
</script>

<script >
import { h, ref } from 'vue'

export default {
  setup(props, { expose }) {
    const count = ref(0)
    const increment = () => ++count.value
    
    // 透传 Attributes(非响应式的对象,等价于 $attrs)
    console.log(context.attrs)

    // 插槽(非响应式的对象,等价于 $slots)
    console.log(context.slots)

    // 触发事件(函数,等价于 $emit)
    console.log(context.emit)

    // 暴露公共属性(函数)
    console.log(context.expose)

    expose({
      increment
    })

    return () => h('div', count.value)
  }
}
</script>

1.2.2 ref

ref 函数 用于定义一个响应式对象

<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="input" />
</template>

1.2.3 reactive

用法同 ref

const obj = reactive({ count: 0 })
obj.count++

1.2.4 computed

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

或者通过对象 get、set 指定的方式

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

1.2.5 watchEffect、watchPostEffect、watchSyncEffect

后两者是前者的语法糖,就是将第二个参数中的 flush,指定为对应值,分别为:flush?: 'pre' | 'post' | 'sync' // 默认:'pre'

初始化 执行顺序:watchEffect,watchSyncEffect,watchPostEffect
变化时 执行顺序:watchSyncEffect,watchEffect,watchPostEffect

监听回调又叫监听副作用。

简单使用

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> 输出 0

count.value++
// -> 输出 1

具有清除与停止侦听的功能

const stop = watchEffect(async (onCleanup) => {
  const { response, cancel } = doAsyncWork(id.value)
  // `cancel` 会在 `id` 更改时调用
  // 以便取消之前
  // 未完成的请求
  onCleanup(cancel)
  data.value = await response
})

// 什么时候需要停止的话,那就
stop()

1.2.6 watch

先来简单回顾一下vue2.x版本中watch的使用

watch: {
    dataName (val, oldVal){
        console.log("改变前的数据-" + oldVal, "改变后的数据-" + val)
    }
}

Vue3中的watch
watch 自定义监听

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

<template>
  <button @click="change">count is: {{ state.count }}</button>
</template>

<script>
import { reactive, watch } from 'vue'
export default {
  setup () {
    let state = reactive({count: 0})
    let change = () => state.count++;
    watch(state, () => {
      console.log(state, '改变')
    })
    return { state, change }
  }
}
</script>

注意上面的代码,第一个参数传入的 state 对象,第二个参数是回调函数,只要 state 中任意的属性发生改变都会执行回调函数,和 vue2 的区别是不要写 deep 属性,默认就是深度监听了。
监听state 对象,没有旧的值,只有新的值

现在是监听整个对象,当然我们也可以监听对象上的某个属性,注意下面代码的写法:第一个是回调函数,第二个参数也是回调函数
监听state 对象上的某个属性,有旧的值,有新的值

<template>
  <button @click="change">count is: {{ state.count }}</button>
</template>

<script>
import { reactive, watch } from 'vue'
export default {
  setup () {
    let state = reactive({count: 0})
    let change = () => state.count++;
    watch(() => state.count, (oldVlaue, newValue) => {//() => state.count, getter方式
      console.log(oldVlaue, newValue, '改变')
    })
    return { state, change }
  }
}
</script>

上面都是以创建响应式对象函数reactive()方式定义的数据,下面再看看使用ref()初始化的数据怎样实现数据监听

<template>
  <button @click="change">count is: {{ count }}</button>
</template>

<script>
import { reactive, watch } from 'vue'
export default {
  setup () {
    let count = ref(0)
    let change = () => count.value++;
    watch(count, () => {
      console.log(count.value '改变')
    })
    return { count , change }
  }
}
</script>

当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值:

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

2、生命周期

2.1 Vue3生命周期钩子

  • setup() : 开始创建组件之前,在 beforeCreatecreated 之前执行,创建的是 data
    method
  • onBeforeMount() : 组件挂载到节点上之前执行的函数;
  • onRenderTracked() : 响应式的收集依赖;追踪,订阅
  • onMounted() : 组件挂载完成后执行的函数;
  • onRenderTriggered() : 响应式的触发依赖变更,发布
  • onBeforeUpdate(): 组件更新之前执行的函数;
  • onUpdated(): 组件更新完成之后执行的函数;
  • onBeforeUnmount(): 组件卸载之前执行的函数;
  • onUnmounted(): 组件卸载完成后执行的函数;
  • onActivated(): 被包含在 <keep-alive> 中的组件,会多出两个生命周期钩子函数,被激活时执行;
  • onDeactivated(): 比如从 A 组件,切换到 B 组件,A 组件消失时执行;
  • onErrorCaptured(): 当捕获一个来自子孙组件的异常时激活钩子函数。

PS: 使用<keep-alive> 组件会将数据保留在内存中,比如我们不想每次看到一个页面都重新加载数据,就可以使用<keep-alive> 组件解决。

在这里插入图片描述

2.2 vue2 和 vue3 关于生命周期的对比

在这里插入图片描述

3、异步组件

当我们的项目达到一定的规模时,对于某些组件来说,我们并不希望一开始全部加载,而是需要的时候进行加载;这样的做得目的可以很好的提高用户体验。
一般用于分割代码,按需加载

为了实现这个功能,Vue3中为我们提供了一个方法,即defineAsyncComponent,这个方法可以传递两种类型的参数,分别是函数类型对象类型

传递工厂函数作为参数

<template>
  <logo-img />
  <hello-world msg="Welcome to Your Vue.js App" />
</template>

<script setup>
import LogoImg from './components/LogoImg.vue'
import HelloWorld from './components/HelloWorld.vue'
</script>

现在我们就将组件修改为异步组件,示例代码如下:

<template>
  <logo-img />
  <hello-world msg="Welcome to Your Vue.js App" />
</template>

<script setup>
import { defineAsyncComponent } from 'vue'
import LogoImg from './components/LogoImg.vue'

// 简单用法
const HelloWorld = defineAsyncComponent(() =>
  import('./components/HelloWorld.vue'),
)
</script>

传递对象类型作为参数

defineAsyncComponent方法也可以接收一个对象作为参数,该对象中有如下几个参数:

  • loader:同工厂函数;
  • loadingComponent:加载异步组件时展示的组件;
  • errorComponent:加载组件失败时展示的组件;
  • delay:显示loadingComponent之前的延迟时间,单位毫秒,默认200毫秒;
  • timeout:如果提供了timeout,并且加载组件的时间超过了设定值,将显示错误组件,默认值为Infinity(单位毫秒);
  • suspensible:异步组件可以退出控制,并始终控制自己的加载状态。具体可以参考文档;
  • onError:一个函数,该函数包含4个参数,分别是error、retry、fail和attempts,这4个参数分别是错误对象、重新加载的函数、加载程序结束的函数、已经重试的次数。
 <template>
  <logo-img />
  <hello-world msg="Welcome to Your Vue.js App" />
</template>

<script setup>
import { defineAsyncComponent } from 'vue'
import LogoImg from './components/LogoImg.vue'
import LoadingComponent from './components/loading.vue'
import ErrorComponent from './components/error.vue'

// 定义一个耗时执行的函数,t 表示延迟的时间, callback 表示需要执行的函数,可选
const time = (t, callback = () => {}) => {
  return new Promise(resolve => {
    setTimeout(() => {
      callback()
      resolve()
    }, t)
  })
}
// 记录加载次数
let count = 0
const HelloWorld = defineAsyncComponent({
  // 工厂函数
  loader: () => {
    return new Promise((resolve, reject) => {
      ;(async function () {
        await time(300) //模拟异步
        const res = await import('./components/HelloWorld.vue')
        if (++count < 3) {
          // 前两次加载手动设置加载失败
          reject(res)
        } else {
          // 大于3次成功
          resolve(res)
        }
      })()
    })
  },
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 0,
  timeout: 1000,
  suspensible: false,
  onError(error, retry, fail, attempts) {
    // 注意,retry/fail 就像 promise 的 resolve/reject 一样:
    // 必须调用其中一个才能继续错误处理。
    if (attempts < 3) {
      // 请求发生错误时重试,最多可尝试 3 次
      console.log(attempts)
      retry()
    } else {
      fail()
    }
  },
})
</script>

通常会与 Suspense 配合
用Suspense 包一下异步组件,用来在组件树中协调对异步依赖的处理。该特性目前还不是稳定版本,后续可能会有变更。

4、自定义指令

  • 自定义指令着眼于组件,围绕组件去做增强
  • composition api着眼于状态,围绕数据做功能增强

我们都知道指令是为了增强组件的,我们常见的指令有:v-if、v-show、v-model、v-bind:value、v-on:click 等。

自定义指令其实非常简单,我们需要始终关注以下几个问题:

  1. 指令的钩子函数,有点类似生命周期函数钩子
  2. 指令钩子函数中的参数
  3. 指令的逻辑处理

注册自定指令,有点像封装了一个组件,封装在外部。有点类似vue2的mixin(功能复用),不过比mixin好很多,没mixin那么多缺点。
自定义指令关注组件或者本身dom(如 v-if,作用于dom节点)。composition api更关注状态

自定义指令 v-focus 、v-drag案例

<template>
  <input v-focus />
  <div class="heyi" v-drag></div>
</template>

<script setup lang="ts">
// 注册自定义指令
import { vDrag } from "../directives/vDrag";
import { vFocus } from "../directives/vFocus";
</script>

<style scoped>
.heyi {
  width: 100px;
  height: 100px;
  background-color: red;
}
</style>

vDrag组件

import { Directive } from "vue";//ts

// mixin  xxxxxx

export const vDrag: Directive = {
  mounted(el) {//el,当前dom
    el.draggable = true;
    el.addEventListener("dragstart", () => {
      console.log("dragstart");
    });
  },
};

vFocus组件

import { Directive } from "vue";

// mixin  xxxxxx

export const vFocus: Directive = {//ts
  mounted(el) {
    el.focus();
  },
};

指令钩子:

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}

5、Teleport

该特性允许你将组件内的某个子组件挂载到任意 HTML 节点上,这个特性像极了 React 中的 createPortal。
整个vue应用都是挂载到根节点,然后下面子组件挂到父组件上行成一棵树。
假设我现在某一个组件想挂到别的地方去,而不是父组件上,就使用Teleport

假设不用Teleport

<template>
    <div class="heyi"></div>
</template>

<script setup lang="ts"></script>

<style scoped>
.heyi {
  width: 100px;
  height: 100px;
  background-color: red;
  position: absolute;
  left: 0;
  top: 0;
}
</style>

打开控制台查看,会发现 <div class="heyi"></div> 挂在我们的vue实例 #app底下,也就是正常的父级dom下面

使用了Teleport

<template>
  <Teleport to="body">
    <div class="heyi"></div>
  </Teleport>
</template>

<script setup lang="ts"></script>

<style scoped>
.heyi {
  width: 100px;
  height: 100px;
  background-color: red;
  position: absolute;
  left: 0;
  top: 0;
}
</style>

打开控制台查看,会发现 <div class="heyi"></div> 挂在body地下,不再是#app底下

  • Teleport 组件创建
    • 首先会在在主视图里插入注释节点或者空白文本节点
    • 接着获取目标元素节点
    • 最后调用mount方法创建子节点往目标元素插入 Teleport 组件的子节点

Teleport通常用来创建模态框modal

  • 弹出层
  • Popover 等
  • tooltip

6、自定义 Hooks

假设我们需要封装一个计数器,该计数器用于实现数字的增加或者减少,并且我们可以指定数字可最大和最小值

使用:(负责视图)

<template>
  <div>
    <p>{{ current }} [max: 10; min: 1;]</p>
    <div class="contain">
      <button @click="inc()">
        Inc()
      </button>
      <button @click="dec()" style="margin-left: 8px">
        Dec()
      </button>
      <button @click="set(3)" style="margin-left: 8px">
        Set(3)
      </button>
      <button @click="reset()" style="margin-left: 8px">
        Reset()
      </button>
    </div>
  </div>
</template>

<script lang="ts" setup>
  import { useCounter } from './useCounter'
  const [current, { inc, dec, set, reset }] = useCounter(20, { min: 1, max: 10 })
</script>

useCounter就是自定义 Hooks(负责状态)

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

// 判断是否为数字
const isNumber = (value: unknown): value is number => typeof value === 'number'

export interface UseCounterOptions {
  /**
   *  Min count
   */
  min?: number

  /**
   *  Max count
   */
  max?: number
}

export interface UseCounterActions {
  /**
   * Increment, default delta is 1
   * @param delta number
   * @returns void
   */
  inc: (delta?: number) => void

  /**
   * Decrement, default delta is 1
   * @param delta number
   * @returns void
   */
  dec: (delta?: number) => void

  /**
   * Set current value
   * @param value number | ((c: number) => number)
   * @returns void
   */
  set: (value: number | ((c: number) => number)) => void

  /**
   * Reset current value to initial value
   * @returns void
   */
  reset: () => void
}

export type ValueParam = number | ((c: number) => number)

function getTargetValue(val: number, options: UseCounterOptions = {}) {
  const { min, max } = options
  let target = val
  if (isNumber(max)) {
    target = Math.min(max, target)
  }
  if (isNumber(min)) {
    target = Math.max(min, target)
  }
  return target
}

function useCounter(
  initialValue = 0,
  options: UseCounterOptions = {},
): [Ref<number>, UseCounterActions] {
  const { min, max } = options

  const current = ref(
    getTargetValue(initialValue, {
      min,
      max,
    }),
  )

  const setValue = (value: ValueParam) => {
    const target = isNumber(value) ? value : value(current.value)
    current.value = getTargetValue(target, {
      max,
      min,
    })
    return current.value
  }

  const inc = (delta = 1) => {
    setValue(c => c + delta)
  }

  const dec = (delta = 1) => {
    setValue(c => c - delta)
  }

  const set = (value: ValueParam) => {
    setValue(value)
  }

  const reset = () => {
    setValue(initialValue)
  }

  return [
    readonly(current),
    {
      inc,
      dec,
      set,
      reset,
    },
  ]
}

export default useCounter

总结:Composition API的优点:将组件 UI逻辑 和状态逻辑解耦,让组件得到更大的复用性

  • 14
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值