# 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 
右侧赞助一下 代码改变世界一块二块也是爱