vue3 实践之 Composition API(ref、reactive、toRef、toRefs、自定义 hooks 等)

作者: tww844475003 分类: 前端开发 发布时间: 2023-03-11 20:14

Vue3 与 Vue2 最大的不同在于新增了 Composition API,Composition API 官方文档:https://cn.vuejs.org/guide/extras/composition-api-faq.html

setup

setup 函数是处于生命周期函数 beforeCreate 之前的函数,新的 option、所有的组合式 API 函数都在此使用,并且只在初始化时执行一次。返回的属性、方法可在 template 直接使用。

  • beforeCreate 之前执行一次,此时组件对象还没有创建
  • setup中,this 是 undefined,不能通过 this 访问 data/computed/methods/props 属性、方法
  • 所有的 Composition API 相关函数中都不可以访问
  • setup 返回值
    1. 一般返回一个对象,为模板提供数据,template 可直接使用对象中的属性、方法
    2. 返回的属性与 data 函数返回对象的属性合并
    3. 返回的方法与 methods 中的方法合并
    4. 如果有重名,setup 优先

注意:

一般 setup、methods、data 不要混合使用,因为 methods 中可以访问的 setup 提供的属性和方法,但在 setup 方法中不能访问 data 和 methods
  • setup 返回参数
  1. setup(props, context) / setup(props, {attrs, slots, emit})
  2. props: 包含 props 配置声明且传入的所有属性的对象
  3. attrs: 包含没有在 props 配置中声明的属性的对象,相当于 this.$attrs
  4. slots: 包含所有传入的插槽内容对象,相当于 this.$slots
  5. emit: 用来颁发自定义事件的函数,相当于 this.$emit

props 与 attrs 区别

  • props 要声明才能使用,attrs 不用先声明
  • porps 声明过的属性,attrs 里不会再出现
  • props 不包含事件,attrs 包含事件
  • props 支持 string 以外的数据类型,attrs 只有 string 类型

ref

作用:定义一个数据的响应式

语法:const name = ref(initValue)

注意:模板语法中可以使用 name,js 中操作数据需要 name.value 语法

setup() {
  const count = ref(0);
  
  function addCount() {
    count.value++
  }
  
  return { count, addCount }
}

reactive

作用:定义多个数据的响应式(对象)

语法:const user = reactive(obj) 接收一个普通对象,返回一个该对象响应式代理对象

响应式转换是 “深层的”,它会影响对象内部的所有嵌套属性,内部是基于 ES6 的 Proxy 实现,通过代理对象操作对象内部数据。

const user = {
  name: '张三',
  age: 18
}

const proxyUser = new Proxy(user, {
  get(target, prop) {
    console.log('劫持 get', prop);
    return Reflect.get(target, prop);
  },
  set(target, prop, val) {
    console.log('劫持 set', prop);
    return Reflect.set(target, prop, val);
  },
  deleteProperty(target, prop) {
    console.log('劫持 delete', prop);
    return Reflect.deleteProperty(target, prop);
  }
});

console.log(proxyUser === user, user);
console.log(proxyUser.name, proxyUser.age);

proxyUser.name = '李四';
proxyUser.age = 30;
console.log(user, proxyUser)

Vue2 与 Vue3 响应式比较

  • Vue2 响应式

对象:通过 defineProperty 对对象的已有属性值的读取和修改进行劫持

数组:通过重写数组、更新数组等一系列更新元素的方法来实现元素修改的支持

问题

1)对象直接新添加属性和删除已有属性。界面不会自动更新;2)数组直接通过下标替换元素或更新元素 length,界面不会自动更新。
  • Vue3 响应式

通过 Proxy(代理):拦截 data 任意属性的任意(13种)操作,包括属性值的读写、属性的添加、属性的删除等

通过 Reflect(反射):动态地对被代理对象的相应属性进行特定的操作

Computed 与 watch、watchEffect

computed 函数:可以只有 getter,也可以同时有 getter 和 setter

watch 函数:监视指定的一个或多个响应式数据,一旦变化自动执行回调方法。immediate: true 初始执行,deep: true 深度监视。

watchEffect 函数:不能直接指定要监视的数据,回调函数中使用了哪些响应式数据就监听哪些响应式数据。默认初始时就会执行一次,收集需要监听的数据:

