vue3学习小札之(六):API

162 阅读6分钟

引子

可以前往专栏阅读该系列其他文章:传送门

本篇文章不会全量的介绍 vue3 中的 API,因为官网已经罗列得足够详细。

只会提纲挈领地总结出几个方向,有针对地介绍部分 API

全局 API

createApp()

该 API 用来创建一个应用实例
可针对该 API 的返回值(一个应用实例),进行更多的 API 调用,包括但不仅限于:

将实例挂载到容器元素;
提供依赖,供后代组件注入使用;
注册全局组件、全局指令、安装插件(往期文章有详细介绍);
应用挂载前,在应用实例暴露的 config 对象上,进行应用的配置设定,等等等等。。。

组合式 API

setup()

setup() 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:

需要在非单文件组件中使用组合式 API 时;
比如,编写组合式函数

需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
此时 setup 就相当于一个选项,内部可以调用组合式 API,并可以返回数据给模板和其他的选项式 API 钩子。

其他情况下,都应优先使用 <script setup> 语法。

响应式

ref()

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

shallowRef()

ref() 的浅层作用形式。
shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。

triggerRef()

强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。
因为浅层 ref 不会触发响应式副作用,所以在浅层 ref 进行深层更新后,可以通过该 API 触发依赖于此 ref 到响应式副作用。

reactive()

返回一个对象的响应式代理

shallowReactive()

reactive() 的浅层作用形式。
一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。

effectScope()

创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。

基本用法

// scope 作用域内创建的 effect, computed, watch, watchEffect 将会被自动收集
const scope = effectScope()

scope.run(() => {
  const doubled = computed(() => counter.value * 2)

  watch(doubled, () => console.log(doubled.value))

  watchEffect(() => console.log('Count: ', doubled.value))
})

// 处理 scope 作用域内的所有副作用
scope.stop()

设计动机

在 vue 的组件 setup() 中,正常情况下的同步代码创建的副作用将会被收集并绑定到当前实例,所以当实例销毁时,副作用也会自动释放。
但是,当我们在组件外通过组合式 API 创建并使用响应式副作用时,就会变得麻烦
比如下面这个例子:

const disposables = []

const counter = ref(0)
const doubled = computed(() => counter.value * 2)

disposables.push(() => stop(doubled.effect))

const stopWatch1 = watchEffect(() => {
  console.log(`counter: ${counter.value}`)
})

disposables.push(stopWatch1)

const stopWatch2 = watch(doubled, () => {
  console.log(doubled.value)
})

disposables.push(stopWatch2)

然后我们需要这样来释放这些响应式副作用:

disposables.forEach((f) => f())
disposables = []

这样,就显得很呆。。。

所以,为了在组件外也能实现 setup 中自动收集并处理的效果,设计了这个 API

主要有以下三个

effectScope(detached = false): EffectScope

创建一个作用域:

const scope = effectScope()

scope 可以执行一个函数,并将捕获函数同步执行期间创建的所有副作用,例如 computedwatch and watchEffect:

scope.run(() => {
  const doubled = computed(() => counter.value * 2)

  watch(doubled, () => console.log(doubled.value))

  watchEffect(() => console.log('Count: ', doubled.value))
})

// 同一个 scope 可以多次调用
scope.run(() => {
  watch(counter, () => {
    /*...*/
  })
})

run 方法还会转发执行函数的返回值:

console.log(scope.run(() => 1)) // 1

当 scope.stop() 被调用,它将递归地停止所有捕获的副作用和嵌套作用域。

scope.stop()
嵌套作用域

嵌套作用域也会由其父作用域收集。当父作用域被释放时,其所有子作用域也将停止。

const scope = effectScope()

scope.run(() => {
  const doubled = computed(() => counter.value * 2)

  // 内部嵌套的作用域的副作用 会被它的外部作用域收集
  effectScope().run(() => {
    watch(doubled, () => console.log(doubled.value))
  })

  watchEffect(() => console.log('Count: ', doubled.value))
})

// 释放所有的副作用,包括内部嵌套作用的副作用
scope.stop()
分离嵌套作用域

effectScope 接受一个参数用来创建分离模式,分离模式下的作用域不会被其父作用域收集:

