浅谈 Vue3 组件开发 — template / jsx 🥳
前言
Vue3 是目前JavaScript框架Vue.js的最新版本,也是默认版本,它于2020年9月正式发布,带来了许多新功能和优化改进。相比于上一个版本Vue2, 它有更高的性能、更小的体积,同时它的CompositionAPI带来了更好的代码复用,并对 typescript 提供了较好的支持,更便于构建大型项目。
在 Vue2 中我们通过 选项式API
(Options API) 进行 Vue 组件的开发, 而在 Vue3 中我们则是通过组合式API
(Composition API) 开发 Vue 组件。两者之间无论是组件的定义,还是Vue API的使用都存在较大的差异。
而本篇以一个 Vue3 的 Todo List 组件在 .vue 单文件 和 .tsx 单文件中的开发, 从 选项式API
到 组合式API
逐一分析,并对 Vue3 template 和 Vue3 JSX 分别进行梳理和说明。
Vue3
(^3.3.4)Vite
(^4.4.9)TypeScript
(^5.1.6)@vitejs/plugin-vue
(^4.3.1)@vitejs/plugin-vue-jsx
(^3.0.2)
定义 Todo List 组件
Todo List 组件代码稍微有点多,我们可以根据图示进行学习,这样有助于选项式API
和 组合式API
的梳理和理解
a) .vue
单文件 — Options API
<template>
<div class="todo-container">
<div class="todo-header">
<div class="todo-title">
Todo List
</div>
</div>
<div class="todo-content">
<div class="todo-input-group">
<input
type="text"
class="todo-input"
:value="innerInput"
placeholder="What needs to be done?"
@input="(event: any) => onInput(event.target.value, event)"
@keydown.enter="(event: any) => onAdd(innerInput, event)"
>
<div
class="todo-button-add"
@click.stop="(event: any) => onAdd(innerInput, event)"
>
Add
</div>
</div>
<div
class="todo-items-group"
@scroll.passive="(event: any) => onScroll(event)"
>
<template
v-for="(item, index) of innerFilter"
:key="index"
>
<div class="todo-item-group">
<div
:class="['todo-item-content', { 'todo-item-done': item.state === true }]"
@click.stop="(event: Event) => onClick(item, event)"
>
<slot
name="item"
:item="item"
:index="index"
>
{{ index + 1 + ') ' + item.title }}
</slot>
</div>
<div class="todo-item-buttons">
<div
v-if="item.state !== true"
class="todo-button-done"
@click.stop="(event: any) => onChange(item, event)"
>
Done
</div>
<div
v-if="item.state === true"
class="todo-button-reset"
@click.stop="(event: any) => onChange(item, event)"
>
Reset
</div>
</div>
</div>
</template>
</div>
<div class="todo-item-actions">
<div class="todo-count">
<span style="margin-right: 3px">Total: </span>
<span style="color: #f34d4d">{{ innerCount }}</span>
</div>
<div class="todo-states">
<div
:class="['todo-state', { 'todo-state-active': innerState === null }]"
@click.stop="(event: any) => onFilter(null, event)"
>
All
</div>
<div
:class="['todo-state', { 'todo-state-active': innerState === false }]"
@click.stop="(event: any) => onFilter(false, event)"
>
Uncompleted
</div>
<div
:class="['todo-state', { 'todo-state-active': innerState === true }]"
@click.stop="(event: any) => onFilter(true, event)"
>
Completed
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import {
PropType,
SlotsType,
defineComponent
} from 'vue'
export interface Item {
title: string;
state: boolean;
}
export default defineComponent({
name: 'TodoList',
props: {
input: {
type: String as PropType<string>,
default: ''
},
items: {
type: Array as PropType<Item[]>,
default: () => []
},
state: {
type: Boolean as PropType<boolean | null>,
default: null
}
},
emits: {
'update:items': (input: Array<Item>) => true,
'update:state': (state: boolean | null) => true,
'scroll': (event: Event) => true,
'click': (item: Item) => true
},
slots: {} as SlotsType<{
item: {
index: number;
item: Item;
}
}>,
data() {
return {
innerInput: this.input,
innerState: this.state,
innerItems: [...this.items]
}
},
computed: {
innerFilter() {
return this.innerState !== null
? this.innerItems.filter(item => item.state === this.innerState)
: this.innerItems
},
innerCount() {
return this.innerFilter.length
}
},
watch: {
input() {
this.innerInput = this.input
},
state() {
this.innerState = this.state
},
items() {
this.innerItems = [...this.items]
}
},
methods: {
onInput(input: string, event: Event) {
this.innerInput = input
},
onAdd(input: string, event: Event) {
if (input.trim()) {
this.innerItems.push({
title: input.trim(),
state: false
})
this.$emit('update:items', [...this.innerItems])
}
this.innerInput = ''
},
onChange(item: Item, event: Event) {
item.state = !item.state
this.$emit('update:items', [...this.innerItems])
},
onFilter(state: boolean | null, event: Event) {
this.innerState = state
this.$emit('update:state', state)
},
onClick(item: Item, event: Event) {
this.$emit('click', item)
},
onScroll(event: Event) {
this.$emit('scroll', event)
}
}
})
</script>
<style lang="less" scoped>
@import './style.less';
</style>
在这里我们完全使用 Vue2 Options API
的方式定义了 Todo List 的组件。但与 Vue2 不同的是,我们提供了typescript 的支持,定义 Props、Emits、Slots的类型
配置 typescipt 支持
<script lang="ts">
定义 Prop 类型 (在父组件中提供类型提示)
props: {
state: {
type: Boolean as PropType<boolean | null>,
default: null
}
}
定义 emit 类型 (在当前组件中提供类型提示)
emits: {
'update:items': (input: Array<Item>) => true
}
定义 slot 类型 (在父组件中提供类型提示)
export interface Item {
title: string;
state: boolean;
}
slots: {} as SlotsType<{
item: { index: number; item: Item; }
}>
注意事项
定义 typescript 类型时,必须使用 export
进行导出,例如
b) .vue
单文件 — Options Setup
<template>
<div class="todo-container">
<div class="todo-header">
<div class="todo-title">
Todo List
</div>
</div>
<div class="todo-content">
<div class="todo-input-group">
<input
type="text"
class="todo-input"
:value="innerInput"
placeholder="What needs to be done?"
@input="(event: any) => onInput(event.target.value, event)"
@keydown.enter="(event: any) => onAdd(innerInput, event)"
>
<div
class="todo-button-add"
@click.stop="(event: any) => onAdd(innerInput, event)"
>
Add
</div>
</div>
<div
class="todo-items-group"
@scroll.passive="(event: any) => onScroll(event)"
>
<template
v-for="(item, index) of innerFilter"
:key="index"
>
<div class="todo-item-group">
<div
:class="['todo-item-content', { 'todo-item-done': item.state === true }]"
@click.stop="(event: Event) => onClick(item, event)"
>
<slot
name="item"
:item="item"
:index="index"
>
{{ index + 1 + ') ' + item.title }}
</slot>
</div>
<div class="todo-item-buttons">
<div
v-if="item.state !== true"
class="todo-button-done"
@click.stop="(event: any) => onChange(item, event)"
>
Done
</div>
<div
v-if="item.state === true"
class="todo-button-reset"
@click.stop="(event: any) => onChange(item, event)"
>
Reset
</div>
</div>
</div>
</template>
</div>
<div class="todo-item-actions">
<div class="todo-count">
<span style="margin-right: 3px">Total: </span>
<span style="color: #f34d4d">{{ innerCount }}</span>
</div>
<div class="todo-states">
<div
:class="['todo-state', { 'todo-state-active': innerState === null }]"
@click.stop="(event: any) => onFilter(null, event)"
>
All
</div>
<div
:class="['todo-state', { 'todo-state-active': innerState === false }]"
@click.stop="(event: any) => onFilter(false, event)"
>
Uncompleted
</div>
<div
:class="['todo-state', { 'todo-state-active': innerState === true }]"
@click.stop="(event: any) => onFilter(true, event)"
>
Completed
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import {
ref,
computed,
PropType,
SlotsType,
defineComponent
} from 'vue'
export interface Item {
title: string;
state: boolean;
}
export default defineComponent({
name: 'TodoList',
props: {
input: {
type: String as PropType<string>,
default: ''
},
items: {
type: Array as PropType<Item[]>,
default: () => []
},
state: {
type: Boolean as PropType<boolean | null>,
default: null
}
},
emits: {
'update:items': (input: Array<Item>) => true,
'update:state': (state: boolean | null) => true,
'scroll': (event: Event) => true,
'click': (item: Item) => true
},
slots: {} as SlotsType<{
item: {
index: number;
item: Item;
}
}>,
setup(props, ctx) {
const innerInput = ref(props.input)
const innerState = ref(props.state)
const innerItems = ref(props.items)
const innerFilter = computed(() => {
return innerState.value !== null
? innerItems.value.filter(item => item.state === innerState.value)
: innerItems.value
})
const innerCount = computed(() => {
return innerFilter.value.length
})
const onInput = (input: string, event: Event) => {
innerInput.value = input
}
const onAdd = (input: string, event: Event) => {
if (input.trim()) {
innerItems.value.push({
title: input.trim(),
state: false
})
ctx.emit('update:items', [...innerItems.value])
}
innerInput.value = ''
}
const onChange = (item: Item, event: Event) => {
item.state = !item.state
ctx.emit('update:items', [...innerItems.value])
}
const onFilter = (state: boolean | null, event: Event) => {
innerState.value = state
ctx.emit('update:state', state)
}
const onClick = (item: Item, event: Event) => {
ctx.emit('click', item)
}
const onScroll = (event: Event) => {
ctx.emit('scroll', event)
}
return {
innerInput,
innerState,
innerItems,
innerFilter,
innerCount,
onInput,
onAdd,
onChange,
onFilter,
onScroll,
onClick
}
}
})
</script>
<style lang="less" scoped>
@import './style.less';
</style>
对比说明
这里我们使用 Options API
+ Options Setup
混合方式定义组件。与上个小节的相比,最大的变化,是将原先 Options 里 data
、computed
、methods
、watch
的写法,改成 setup
函数中的 Composition API
定义和使用。
注意事项
-
在
Setup
函数中定义的变量、属性以及方法,必须通过return
导出,才能在 template 中进行使用 -
在
Setup
函数中定义的变量、属性以及方法,必须通过return
导出,父组件才能在引用该组件时,才能通过 ref 获取Setup
函数中定义的变量、属性以及方法
c) .vue
单文件 — Options Setup Render
<script lang="tsx">
import {
ref,
computed,
PropType,
SlotsType,
withModifiers,
defineComponent
} from 'vue'
export interface Item {
title: string;
state: boolean;
}
export default defineComponent({
name: 'TodoList',
props: {
input: {
type: String as PropType<string>,
default: ''
},
items: {
type: Array as PropType<Item[]>,
default: () => []
},
state: {
type: Boolean as PropType<boolean | null>,
default: null
}
},
emits: {
'update:items': (input: Array<Item>) => true,
'update:state': (state: boolean | null) => true,
'scroll': (event: Event) => true,
'click': (item: Item) => true
},
slots: {} as SlotsType<{
item: {
index: number;
item: Item;
}
}>,
setup(props, ctx) {
const innerInput = ref(props.input)
const innerState = ref(props.state)
const innerItems = ref(props.items)
const innerFilter = computed(() => {
return innerState.value !== null
? innerItems.value.filter(item => item.state === innerState.value)
: innerItems.value
})
const innerCount = computed(() => {
return innerFilter.value.length
})
const onInput = (input: string, event: Event) => {
innerInput.value = input
}
const onAdd = (input: string, event: Event) => {
if (input.trim()) {
innerItems.value.push({
title: input.trim(),
state: false
})
ctx.emit('update:items', [...innerItems.value])
}
innerInput.value = ''
event.stopPropagation()
}
const onChange = (item: Item, event: Event) => {
item.state = !item.state
ctx.emit('update:items', [...innerItems.value])
event.stopPropagation()
}
const onFilter = (state: boolean | null, event: Event) => {
innerState.value = state
ctx.emit('update:state', state)
}
const onClick = (item: Item, event: Event) => {
ctx.emit('click', item)
event.stopPropagation()
}
const onScroll = (event: Event) => {
ctx.emit('scroll', event)
}
ctx.expose({
innerInput,
innerState,
innerItems,
innerFilter,
innerCount,
onInput,
onAdd,
onChange,
onFilter,
onScroll,
onClick
})
return () => (
<div class='todo-container'>
<div class='todo-header'>
<div class='todo-title'>
Todo List
</div>
</div>
<div class='todo-content'>
<div class='todo-input-group'>
<input
type='text'
class='todo-input'
value={innerInput.value}
placeholder='What needs to be done?'
onInput={(event: any) => onInput(event.target.value, event)}
onKeydown={(event: any) => event.keyCode === 13 && onAdd(innerInput.value, event)}
/>
<div
class='todo-button-add'
onClick={(event: any) => onAdd(innerInput.value, event)}
>
Add
</div>
</div>
<div
class='todo-items-group' // @ts-ignore
onScrollPassive={(event: any) => onScroll(event)}
>
{
innerFilter.value.map((item, index) => {
return (
<div class='todo-item-group'>
<div
class={['todo-item-content', { 'todo-item-done': item.state === true }]}
onClick={(event: Event) => onClick(item, event)}
>
{ ctx.slots.item ? ctx.slots.item({ index, item }) : index + 1 + ') ' + item.title }
</div>
<div class='todo-item-buttons'>
<div
class={{ 'todo-button-done': item.state !== true, 'todo-button-reset': item.state === true }}
onClick={(event: any) => onChange(item, event)}
>
{ item.state !== true ? 'Done' : 'Reset' }
</div>
</div>
</div>
)
})
}
</div>
<div class='todo-item-actions'>
<div class='todo-count'>
<span style='margin-right: 3px'>Total: </span>
<span style='color: #f34d4d'>{ innerCount.value }</span>
</div>
<div class='todo-states'>
<div
class={['todo-state', { 'todo-state-active': innerState.value === null }]}
onClick={withModifiers((event: any) => onFilter(null, event), ['stop'])}
>
All
</div>
<div
class={['todo-state', { 'todo-state-active': innerState.value === false }]}
onClick={withModifiers((event: any) => onFilter(false, event), ['stop'])}
>
Uncompleted
</div>
<div
class={['todo-state', { 'todo-state-active': innerState.value === true }]}
onClick={withModifiers((event: any) => onFilter(true, event), ['stop'])}
>
Completed
</div>
</div>
</div>
</div>
</div>
)
}
})
</script>
<style lang="less" scoped>
@import './style.less';
</style>
对比说明
这次我们使用 Options Setup Render
方式定义组件,将原先 template 元素定义改成 Setup Render 函数定义。
注意事项
-
需要 jsx 语法的支持 (
typescript
和@vitejs/plugin-vue-jsx
)<script lang="tsx">
-
由于
Setup
return 导出的是渲染函数,所以如果想让父组件ref获取子组件Setup 函数中定义的属性、方法,必须使用 expose 进行导出,例ctx.expose({ innerInput, innerState, innerItems, innerFilter, innerCount, onInput, onAdd, onChange, onFilter, onScroll, onClick })
-
jsx 语法中,如何定义事件修饰符
-
如果是
stop
、prevent
、self
、ctrl
、shift
、alt
、meta
、left
、middle
、right
、exact
修饰符,可以使用 withModifiers 进行处理 -
如果是
.passive
、.capture
和.once
等修饰符,可以使用驼峰写法将他们拼接在事件名后面 -
还有一些修饰符,可以通过 Event 自定义,例如回车事件
-
d) .vue
单文件 — Script Setup
+ JSX
(推荐)
<template>
<div class="todo-container">
<div class="todo-header">
<div class="todo-title">
Todo List
</div>
</div>
<div class="todo-content">
<div class="todo-input-group">
<input
type="text"
class="todo-input"
:value="innerInput"
placeholder="What needs to be done?"
@input="(event: any) => onInput(event.target.value, event)"
@keydown.enter="(event: any) => onAdd(innerInput, event)"
>
<div
class="todo-button-add"
@click.stop="(event: any) => onAdd(innerInput, event)"
>
Add
</div>
</div>
<div
class="todo-items-group"
@scroll.passive="(event: any) => onScroll(event)"
>
<template
v-for="(item, index) of innerFilter"
:key="index"
>
<div class="todo-item-group">
<div
:class="['todo-item-content', { 'todo-item-done': item.state === true }]"
@click.stop="(event: Event) => onClick(item, event)"
>
<slot
name="item"
:item="item"
:index="index"
>
{{ index + 1 + ') ' + item.title }}
</slot>
</div>
<div class="todo-item-buttons">
<div
v-if="item.state !== true"
class="todo-button-done"
@click.stop="(event: any) => onChange(item, event)"
>
Done
</div>
<div
v-if="item.state === true"
class="todo-button-reset"
@click.stop="(event: any) => onChange(item, event)"
>
Reset
</div>
</div>
</div>
</template>
</div>
<div class="todo-item-actions">
<div class="todo-count">
<span style="margin-right: 3px">Total: </span>
<span style="color: #f34d4d">{{ innerCount }}</span>
</div>
<div class="todo-states">
<div
:class="['todo-state', { 'todo-state-active': innerState === null }]"
@click.stop="(event: any) => onFilter(null, event)"
>
All
</div>
<div
:class="['todo-state', { 'todo-state-active': innerState === false }]"
@click.stop="(event: any) => onFilter(false, event)"
>
Uncompleted
</div>
<div
:class="['todo-state', { 'todo-state-active': innerState === true }]"
@click.stop="(event: any) => onFilter(true, event)"
>
Completed
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="tsx">
import { ref, computed } from 'vue'
export interface Item {
title: string;
state: boolean;
}
export interface Emits {
(e: 'update:items', input: Item[]): void;
(e: 'update:state', state: boolean | null): void;
(e: 'scroll', event: Event): void;
(e: 'click', item: Item): void;
}
export interface Slots {
item(props: { index: number; item: Item; }): any;
}
export interface Props {
input: string;
items: Item[];
state: boolean | null;
}
defineOptions({
name: 'TodoList',
inheritAttrs: false
})
// eslint-disable-next-line no-unused-vars
const slots = defineSlots<Slots>()
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const innerInput = ref(props.input)
const innerState = ref(props.state)
const innerItems = ref(props.items)
const innerFilter = computed(() => {
return innerState.value !== null
? innerItems.value.filter(item => item.state === innerState.value)
: innerItems.value
})
const innerCount = computed(() => {
return innerFilter.value.length
})
const onInput = (input: string, event: Event) => {
innerInput.value = input
}
const onAdd = (input: string, event: Event) => {
if (input.trim()) {
innerItems.value.push({
title: input.trim(),
state: false
})
emit('update:items', [...innerItems.value])
}
innerInput.value = ''
event.stopPropagation()
}
const onChange = (item: Item, event: Event) => {
item.state = !item.state
emit('update:items', [...innerItems.value])
event.stopPropagation()
}
const onFilter = (state: boolean | null, event: Event) => {
innerState.value = state
emit('update:state', state)
}
const onClick = (item: Item, event: Event) => {
emit('click', item)
event.stopPropagation()
}
const onScroll = (event: Event) => {
emit('scroll', event)
}
defineExpose({
innerInput,
innerState,
innerItems,
innerFilter,
innerCount,
onInput,
onAdd,
onChange,
onFilter,
onScroll,
onClick
})
</script>
<style lang="less" scoped>
@import './style.less';
</style>
配置 typescipt + jsx 支持
<script setup lang="tsx">
定义 组件名称 name
defineOptions({
name: 'TodoList',
inheritAttrs: false
})
定义 Prop 类型 (在父组件中提供类型提示)
export interface Props {
input: string;
items: Item[];
state: boolean | null;
}
const props = defineProps<Props>()
定义 emit 类型 (在当前组件中提供类型提示)
export interface Emits {
(e: 'update:items', input: Item[]): void;
(e: 'update:state', state: boolean | null): void;
(e: 'scroll', event: Event): void;
(e: 'click', item: Item): void;
}
const emit = defineEmits<Emits>()
定义 slot 类型 (在父组件中提供类型提示)
export interface Slots {
item(props: { index: number; item: Item; }): any;
}
const slots = defineSlots<Slots>()
定义 导出的变量、属性以及方法
defineExpose({
innerInput,
innerState,
innerItems,
innerFilter,
innerCount,
onInput,
onAdd,
onChange,
onFilter,
onScroll,
onClick
})
定义 script setup
中的 Vue Node
我们还可以把 Todo List 列表部分,从 template 剥离出来,定义在 script setup
中。
-
在
script setup
中定义 TodoItemsconst TodoItems = () => innerFilter.value.map((item, index) => { return ( <div class='todo-item-group'> <div class={['todo-item-content', { 'todo-item-done': item.state === true }]} onClick={(event: Event) => onClick(item, event)} > { slots.item ? slots.item({ index, item }) : index + 1 + ') ' + item.title } </div> <div class='todo-item-buttons'> <div class={{ 'todo-button-done': item.state !== true, 'todo-button-reset': item.state === true }} onClick={(event: any) => onChange(item, event)} > { item.state !== true ? 'Done' : 'Reset' } </div> </div> </div> ) })
-
在 template 中
class="todo-items-group"
部分替换成如下即可<div class="todo-items-group" @scroll.passive="(event: any) => onScroll(event)" > <TodoItems /> </div>
-
但是美中不足,这样一来,样式上不能设置组件作用域,需移除 scoped
<style lang="less">
@import './style.less';
</style>
e) .tsx
单文件 — Options Setup Render
import './style.less'
import {
ref,
computed,
PropType,
SlotsType,
withModifiers,
defineComponent
} from 'vue'
export interface Item {
title: string;
state: boolean;
}
export default defineComponent({
name: 'TodoList',
props: {
input: {
type: String as PropType<string>,
default: ''
},
items: {
type: Array as PropType<Item[]>,
default: () => []
},
state: {
type: Boolean as PropType<boolean | null>,
default: null
}
},
emits: {
'update:items': (input: Array<Item>) => true,
'update:state': (state: boolean | null) => true,
'scroll': (event: Event) => true,
'click': (item: Item) => true
},
slots: {} as SlotsType<{
item: {
index: number;
item: Item;
}
}>,
setup(props, ctx) {
const innerInput = ref(props.input)
const innerState = ref(props.state)
const innerItems = ref(props.items)
const innerFilter = computed(() => {
return innerState.value !== null
? innerItems.value.filter(item => item.state === innerState.value)
: innerItems.value
})
const innerCount = computed(() => {
return innerFilter.value.length
})
const onInput = (input: string, event: Event) => {
innerInput.value = input
}
const onAdd = (input: string, event: Event) => {
if (input.trim()) {
innerItems.value.push({
title: input.trim(),
state: false
})
ctx.emit('update:items', [...innerItems.value])
}
innerInput.value = ''
event.stopPropagation()
}
const onChange = (item: Item, event: Event) => {
item.state = !item.state
ctx.emit('update:items', [...innerItems.value])
event.stopPropagation()
}
const onFilter = (state: boolean | null, event: Event) => {
innerState.value = state
ctx.emit('update:state', state)
}
const onClick = (item: Item, event: Event) => {
ctx.emit('click', item)
event.stopPropagation()
}
const onScroll = (event: Event) => {
ctx.emit('scroll', event)
}
ctx.expose({
innerInput,
innerState,
innerItems,
innerFilter,
innerCount,
onInput,
onAdd,
onChange,
onFilter,
onScroll,
onClick
})
return () => (
<div class='todo-container'>
<div class='todo-header'>
<div class='todo-title'>
Todo List
</div>
</div>
<div class='todo-content'>
<div class='todo-input-group'>
<input
type='text'
class='todo-input'
value={innerInput.value}
placeholder='What needs to be done?'
onInput={(event: any) => onInput(event.target.value, event)}
onKeydown={(event: any) => event.keyCode === 13 && onAdd(innerInput.value, event)}
/>
<div
class='todo-button-add'
onClick={(event: any) => onAdd(innerInput.value, event)}
>
Add
</div>
</div>
<div
class='todo-items-group' // @ts-ignore
onScrollPassive={(event: any) => onScroll(event)}
>
{
innerFilter.value.map((item, index) => {
return (
<div class='todo-item-group'>
<div
class={['todo-item-content', { 'todo-item-done': item.state === true }]}
onClick={(event: Event) => onClick(item, event)}
>
{ ctx.slots.item ? ctx.slots.item({ index, item }) : index + 1 + ') ' + item.title }
</div>
<div class='todo-item-buttons'>
<div
class={{ 'todo-button-done': item.state !== true, 'todo-button-reset': item.state === true }}
onClick={(event: any) => onChange(item, event)}
>
{ item.state !== true ? 'Done' : 'Reset' }
</div>
</div>
</div>
)
})
}
</div>
<div class='todo-item-actions'>
<div class='todo-count'>
<span style='margin-right: 3px'>Total: </span>
<span style='color: #f34d4d'>{ innerCount.value }</span>
</div>
<div class='todo-states'>
<div
class={['todo-state', { 'todo-state-active': innerState.value === null }]}
onClick={withModifiers((event: any) => onFilter(null, event), ['stop'])}
>
All
</div>
<div
class={['todo-state', { 'todo-state-active': innerState.value === false }]}
onClick={withModifiers((event: any) => onFilter(false, event), ['stop'])}
>
Uncompleted
</div>
<div
class={['todo-state', { 'todo-state-active': innerState.value === true }]}
onClick={withModifiers((event: any) => onFilter(true, event), ['stop'])}
>
Completed
</div>
</div>
</div>
</div>
</div>
)
}
})
定义说明
近乎等同于 .vue
单文件 — Options Setup Render
,一般用于开发公共组件(非业务组件)
定义 Todo List 父组件
现在,我们已经梳理好了 Todo List 组件各种方式的定义和各自的差异。接下来我们定义使用 Todo List 的父组件 UseTodoList 组件
a) .vue
单文件 — Script Setup
+ JSX
(推荐)
<template>
<TodoList
ref="todo"
v-model:items="items"
v-model:state="state"
:input="input"
>
<template #item="{index, item}">
<span>{{ index + 1 + '、' + item.title }}</span>
</template>
</TodoList>
</template>
<script setup lang="ts">
import TodoList from '@/components/TodoList.vue'
import { onMounted, ref, watch } from 'vue'
defineOptions({
name: 'UseTodoList'
})
const input = ref('')
const state = ref(null)
const todo = ref(null)
const items = ref([
{
title: '晨跑3公里',
state: true
},
{
title: '午休30分钟',
state: true
},
{
title: '看书2小时',
state: false
}
])
watch(state, () => { console.log('watch/state: ', state.value) })
watch(items, () => { console.log('watch/items: ', items.value) })
onMounted(() => { console.log(todo.value) })
</script>
b) .tsx
单文件 — Options Setup Render
import { defineComponent, onMounted, watch, ref } from 'vue'
import TodoList, { Item } from '@/components/TodoList'
export default defineComponent({
name: 'UseTodoList',
setup() {
const input = ref('')
const state = ref(null)
const todo: any = ref(null)
const items = ref([
{
title: '晨跑3公里',
state: true
},
{
title: '午休30分钟',
state: true
},
{
title: '看书2小时',
state: false
}
])
watch(state, () => { console.log('watch/state: ', state.value) })
watch(items, () => { console.log('watch/items: ', items.value) })
onMounted(() => { console.log(todo.value) })
return () => (
<TodoList
ref={el => { todo.value = el }}
input={input.value}
v-models={[[items.value, 'items'], [state.value, 'state']]}
v-slots={{ item: (opts: { index: number, item: Item }) => opts.index + 1 + '、' + opts.item.title }}
/>
)
}
})
注意事项
jsx 语法中需注意 ref
、v-models
、slots
的定义和使用,其中 v-model
和 v-models
都只是语法糖:
- 单个
v-model
: 例v-model={[items.value, 'items']}
- 多个
v-model
: 例v-models={[[items.value, 'items'], [state.value, 'state']]}
它等价于
<TodoList
{
...{
'items': items.value,
'state': state.value,
'onUpdate:items': value => { items.value = value },
'onUpdate:state': value => { state.value = value }
}
}
ref={el => { todo.value = el }}
input={input.value}
v-slots={{ item: (opts: { index: number, item: Item }) => opts.index + 1 + '、' + opts.item.title }}
/>
相关链接
转载自:https://juejin.cn/post/7279720525362216960