watch 与 watchEffect 区别:

  • watchEffect 不需要手动传入依赖
  • watchEffect 每次初始化时会执行一次回调函数来自动收集依赖
  • watchEffect 无法获取到原始值,只能得到变化后的值

自定义 hooks

import { ref, onMounted, onUnmounted } from 'vue'

export default function useMousePosition () {
  const x = ref(-1)
  const y = ref(-2)
  const updatePosition = (e: MouseEvent) => {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => {
    document.addEventListener('click', updatePosition)
  })

  onUnmounted(() => {
    document.addEventListener('click', updatePosition)
  })

  return { x, y }
}

toRef、toRefs

把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref

解决问题:reactive 对象取出的所有属性值都是非响应式的

<template>
  <div>
    <p>名字:{{name}}</p>
    <p>年龄:{{age}}</p>
    <p>{{age2}}</p>
    <button @click="update">更新</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs, toRef, watch } from 'vue'

export default defineComponent({
  setup () {
    const user = reactive({
      name: '张三',
      age: 100
    })

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

    watch(user, () => {
      console.log(user, 'change')
    }, {
      immediate: true,
      deep: true
    })

    function update () {
      const name = toRef(user, 'name')
      name.value += 'update'
    }

    const age2 = toRef(user, 'age')

    console.log(toRefs(user), user)
    return {
      ...toRefs(user),
      age2,
      update
    }
  }
})
</script>

这里的 age2 ,还有展开后的 user 属性,如果不用 toRef、toRefs 转换一下,就不会保持响应式。

ref

利用 ref 函数可以获取组件中的标签元素

<template>
  <input type="text" ref="inputRef" />
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue'

export default defineComponent({
  setup () {
    const inputRef = ref<HTMLElement | null>(null)

    onMounted(() => {
      inputRef.value && inputRef.value.focus()
    })

    return { inputRef }
  }
})
</script>

shallowReactive 与 shallowRef

shallowReactive:只处理对象最外层属性的响应式

shallowRef:只处理 value 的响应式,不进行对象的 reactive 处理

使用场景:

  • 多数场景使用 ref、reactive
  • 如果一个对象数据,结构比较深,但变化时只是外层属性变化,则可以使用 shallowReactive
  • 如果一个对象数据后面会被新产生的对象替换,则可以使用 shallowRef

readonly 与 shallowReadonly

readonly

  • 尝试只读数据
  • 获取一个对象或 ref 并返回原始代理的只读代理
  • 只读代理是深层的,访问的任何嵌套 property 也是只读的

shallowReadonly

  • 浅只读数据
  • 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换

toRaw 与 markRaw

toRaw

  • 返回由 reactive 或 readonly 方法转换成响应式代理的普通对象
  • 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新

markRaw 标记一个对象,使其永远不会转换为代理,而是返回对象本身

  • 有些值不应被设置为响应式的,例如复杂的第三方类实例或 vue 组件对象
  • 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能
<template>
  <h3>用户:{{user}}</h3>
  <button @click="testToRaw">测试toRaw</button>
  <button @click="testMarkRaw">测试markRaw</button>
</template>

<script lang="ts">
import { defineComponent, markRaw, reactive, toRaw } from 'vue'

interface UserInfo {
  name: string
  age: number
  like?: string[]
}

export default defineComponent({
  setup () {
    const user = reactive<UserInfo>({
      name: '张三',
      age: 18
    })

    const testToRaw = () => {
      // 把代理对象变成了普通对象,数据变化,界面不变化
      const newUser = toRaw(user)
      newUser.name = '李四'
      console.log(newUser, user)
    }

    const testMarkRaw = () => {
      user.like = ['看书', '看电视']
      console.log(user)
      const like = ['看书', '看电视']
      // markRaw 标记的对象数据,以后都不能成为代理对象
      user.like = markRaw(like)
      setTimeout(() => {
        if (user.like) {
          user.name = '李四'
          user.like[0] = '跑步'
          console.log('延时执行')
        }
      }, 1000)
    }

    return { user, testToRaw, testMarkRaw }
  }
})
</script>

unRef

如果参数是 ref 属性,则返回它的值,否则返回本身

