vue记录点
1.vue2路由监听
watch: { $route: function (to, from) { //事件处理 } }
2.vue3路由监听
watch( () => Router.currentRoute.value.path, () => { // 处理事件 } );
3.setup
setup 不是生命周期钩子函数!
它只是基于 beforeCreate 运行,但函数内部无法通过 this 获取组件实例
而且 setup 有着生命周期钩子不具备的参数:props 和 context
setup(props, context) { // 组件 props console.log(props); const { attrs, slots, emit } = context; // Attribute (非响应式对象) console.log(attrs); // 组件插槽 (非响应式对象) console.log(slots); // 触发事件 (方法) console.log(emit); }
这里的 props 就是组件内部定义的 props,是由父组件传入的参数
假如父组件传入了一个属性名为 title 的 props,在 setup 中可以通过 props.title 直接取到
而且这个 props 是响应式的,当传入新的 prop 时,props 会被更新
但正是因为 props 是响应式的,所以不能直接对 props 使用 ES6 解构,因为它会消除 prop 的响应性
Vue 3 提供了一个 toRefs 全局方法来解决这个问题:
import { toRefs } from 'vue'; setup (props) { // 通过 toRefs 包装后的 props 可以在 ES6 解构之后依然具有响应性 const { title } = toRefs(props); console.log(title); }
4. 在 setup 中注册生命周期钩子
如果是在 setup 内部注册生命周期钩子,则需要从 Vue 引入带有 on 前缀的生命周期工具函数
import { toRefs, onMounted } from 'vue'; setup (props) { // 使用 `toRefs` 创建对prop的 `title` property 的响应式引用 const { title } = toRefs(props); // 注册生命周期钩子 onMounted(() => { console.log('onMounted', title); }); }
上面的 onMounted 会接收一个函数作为参数,在组件中对应的生命周期钩子 (mounted) 触发时,会执行这个被传入的函数
除了 onMounted 以外,其他 created 之后的生命周期也有对应的注册函数,如 onUpdated、onBeforeUnmount 等
但 setup 中并没有 beforeCreate 和 created 这两个生命周期的注册函数
因为 setup 本身是基于 beforeCreate 运行,会用到 beforeCreate 和 created 的场景都可以通过 setup 替代
也就是说,在 beforeCreate 和 created 中编写的任何代码都应该直接在 setup 函数中编写
5. setup 的返回值
如果 setup 显式的返回了一个对象,这个对象的所有内容都会暴露给组件的其余部分
setup() { return { name: 'wise wrong', foo: (text: string) => { console.log(`Hello ${text}`); }, }; }, mounted() { // 直接使用 setup 的返回值 this.foo(this.name); }
上面的 setup 返回了一个 foo 函数,还有一个 name 字段,目前这个 name 还不是一个响应式变量
为此我们需要使用 ref 全局方法
import { ref } from 'vue'; setup (props) { return { name: ref('wise wrong'); } }
通过 ref 包装的变量会具有响应性,在组件中可以像正常的组件属性一样使用
但在 setup 内部,ref 包装后的变量需要通过 value 来修改变量的值
import { ref } from 'vue'; setup() { const name = ref('wise wrong'); onMounted(() => { // 在 setup 内部通过 value 修改变量值 name.value = "Let's study Vue 3"; }); return { name, }; }, methods: { test() { // 在组件中可以直接修改 ref 变量 this.name = 'good job'; }, },
除了返回一个对象以外,setup 还可以返回一个渲染函数
import { h, ref, reactive } from 'vue' export default { setup() { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // 返回一个渲染函数以覆盖 template return () => h('div', [readersNumber.value, book.title]) } }
如果直接返回了渲染函数,组件中定义的 template 将会失效
在 Setup 中使用 Computed、Watch
setup 的本意是用来替代 mixin,因此除了 data、methods、生命周期钩子之外,还有许多组件选项需要解决
这里先介绍 computed 和 watch
1. Computed
在组件中 computed 用来管理包含响应式数据的复杂逻辑
computed: { booksMessage() { return this.books.length > 0 ? 'Yes' : 'No' } }
计算属性可以在组件中直接使用,并会随着响应式数据的更新而更新
<template> <p>{{ booksMessage }}</p> </template>
而在 setup 中,需要通过全局函数 computed 来包装
import { ref, computed } from 'vue' setup() { const books = ref([]); const booksMessage = computed(() => books.value.length > 0 ? 'Yes' : 'No'); return { books, booksMessage, } }
2. Watch
watch 用来监听数据的变化
可以通过 watch 定义对应的处理函数,当数据发生变化时候会调用该函数
data: () => ({ question: '', answer: 'good luck :)' }), watch: { question(newQuestion, oldQuestion) { // 当 question 变化的时候会执行该函数 if (newQuestion.indexOf('?') > -1) { console.log('answer: ', this.answer); } } },
在 setup 中,也需要使用全局函数 watch 来包装
import { ref, watch } from 'vue' setup() { const question = ref(''); const answer = ref('good luck :)'); watch(question, (newValue, oldValue) => { if (newValue.indexOf('?') > -1) { console.log('answer: ', this.answer.value); } }); return { question, answer, } }
抽取公共逻辑
在了解了基本的 Composition API 之后,可以尝试将公共逻辑抽取出来单独维护
假设有以下两个逻辑点需要被抽取:
1. 在组件加载完成后,通过父组件传入的参数 query 请求数据列表 list,当 query 变化时需要重新请求并更新 list;
2. 根据关键字 keyword 从数据列表 list 中筛选数据,并使用 computed 记录结果。
这两段逻辑可以直接在 setup 中实现,但这样会使得 setup 看起来非常累赘
我们可以把这段逻辑拆成两个函数并单独维护
首先在项目目录 src 下创建一个新的文件夹 composables
然后创建 useList.js 文件,完成第一个逻辑:
// src/composables/useList.js import { ref, onMounted, watch } from 'vue'; // 发起接口请求 function fetchList() { // ... } export default function useList(query) { // 创建数据列表 list (data) const list = ref([]); // 创建查询并更新 list 的方法 (methods) const getList = async () => { list.value = await fetchList(query); }; // 生命周期 mounted onMounted(getList); // 监听 query 的变化并更新 list watch(query, getList); return { list, getList, }; }
然后创建 useFilter.js 文件,完成第二个逻辑:
// src/composables/useFilter.js import { ref, computed } from 'vue'; export default function useFilter(list) { // 创建搜索关键字 keyword (data) const keyword = ref(''); // 创建筛选结果 filterRes (computed) const filterRes = computed(() => { return list.filter((value) => { return value.includes(keyword.value); }); }); return { keyword, filterRes, }; }
然后在组件中引入,并在 setup 返回组件需要的字段
import { defineComponent, toRefs } from 'vue'; import useList from '@/composables/useList'; import useFilter from '@/composables/useFilter'; export default defineComponent({ props: ['query'], setup(props) { const { query } = toRefs(props); const { list, getList } = useList(query); const { keyword, filterRes } = useFilter(list); return { keyword, // 筛选关键字 getList, // 查询并更新数据列表 list: filterRes, // 不需要返回未经筛选的列表 }; }, });
表单部分
校验相关
<el-form ref="ruleForm" :rules="myRules"></el-form> // 整个表单校验 this.$refs.ruleForm.validate(); // 单个表单字段credentialsFiles校验 this.$refs.ruleForm.validateField("credentialsFiles"); // 移除校验结果 this.$refs.ruleForm.clearValidate(); // 移除校验结果并重置字段值 this.$refs.ruleForm.resetFields(); // 校验规则 myRules: { enterpriseId: [{ required: true, message: messageContant, trigger: "change" }], contractTimeData: { validator: (rule, value, cb) => { if (value == "" || value == null || (value.length && value.length == 0)) cb(new Error($i18n.$ct("请输入内容", "Please input"))); cb(); }, trigger: "blur", }, description: this.isChecked ? [] : [ { required: true, message: messageContant, trigger: "change" }, { pattern: /^.{5,200}$/, message: $i18n.$ct("内容长度5-200", "Content length 5-200"), trigger: "change" }, ], },
表格部分
根据某一属性合并行或列
// 在table加span-method属性 <el-table :data="tableData" :span-method="objectSpanMethod" > <el-table-column :type="要合并列标志" :label="测试" > <el-table-column :label="测试2" > </el-table> // data spanArr: [], pos: 0, // methods /** * 计算合并数组 在获取表格数据后调用 * data 表格数据 * prop 根据这个属性合并 */ getSpanArr(data, prop) { // data就是我们从后台拿到的数据 for (let i = 0; i < data.length; i += 1) { if (i === 0) { this.spanArr.push(1); this.pos = 0; } else { // 判断当前元素与上一个元素是否相同 if (data[i][prop] === data[i - 1][prop]) { this.spanArr[this.pos] += 1; this.spanArr.push(0); } else { this.spanArr.push(1); this.pos = i; } } } }, // 行合并 objectSpanMethod({ column, rowIndex }) { if (column.type === "merge") { const myRow = this.spanArr[rowIndex]; const myCol = myRow > 0 ? 1 : 0; return { // [0,0] 表示这一行不显示, [2,1]表示行的合并数 rowspan: myRow, colspan: myCol, }; } },