# composition(组合式api)
## 1.为什么使用composition
vue3里面不需要Mixins了?因为有compoition api 能讲逻辑进行抽离和复用
大型组件中,其中**逻辑关注点**按颜色进行分组。
这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
如果能够将同一个逻辑关注点相关代码收集在一起会更好。而这正是组合式 API 使我们能够做到的

## 2.setup函数
执行顺序在beforeCreate,created之前,不能在此操作data
### props
使用 `setup` 函数时,它将接收两个参数:
1. `props`
2. `context`
### Props
`setup` 函数中的第一个参数是 `props`。正如在一个标准组件中所期望的那样,`setup` 函数中的 `props` 是响应式的,当传入新的 prop 时,它将被更新。
```js
// MyBook.vue
export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
}
}
```
> WARNING
但是,因为 `props` 是响应式的,你**不能使用 ES6 解构**,它会消除 prop 的响应性。
如果需要解构 prop,可以在 `setup` 函数中使用 [`toRefs`](https://v3.cn.vuejs.org/guide/reactivity-fundamentals.html#响应式状态解构) 函数来完成此操作:
```js
// MyBook.vue
import { toRefs } from 'vue'
setup(props) {
const { title } = toRefs(props)
console.log(title.value)
}
```
如果 `title` 是可选的 prop,则传入的 `props` 中可能没有 `title` 。在这种情况下,`toRefs` 将不会为 `title` 创建一个 ref 。你需要使用 `toRef` 替代它:
```js
// MyBook.vue
import { toRef } from 'vue'
setup(props) {
const title = toRef(props, 'title')
console.log(title.value)
}
```
### Context
传递给 `setup` 函数的第二个参数是 `context`。`context` 是一个普通 JavaScript 对象,暴露了其它可能在 `setup` 中有用的值:
```js
// MyBook.vue
export default {
setup(props, context) {
// Attribute (非响应式对象,等同于 $attrs)
console.log(context.attrs)
// 插槽 (非响应式对象,等同于 $slots)
console.log(context.slots)
// 触发事件 (方法,等同于 $emit)
console.log(context.emit)
// 暴露公共 property (函数)
console.log(context.expose)
}
}
```
`context` 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 `context` 使用 ES6 解构。
```js
// MyBook.vue
export default {
setup(props, { attrs, slots, emit, expose }) {
...
}
}
```
### setup中的this
在 `setup()` 内部,`this` 不是该活跃实例的引用**,因为 `setup()` 是在解析其它组件选项之前被调用的,所以 `setup()` 内部的 `this` 的行为与其它选项中的 `this` 完全不同。这使得 `setup()` 在和其它选项式 API 一起使用时可能会导致混淆。
beforeCreate
setup(){}
### setup中获取data
通过getCurrentInstance获取的是当前组件的实例,但是我们无法去获取具体的data属性,因为setup的执行顺序在created之前
我们的编程思路不应该考虑在setup中如何获取data,我们正确的做法是使用composition给我们提供的响应式api
```js
import {getCurrentInstance} from "vue"
export default {
setup(props,context){
console.log(props)
console.log(context)
console.log(getCurrentInstance())
}
}
```
## 3.响应式语法
### ref
ref可以对数据进行响应,也可以对复杂数据类型进行响应,也可以用于对模板的引用
在 Vue 3.0 中,我们可以通过一个新的 `ref` 函数使任何响应式变量在任何地方起作用,如下所示:
```js
import { ref } from 'vue'
const counter = ref(0)
```
`ref` 接收参数并将其包裹在一个带有 `value` property 的对象中返回,然后可以使用该 property 访问或更改响应式变量的值:
```js
import { ref } from 'vue'
const counter = ref(0)
console.log(counter) // { value: 0 }
console.log(counter.value) // 0
counter.value++
console.log(counter.value) // 1
```
将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,`Number` 或 `String` 等基本类型是通过值而非引用传递的:

### 模板引用
在使用组合式 API 时,响应式引用和模板引用的概念是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明 ref 并从 setup() 返回:
- 作用在dom元素返回dom
- 作用在组件返回组件实例
```jsx
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// DOM 元素将在初始渲染后分配给 ref
console.log(root.value) // <div>This is a root element</div>
})
return {
root
}
}
}
</script>
```
### ref在v-for中的使用
组合式 API 模板引用在 `v-for` 内部使用时没有特殊处理。相反,请使用函数引用执行自定义处理:
```html
<template>
<div v-for="(item, i) in list" :ref="el => { if (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([])
// 确保在每次更新之前重置ref
onBeforeUpdate(() => {
divs.value = []
})
return {
list,
divs
}
}
}
</script>
```
### 模板改变后引用
侦听模板引用的变更可以替代前面例子中演示使用的生命周期钩子。
但与生命周期钩子的一个关键区别是,`watch()` 和 `watchEffect()` 在 DOM 挂载或更新*之前*运行副作用,所以当侦听器运行时,模板引用还未被更新。
```vue
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, watchEffect } from 'vue'
export default {
setup() {
const root = ref(null)
watchEffect(() => {
// 这个副作用在 DOM 更新之前运行,因此,模板引用还没有持有对元素的引用。
console.log(root.value) // => null
})
return {
root
}
}
}
</script>
```
因此,使用模板引用的侦听器应该用 `flush: 'post'` 选项来定义,这将在 DOM 更新*后*运行副作用,确保模板引用与 DOM 保持同步,并引用正确的元素。
```vue
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, watchEffect } from 'vue'
export default {
setup() {
const root = ref(null)
watchEffect(() => {
console.log(root.value) // => <div>This is a root element</div>
},
{
flush: 'post'
})
return {
root
}
}
}
</script>
```
### reactive
使用reactive可以直接将对象变成响应式,甚至可以往对象中继续添加computed这样的属性,比如
```js
setup() {
let rea = reactive({
name: "李雷",
count: 1,
jisuan: computed(() => rea.count + 99)
})
function increase() {
rea.count++
}
}
```
这样写的好处就是,可以不需要每次在处理这个数据的使用想ref那样去.value了,但是这样写的时候也需要注意,返回数据的时候如果操作不当,那么就会丢失对数据的响应,比如下面这样
```js
return { ...rea, increase } //这样写能够展示数据,但是不能对数据保持响应
```
那这个时候我们就需要使用到下面个方法了,toRefs
一份完整的代码
```jsx
<template id="test">
<fieldset>
<legend>测试组件</legend>
<button @click="increaseCapacity()">++</button>
<h1>{{capacity}}</h1>
<h2>{{spacesLeft}}</h2>
</fieldset>
</template>
<scirpt>
const { createApp,reactive, computed, toRefs } = Vue;
let app = createApp({})
app.component('test-com', {
template: "#test",
setup() {
let event = reactive({
capacity: 4,
attending: ["Tim","Bob","Joe"],
spaceLeft: computed(() => event.capacity-event.attending.length + 99)
})
function increaseCapacity() {
event.capacity++
}
return { ...event, increaseCapacity } //这样写能够展示数据,但是不能对数据保持响应
}
})
app.mount('#app')
</script>
```
### ref和reactive的区别
1. reactive在使用的时候,会自动解包不需要像ref那样去.value
2. reactive不能对简单数据类型进行响应,需要在reactive中传入引用数据类型
### toRefs
如果直接返回reactive对应的变量,那么会失去响应,正确的写法是将reactive的属性放到toRefs方法中,保持其响应式
<img src="img/image-20220728155544943.png" alt="image-20220728155544943" style="zoom:45%;" />
可以这样写
```js
return { ...toRefs(event), increaseCapacity }
```
如果不需要返回方法,只需要event那么也可以这样写
```js
return toRefs(event)
```
## 4.methods
如果我们想要让这个值增加,那么在composition的代码中应该如何编写?传统的方式我们可以使用mehods来定义方法,但是现在我们只需要写成传统函数就可以了
- 传统写法
<img src="img/image-20220728154943165.png" alt="image-20220728154943165" style="zoom:50%;" />
- composition
- 使用ref响应属性的时候,不然要忘记了.value属性
<img src="img/image-20220728153215064.png" alt="image-20220728153215064" style="zoom:20%;" />
## 5.computed属性
接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 [ref](https://v3.cn.vuejs.org/api/refs-api.html#ref) 对象。
```js
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
```
或者,接受一个具有 `get` 和 `set` 函数的对象,用来创建可写的 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
```
## 6.生命周期
在composition中使用"on"来访问组件的钩子
created,beforeCreated在组合式api这种模式下,是没有的
### 新增钩子
- errorCaptured
在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 `false` 以阻止该错误继续向上传播。
一般情况下可以用抽象Error组件,然后专门用于处理错误
单独创建一个叫做Error的组件
```vue
<template>
<slot></slot>
</template>
<script setup>
import { onErrorCaptured } from 'vue';
onErrorCaptured((err, instance, info) => {
console.log(err)
console.log(instance)
console.log(info)
// 可以在这通过条件判断来决定是否要阻止错误的传播
//return true 继续传播
//return false 阻止错误的传播
return false
})
</script>
```
后续使用,将这个Error组件作为其它组件的父组件使用
```vue
<template>
<fieldset>
<legend>app</legend>
<div id="app">
<Error>
<lifeCircle></lifeCircle>
<reactiveCom></reactiveCom>
<counter></counter>
<computedCom></computedCom>
</Error>
</div>
</fieldset>
</template>
<script setup>
import reactiveCom from "./components/01.Reactive.vue"
import counter from "./components/02.计数器/counter.vue"
import computedCom from "./components/03.计算属性computed.vue"
import lifeCircle from "./components/03.生命周期.vue"
import Error from "./components/Error.vue"
</script>
```
- renderTracked
跟踪虚拟 DOM 重新渲染时调用。钩子接收 `debugger event` 作为参数。此事件告诉你哪个操作跟踪了组件以及该操作的目标对象和键。
```js
当组件第一次渲染时,这将被记录下来
```
- renderTriggered
当虚拟 DOM 重新渲染被触发时调用。和 [`renderTracked`](https://v3.cn.vuejs.org/api/options-lifecycle-hooks.html#rendertracked) 类似,接收 `debugger event` 作为参数。此事件告诉你是什么操作触发了重新渲染,以及该操作的目标对象和键。
```js
import {onMounted} from "vue"
export default {
setup(){
onMounted(()=>{
console.log('onMounted')
})
}
}
```
| 选项式 API | Hook inside `setup` |
| ------------------------------------------- | ------------------- |
| `beforeCreate` | Not needed* |
| `created` | Not needed* |
| `beforeMount` | `onBeforeMount` |
| `mounted` | `onMounted` |
| `beforeUpdate` | `onBeforeUpdate` |
| `updated` | `onUpdated` |
| `beforeUnmount` | `onBeforeUnmount` |
| `unmounted` | `onUnmounted` |
| `errorCaptured` | `onErrorCaptured` |
| `renderTracked`跟踪虚拟dom更新的时候,会调用 | `onRenderTracked` |
| `renderTriggered` | `onRenderTriggered` |
| `activated` | `onActivated` |
| `deactivated` | `onDeactivated` |

## 7.watch
### `watch` 响应式更改
就像我们在组件中使用 `watch` 选项并在 `user` property 上设置侦听器一样,我们也可以使用从 Vue 导入的 `watch` 函数执行相同的操作。它接受 3 个参数:
- 一个想要侦听的**响应式引用**或 getter 函数
- 一个回调
- 可选的配置选项
**下面让我们快速了解一下它是如何工作的**
```js
import { ref, watch } from 'vue'
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('The new counter value is: ' + counter.value)
})
```
每当 `counter` 被修改时,例如 `counter.value=5`,侦听将触发并执行回调 (第二个参数),在本例中,它将把 `'The new counter value is:5'` 记录到控制台中。
**以下是等效的选项式 API:**
```js
export default {
data() {
return {
counter: 0
}
},
watch: {
counter(newValue, oldValue) {
console.log('The new counter value is: ' + this.counter)
}
}
}
```
有关 `watch` 的详细信息,请参阅我们的[深入指南](https://v3.cn.vuejs.org/guide/reactivity-computed-watchers.html#watch)。
### 在setup语法糖获取props
明确引入`defineProps`来获取props
如果是定义触发的事件名可以使用`defineEmits`来进行声明
```vue
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
// setup code
</script>
```
**现在我们将其应用到我们的示例中:**
```js
<script setup>
import { watch, ref, defineProps, toRefs } from "vue"
//监听props的变化
let props = defineProps({
msg: String,
age: String
})
// console.log(props)
//1.监听写法,第一个参数传入一个函数,返回一个需要监听的字段
watch(() => props.msg, (newVal, oldVal) => {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
})
//2.第二种监听props变化的写法
//toRefs可以保持解构之后的响应性
let { age } = toRefs(props)
watch(age, (newVal, oldVal) => {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
})
</sript>
```
你可能已经注意到在我们的 `setup` 的顶部使用了 `toRefs`。这是为了确保我们的侦听器能够根据 `user` prop 的变化做出反应。
### watch可以一次侦听多个
侦听器还可以使用数组同时侦听多个源:
```js
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
firstName.value = 'John' // logs: ["John", ""] ["", ""]
lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]
```
尽管如此,如果你在同一个函数里同时改变这些被侦听的来源,侦听器仍只会执行一次:
```js
setup() {
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
const changeValues = () => {
firstName.value = 'John'
lastName.value = 'Smith'
// 打印 ["John", "Smith"] ["", ""]
}
return { changeValues }
}
```
### 强制触发侦听器
> 多个同步更改只会触发一次侦听器。我们也办法强制触发
通过更改设置 `flush: 'sync'`,我们可以为每个更改都强制触发侦听器,尽管这通常是不推荐的。或者,可以用 [nextTick](https://v3.cn.vuejs.org/api/global-api.html#nexttick) 等待侦听器在下一步改变之前运行。例如:
```js
import {next}
const changeValues = async () => {
firstName.value = 'John' // 打印 ["John", ""] ["", ""]
await nextTick()
lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
}
```
### 监听对象中属性变化
```vue
<template>
<div>
<div>{{obj.name}}</div>
<div>{{obj.age}}</div>
<button @click="changeName">改变值</button>
</div>
</template>
<script>
import { reactive, watch } from 'vue';
export default {
setup(){
const obj = reactive({
name:'zs',
age:14
});
const changeName = () => {
obj.name = 'ls';
};
watch(() => obj.name,() => {
console.log('监听的obj.name改变了')
})
return {
obj,
changeName,
}
}
}
</script>
```
### 深度监听
(deep)、默认执行(immediate)
```javascript
<template>
<div>
<div>{{obj.brand.name}}</div>
<button @click="changeBrandName">改变值</button>
</div>
</template>
<script>
import { reactive, ref, watch } from 'vue';
export default {
setup(){
const obj = reactive({
name:'zs',
age:14,
brand:{
id:1,
name:'宝马'
}
});
const changeBrandName = () => {
obj.brand.name = '奔驰';
};
watch(() => obj.brand,() => {
console.log('监听的obj.brand.name改变了')
},{
deep:true,
immediate:true,
})
return {
obj,
changeBrandName,
}
}
}
</script>
```
## 8.watchEffect高级侦听器(变化)
### watchEffect 的使用
watchEffect 也是一个帧听器,是一个副作用函数。 它会监听引用数据类型的所有属性,不需要具体到某个属性,一旦运行就会立即监听,组件卸载的时候会停止监听。
```javascript
<template>
<div>
<input type="text" v-model="obj.name">
</div>
</template>
<script>
import { reactive, watchEffect } from 'vue';
export default {
setup(){
let obj = reactive({
name:'zs'
});
watchEffect(() => {
console.log('name:',obj.name)
})
return {
obj
}
}
}
</script>
```
<img src="img/4.png" alt="在这里插入图片描述" style="zoom: 67%;" />
### 停止侦听
当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。 在一些情况下,也可以显式调用返回值以停止侦听:
```javascript
<template>
<div>
<input type="text" v-model="obj.name">
<button @click="stopWatchEffect">停止监听</button>
</div>
</template>
<script>
import { reactive, watchEffect } from 'vue';
export default {
setup(){
let obj = reactive({
name:'zs'
});
const stop = watchEffect(() => {
console.log('name:',obj.name)
})
const stopWatchEffect = () => {
console.log('停止监听')
stop();
}
return {
obj,
stopWatchEffect,
}
}
}
</script>
```

### onInvalidate
当执行副作用函数时,它势必会对系统带来一些影响,如在副作用函数里执行了一个定时器`setInterval`,因此我们必须处理副作用。`Vue3`的`watchEffect`侦听副作用传入的函数可以接收一个 `onInvalidate` 函数作为入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:
- 副作用即将重新执行时(即依赖的值改变)
- 侦听器被停止 (通过显示调用返回值停止侦听,或组件被卸载时隐式调用了停止侦听)
```js
import { watchEffect, ref } from 'vue'
const count = ref(0)
watchEffect((onInvalidate) => {
console.log(count.value)
onInvalidate(() => {
console.log('执行了onInvalidate')
})
})
setTimeout(()=> {
count.value++
}, 1000)
```
上述代码打印的顺序为: `0` -> `执行了onInvalidate,最后执行` -> `1`
### 案例:
有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (场景:有一个页码组件里面有5个页码,点击就会异步请求数据。于是做一个监听,监听当前页码,只要有变化就请求一次。问题:如果点击的比较快,从1到5全点了一遍,那么会有5个请求,最终页面会显示第几页的内容?第5页?那是假定请求第5页的ajax响应的最晚,事实呢?并不一定。于是这就会导致错乱。还有一个问题,连续快速点5次页码,等于我并不想看前4页的内容,那么是不是前4次的请求都属于带宽浪费?这也不好。
于是官方就给出了一种解决办法: 侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。 当以下情况发生时,这个失效回调会被触发:
- 副作用即将重新执行时;
- 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)
```javascript
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
```
构建一个结构
```javascript
import axios from 'axios';
import { ref, watchEffect } from 'vue';
export default {
setup() {
let pageNumber = ref(1);
let content = ref('');
const changePageNumber = () => {
pageNumber.value++;
}
watchEffect((onInvalidate) => {
// const CancelToken = axios.CancelToken;
// const source = CancelToken.source();
// onInvalidate(() => {
// source.cancel();
// });
axios.get(`http://chst.vip:1234/api/getmoneyctrl?pageid=${pageNumber.value}`, {
// cancelToken: source.token,
}).then((response) => {
content.value = response.data.result[0].productName;
}).catch(function (err) {
if (axios.isCancel(err)) {
console.log('Request canceled', err.message);
}
});
});
return {
pageNumber,
content,
changePageNumber,
};
},
};
</script>
```
上面注释掉的代码**先保持注释**,然后经过多次疯狂点击之后,得到这个结果,显然,内容错乱了:

现在**取消注释**,重新多次疯狂点击,得到的结果就正确了:

除了最后一个请求,上面那些请求有2种结局:
- 一种是响应的太快,来不及取消的请求,这种请求会返回200,不过既然它响应太快,没有任何一次后续 ajax 能够来得及取消它,说明任何一次后续请求开始之前,它就已经结束了,那么它一定会被后续某些请求所覆盖,所以这类请求的 content 会显示一瞬间,然后被后续的请求覆盖,绝对不会比后面的请求还晚。
- 另一种就是红色的那些被取消的请求,因为响应的慢,所以被取消掉了。
所以最终结果一定是正确的,而且节省了很多带宽,也节省了系统开销。
### 面试题
当一个用户快速的点击分页的时候,导致页面数据加载混乱怎么处理?
答:我用的是vue3,会在watchEffect的onInvalidate中调用axios.cancel这个方法来取消上一个axios请求就可以了
如果你在vue2中会怎么做?
如果是这样的话,可以使用防抖的思路,用户一直点击的时候,总是取消上一个axios请求,停止点击之后会发送最后一个请求
### watchEffect的配置项
watchEffect(()=>{},{flush:"post"}),第二个参数是个配置项,有个属性叫flush,有几个值
- pre dom加载之前运行
- post dom加载之后运行watchEffect的回调
- sync 如果在相同的函数中修改多个数据,而这些数据被watchEffect侦听了,只会触发一次回调,如果希望每次修改都触发,那么可以加上flush:'sync'
### 总结
**watch 特点**
watch 监听函数可以添加配置项,也可以配置为空,配置项为空的情况下,watch的特点为:
- 有惰性:运行的时候,不会立即执行;
- 更加具体:需要添加监听的属性;
- 可访问属性之前的值:回调函数内会返回最新值和修改之前的值;
- 可配置:配置项可补充 watch 特点上的不足: immediate:配置 watch 属性是否立即执行,值为 true 时,一旦运行就会立即执行,值为 false 时,保持惰性。 deep:配置 watch 是否深度监听,值为 true 时,可以监听对象所有属性,值为 false 时保持更加具体特性,必须指定到具体的属性上。
**watchEffect 特点**
- 非惰性:一旦运行就会立即执行;
- 更加抽象:使用时不需要具体指定监听的谁,回调函数内直接使用就可以;
- 不可访问之前的值:只能访问当前最新的值,访问不到修改之前的值;
## 9.自定义指令的使用
## vite代理配置
```js
// vite.config.js
import { defineConfig } from "vite";
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:3001",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
});
```
## 响应式原理
vue3的响应式主要使用了es6的reflect和proxy

右侧赞助一下 代码改变世界一块二块也是爱
浙公网安备 33010602011771号