Vue3-01 组合式API

Vue3学习笔记-01 组合式API

升级到Vue3

升级VueCLI

VueCLI需要在4.3.1以上才可以支持Vue3

1
2
3
4
npm update -g @vue/cli

vue -V
@vue/cli 4.4.6

创建项目

1
2
vue create vue3-learning
vue add vue-next # 添加 vue3 插件升级为 vue3

在创建项目时选择手动添加配置,选择vue-router和Vuex,这样创建完的项目各个插件也都会升级为支持Vue3的版本

1
2
3
4
5
6
7
8
{
"dependencies": {
"core-js": "^3.6.5",
"vue": "^3.0.0-beta.1",
"vue-router": "^4.0.0-alpha.6",
"vuex": "^4.0.0-alpha.1"
}
}

创建Vue实例

1
2
3
4
5
6
7
8
9
import {createApp} from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

createApp(App)
.use(router)
.use(store)
.mount('#app');

创建Router

1
2
3
4
5
6
7
8
import {createRouter, createWebHistory} from 'vue-router';

const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});

export default router;

创建路由的方式与以前有所变化,路由模式除了Hash模式(createWebHashHistory)和History模式(createWebHistory),还多了带缓存的History路由(createMemoryHistory

使用路由跳转的API也有所变化:

1
2
3
4
5
6
7
8
9
import {useRouter} from 'vue-router';

export default {
setup() {
const router = useRouter();
const goHome = () => router.push('/');
return {goHome}
}
}

关于router的具体变化,后面再单独学习

创建Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {createStore} from 'vuex';

export default createStore({
state: {
count: 0
},
mutations: {
changeCount(state, isAdd) {
state.count = isAdd ? state.count + 1 : state.count - 1;
}
},
actions: {},
modules: {}
});

使用:

1
2
3
4
5
6
7
8
9
import {useStore} from 'vuex';

export default {
setup() {
const store = useStore();
const storeState = store.state;
return {storeState}
}
}

可以看出来,Vuex和vue-router,API都有了一些变化,与React Hooks的API很类似,但是基本原理没有太大变化

setup

setup函数是新的Composition API的入口点

调用时机

setup在创建组件之前调用(在beforeCreate之前),Props初始化后就会调用setup函数,在beforeCreate钩子前被调用

返回值

setup返回的对象的属性将被合并到组件模板的渲染上下文,也可以返回一个渲染函数

参数

接受两个参数:

  • props
  • context

props

接受props作为第一个参数,使用的时候,需要首先声明props

1
2
3
4
5
6
7
8
export default {
props: {
name: String,
},
setup(props) {
console.log(props.name)
},
}

==props是响应式的,前提是不对props进行解构,解构后会失去响应性==

如果需要结构props,可以通过使用roRefs来完成解构操作,并且保持响应式:

1
2
3
4
5
6
7
import { toRefs } from 'vue'

setup(props) {
const { title } = toRefs(props)

console.log(title.value)
}

如果title是一个可选的Prop,此时toRefs不会为title创建一个ref,需要使用toRef来创建响应式变量:

1
2
3
4
5
6
7
import { toRef } from 'vue'

setup(props) {
const title = toRef(props, 'title')

console.log(title.value)
}

context

setup第二个参数是上下文对象context,从2.x中的this选择性地暴露出三个组件的属性attrsslotsemitcontext就是一个普通的JavaScript对象,它不是响应式的,所以可以对context进行解构,不需要担心失去响应性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs)

// 插槽 (非响应式对象)
console.log(context.slots)

// 触发事件 (方法)
console.log(context.emit)
}
}

// 或者
export default {
setup(props, { attrs, slots, emit }) {
...
}
}

this的用法

thissetup中不可用,它并不会指向该组件实例

访问组件的Property

执行setup时,==组件实例尚未被创建==,因此只能访问slots/props/attrs/emit这些组件属性,无法访问computeddatamethods组件选项

结合模板使用

如果setup返回一个对象,那么可以在组件模板中直接使用,就如同使用Props一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>{{ readersNumber }} {{ book.title }}</div>
</template>

<script>
import { ref, reactive } from 'vue'

export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })

// expose to template
return {
readersNumber,
book
}
}
}
</script>

setup返回的refs在模板中访问是被自动解开的,不需要在模板中使用.value

生命周期

生命周期钩子函数只能在setup期间同步使用,在组件卸载时,生命周期内部创建的侦听器和计算状态也会被自动删除

与Vue2.x相比,beforeCreatedcreated被删除了,对应的逻辑在setup内部完成,其他的生命周期钩子都改为了onXxx的形式(beforeDestoryed改为了onBeforeUnmountdestroyed改为了onUnmounted

两个新增的调试钩子函数onRenderTrackedonRenderTriggered

1
2
3
4
5
6
export default {
onRenderTriggered(e) {
debugger
// 检查哪个依赖性导致组件重新渲染
},
}

函数接受一个回调函数,当钩子被组件调用时将会被执行

1
2
3
4
5
6
7
8
export default {
setup() {
// mounted
onMounted(() => {
console.log('Component is mounted!')
})
}
}

依赖注入

基本使用

使用provideinject实现依赖注入,与2.x版本中基本一致,只能在setup中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { provide, inject } from 'vue'

const ThemeSymbol = Symbol()

const Ancestor = {
setup() {
provide(ThemeSymbol, ref('dark'))
},
}

const Descendent = {
setup() {
const theme = inject(ThemeSymbol, ref('light') /* optional default value */)
return {
theme,
}
},
}

使用ref传值可以保证providedinjected之间值的响应性

响应式修改

在使用响应式provide/inject时,建议尽可能,==在提供者内保持响应式Property的任何更改==

如果需要在注入数据的组件内部更新inject的数据,在这种情况下,建议provide一个方法来负责改变响应式Property的方法,让注入方调用

如果要确保通过provide传递的数据不会被inject的组件更改,可以对提供者的Property使用readonly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { provide, reactive, readonly, ref } from 'vue'

export default {
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})

