Vue3常用组合式API

219 阅读11分钟

基本了解

当我们创建完成vue3项目后,点击它的main.js,你会发现写法发生了改变

1636688554(1).jpg

image.png

引入的不是vue构造函数,而是createApp工厂函数然而,创建实例对象其实就相当于vue2中的vmmount('#app')就相当于$mount('#app'),并且vue2的写法在vue3不能兼容

现在我们进入App组件,你会发现什么不一样的地方,他没有了根标签,在vue2的时候,我们都是在div根标签里面写东西,所以在vue3里面可以没有根标签

1636697972(1).jpg

createApp()#

创建一个应用实例。

function createApp(rootComponent: Component, rootProps?: object): App
  • 详细信息

    第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props。

app.use()#

安装一个插件

  • 类型
interface App {
  use(plugin: Plugin, ...options: any[]): this
}
  • 详细信息

    第一个参数应是插件本身,可选的第二个参数是要传递给插件的选项。

    插件可以是一个带 install() 方法的对象,亦或直接是一个将被用作 install() 方法的函数。插件选项 (app.use() 的第二个参数) 将会传递给插件的 install() 方法。

    若 app.use() 对同一个插件多次调用,该插件只会被安装一次。

app.mount()#

将应用实例挂载在一个容器元素中。

  • 类型
interface App {
  mount(rootContainer: Element | string): ComponentPublicInstance
}
  • 详细信息

    参数可以是一个实际的 DOM 元素或一个 CSS 选择器 (使用第一个匹配到的元素)。返回根组件的实例。

    如果该组件有模板或定义了渲染函数,它将替换容器内所有现存的 DOM 节点。否则在运行时编译器可用的情况下,容器元素的 innerHTML 将被用作模板。

    在 SSR 激活模式下,它将激活容器内现有的 DOM 节点。如果出现了激活不匹配,那么现有的 DOM 节点将会被修改以匹配客户端的实际渲染结果。

    对于每个应用实例,mount() 仅能调用一次。

  • 示例

    import { createApp } from 'vue'
    const app = createApp(/* ... */)
    
    app.mount('#app')
    

    也可以挂载到一个实际的 DOM 元素。

    app.mount(document.body.firstChild)
    

常用组合式API(重点!!!)

setup

image.png

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

  1. 需要在非单文件组件中使用组合式 API 时。
  2. 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
基本使用#

我们可以使用响应式 API 来声明响应式的状态,在 setup() 函数中返回的对象会暴露给模板和组件实例。其他的选项也可以通过组件实例来获取 setup() 暴露的属性:

vue

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    // 返回值会暴露给模板和其他的选项式 API 钩子
    return {
      count
    }
  },

  mounted() {
    console.log(this.count) // 0
  }
}
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。

setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。

setup() 应该同步地返回一个对象。唯一可以使用 async setup() 的情况是,该组件是 Suspense 组件的后裔。

访问 Props#

setup 函数的第一个参数是组件的 props。和标准的组件一致,一个 setup 函数的 props 是响应式的,并且会在传入新的 props 时同步更新。

js

export default {
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
  }
}

请注意如果你解构了 props 对象,解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx 的形式来使用其中的 props。

如果你确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs() 和 toRef() 这两个工具函数:

js

import { toRefs, toRef } from 'vue'

export default {
  setup(props) {
    // 将 `props` 转为一个其中全是 ref 的对象,然后解构
    const { title } = toRefs(props)
    // `title` 是一个追踪着 `props.title` 的 ref
    console.log(title.value)

    // 或者,将 `props` 的单个属性转为一个 ref
    const title = toRef(props, 'title')
  }
}
Setup 上下文#

传入 setup 函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup 中可能会用到的值:

js

