[TOC]

使用Vue2.x的小伙伴都熟悉,Vue2.x中所有数据都是定义在data中,方法定义在methods中的,并且使用this来调用对应的数据和方法。那Vue3.x中就可以不这么玩了, 具体怎么玩我们后续再说, 先说一下Vue2.x版本这么写有什么缺陷,所以才会进行升级变更的。

文档

https://vue3js.cn/vue-composition-api/

回顾 Vue2.x 实现加减

<template>
  <div class="homePage">
    <p>count: {{ count }}</p>
    <p>倍数: {{ multiple }}</p>
    <div>
        <button style="margin-right:10px" @click="increase">加1</button>
        <button @click="decrease">减一</button>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  computed: {
    multiple() {
      return 2 * this.count;
    },
  },
  methods: {
    increase() {
      this.count++;
    },
    decrease() {
      this.count++;
    },
  },
};
</script>

上面代码只是实现了对count的加减以及显示倍数, 就需要分别在data、methods、computed中进行操作,当我们增加一个需求,就会出现下图的情况:

Vue3 Composition 组合式 API - 图1

当我们业务复杂了就会大量出现上面的情况, 随着复杂度上升,就会出现这样一张图, 每个颜色的方块表示一个功能:

Vue3 Composition 组合式 API - 图2

甚至一个功能还有会依赖其他功能,全搅合在一起。
当这个组件的代码超过几百行时,这时增加或者修改某个需求, 就要在data、methods、computed以及mounted中反复的跳转,这其中的的痛苦写过的都知道。
那我们就想啊, 如果可以按照逻辑进行分割,将上面这张图变成下边这张图,是不是就清晰很多了呢, 这样的代码可读性和可维护性都更高:

Vue3 Composition 组合式 API - 图3

那么vue2.x版本给出的解决方案就是Mixin, 但是使用Mixin也会遇到让人苦恼的问题:

  1. 命名冲突问题
  2. 不清楚暴露出来的变量的作用
  3. 逻辑重用到其他 component 经常遇到问题

关于上面经常出现的问题我就不一一举例了,使用过的小伙伴多多少少都会遇到。文章的重点不是Mixin,如果确实想知道的就留言啦~

所以,我们Vue3.x就推出了Composition API主要就是为了解决上面的问题,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。接下来我们就重点认识Composition API

Composition API

image.png

setup()

setup函数是一个新的组件选项。作为在组件内使用**Composition API**的入口点。从生命周期钩子的视角来看,它会在beforeCreate钩子之前被调用,所有变量、方法都在setup函数中定义,之后return出去供外部使用。从setup返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。

export default defineComponent ({
    beforeCreate() {
        console.log("----beforeCreate----");
    },
    created() {
        console.log("----created----");
    },
    setup() {
        console.log("----setup----");
    },
})

image.png
由于在执行setup 时尚未创建组件实例,因此在 setup 选项中没有 this

你只能访问以下 property:

  • props
  • attrs
  • slots
  • emit

换句话说,你将无法访问以下组件选项:

  • data
  • computed
  • methods

该函数有2个参数:

  • props
  • context

    Props 传值

<!-- 组件传值 -->
<com-setup p1="传值给 com-setup"/>
// 通过 setup 函数的第一个形参,接收 props 数据:
setup(props) {
  console.log(props)
},
// 在 props 中定义当前组件允许外界传递过来的参数名称:
props: {
    p1: String
}

setup中接受的props是响应式的, 当传入新的props 时,会及时被更新。由于是响应式的, 所以不可以使用ES6解构,解构会消除它的响应式。

错误代码示例, 这段代码会让props不再支持响应式:

// demo.vue
export default defineComponent ({
    setup(props, context) {
        const { name } = props
        console.log(name)
    },
})

通过toRefs、toRef解构props

如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来完成此操作

import { toRefs } from 'vue'

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

  console.log(title.value)
}

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:

// MyBook.vue
import { toRef } from 'vue'
setup(props) {
  const title = toRef(props, 'title')
  console.log(title.value)
}

Context

