关于此分类

关于初识 vue3分类主要是记录一些通过 vue3 进行的实践与学习记录。

此文主要记录关于 vue3 响应式数据的尝试。

本文会随着作者日常使用进行补充及内容修正

refs

类型声明

import { ref, Ref } from 'vue'
export default {
  // setup参数参考 https://v3.cn.vuejs.org/guide/composition-api-setup.html#%E5%8F%82%E6%95%B0
  setup() {
    // 声明类型 字符串
    const stringVal = ref<string>('1')
    // 声明类型 数字
    const numberVal = ref<number>(1)
    // 声明类型 泛型
    function useState<State extends string>(initial: State) {
      const state = ref(initial) as Ref<State> // state.value -> State extends string
      return state
    }
    return { stringVal, numberVal, useState }
  }
}

unref

如果参数是一个 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 的语法糖函数。

toRef、toRefs

API作用简述使用简述
toRef用于创建源响应式对象上某个属性的响应式变量
此变量可以被传递并保持响应式链接
用于将响应式对象某个属性传递给子组件并且不丢失响应式链接
toRefs将响应式对象转换为普通对象(值是响应式的)用于复合函数返回响应式对象,父组件解构/展开

toRef

可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。

// toRef
const refReactive = reactive({
  a: 1,
  b: 2
})
// 创建ref引用
const refA = toRef(refReactive, 'a')

useTest(refA) // useTest中修改传入的参数
// 会导致refReactive.a的值也会响应式修改为100

useTest

import { Ref } from 'vue'
export default function useTest(a: Ref<number>) {
  console.log(a.value)
  a.value = 100
  return { a }
}

因此,当需要向复合函数传递一个响应式对象的值时可以使用toRef将一个响应式对象某个值转为ref引用。

toRefs

将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref

useTest

import { Ref, reactive, toRefs } from 'vue'
export default function useTest(a: Ref<number>, b: Ref<number>) {
  console.log(a.value)
  a.value = 100
  b.value = 200
  const testReactive = reactive({
    name: '小康',
    website: 'https://baidu.com'
  })
  // 暴露的响应式对象使用toRefs包装
  return { testReactive: toRefs(testReactive) }
}

父组件中

const { testReactive } = useTest(refA, numberVal) // useTest中修改传入的参数
setTimeout(() => {
  testReactive.name.value = '父组件修改'
  // testReactive.name会被响应式修改
}, 2000)

customRef

创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 tracktrigger 函数作为参数,并且应该返回一个带有 getset 的对象。

使用官网的一个小 Demo

function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        // 追踪
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          // 触发视图更新
          trigger()
        }, delay)
      }
    }
  })
}
const text = useDebouncedRef('test', 3000)

虽然看到网上有各种各样在此函数中发送 Ajax 请求,但我个人认为,发送 Ajax 获取数据使用 Hooks 更合适。

类型声明

function customRef<T>(factory: CustomRefFactory<T>): Ref<T>

type CustomRefFactory<T> = (
  track: () => void,
  trigger: () => void
) => {
  get: () => T
  set: (value: T) => void
}

Computed 与 Watch

computed

接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。或者,接受一个具有 getset 函数的对象,用来创建可写的 ref 对象。

const newVal = computed(() => {
  // 返回val加10的结果
  return val.value + 10
})

const newVal1 = computed({
  get: () => {
    // 获取值
    return val.value + 10
  },
  set: () => {
    // 设置值
    val.value += 1
  }
})

3.2 中该函数在开发模式下可传入第二个参数用于调试。

const plusOne = computed(() => count.value + 1, {
  onTrack(e) {
    // 当 count.value 作为依赖被追踪时触发
    debugger
  },
  onTrigger(e) {
    // 当 count.value 被修改时触发
    debugger
  }
})
// 访问 plusOne,应该触发 onTrack
console.log(plusOne.value)
// 修改 count.value,应该触发 onTrigger
count.value++

类型声明