let nestedScope

const parentScope = effectScope()

parentScope.run(() => {
  const doubled = computed(() => counter.value * 2)

  // 传入了分离模式的参数
  // 所以此作用域不会被外层作用域收集并释放
  nestedScope = effectScope(true /* detached */)
  nestedScope.run(() => {
    watch(doubled, () => console.log(doubled.value))
  })

  watchEffect(() => console.log('Count: ', doubled.value))
})

// 释放了除嵌套作用域的所有副作用
parentScope.stop()

// 此时才会释放嵌套作用域
nestedScope.stop()

getCurrentScope(): EffectScope | undefined

如果有的话,返回当前活跃的 effect 作用域

import { getCurrentScope } from 'vue'

getCurrentScope() // EffectScope | undefined

onScopeDispose(fn: () => void): void

onScopeDispose() 具有与 onUnmounted() 类似的功能,但是它作用于当前作用域而不是组件实例。这有助于组合式函数在释放作用作用域时伴随着清除它内部的副作用。
因为 setup() 也为组件创建了一个作用域,当没有创建显式副作用作用域时,onScopeDispose将等同于onUnmounted

import { onScopeDispose } from 'vue'

const scope = effectScope()

scope.run(() => {
  onScopeDispose(() => {
    console.log('cleaned!')
  })
})

scope.stop() // 打印 'cleaned!'

例子A 可共享状态的组合式函数

还是基于之前的鼠标坐标的例子
之前的组合式函数,会为每个调用此组合式函数的组件实例,提供独立的事件监听和数据引用。然后通过组件的 onUnmounted 来释放组合式函数。
我们可以通过分离模式的作用域和 onScopeDispose 来优化。

首先,用onScopeDispose替换onUnmounted

function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function handler(e) {
    x.value = e.x
    y.value = e.y
  }

  window.addEventListener('mousemove', handler)

  // 当组件销毁时 onScopeDispose 同样会被调用
  // 起到类似于 onUnmounted 的效果
  - onUnmounted(() => {
  + onScopeDispose(() => {
    window.removeEventListener('mousemove', handler)
  })

  return { x, y }
}

然后,创建一个函数,来管理父作用域订阅:

// 入参为需要包装的组合式函数
function createSharedComposable(composable) {
  // 记录订阅者数量
  let subscribers = 0
  let state, scope
  
  // 释放的回调函数
  const dispose = () => {
    // 最后一个订阅者销毁的时候 才真正去释放作用域
    if (scope && --subscribers <= 0) {
      scope.stop()
      state = scope = null
    }
  }

  return (...args) => {
    subscribers++
    // 首次调用需要初始化 分离模式作用域 和 数据状态
    if (!state) {
      scope = effectScope(true)
      // run 方法会转发返回值
      state = scope.run(() => composable(...args))
    }
    // 注册释放的方法
    onScopeDispose(dispose)
    return state
  }
}

这样,就可以创建一个单例的共享模式组合式函数

const useSharedMouse = createSharedComposable(useMouse)

例子B 短暂性作用域

我们可以动态创建和处理一些作用域,达到短暂性的效果

export default {
  setup() {
    // 修改 enabled 的值 就可以动态创建销毁
    // 组合式函数 useMouse 的作用域
    const enabled = ref(false)
    let mouseState, mouseScope

    const dispose = () => {
      // 触发 stop,这样就算组件的 onUnmounted 不被触发
      // 也可以通过 useMouse 内部的 onScopeDispose 正确得触发清理
      mouseScope && mouseScope.stop()
      mouseState = null
    }

    watch(
      enabled,
      () => {
        if (enabled.value) {
          mouseScope = effectScope()
          mouseState = mouseScope.run(() => useMouse())
        } else {
          dispose()
        }
      },
      { immediate: true }
    )
    
    // 组件销毁时 起到类似于 onUnmounted 的作用
    onScopeDispose(dispose)
  },
}

生命周期钩子

所有生命周期的 API 都应该在组件的 setup() 阶段被同步调用。相关细节请看指南 - 生命周期钩子

总结

主要是介绍了一些我认为 vue3 比较大的更新点和比较不好理解的内容,全量的 API 使用语法可以前往官网查阅。