setup 函数的第二个形参 context 是一个上下文对象,前面说了setup中不能访问Vue2中最常用的this对象,所以context中提供属性(attrsslotsemitparentroot),其对应于vue2中的this.$attrsthis.$slotsthis.$emitthis.$parentthis.$root

在 vue 3.x 中,它们的访问方式如下:

setup(props, context) {
  console.log(context)
  // Attribute (非响应式对象)
  console.log(context.attrs)

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

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

  console.log(this) // undefined
},

/*
attrs: Object
emit: ƒ ()
listeners: Object
parent: VueComponent
refs: Object
root: Vue
...
*/

解构context


export default {
  setup (props, { emit }) {
    const handleUpdate = () => {
      emit('update', 'Hello World')
    }

    return { handleUpdate }
  }
}

setup也用作在tsx中返回渲染函数:

 setup(props, { attrs, slots }) {
    return () => {
      const propsData = { ...attrs, ...props } as any;
      return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
    };
  },

注意:this关键字在setup()函数内部不可用,在方法中访问setup中的变量时,直接访问变量名就可以使用。

为什么props没有被包含在上下文中?

  1. 组件使用props的场景更多,有时甚至只需要使用props
  2. 将props独立出来作为一个参数,可以让TypeScript对props单独做类型推导,不会和上下文中其他属性混淆。这也使得setup、render和其他使用了TSX的函数式组件的签名保持一致。

使用渲染函数

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

import { h, ref, reactive } from 'vue'

export default {
  setup() {
    const readersNumber = ref(0)
    const book = reactive({ title: 'Vue 3 Guide' })
    // Please note that we need to explicitly expose ref value here
    return () => h('div', [readersNumber.value, book.title])
  }
}

reactive(), ref() 创建响应式数据

在 Vue 3.0 中,我们可以通过一个新的 ref 函数使任何响应式变量在任何地方起作用,作用等同于在vue2中的data,不同的是他们使用了ES6Porxy API解决了vue2 defineProperty 无法监听数组和对象新增属性的痛点,而且使任何响应式变量在任何地方起作用。

<template>
  <div class="contain">
    <el-button type="primary" @click="numadd">add</el-button>
    <span>{{ `${state.str}-${num}` }}</span>
  </div>
</template>

<script lang="ts">
  import { reactive, ref } from 'vue';
  interface State {
    str: string;
    list: string[];
  }

  export default {
    setup() {
      const state = reactive<State>({
        str: 'test',
        list: [],
      });
      //ref需要加上value才能获取
      const num = ref(1);
      console.log(num) // { value: 1 }
            console.log(num.value) // 1
      const numadd = () => {
        num.value++;
      };
      return { state, numadd, num };
    },
    method:{
        numAdd(){
          this.num++ //setup return的就是value,所以这里不需要加value
      }
    }

  };
</script>

reactive函数可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean等。

image.png

上面的代码中,我们绑定到页面是通过user.name,user.age;这样写感觉很繁琐,我们能不能直接将user中的属性解构出来使用呢?答案是不能直接对**user**进行解构, 这样会消除它的响应式, 这里就和上面我们**props**不能使用ES6直接解构就呼应上了。那我们就想使用解构后的数据怎么办,解决办法就是使用**toRefs**

toRefs()

将传入的reactive对象里所有的属性都转化为响应式数据对象(ref)

使用reactive return 出去的值每个都需要通过reactive对象 .属性的方式访问非常麻烦,我们可以通过解构赋值的方式范围,但是直接解构的参数不具备响应式,此时可以使用到这个api(也可以对props中的响应式数据做此处理)

将前面的例子作如下👇修改使用起来更加方便:

<template>
  <div class="contain">
    <el-button type="primary" @click="numadd">add</el-button>
-    <span>{{ `${state.str}-${num}` }}</span>
+    <span>{{ `${str}-${num}` }}</span>
  </div>
</template>

<script lang="ts">
  import { reactive, ref, toRefs } from 'vue';
  interface State {
    str: string;
    list: string[];
  }

  export default {
    setup() {
      const state = reactive<State>({
        str: 'test',
        list: [],
      });
      //ref需要加上value才能获取
      const num = ref(1);
      const numadd = () => {
        num.value++;
      };
-      return { state, numadd, num };
+      return { ...toRefs(state), numadd, num };
    },
  };