const refValue = ref('ref value')
const reactiveValue = reactive({
  name: '张三',
  age: 18
})

console.log(unref(refValue))
console.log(unref(reactiveValue))

customRef

customRef 用于创建一个自定义的 ref 对象,可以显式地控制其依赖跟踪和触发响应

customRef 方法接收两个参数,分别是用于追踪的 track 与用于触发响应的 trigger,并返回一个带有 get 和 set 属性的对象

customRef 实现防抖节流

import { customRef } from 'vue'

// 防抖
export function useDebouncedRef<T> (value: T, delay = 300) {
  let timeoutId: number

  return customRef((track, trigger) => {
    return {
      get () {
        track()
        return value
      },
      set (newValue: T) {
        // 清除定时器
        clearTimeout(timeoutId)
        timeoutId = setTimeout(() => {
          value = newValue
          // 更新
          trigger()
        }, delay)
      }
    }
  })
}

// 节流
export function useThrottleRef<T> (value: T, delay = 300) {
  let flag = true

  return customRef((track, trigger) => {
    return {
      get () {
        track()
        return value
      },
      set (newValue: T) {
        if (flag) {
          flag = false
          const timer = setTimeout(() => {
            value = newValue
            trigger()
            flag = true
          }, delay)
        }
      }
    }
  })
}

使用案例

<template>
  <p>{{keyword}}</p>
  <input type="text" v-model="keyword" />
</template>

<script lang="ts">
import { defineComponent, watch } from 'vue'
import { useDebouncedRef, useThrottleRef } from '../hooks/useHooks'

export default defineComponent({
  setup () {
    const keyword = useDebouncedRef('vue.js', 1000)

    watch(
      keyword,
      (value) => {
        console.log(value)
      }
    )

    return {
      keyword
    }
  }
})
</script>

provide 与 inject

provide 和 inject 是成对出现的,在主组件通过 provide 提供数据和方法,在子组件或者孙辈等下级组件中通过 inject 调用主组件提供的数据和方法

// provideDemo.vue

<template>
  <div>
    {{message}}
    <button @click="clickFn">set</button>
    <hr />
    <SonComponent />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, provide } from 'vue'
import SonComponent from './SonComponent.vue'

export default defineComponent({
  components: {
    SonComponent
  },
  setup () {
    const message = ref<string>('')
    const name = ref<string>('')
    const grandSonName = ref<string>('')

    // 提供数据
    provide('name', name)
    provide('grandSonName', grandSonName)

    // 提供方法
    provide('callBack', () => {
      message.value = '更新消息'
      name.value = '更新姓名'
    })

    const clickFn = () => {
      message.value = 'provide 更新消息'
      name.value = 'provide 更新姓名'
      grandSonName.value = 'provide 更新孙子组件姓名'
    }

    return { clickFn, message }
  }
})
</script>


// SonComponent.vue
<template>
  <div>
    儿子:{{name}}
    <hr />
    <GrandSon />
  </div>
</template>

<script lang="ts">
import { defineComponent, inject } from 'vue'
import GrandSon from './GrandSon.vue'

export default defineComponent({
  components: {
    GrandSon
  },
  setup () {
    // 注入操作
    const name = inject('name')
    return { name }
  }
})
</script>

// GrandSon.vue

<template>
  <div>
    孙子:{{grandSonName}} <button @click="setName">设置Name</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, inject } from 'vue'

export default defineComponent({
  setup () {
    const setName = inject('callBack')
    const grandSonName = inject('grandSonName')

    return { setName, grandSonName }
  }
})
</script>

响应式数据的判断

  • isRef:检查一个值是否为一个 ref 对象
  • isReactive:检查一个对象是否是由 reactive 方法创建的响应式代理
  • isReadonly:检查一个对象是否是由 readonly 方法创建的只读代理
  • isProxy:检查一个对象是 否是由 reactive 或者 readonly 方法创建的代理
console.log(isRef(ref({}))) // true
console.log(isReactive(reactive({}))) // true
console.log(isReadonly(readonly({}))) // true
console.log(isProxy(readonly({}))) // true
console.log(isProxy(reactive({}))) // true
前端开发那点事
微信公众号搜索“前端开发那点事”

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注