export default {
  setup(props, context) {
    // 透传 Attributes(非响应式的对象,等价于 $attrs)
    console.log(context.attrs)

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

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

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

该上下文对象是非响应式的,可以安全地解构:

js

export default {
  setup(props, { attrs, slots, emit, expose }) {
    ...
  }
}

attrs 和 slots 都是有状态的对象,它们总是会随着组件自身的更新而更新。这意味着你应当避免解构它们,并始终通过 attrs.x 或 slots.x 的形式使用其中的属性。此外还需注意,和 props 不同,attrs 和 slots 的属性都不是响应式的。如果你想要基于 attrs 或 slots 的改变来执行副作用,那么你应该在 onBeforeUpdate 生命周期钩子中编写相关逻辑。

暴露公共属性#

expose 函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose 函数暴露出的内容:

js

export default {
  setup(props, { expose }) {
    // 让组件实例处于 “关闭状态”
    // 即不向父组件暴露任何东西
    expose()

    const publicCount = ref(0)
    const privateCount = ref(0)
    // 有选择地暴露局部状态
    expose({ count: publicCount })
  }
}
与渲染函数一起使用#

setup 也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态:

js

import { h, ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return () => h('div', count.value)
  }
}

返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说,这样没有问题,但如果我们想通过模板引用将这个组件的方法暴露给父组件,那就有问题了。

我们可以通过调用 expose() 解决这个问题:

js

import { h, ref } from 'vue'

export default {
  setup(props, { expose }) {
    const count = ref(0)
    const increment = () => ++count.value

    expose({
      increment
    })

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

此时父组件可以通过模板引用来访问这个 increment 方法。

setup script(组合式API)

自动注册子组件
<template>
    <div class="home">
       <ChildrenView></ChildrenView>
    </div>
</template>

<script setup>
import ChildrenView from './ChildrenView.vue'
</script>

属性和方法无需返回
<template>
    <div>
        <el-button @click="addNum">+1</el-button>
        {{count}}
    </div>
</template>

<script setup>
import { ref } from 'vue';
const count = ref(0);
const addNum = () => {
	count.value++;
}
</script>

支持props、emit、context
使用 props

通过defineProps指定当前 props 类型,获得上下文的props对象。示例:

<template>
    <div>
        <h3>{{title}}</h3>
        <h4>{{props.title}}</h4>
    </div>
</template>

<script setup>
import { ref, toRef, useAttrs, useSlots, defineEmits, defineProps, defineExpose } from 'vue';
const props = defineProps({
    title: {
        type: String,
        default: 'milo'
    }
});
const title = toRef(props, 'title');
</script>
使用 emits

使用defineEmit定义当前组件含有的事件,并通过返回的上下文去执行 emit。示例:

<script setup>
import { ref, toRef, useAttrs, useSlots, defineEmits, defineProps, defineExpose } from 'vue';
const emit = defineEmits(['change']);
const props = defineProps({
    title: {
        type: String,
        default: 'milo'
    }
});
const title = toRef(props, 'title');
const count = ref(0);
const addNum = () => {
    count.value++;
    emit('change', count.value);
}
</script>
获取 slots 和 attrs

useAttrs 可以获取父组件传过来的id和class等值。 useSlots 可以获得插槽的内容。 例子中,我们使用useAttrs获取父组件传过来的id和class,useSlots获取插槽的内容。 可以通过useContext从上下文中获取 slots 和 attrs。不过提案在正式通过后,废除了这个语法,被拆分成了useAttrsuseSlots。示例:

// 旧
<script setup>
  import { useContext } from 'vue'

  const { slots, attrs } = useContext()
</script>

// 新
<script setup>
  import { useAttrs, useSlots } from 'vue'

  const attrs = useAttrs()
  const slots = useSlots()
</script>

1. attrs

// 父组件1
<template>
    <div class="home">
        <ChildrenView
            @change="changeCount"
            id="add-count"
            class="count-box add-box"
        ></ChildrenView>
    </div>
</template>
// 子组件1
<script setup>
import { ref, toRef, useAttrs, useSlots, defineEmits, defineProps, defineExpose } from 'vue';

const count = ref(0);
const attrs = useAttrs();
const addNum = () => {
    count.value++;
    console.log("attrs:", attrs);
    console.log("attrs.id:", attrs.id);
    console.log("attrs.class:", attrs.class);
};
</script>

image.png

2. slots

defineExpose API

传统的写法,我们可以在父组件中,通过 ref 实例的方式去访问子组件的内容,但在 script setup 中,该方法就不能用了,setup 相当于是一个闭包,除了内部的 template模板,谁都不能访问内部的数据和方法。

如果需要对外暴露 setup 中的数据和方法,需要使用 defineExpose API。示例:

// 子组件
<script setup>
import { defineExpose, ref } from 'vue';

const num1 = 1;
const count = ref(0);
function addNum () {
    count.value++;
}
defineExpose({
    num1,
    addNum
})
</script>

// 父组件
<template>
    <div class="home">
        <ChildrenView ref="childRef">
        </ChildrenView>
        <div>
            <el-button @click="changeCount">父组件通过defineExpose获取子组件的数据</el-button>
        </div>
    </div>
</template>

<script setup>
import { ref } from 'vue';
import ChildrenView from './ChildrenView.vue';
const childRef = ref();
const changeCount = () => {
    console.log("num1:", childRef.value.num1);
    console.log("执行子组件的方法:", childRef.value.addNum, childRef.value.addNum());
}
</script>

image.png

响应式:核心

ref()#

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

  • 类型

    ts

    function ref<T>(value: T): Ref<UnwrapRef<T>>
    
    interface Ref<T> {
      value: T
    }
    
  • 详细信息

    ref 对象是可更改的,也就是说你可以为 .value 赋予新的值。它也是响应式的,即所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。

    如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。

    若要避免这种深层次的转换,请使用 shallowRef() 来替代。

  • 示例

    js

    const count = ref(0)
    console.log(count.value) // 0
    
    count.value++
    console.log(count.value) // 1
    
  • 参考:

computed()#

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

  • 类型

    ts

    // 只读
    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>
    
  • 示例

    创建一个只读的计算属性 ref:

    js

    const count = ref(1)
    const plusOne = computed(() => count.value + 1)
    
    console.log(plusOne.value) // 2
    
    plusOne.value++ // 错误
    

    创建一个可写的计算属性 ref:

    js

    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
    

    调试:

    js

    const plusOne = computed(() => count.value + 1, {
      onTrack(e) {
        debugger
      },
      onTrigger(e) {
        debugger
      }
    })
    
  • 参考:

reactive()#

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

  • 类型

    ts

    function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
    
  • 详细信息

    响应式转换是“深层”的:它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。

    值得注意的是,当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。

    若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,请使用 shallowReactive() 作替代。

    返回的对象以及其中嵌套的对象都会通过 ES Proxy 包裹,因此不等于源对象,建议只使用响应式代理,避免使用原始对象。

  • 示例

    创建一个响应式对象:

    js

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

    ref 的解包:

    ts

    const count = ref(1)
    const obj = reactive({ count })
    
    // ref 会被解包
    console.log(obj.count === count.value) // true
    
    // 会更新 `obj.count`
    count.value++
    console.log(count.value) // 2
    console.log(obj.count) // 2
    
    // 也会更新 `count` ref
    obj.count++
    console.log(obj.count) // 3
    console.log(count.value) // 3
    

    注意当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包:

    js

    const books = reactive([ref('Vue 3 Guide')])
    // 这里需要 .value
    console.log(books[0].value)
    
    const map = reactive(new Map([['count', ref(0)]]))
    // 这里需要 .value
    console.log(map.get('count').value)
    

    将一个 ref 赋值给为一个 reactive 属性时,该 ref 会被自动解包:

    ts

    const count = ref(1)
    const obj = reactive({})
    
    obj.count = count
    
    console.log(obj.count) // 1
    console.log(obj.count === count.value) // true
    
  • 参考:

readonly()#

接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。

  • 类型

    ts

    function readonly<T extends object>(
      target: T
    ): DeepReadonly<UnwrapNestedRefs<T>>
    
  • 详细信息

    只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。

    要避免深层级的转换行为,请使用 shallowReadonly() 作替代。

  • 示例

    js

    const original = reactive({ count: 0 })
    
    const copy = readonly(original)
    
    watchEffect(() => {
      // 用来做响应性追踪
      console.log(copy.count)
    })
    
    // 更改源属性会触发其依赖的侦听器
    original.count++
    
    // 更改该只读副本将会失败,并会得到一个警告
    copy.count++ // warning!
    
watch

可以监听单个或者多个

<script setup>
import { ref, reactive, computed, readonly, watch } from 'vue';
import ChildrenView from './ChildrenView.vue';
const count = ref(0);
const num = ref(0);
watch(count, (newValue, oldValue) => {
	console.log('count数字增加了', newValue, oldValue);
});
watch([count, num], (newValue, oldValue) => {
	console.log('监听count和num', newValue, oldValue);
});
const changeCount = () => {
	count.value++;

}
</script>

image.png

<script setup>
import { ref, reactive, computed, readonly, watch } from 'vue';
import ChildrenView from './ChildrenView.vue';
const count = ref(0);
const num = ref(0);
const names = reactive({
    age: 20,
    fullName: "milo"
});
watch(names, (newValue, oldValue) => {
    console.log('监听对象', newValue, oldValue);
}, { deep: false });
const changeCount = () => {
    count.value++;
    names.age++;
};
</script>

image.png

<script setup>
import { ref, reactive, computed, readonly, watch } from 'vue';
import ChildrenView from './ChildrenView.vue';
const count = ref(0);
const num = ref(0);
const names = reactive({
	age: 20,
	fullName: "milo",
	obj: {
		age: 1
	}
});
watch(() => names.age, (newValue, oldValue) => {
	console.log('监听对象names.age', newValue, oldValue);
});
watch(() => names.fullName, (newValue, oldValue) => {
	console.log('监听对象names.fullName', newValue, oldValue);
});
watch(() => names.obj.age, (newValue, oldValue) => {
	console.log('监听对象names.obj', newValue, oldValue);
});
// 监听多个值
watch([() => names.age, () => names.fullName], (newValue, oldValue) => {
	console.log('监听names.age和names.fullName', newValue, oldValue);
});
const changeCount = () => {
	count.value++;
	names.age++;
	names.fullName += '1';
	names.obj.age++;
};
</script>

image.png

watchEffect()#

watchEffect是vue3的新函数,它是来和watch来抢饭碗的,它和watch是一样的功能,那它有什么优势呢?

  • 自动默认开启了immediate:true
  • 用到了谁就监视谁

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

  • 类型

    ts

    function watchEffect(
      effect: (onCleanup: OnCleanup) => void,
      options?: WatchEffectOptions
    ): StopHandle
    
    type OnCleanup = (cleanupFn: () => void) => void
    
    interface WatchEffectOptions {
      flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
      onTrack?: (event: DebuggerEvent) => void
      onTrigger?: (event: DebuggerEvent) => void
    }
    
    type StopHandle = () => void
    
  • 详细信息

    第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求 (参见下面的示例)。

    第二个参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖。

    默认情况下,侦听器将在组件渲染之前执行。设置 flush: 'post' 将会使侦听器延迟到组件渲染之后再执行。详见回调的触发时机。在某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置 flush: 'sync' 来实现。然而,该设置应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。

    返回值是一个用来停止该副作用的函数。

  • 示例

    js

    const count = ref(0)
    
    watchEffect(() => console.log(count.value))
    // -> 输出 0
    
    count.value++
    // -> 输出 1
    

    副作用清除:

    js

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

    停止侦听器:

    js

    const stop = watchEffect(() => {})
    
    // 当不再需要此侦听器时:
    stop()
    

    选项:

    js

    watchEffect(() => {}, {
      flush: 'post',
      onTrack(e) {
        debugger
      },
      onTrigger(e) {
        debugger
      }
    })
    
  • 参考:

watchPostEffect()#

watchEffect() 使用 flush: 'post' 选项时的别名。

watchSyncEffect()#

watchEffect() 使用 flush: 'sync' 选项时的别名。

watch()#

侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

  • 类型

    ts

    // 侦听单个来源
    function watch<T>(
      source: WatchSource<T>,
      callback: WatchCallback<T>,
      options?: WatchOptions
    ): StopHandle
    
    // 侦听多个来源
    function watch<T>(
      sources: WatchSource<T>[],
      callback: WatchCallback<T[]>,
      options?: WatchOptions
    ): StopHandle
    
    type WatchCallback<T> = (
      value: T,
      oldValue: T,
      onCleanup: (cleanupFn: () => void) => void
    ) => void
    
    type WatchSource<T> =
      | Ref<T> // ref
      | (() => T) // getter
      | T extends object
      ? T
      : never // 响应式对象
    
    interface WatchOptions extends WatchEffectOptions {
      immediate?: boolean // 默认:false
      deep?: boolean // 默认:false
      flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
      onTrack?: (event: DebuggerEvent) => void
      onTrigger?: (event: DebuggerEvent) => void
    }
    

    为了便于阅读,对类型进行了简化。

  • 详细信息

    watch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。

    第一个参数是侦听器的。这个来源可以是以下几种:

    • 一个函数,返回一个值
    • 一个 ref
    • 一个响应式对象
    • ...或是由以上类型的值组成的数组

    第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。

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

    第三个可选的参数是一个对象,支持以下这些选项:

    • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
    • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器
    • flush:调整回调函数的刷新时机。参考回调的刷新时机及 watchEffect()
    • onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器

    与 watchEffect() 相比,watch() 使我们可以:

    • 懒执行副作用;
    • 更加明确是应该由哪个状态触发侦听器重新执行;
    • 可以访问所侦听状态的前一个值和当前值。
  • 示例

    侦听一个 getter 函数:

    js

    const state = reactive({ count: 0 })
    watch(
      () => state.count,
      (count, prevCount) => {
        /* ... */
      }
    )
    

    侦听一个 ref:

    js

    const count = ref(0)
    watch(count, (count, prevCount) => {
      /* ... */
    })
    

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

    js

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

    当使用 getter 函数作为源时,回调只在此函数的返回值变化时才会触发。如果你想让回调在深层级变更时也能触发,你需要使用 { deep: true } 强制侦听器进入深层级模式。在深层级模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象。

    js

    const state = reactive({ count: 0 })
    watch(
      () => state,
      (newValue, oldValue) => {
        // newValue === oldValue
      },
      { deep: true }
    )
    

    当直接侦听一个响应式对象时,侦听器会自动启用深层模式:

    js

    const state = reactive({ count: 0 })
    watch(state, () => {
      /* 深层级变更状态所触发的回调 */
    })
    

    watch() 和 watchEffect() 享有相同的刷新时机和调试选项:

    js

    watch(source, callback, {
      flush: 'post',
      onTrack(e) {
        debugger
      },
      onTrigger(e) {
        debugger
      }
    })
    

    停止侦听器:

    js

    const stop = watch(source, callback)
    
    // 当已不再需要该侦听器时:
    stop()
    

    副作用清理:

    js

    watch(id, async (newId, oldId, onCleanup) => {
      const { response, cancel } = doAsyncWork(newId)
      // 当 `id` 变化时,`cancel` 将被调用,
      // 取消之前的未完成的请求
      onCleanup(cancel)
      data.value = await response
    })