</script>

具体使用方式如下:

image.png

toRef()

toRef 用来将引用数据类型或者reavtive数据类型中的某个属性转化为响应式数据

reactive 数据类型

  /* reactive数据类型 */
      let obj = reactive({ name: '小黄', sex: '1' });
      let state = toRef(obj, 'name');

      state.value = '小红';
      console.log(obj.name); // 小红
      console.log(state.value); // 小红

      obj.name = '小黑';
      console.log(obj.name); // 小黑
      console.log(state.value); // 小黑

引用数据类型

<template>
  <span>ref----------{{ state1 }}</span>
  <el-button type="primary" @click="handleClick1">change</el-button>
  <!-- 点击后变成小红 -->
  <span>toRef----------{{ state2 }}</span>
  <el-button type="primary" @click="handleClick2">change</el-button>
  <!-- 点击后还是小黄 -->
</template>

<script>
  import { ref, toRef, reactive } from 'vue';
  export default {
    setup() {
      let obj = { name: '小黄' };
      const state1 = ref(obj.name); // 通过ref转换
      const state2 = toRef(obj, 'name'); // 通过toRef转换

      const handleClick1 = () => {
        state1.value = '小红';
        console.log('obj:', obj); // obj:小黄
        console.log('ref', state1); // ref:小红
      };

      const handleClick2 = () => {
        state2.value = '小红';
        console.log('obj:', obj); // obj:小红
        console.log('toRef', state2); // toRef:小红
      };
      return { state1, state2, handleClick1, handleClick2 };
    },
  };
</script>

Vue3 Composition 组合式 API - 图8

https://mp.weixin.qq.com/s/avfb-jJeW7f_tQVOtO93NQ


watch() 响应式更改

就像我们在组件中使用 watch 选项或者 $watch api 在 data property 上设置侦听器一样,我们也可以使用从 Vue 导入的 watch 函数执行相同的操作。
watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。

watch(source, callback, [options])

参数说明:

  • source:可以支持string,Object,Function,Array; 用于指定要侦听的响应式变量
  • callback: 执行的回调函数
  • options:支持deep、immediate 和 flush 选项。

侦听 reactive 定义的数据

import { defineComponent, ref, reactive, toRefs, watch } from "vue";
export default defineComponent({
  setup() {
    const state = reactive({ nickname: "xiaofan", age: 20 });

    setTimeout(() =>{
        state.age++
    },1000)

    // 修改age值时会触发 watch的回调
    watch(
      () => state.age,
      (curAge, preAge) => {
        console.log("新值:", curAge, "老值:", preAge);
      }
    );

    return {
        ...toRefs(state)
    }
  },
});

侦听 ref 定义的数据

const year = ref(0)
setTimeout(() =>{
    year.value ++ 
},1000)
watch(year, (newVal, oldVal) =>{
    console.log("新值:", newVal, "老值:", oldVal);
})

侦听多个数据

上面两个例子中,我们分别使用了两个watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据:

watch([() => state.age, year], ([curAge, preAge], [newVal, oldVal]) => {
    console.log("新值:", curAge, "老值:", preAge);
    console.log("新值:", newVal, "老值:", oldVal);
});

侦听复杂的嵌套对象

我们实际开发中,复杂数据随处可见, 比如:

const state = reactive({
    room: {
    id: 100,
    attrs: {
        size: "140平方米",
        type:"三室两厅"
    },
    },
});
watch(() => state.room, (newType, oldType) => {
    console.log("新值:", newType, "老值:", oldType);
}, {deep:true});

如果不使用第三个参数deep:true, 是无法监听到数据变化的。

前面我们提到,默认情况下,watch是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢?其实使用也很简单, 给第三个参数中设置immediate: true即可。关于flush配置,还在学习,后期会补充

stop 停止监听

我们在组件中创建的watch监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()函数的返回值,操作如下:

