基本了解
当我们创建完成vue3项目后,点击它的main.js,你会发现写法发生了改变
引入的不是vue构造函数,而是createApp工厂函数然而,创建实例对象其实就相当于vue2中的vm,mount('#app')就相当于$mount('#app'),并且vue2的写法在vue3不能兼容
现在我们进入App组件,你会发现什么不一样的地方,他没有了根标签,在vue2的时候,我们都是在div根标签里面写东西,所以在vue3里面可以没有根标签
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
setup() 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:
- 需要在非单文件组件中使用组合式 API 时。
- 需要在基于选项式 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。不过提案在正式通过后,废除了这个语法,被拆分成了useAttrs和useSlots。示例:
// 旧
<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>
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>
响应式:核心
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>
<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>
<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>
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 })