Vue 2
父组件:📃src/views/design/indexV2.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
// src/views/design/indexV2.vue <template> <div style="padding: 15px"> <!-- 可以动态赋予一个变量的值 --> <!-- 也可以不用`:`传入一个静态的值 --> <design-tabs :tab-options="tabOptions" :active-tab="activeName" ></design-tabs> </div> </template> <script> import DesignTabs from './DesignTabsV2.vue' export default { name: 'DesignV2', components: { DesignTabs }, data() { return { tabOptions: [ { label: '衣服', key: 'clothingDesign', icon: 'cherry' }, { label: '帽子', key: 'hatDesign', icon: 'cold-drink' }, { label: '图案', key: 'patternDesign', icon: 'lollipop' }, ], activeName: 'clothingDesign', } }, } </script> |
子组件 DesignTabs
:📃src/views/design/components/DesignTabsV2.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
// src/views/design/components/DesignTabsV2.vue <template> <el-tabs v-model="activeName"> <el-tab-pane v-for="item in tabOptions" :label="item.label" :name="item.key" > <template #label> <span> <i :class="`el-icon-${item.icon}`" /> {{ item.label }} </span> </template> <template #default> 假设这是一个渲染<strong style="color: #409eff">{{ item.label }}</strong >列表的子组件 </template> </el-tab-pane> </el-tabs> </template> <script> export default { name: 'DesignTabsV2', // 表示接收的 props // 如果定义成对象而不是数组,可以设置 prop 类型、默认值、是否必须 props: ['tabOptions', 'activeTab'], data() { return { // 如果 prop 用来传递一个初始值 // 子组件希望将其作为一个本地的数据来使用: activeName: this.activeTab, } } } </script> |
Vue 3
转化成 Composition API 后的子组件:(一贯省略重复代码)
📃src/views/design/components/DesignTabsV3.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
// src/views/design/components/DesignTabsV3.vue <script> import { ref, toRefs } from 'vue' export default { name: 'DesignTabsV3', props: ['tabOptions', 'activeTab'], setup(props) { const { tabOptions, activeTab } = toRefs(props) const activeName = ref(activeTab) const handleClick = (tab, event) => { console.log(tab, event) } return { tabOptions, activeTab, activeName, handleClick, } }, } </script> |
5. 计算属性computed
比如我们现在希望在点击 tab 标签的时候能获得 tab 的序号(index):
Vue 2
组件 DesignTabs
:📃src/views/design/components/DesignTabsV2.vue
|
// src/views/design/components/DesignTabsV2.vue export default { name: 'DesignTabsV2', computed: { tabIndex() { return this.tabOptions.findIndex(item => item.key === this.activeName) }, }, methods: { handleClick(tab, event) { console.log(this.tabIndex) // tab 对应的 index console.log(tab.index, tab.props) // 这样获得的`index`是字符串,props 可以获得传入`tab-pane`的属性 }, }, } |
Vue 3
这里就比较不一样了,props 的值是不允许在子组件直接修改的。子组件的activeName
状态初始值为传入的activeTab
,如果activeTab
和tabOptions
一样通过 toRefs
或 toRef
包装,那么activeName
的修改是和activeTab
深度响应的,这样会报错;
如果要同步修改父组件的值,可以通过emit
事件(后面会说)
组件 DesignTabs
:📃src/views/design/components/DesignTabsV3.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
// src/views/design/components/DesignTabsV3.vue import { ref, toRefs, computed } from 'vue' export default { name: 'DesignTabsV3', setup(props) { const { tabOptions } = toRefs(props) // 切记 props 只读,不允许在子组件直接修改 const activeName = ref(props.activeTab) // 将其赋为组件状态的初始值,直接 props.xx // const { tabOptions, activeTab } = toRefs(props) // const activeName = ref(activeTab) // 这样达咩 const tabIndex = computed(() => tabOptions.value.findIndex(item => { return item.key === activeName.value }) ) const handleClick = (tab, event) => { console.log(tabIndex.value) } return { tabOptions, activeName, handleClick, } } } |
这里想额外提一嘴computed
可以包在reactive
内使用,在组件数据比较简单的情况下甚至可以直接用reactive
包起组件全部数据,就好像 Option Api 的 data 选项那样。这样做的目的是在setup()
访问不用再带上.value
。
但这同时会带来一个问题,在模版需要使用state.xx
去渲染,而且如果我们 return 的时候把state
解构,包含的状态会失去响应性,那就得不偿失了。需要用toRefs
包裹再传递,即可维持其响应性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
// src/views/design/components/DesignTabsV3.vue import { reactive, computed, toRefs } from 'vue' export default { name: 'DesignTabsV3', setup(props) { const state = reactive({ tabOps: props.tabOptions activeName: props.activeTab tabIndex: computed(() => tabOptions.findIndex(item => { return item.key === state.activeName }) }) return { // ...state // 这样会失去属性的响应式 ...toRefs(state) } // 甚至可以直接这样: return toRefs(state) } } |
6. 侦听器watch
获取这个tabIndex
有什么用呢,目的是在当前标签变化时根据index
刷新对应TabPane
的子组件的数据。但是如果在标签点击事件触发数据刷新也不合适,因为我们不希望重复点击相同tab
时也去刷新。那么这个时候watch
或者watchEffect
就登场了。
Vue 2
组件 DesignTabs
:📃src/views/design/components/DesignTabsV2.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
// src/views/design/components/DesignTabsV2.vue <template> <!--省略部分 ...--> <template #default> 这是一个渲染<strong style="color: #409eff">{{ item.label }}</strong >列表的子组件 <!--现在我们加上了 DesignList 组件--> <design-list :ref="'designList' + i" :type="item.key" /> </template> <!--省略部分...--> </template> <script lang="jsx"> import { defineComponent, toRef } from 'vue' // DesignList 组件,这里图方便混合组合式和选项式 API 写了 const DesignList = defineComponent({ name: 'DesignList', props: ['type'], setup(props) { const key = toRef(props, 'type') return () => <div>{key.value}</div> }, methods: { fetchData() { console.log(`重新请求${this.type}数据`) }, }, }) export default { name: 'DesignTabsV2', watch: { activeName(val) { console.log(`${val}`) // 触发子组件DesignList的数据请求 this.$refs[`designList${this.tabIndex}`].fetchData() }, }, } |
Vue 3
这个写法可能不是很直观,可以看下 ➡️ 组合式 API 模板引用在 v-for 中的用法
组件 DesignTabs
:📃src/views/design/components/DesignTabsV3.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
// src/views/design/components/DesignTabsV3.vue <template> <!--省略部分 ...--> <!--别忘了这是v-for遍历生成的组件--> <design-list :ref="el => { if (el) designList[i] = el }" :type="item.key" :key="item.key + i" /> <!--省略部分 ...--> </template> <script> import { ref, toRefs, computed, watch, defineComponent, toRef, onBeforeUpdate } from 'vue' // DesignList 组件同上面代码👆 export default { name: 'DesignTabsV3', components: { DesignList }, setup(props) { const designList = ref([]) // 确保在每次更新之前重置ref onBeforeUpdate(() => { designList.value = [] }) watch(activeName, val => { console.log(`${val}`) designList.value[tabIndex.value].fetchData() }) }, } </script> |
watch 和 watchEffect 的功能是等效的,都是侦听其依赖,并在依赖值变更时重新运行定义的函数。两者区别:
watch
:
- 必须在第一个参数明确指定跟踪的依赖;
侦听器数据源只能是getter/effect
函数、ref
、reactive
对象,或者包含这些类型(的数据)的数组。
换句话说,只要侦听数据不是ref
、reactive
对象,就必须传入一个箭头函数。
打个比方,若要侦听reactive
对象的某个属性(例:const state = reactive({ count: 0 })
的 count
),便不能像侦听单个ref
或整个reactive
对象那样直接传一个变量,而是必须在第一个参数传入一个回调函数,如() => state.count
;
- 第二个参数是依赖值变更时执行的回调,函数内能访问被侦听状态的当前值和前一个值;
- 组件初始化时不会执行回调。如果需要可在第三个参数(
Object
)中设置immediate: true
;
- 如果要对多层嵌套状态深度侦听,在第三个参数中设置
deep: true
watchEffect
:
- 无需手动传入依赖项;
- 只有一个参数,即侦听数据变更的回调函数,会自动跟踪所有函数中用到的变量;
- 组件初始化时即会执行一次。
7. 子组件向父组件通信(触发父组件方法)
Vue 2
父组件:📃src/views/design/indexV2.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
// src/views/design/indexV2.vue <template> <div style="padding: 15px"> <design-tabs :tab-options="tabOptions" :active-tab="activeName" :tab-change="handleTabChange" ></design-tabs> </div> </template> <script> export default { name: 'DesignV2', methods: { handleTabChange(tab) { this.activeName = tab }, } } </script> |
子组件 DesignTabs
:📃src/views/design/components/DesignTabsV2.vue
|
// src/views/design/components/DesignTabsV2.vue export defalut { name: 'DesignTabsV2', watch: { activeName(val) { this.$emit('tabChange', val) }, }, } |
Vue 3
子组件 DesignTabs
:📃src/views/design/components/DesignTabsV3.vue
|
// src/views/design/components/DesignTabsV3.vue export default { name: 'DesignTabsV3', emits: ['tabChange'], setup(props, context) { watch(activeName, val => { context.emit('tabChange', val) }) } } |
8. 使用 Vue Router
Vue 2
📃src/views/design/components/DesignTabsV2.vue
|
// src/views/design/components/DesignTabsV2.vue export defalut { name: 'DesignTabsV2', watch: { activeName(val) { this.$router.push(`${this.$route.path}?tab=${val}`) }, }, } |
Vue 3
📃src/views/design/components/DesignTabsV3.vue
|
// src/views/design/components/DesignTabsV3.vue import { useRouter, useRoute } from 'vue-router' export default { name: 'DesignTabsV3', setup() { const router = useRouter() const route = useRoute() watch(activeName, val => { router.push(`${route.path}?tab=${val}`) }) }, } |
9. 获取 Vuex 对象
Vue 2
|
export default { mounted() { console.log(this.$store.state.name) this.$store.commit('show') } } |
Vue 3
|
<script> import { onMounted } from 'vue' import { useStore } from 'vuex' export default { setup(props) { const store = useStore() onMounted(() => { console.log(store.name) store.commit('show') }) } } </script> |
参考链接
从 Vue 2 到 Vue 3 组织代码的思维转变(一)- 获取组件实例