const stopWatchRoom = watch(() => state.room, (newType, oldType) => {
    console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
setTimeout(()=>{
    // 停止监听
    stopWatchRoom()
}, 3000)

watchEffect()

watchEffect 监听器

computed()

与 ref 和 watch 类似,也可以使用从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性。

import { ref, computed } from 'vue'

const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)

counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2

vue3 生命周期钩子

可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。
下表包含如何在 setup () 内部调用生命周期钩子:

vue2选项式 API vue3 Hook inside setup
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
  1. 可以看出来vue2的beforeCreatecreated变成了setup
  2. 绝大部分生命周期都是在原本vue2的生命周期上带上了on前缀使用

因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

Vue3.x中的钩子是需要从vue中导入的:

这些函数接受一个回调函数,当钩子被组件调用时将会被执行
在setup中使用生命周期:

import {  onMounted } from 'vue';

export default {
  setup() {
    onMounted(() => {
      // 在挂载后请求数据
      getList();
    })
  }
};
import { defineComponent, onBeforeMount, onMounted, onBeforeUpdate,onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured, onRenderTracked, onRenderTriggered
} from "vue";

export default defineComponent({
  // beforeCreate和created是vue2的
  beforeCreate() {
    console.log("------beforeCreate-----");
  },
  created() {
    console.log("------created-----");
  },
  setup() {
    console.log("------setup-----");

    // vue3.x生命周期写在setup中
    onBeforeMount(() => {
      console.log("------onBeforeMount-----");
    });
    onMounted(() => {
      console.log("------onMounted-----");
    });
    // 调试哪些数据发生了变化
    onRenderTriggered((event) =>{
        console.log("------onRenderTriggered-----",event);
    })
  },
});

Provide/Inject

我们也可以在组合式 API 中使用 provide/inject。两者都只能在当前活动实例的 setup() 期间调用。

设想场景

假设我们要重写以下代码,其中包含一个 MyMap 组件,将该组件改为使用组合式 API 为 MyMarker 组件提供用户的位置。

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  provide: {
    location: 'North Pole',
    geolocation: {
      longitude: 90,
      latitude: 135
    }
  }
}
</script>
<!-- src/components/MyMarker.vue -->
<script>
export default {
  inject: ['location', 'geolocation']
}
</script>

使用 Provide

在 setup() 中使用 inject 时,还需要从 vue 显式导入它。一旦我们这样做了,我们就可以调用它来定义如何将它暴露给我们的组件。
inject 函数有两个参数:

  1. 要 inject 的 property 的名称
  2. 一个默认的值 (可选) ```javascript

<a name="s1C3A"></a>
### 使用 inject

在 setup() 中使用 inject 时,还需要从 vue 显式导入它。一旦我们这样做了,我们就可以调用它来定义如何将它暴露给我们的组件。<br />inject 函数有两个参数:

1. 要 inject 的 property 的名称
1. 一个默认的值 (可选)
```javascript
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')

    return {
      userLocation,
      userGeolocation
    }
  }
}
</script>

响应性

添加响应性

为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用 ref 或 reactive

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue

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

    provide('location', location)
    provide('geolocation', geolocation)
  }
}
</script>

现在,如果这两个 property 中有任何更改,MyMarker 组件也将自动更新!

修改响应式Property

当使用响应式 provide / inject 值时,建议尽可能,在提供者内保持响应式 property 的任何更改。
例如,在需要更改用户位置的情况下,我们最好在 MyMap 组件中执行此操作。

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue

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

    provide('location', location)
    provide('geolocation', geolocation)

    return {
      location
    }
  },
  methods: {
    updateLocation() {
      this.location = 'South Pole'
    }
  }
}
</script>

然而,有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,我们建议 provide 一个方法来负责改变响应式 property。

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue

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

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

    provide('location', location)
    provide('geolocation', geolocation)
    provide('updateLocation', updateLocation)
  }
}
</script>
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')
    const updateUserLocation = inject('updateLocation')

    return {
      userLocation,
      userGeolocation,
      updateUserLocation
    }
  }
}
</script>

只读Provide禁止Inject中修改Property

最后,如果要确保通过 provide 传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly。

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, readonly, ref } from 'vue'
import MyMarker from './MyMarker.vue

export default {
  components: {
    MyMarker
  },
  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)
  }
}
</script>