// 只读的
function computed<T>(
  getter: () => T,
  debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>

// 可写的
function computed<T>(
  options: {
    get: () => T
    set: (value: T) => void
  },
  debuggerOptions?: DebuggerOptions
): Ref<T>
interface DebuggerOptions {
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}
interface DebuggerEvent {
  effect: ReactiveEffect
  target: any
  type: OperationTypes
  key: string | symbol | undefined
}

watch

watch API 与选项式 API this.$watch (以及相应的 watch 选项) 完全等效。watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。

侦听器数据源可以是一个具有返回值的getter函数,也可以直接是一个ref

// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

侦听器还可以使用数组以同时侦听多个源

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

如果在同一个函数里同时改变这些被侦听的来源,侦听器仍只会执行一次

如果需要监听多个源时每一次更新都响应式变化,那么建议使用nextTick等等监听器再一次步改变之前运行。

const changeValues = async () => {
  firstName.value = 'John' // 打印 ["John", ""] ["", ""]
  await nextTick()
  lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
}
  1. 监听响应式对象或数组变化前与变化后的值,则需要监听由值构成的副本。

    const numbers = reactive([1, 2, 3, 4])
    watch(
      () => [...numbers],
      (val, newVal) => {
        console.log(val, newVal, 'numbers')
        // logs: [1,2,3,4,5] [1,2,3,4]
      }
    )
  2. 尝试检查深度嵌套对象或数组中(非响应式对象)的 property 变化时,仍然需要 deep 选项设置为 true。

    const member = reactive({
      id: 1,
      attr: {
        name: '小康'
      }
    })
    
    watch(
      () => member,
      (newVal, val) => {
        console.log('not deep', newVal.attr.name, val.attr.name)
      }
    )
    watch(
      () => member,
      (newVal, val) => {
        console.log('deep', newVal.attr.name, val.attr.name)
      },
      {
        deep: true
      }
    )
    
    member.attr.name = 'new小康' // deep new小康 new小康
  3. 侦听一个响应式对象或数组将始终返回该对象的当前值和上一个状态值的引用。为了完全侦听深度嵌套的对象和数组,可能需要对值进行深拷贝。

    import _ from 'lodash'
    
    const member = reactive({
      id: 1,
      attr: {
        name: '小康'
      }
    })
    
    watch(
      () => _.cloneDeep(state),
      (newVal, val) => {
        console.log('not deep', newVal.attr.name, val.attr.name)
      }
    )
    
    member.attr.name = 'new小康' // deep new小康 小康

watchEffect

立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。(根据响应式状态自动应用重新应用副作用)

const count = ref(0)

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

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

Provide / Inject

当子组件层级过深时仍需要外层组件传递数据,那么可以使用Provide / Inject,但默认情况下,此 API 并不是响应式的,如果需要响应式则需要在provide时使用ref或者reactive进行包装。

父组件

import { ref, provide } from 'vue'
const userName = ref<string>('小康')

setTimeout(() => {
  userName.value = 'XiaoKang'
  // 2秒后视图更新为 XiaoKang
}, 2000)

// 向子组件提供一个名为 name 值为 userName 数据
provide('name', userName)

子组件

import { inject, Ref } from 'vue'

// 获取父组件提供的名为 name 的值并赋值给userName变量
const userName = inject('name') as Ref<string>

修改值

当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部

例如:

const changeName = (str) => {
  userName.value = 'change Name 小康 ' + str
}
// 向子组件提供修改userName的方法
provide('changeName', changeName)

子组件中

// 获取父组件提供的名为 name 的值并赋值给userName变量
const changeUserName = inject<(str: string) => void>('changeName')

const changeUserNameHandler = () => {
  changeUserName && changeUserName('123')
}

子组件当触发changeUserNameHandler函数时即可调用父组件提供的修改方法将userName的值进行修改。

只读

当需要确保通过provide传递的数据不会被inject的组件更改,可以使用readonly进行修饰。

provide('testChange', readonly(testChange))

此时如果子组件修改值则不会修改成功。