const updateLocation = () => {
location.value = 'South Pole'
}

provide('location', readonly(location))
provide('geolocation', readonly(geolocation))
provide('updateLocation', updateLocation)
}
}

getCurrentInstance

如果在setup中要访问组件实例,需要使用这个方法来访问,例如在app.config.globalProperties定义了全局变量,如果希望在setup中访问,就要用到这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main.js
app.config.globalProperties.$http = () => {
console.log(123);
};

// 组件中
import { getCurrentInstance } from 'vue'

const MyComponent = {
setup() {
const internalInstance = getCurrentInstance()

internalInstance.appContext.config.globalProperties.$http(); // 访问 globalProperties
}
}

注意,它只能在setup和生命周期钩子中调用,如果在其外调用(例如Event Listener中)需要在setup中调用getCurrentInstance()获取实例后传入对应的函数中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const MyComponent = {
setup() {
const internalInstance = getCurrentInstance(); // works

// 在组合式函数中调用也可以正常执行
const id = useComponentId();// works

const handleClick = () => {
getCurrentInstance(); // doesn't work
useComponentId(); // doesn't work

internalInstance; // works
}

onMounted(() => {
getCurrentInstance() // works
});

return () =>
h(
'button',
{
onClick: handleClick
},
`uid: ${id}`
)
}
}


function useComponentId() {
return getCurrentInstance().uid
}

模板Refs

常规使用

Vue2.x中的ref原本是用于获取DOM的, Vue3中ref不仅可以响应化数据,也可以实现获取DOM的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div ref="root"></div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
setup() {
const root = ref(null)

onMounted(() => {
// 在渲染完成后, 这个 div DOM 会被赋值给 root ref 对象
console.log(root.value) // <div/>
})

return {
root,
}
},
}
</script>

setup中声明一个ref并返回,在模板中声明ref并且值与返回的ref相同,这时在渲染初始化后(onMounted)就可以获取分配的DOM或组件实例

v-for中使用

v-for中使用时,需要使用3.0新增的函数形的ref,为ref赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div v-for="(item, i) in list" :ref="el => { divs[i] = el }">
{{ item }}
</div>
</template>

<script>
import { ref, reactive, onBeforeUpdate } from 'vue'

export default {
setup() {
const list = reactive([1, 2, 3])
const divs = ref([])

// 确保在每次变更之前重置引用
onBeforeUpdate(() => {
divs.value = []
})

return {
list,
divs,
}
},
}
</script>

侦听模板引用

watch或者watchEffect中引用Dom Ref,需要使用flush: 'post'选项,因为默认情况下watchwatchEffect的副作用函数是在DOM被挂载或者更新之前运行的,此时模板引用还没有被更新

使用了flush: 'post' ,保证监听器在DOM更新后执行副作用,确保模板引用于DOM保持同步,引用正确元素

组合式API实现更灵活的复用

Mixin的缺点

  • 容易发生冲突
  • 可复用性优先,不能向Mixin传递参数来改变逻辑,降低了在抽象逻辑方面的灵活行

Vue3的组合式API就是为了解决这些问题而存在的

更多的灵活性来自更多的自我克制

组合式API的初衷就是为了实现更有组织的代码,实现更灵活的逻辑提取与复用,在代码中会出现更多的、零碎的函数模块,在不同的位置、不同的组件间进行重复调用

它可以避免Vue2.x时代逻辑复用的几种主要形式(Mixin/HOS/SLOT)的弊端,带来了比较明显的好处:

但是它在提到了代码质量的上限的同时,降低了下线,setup中会出现大量面条式的代码,避免这种糟糕情况的关键就是,将逻辑更合理的划分为单独的函数,将setup作为一个入口,在其中进行不同组合函数的调用。

与React Hooks比较

Vue3的基于函数的组合式API受到了React Hooks的启发,在很多思维模型方面与React Hooks很类似,提供了同等级别的逻辑组合能力,但是也有着比较明显的不同,组合式API的setup函数只会被调用一次,也就意味着使用组合式API时:

  1. 不需要考虑调用顺序,可以用在条件语句中(React Hooks不可以)
  2. 不会再每次渲染时重复执行,降低垃圾回收的压力(React Hooks每次渲染都会重复执行)
  3. 不存在内联处理函数导致子组件永远更新的问题,也不需要useCallback(React Hooks需要用useCallback进行性能优化)
  4. 不存在忘记记录依赖的问题,也不需要useEffecruseMemo并传入依赖数组以捕获过时的变量,Vue的自动以来可以确保侦听器和计算值总是准确无误的(React Hooks需要手动记录依赖)