Skip to content

vue3 组合式 api

概述

传统的Optional API的缺点是,结构性要求是强制的,因为必须要把所有的data写到一起,methods写到一起,computed写到一起

Composition API最大的优点在于,不强制要求结构性,只要保证把data、method、computed暴露出去即可

选项式api

js
export default {
  data() {},
  methods: {},
  computed: {},
  watch: {},
  props: [],
  emits: [],
  beforeCreate() {},
  created() {},
  beforeMount() {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {},
  unmounted() {},
  beforeRouteEnter() {},
  beforeRouteUpdate() {},
  beforeRouteLeave() {},
}

快速入门

setup方法

setup也是一种选项式api,但是作为组合式api的入口

即setup内部就要随意编写,使用return语句导出即可

ref

如果导出了一个字面量,只是导出了它的值(值传递),想要做到引用传递,就需要使用ref,并搭配value

vue
<script>
import {ref} from "vue";

export default {
  setup() {
    // 找到"张三"这一个字符串的引用
    // 将这个引用,赋值给name
    let name = ref("张三");

    let click = () => {
      // 让name这一个引用所指向的值,设置为"李四"
      name.value = "李四";
    }

    return {name, click};
  }
}
</script>

reactive

如果导出一个对象,那么使用ref是不生效的,不能用传统的(C语言、Java语言)传值、传引用思维去思考。因为传统的思考模式,如果是一个对象,那么就一定是传引用,但是在vue3中不是这样的

如果要导出一个对象,就需要在对象外边,用reactive包裹即可,不需要搭配value就可以使用

vue
<script>
import { reactive } from "vue"

export default {
  setup() {
    let person = reactive({name: '张三', age: 18})

    let click = function() {
      person.name = "李四";
    }

    return {person, click}
  }
}
</script>

数组购物车案例

如果数组中的元素是对象类型,那么ref必须用

如果数组中的所有元素都是字面值,ref可用可不用

但为了记忆方便,建议全部使用

vue
<template>
  <el-row v-for="item of cart">
    <el-col :span="6">
      <span>{{ item.name }}</span>
    </el-col>

    <el-col :span="12">
      <el-button @click="decrease(item)">-</el-button>
      <el-input v-model="item.count" style="width: 100px"/>
      <el-button @click="increase(item)">+</el-button>
    </el-col>
  </el-row>
</template>

<script>
import {ref} from 'vue'

export default {
  setup() {
    const cart = ref([{
      name: '小米',
      count: 1
    }, {
      name: '小辣椒红辣椒',
      count: 1
    }])

    const decrease = (item) => {
      item.count--;
    }

    const increase = (item) => {
      item.count++;
    }

    return {cart, decrease, increase}
  }
}
</script>

数组

如果数组中的所有元素都是字面值,ref可用可不用,但绝对不能用reactive

注意

如果用了ref,注意value的搭配使用

vue
<template>
  <ul>
    <li v-for="item of menu">{{item}}</li>
  </ul>
  <input v-model="newItem"/>
  <button @click="addItem">添加</button>
</template>

<script>
import {ref} from 'vue'

export default {
  setup() {
    let menu = ["宫保鸡丁", "鱼香肉丝", "溜肉段"];
    let newItem = ref('');
    let addItem = () => {
      menu.push(newItem.value);
      newItem.value = '';
    }

    return {menu, newItem, addItem};
  }
}
</script>

计算属性

vue
<template>
  件数: <input type="text" v-model="count"><br/>
  单价: <input type="text" v-model="price"><br/>
  总价: <input type="text" v-model="total" readonly disabled>
</template>

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

export default {
  setup() {
    const count = ref(1);
    const price = ref(9.9);

    // 组合式API中computed的用法
    // computed是一个函数
    // 在这个函数的参数中,再套一层函数
    // 内部嵌套的函数的返回值就是计算属性
    const total = computed(() => {
      return count.value * price.value;
    });

    return {count, price, total}
  }
}
</script>

监听器

vue
<template>
  <el-button @click="decrease">-</el-button>
  <el-input v-model="count" style="width:100px"/>
  <el-button @click="increase">+</el-button>
</template>

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

export default {
  setup() {
    const count = ref(1);

    const decrease = function() {
      count.value--;
    }

    const increase = function() {
      count.value++;
    }

    watch(count, (newval, oldval) => {
      console.log(newval);
      console.log(oldval);

      if (newval <= 0) {
        count.value = oldval;
      }
    })

    return {count, decrease, increase}
  }
}
</script>

生命周期函数

vue
<template>
  <el-input v-model="msg"/>
</template>

<script>
import { ref } from 'vue'
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

export default {
  setup() {
    const msg = ref("hello vue");

    console.log("beforeCreate...");
    console.log("created...");

    onBeforeMount(() => {
      console.log("beforeMount...");
    })

    onMounted(function() {
      console.log("mounted...");
    })

    onBeforeUpdate(function() {
      console.log("beforeUpdate...");
    })

    onUpdated(function() {
      console.log("updated...");
    })

    onBeforeUnmount(function() {
      console.log("beforeUnmount...");
    })

    onUnmounted(function() {
      console.log("unmounted...");
    })
    
    return {msg};
  }
}
</script>

组件通信

props down

setup的第一个参数: props

  • 父组件
vue
<template>
  <div>
    <Child msg="父传子参数"/>
  </div>
</template>

<script>
import Child from "@/components/Child.vue";
import { ref } from 'vue'

export default {
  components: {Child}
}
</script>
  • 子组件
vue
<template>
  <div>
    <h2>子组件</h2>
    <span>{{msg}}</span>
  </div>
</template>

<script>
export default {
  setup(props, context) {
    console.log(props.msg);
  },
  props: ["msg"]
}
</script>

events up

setup的第二个参数: context

  • 父组件
vue
<template>
  <Child2 @send="receive"/>
</template>

<script>
import Child2 from "@/components/Child2.vue";

export default {
  setup() {
    const receive = function(msg) {
      console.log("父组件接到了消息:");
      console.log(msg);
    }

    return { receive };
  },
  components: {Child2}
}
</script>
  • 子组件
vue
<template>
  <div>
    <button @click="send">向父组件传消息</button>
  </div>
</template>

<script>
export default {
  setup(props, context) {

    const send = function() {
      // 选项式api:
      // this.$emit("send", "参数")

      // 组合式api
      context.emit("send", "子组件传来的消息")
    }

    return {send}
  }
}
</script>

ref通信

vue
<template>
  <el-tag type="success" ref="tag">tag</el-tag>

  <button @click="getTagProp">获取tag里的属性</button>
</template>

<script>
import {ref} from 'vue'

export default {
  setup() {
    // 注意,js中的引用名,必须和组件中的ref名一模一样

    // 页面加载过程
    // 第一步,先执行setup里边的内容,再把tag作为return暴露出去
    // 第二步,渲染template标签
    // 第三步,在渲染的过程中,发现了某一个组件的ref为tag,且setup中导出了一个tag,且这个tag定义是使用了ref方法,且没写ref方法的参数
    //        所以将组件绑定到tag变量上
    const tag = ref();

    const getTagProp = function() {
      console.log(tag.value.type);
    }

    return {getTagProp, tag}
  }
}
</script>

多级组件通信

  • 第一级组件
js
import { provide } from "vue";

// 发msg,msg为hello
provide('msg','hello');
  • 第三级组件
js
import {inject} from 'vue'

// 收msg
console.log(inject('msg'));

总线通信

Vue3官方推荐mitt或tiny-emitter

在组合式api中,tiny-emitter会方便一点

mitt

  1. 安装

    bash
    pnpm i mitt
  2. 封装

    • bus/index.js
    js
    import mitt from 'mitt'
    const bus = mitt()
    
    export default bus;
  3. 消息发布

    vue
    <script>
    import bus from '@/bus'
    
    export default {
      setup() {
        bus.emit("event", "Hello!");
      }
    }
    </script>
  4. 消息订阅

    vue
    <script>
    import bus from '@/bus'
    
    export default {
      setup() {
        bus.on("event", (msg) => {
          console.log(msg);
        })
      }
    }
    </script>

tiny-emitter

  1. 安装

    bash
    pnpm i tiny-emitter
  2. 消息发布

    vue
    <script>
    import bus from 'tiny-emitter/instance'
    
    export default {
      setup() {
        bus.emit("event", "Hello");
      }
    }
    </script>
  3. 消息订阅

    vue
    <script>
    import bus from 'tiny-emitter/instance'
    
    export default {
      setup() {
        bus.on("event", msg => console.log(msg));
      }
    }
    </script>

局部守卫

注意

因为 setup 的执行时机在 routeEnter 以后

所以在组合式api中并不支持 routeEnter

需借助选项式api

vue
<script>
import { onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'

export default {
  setup() {
    onBeforeRouteUpdate(() => {
      console.log("route update...");
    })

    onBeforeRouteLeave(() => {
      console.log("route leave...");
    })
  }
}
</script>

编程式路由

源组件

vue
<script>
import { useRouter } from 'vue-router'

export default {
  setup() {
    const router = useRouter();

    const jump = function() {
      router.push({path: "/jump-to", query: {movieId: 210}});
    }

    return {jump}
  }
}
</script>

目标组件

vue
<script>
import { useRoute } from 'vue-router'
export default {
  setup() {
    const route = useRoute();

    console.log(route.query.movieId);
  }
}
</script>

vuex

vue
<template>
  <div>{{$store.state.flag}}</div>
  <div>{{$store.getters.getFlag}}</div>

  <button @click="setFlag">点击将vuex中的flag设置为true</button>
  <button @click="queryFlag">点击触发vuex中的queryFlag action</button>
</template>

<script>
import { useStore } from 'vuex'

export default {
  setup() {
    // setup时机,等于beforeCreate或created

    const store = useStore();
    console.log(store.state.flag);
    console.log(store.getters.getFlag);

    const setFlag = function() {
      store.commit("setFlag", true);
    }

    const queryFlag = function() {
      store.dispatch("queryFlag");
    }

    return {setFlag, queryFlag}
  }
}
</script>

<script setup>语法糖

优势

  1. 自动导出,无需 return
  2. 自动组件注册,无需编写 components 选项

劣势

  1. 不支持setup选项中的propscontext参数,可使用definePropsdefineEmits解决
  2. 不支持beforeRouteEnter选项,可叠加声明script标签解决

defineProps

vue
<script setup>
// 无需import

const props = defineProps({
  foo: String
})
</script>

defineEmits

vue
<script setup>
const emit = defineEmits(['send'])

emit('send', 'param');
</script>

this关键字解决方案

组合式APIthisundefined,vue3提供了getCurrentInstance() 方法用于获取当前组件实例。

vue
<script setup>
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance()
</script>

参考资料