一、jsx 结构
1. 结构代码示例
setup()返回一个渲染函数- 渲染函数返回 JSX 结构
- JSX 最终被编译为
h()函数调用 index.jsx
export default {
setup() {
// 返回一个函数,该函数返回JSX元素
return () => <div>123</div>;
}
};
2. setup 说明
setup() 关键特性解析:
1. 渲染函数模式:
setup()返回的是一个函数 而不是对象- 这个返回的函数就是组件的 渲染函数
- 当组件需要渲染时,Vue 会自动调用这个函数
2. JSX 语法:
- 使用 JSX 语法 (
<div>123</div>) 描述 UI- 相当于 Vue 的
h()函数 :h('div', {}, '123')- 需要 Babel 插件(如
@vue/babel-plugin-jsx)支持
3. 与常规 setup 的区别:
常规 setup 本例 setup 返回数据对象给模板使用 直接返回渲染函数 需要单独的 <template>部分渲染逻辑全在 JS 中 适合简单组件 适合需要灵活控制渲染的场景
4. 实际等效代码:
import { h } from 'vue'; export default { setup() { // 使用 h 函数实现相同效果 return () => h('div', {}, '123'); } }
3. h 函数
在 Vue 中,h 函数是创建虚拟 DOM 节点(VNode)的核心函数。它的名字来源于"hyperscript",意为"生成 HTML 结构的脚本"。让我们深入理解它的作用和用法:
核心概念
1. 虚拟 DOM (Virtual DOM) :
- JavaScript 对象表示的真实 DOM 的轻量级副本
- Vue 通过比较新旧 VNode 来高效更新真实 DOM
- 避免直接操作真实 DOM,提高性能
2.
h函数的作用:
- 创建描述 DOM 节点的 JavaScript 对象(VNode)
- 接收参数定义节点类型、属性和子元素
- JSX 语法最终会被编译为
h函数调用
函数签名
function h(
type: string | Component, // HTML 标签名或 Vue 组件
props?: object, // 属性/事件对象
children?: string | Array<VNode> // 文本内容或子 VNode
): VNode
使用示例
| 场景 | JSX 写法 | h 函数等价写法 |
|---|---|---|
| 文本元素 | <div>Hello</div> | h('div', 'Hello') |
| 带属性的元素 | <div class="box"></div> | h('div', { class: 'box' }) |
| 嵌套元素 | <div><span>Hi</span></div> | h('div', [h('span', 'Hi')]) |
| 组件使用 | <MyComponent prop1="value" /> | h(MyComponent, { prop1: 'value' }) |
在 Vue 3 中的特殊作用
// JSX
<button onClick={handler}>Click</button>
// 编译后
h('button', { onClick: handler }, 'Click')
为什么需要 h 函数?
- 跨平台能力:相同的 VNode 可在不同环境渲染(Web、Canvas、Native)
- 渲染优化:通过 diff 算法最小化 DOM 操作
- 灵活组合:可用纯 JavaScript 表达任意 UI 结构
- 类型安全:在 TypeScript 中提供更好的类型支持
二、常用指令
1. v-model
import { reactive } from "vue"
export default {
setup() {
// 创建响应式状态对象,
const state = reactive({
msg: 'hello',
})
// 返回渲染函数(JSX)
return () =>
<div>
{/* `state.msg` 是响应式属性,值变化会自动更新 UI */}
<el-input v-model={state.msg} />
</div>
}
}
双向数据绑定原理
<el-input
modelValue={state.msg}
onUpdate:modelValue={(value) => state.msg = value}
/>
相当于:
- 将
state.msg作为初始值传递给输入框- 当输入框值变化时,通过事件更新
state.msgstate.msg变化触发重新渲染
2. v-if
使用逻辑与 (&&) 运算符
import { reactive } from "vue"
export default {
setup () {
const state = reactive({
condition: true,
})
return () => <>
{/* 使用逻辑与 (&&) 运算符 */}
{state.condition && <div>条件为真时显示</div>}
</>
}
}
使用三元表达式
import { reactive } from "vue"
export default {
setup () {
const state = reactive({
condition: false,
})
return () => <>
{state.condition
? <div>条件为真时显示</div>
: <div>条件为假时显示</div>
}
</>
}
}
使用函数
import { reactive } from "vue";
export default {
setup() {
const state = reactive({
condition: false,
anotherCondition: true,
});
const renderContent = () => {
if (state.condition) {
return <div>复杂条件分支1</div>;
} else if (state.anotherCondition) {
return <div>复杂条件分支2</div>;
} else {
return <div>默认内容</div>;
}
};
return () => (
<>
{renderContent()}
</>
);
}
};
3. v-show
import { reactive } from "vue";
export default {
setup() {
const state = reactive({
condition: true,
});
return () => (
<>
<div v-show={state.condition}>233333354</div>
</>
);
}
};
4. v-for
在 Vue 3 的 JSX 中,没有直接的 v-for 指令,但我们可以使用 JavaScript 的数组方法(如 map())来实现相同的功能。下面我将详细介绍如何在 JSX 中实现 v-for 功能,并提供完整的示例代码。
v-for 在 JSX 中的实现原理
v-for的本质是遍历数组或对象并渲染多个元素。在 JSX 中,我们使用map()方法来实现:
索引值的使用
import { reactive } from "vue";
export default {
setup () {
const items = reactive([
{ id: 1, name: 'Apple', price: 2.5, category: 'fruit' },
{ id: 2, name: 'Banana', price: 1.2, category: 'fruit' },
{ id: 3, name: 'Carrot', price: 0.8, category: 'vegetable' },
{ id: 4, name: 'Milk', price: 2.0, category: 'dairy' },
{ id: 5, name: 'Bread', price: 1.5, category: 'bakery' },
]);
return () => (
<>
{items.map((item, index) => (
<div key={item.id} class="item">
<span class="index">{index + 1}.</span>
<span class="name">{item.name}</span>
</div>
))}
</>
);
}
};
嵌套循环
import { reactive } from "vue";
export default {
setup () {
const categories = reactive([
{
id: 1,
name: "Fruits",
products: [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Orange" }
]
},
{
id: 2,
name: "Vegetables",
products: [
{ id: 4, name: "Carrot" },
{ id: 5, name: "Broccoli" },
{ id: 6, name: "Tomato" }
]
},
{
id: 3,
name: "Dairy",
products: [
{ id: 7, name: "Milk" },
{ id: 8, name: "Cheese" },
{ id: 9, name: "Yogurt" }
]
}
]);
return () => (
<>
{categories.map(category => (
<div key={category.id}>
<h3>{category.name}</h3>
{category.products.map(product => (
<div key={product.id} class="product">
{product.name}
</div>
))}
</div>
))}
</>
);
}
};
遍历对象
import { reactive } from "vue";
export default {
setup () {
const user = reactive({
name: "John Doe",
age: 30,
email: "johndoe@example.com",
address: "123 Main St, Anytown, USA",
phone: "555-555-1234"
});
return () => (
<>
{Object.entries(user).map(([key, value]) => (
<div key={key} class="property">
<span class="key">{key}:</span>
<span class="value">{value}</span>
</div>
))}
</>
);
}
};
范围迭代(模拟 v-for="n in 10")
export default {
setup () {
return () => (
<>
{Array.from({ length: 10 }).map((_, index) => (
<div key={index} class="item">
项目 {index + 1}
</div>
))}
</>
);
}
};
5. v-for 与 v-if 结合使用
推荐做法:先过滤再渲染
import { reactive } from "vue";
export default {
setup () {
const items = reactive([
{ id: 1, name: 'Apple', price: 2.5, category: 'fruit' },
{ id: 2, name: 'Banana', price: 1.2, category: 'fruit' },
{ id: 3, name: 'Carrot', price: 0.8, category: 'vegetable' },
{ id: 4, name: 'Milk', price: 2.0, category: 'dairy' },
{ id: 5, name: 'Bread', price: 1.5, category: 'bakery' },
]);
return () => (
<>
{items
.filter(item => item.price > 1)
.map(item => (
<div key={item.id}>{item.name}</div>
))
}
</>
);
}
};
复杂场景:使用条件渲染
import { reactive } from "vue";
export default {
setup () {
const items = reactive([
{ id: 1, name: 'Apple', price: 2.5, category: 'fruit' },
{ id: 2, name: 'Banana', price: 1.2, category: 'fruit' },
{ id: 3, name: 'Carrot', price: 0.8, category: 'vegetable' },
{ id: 4, name: 'Milk', price: 2.0, category: 'dairy' },
{ id: 5, name: 'Bread', price: 1.5, category: 'bakery' },
]);
return () => (
<>
{items.map(item => (
item.price > 1 ? (
<div key={item.id} class="expensive">
{item.name} (${item.price})
</div>
) : (
<div key={item.id} class="cheap">
{item.name} (${item.price})
</div>
)
))}
</>
);
}
};
6. v-for 性能优化技巧
1. 使用唯一且稳定的 key
// 好 - 使用唯一ID
<div key={item.id}>...</div>// 避免 - 使用索引
<div key={index}>...</div>2. 虚拟滚动优化长列表
import { VirtualList } from 'vue-virtual-scroller'; <VirtualList items={largeList.value} itemSize={50} > {item => ( <div key={item.id} class="item"> {item.name} </div> )} </VirtualList>3. 提取子组件
// ItemComponent.jsx export default defineComponent({ props: ['item'], setup(props) { return () => ( <div class="item"> <h4>{props.item.name}</h4> <p>${props.item.price}</p> </div> ); } }); // 在父组件中使用 {items.value.map(item => ( <ItemComponent key={item.id} item={item} /> ))}
与模板语法对比
| 模板语法 | JSX 实现 |
|---|---|
<div v-for="item in items" :key="item.id"> | {items.map(item => <div key={item.id}>)} |
<div v-for="(item, index) in items"> | {items.map((item, index) => <div key={item.id}>)} |
<div v-for="(value, key) in object"> | {Object.entries(object).map(([key, value]) => <div>)} |
<div v-for="n in 10"> | {Array.from({length: 10}).map((_, n) => <div key={n}>)} |
三、事件
1. 父子组件事件
注意: 父组件需要 on +子组件 emit 上报的方法
// 父组件:onChangePageTitle
<Header title={state.msg} onChangePageTitle={onChangePageTitle}/>
// 子组件: changePageTitle上报
<el-button onClick={() => emit('changePageTitle', '用户页面')}>子组件事件上报</el-button>
四、父子组件
1. 子组件
接收数据:
setup (props, { emit, slots })接收props- 书写
props对象export default { name: 'ChildComponent', // 定义组件的 props props: { // message prop,类型为 String,默认值为 '默认消息' message: { type: String, default: '默认消息' }, // items prop,类型为 Array,默认值为空数组 items: { type: Array, default: () => [] } }, // setup 函数是 Vue 3 Composition API 的入口 setup (props, { emit, slots }) { } };
import { ref } from 'vue';
export default {
name: 'ChildComponent',
// 定义组件的 props
props: {
// message prop,类型为 String,默认值为 '默认消息'
message: {
type: String,
default: '默认消息'
},
// items prop,类型为 Array,默认值为空数组
items: {
type: Array,
default: () => []
}
},
// 声明组件可以发出的事件
emits: ['childEvent'], // 声明自定义事件
// setup 函数是 Vue 3 Composition API 的入口
setup (props, { emit, slots }) {
// 定义子组件内部状态
const childMessage = ref('子组件内部状态');
const inputValue = ref('');
// 定义一个方法,用于向父组件发送数据
function sendToParent () {
emit('childEvent', {
childMessage: childMessage.value,
inputValue: inputValue.value,
timestamp: Date.now()
})
}
// 返回渲染函数
return () =>
<div class="child-container" style="margin-top: 20px">
<h3>子组件</h3>
<p>接收父组件Props: {props.message}</p>
{/* 渲染传入的 items 列表 */}
<ul>
{props.items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
{/* 输入框,双向绑定 inputValue */}
<input
type="text"
v-model={inputValue.value}
placeholder="输入内容回传父组件"
/>
<button onClick={sendToParent}>发送数据到父组件</button>
{/* 渲染插槽内容,如果没有提供默认插槽内容则显示 '默认插槽内容' */}
<div class="slot-area">
{slots.default ? slots.default({ text: childMessage.value }) : '默认插槽内容'}
</div>
</div>
}
};
2. 父组件下发数据
import { ref } from 'vue';
import ChildComponent from './child.jsx';
export default {
name: 'ParentComponent',
setup () {
// 父组件状态
const parentMessage = ref('来自父组件的消息');
const childData = ref(null);
const items = ref(['Apple', 'Banana', 'Cherry']);
// 接收子组件事件的方法
function handleChildEvent (data) {
console.log('收到子组件数据:', data);
childData.value = data;
};
// 更新列表方法
function updateItems() {
items.value = [...items.value, 'New Item ' + Date.now()];
};
return () =>
<div class="parent-container">
<h2>父组件</h2>
<p>父组件消息: {parentMessage.value}</p>
<p>来自子组件的数据: {JSON.stringify(childData.value)}</p>
{/* 基础Props传递 */}
<ChildComponent
message={parentMessage.value}
items={items.value}
onChildEvent={handleChildEvent}
/>
<button onClick={updateItems}>更新列表</button>
</div>
}
};
五、watch 方法
1. 用 Composition API 的 watch
import { ref, watch } from 'vue';
export default {
setup() {
// 使用 ref 创建一个响应式引用,初始值为 0
const count = ref(0);
// 使用 watch 监听 count 的变化
watch(count, (newValue, oldValue) => {
// 当 count 值发生变化时,打印旧值和新值
console.log(`count changed from ${oldValue} to ${newValue}`);
});
// 返回渲染函数,生成包含按钮和计数的 JSX 元素
return () => (
<div>
{/* 点击按钮时,count 值增加 */}
<button onClick={() => count.value++}>Increment</button>
{/* 显示当前的 count 值 */}
<p>Count: {count.value}</p>
</div>
);
}
};
2. 监听多个源
import { ref, watch } from 'vue';
export default {
setup () {
// 创建一个响应式引用 count,初始值为 0
const count = ref(0);
// 创建一个响应式引用 name,初始值为 'Vue'
const name = ref('Vue');
// 使用 watch 函数同时监听 count 和 name 的变化
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
// 当 count 或 name 值发生变化时,打印它们的旧值和新值
console.log(`Count: ${oldCount} -> ${newCount}`);
console.log(`Name: ${oldName} -> ${newName}`);
});
// 返回渲染函数,生成包含按钮、输入框和显示计数及名称的 JSX 元素
return () =>
<div>
{/* 点击按钮时,count 值增加 */}
<button onClick={() => count.value++}>Increment</button>
{/* 输入框,双向绑定 name 值 */}
<input vModel={name.value} />
{/* 显示当前的 count 和 name 值 */}
<p>Count: {count.value}, Name: {name.value}</p>
</div>
}
};
3. watchEffect 和 watch 的区别
在 Vue 3 的 Composition API 中,watchEffect 和 watch 都是用于响应式地执行副作用代码的函数,但它们有一些关键区别:
主要区别
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖收集方式 | 显式指定要监听的数据源 | 自动收集回调函数中的响应式依赖 |
| 初始执行 | 默认不立即执行(可配置) | 立即执行 |
| 参数获取 | 可以访问新旧值 | 无法直接获取旧值 |
| 使用场景 | 需要精确控制监听的数据源时 | 依赖关系简单或需要立即执行时 |
六、computed 方法
1. 用 Composition API 的 computed
import { computed, reactive } from 'vue'
export default {
setup() {
const state = reactive({
count: 0
})
// 计算属性
const doubleCount = computed(() => {
return state.count * 2
})
return () => (
<div>
<p>Count: {state.count}</p>
// 需要通过 `.value` 访问 computed 值
<p>Double Count: {doubleCount.value}</p>
<button onClick={() => state.count++}>Increment</button>
</div>
)
}
}
2. computed 缓存
Computed 属性的缓存机制是 Vue 响应式系统的核心特性之一.
说明:
- 只有点击"修改 count"按钮时,控制台才会打印"计算 doubleCount"
- 点击"修改 unrelatedValue"按钮不会触发重新计算,因为计算属性不依赖它
import { computed, ref } from 'vue'
export default {
setup() {
const count = ref(0)
const unrelatedValue = ref(100)
// 这个计算属性只依赖于 count
const doubleCount = computed(() => {
console.log('计算 doubleCount') // 仅当 count 变化时才会打印
return count.value * 2
})
return () => (
<div>
<p>Count: {count.value}</p>
<p>Double Count: {doubleCount.value}</p>
<button onClick={() => count.value++}>修改 count (会触发重新计算)</button>
<button onClick={() => unrelatedValue.value++}>修改 unrelatedValue (不会触发重新计算)</button>
</div>
)
}
}
2. 注意事项
- 访问 computed 值:在 Composition API 中,需要通过
.value访问 computed 值- JSX 中的 v-model:在 JSX 中使用
vModel而不是v-model- 性能优化:computed 属性会缓存计算结果,只有当依赖的响应式数据变化时才会重新计算
七、hooks
1. hooks 结构
命名规范
- 始终以
use开头(如useDarkMode) - 文件名使用驼峰命名(
useEventListener.js)
2. 新建 hooks.js
import { reactive } from 'vue' // 引入Vue的reactive函数,用于创建响应式状态
import { defineFormConfig } from '@common/hooks' // 引入定义表单配置的函数
import { waybillConfirmBindService } from '@/request' // 引入请求服务,用于处理运单绑定
import { ElMessage } from 'element-plus' // 引入Element Plus的消息提示组件
// 定义一个用于绑定运单的函数
export function useBindWaybill(pageListRef) {
// 创建响应式状态
const state = reactive({
isShow: false, // 控制对话框显示的状态
currentRow: {}, // 当前选中的行数据
})
// 定义表单配置
const formConfig = defineFormConfig({
configList: [
{ label: '联单编号', prop: 'waybillId', required: true }, // 联单编号字段,必填
],
labelWidth: 80, // 表单标签的宽度
})
// 确认按钮的回调函数
function onConfirm({ form }) {
// 调用绑定运单的服务
return waybillConfirmBindService.bind_waybill({id: form.id, waybillId: form.waybillId}).then(() => {
ElMessage.success('编辑联单编号成功') // 显示成功消息
pageListRef.value.search() // 刷新页面列表
})
}
// 返回对象,包含组件所需的状态和方法
return {
csoStte: state, // 返回响应式状态
// 打开绑定运单对话框
openBindWaybill(currentRow) {
state.currentRow = currentRow // 设置当前选中的行数据
state.isShow = true // 显示对话框
},
// 渲染绑定运单对话框
renderBindWaybill() {
return (
<DialogForm
v-model={state.isShow} // 绑定对话框显示状态
title="编辑联单编号" // 对话框标题
formConfig={formConfig} // 表单配置
width={560} // 对话框宽度
form={state.currentRow} // 表单数据
onConfirm={onConfirm} // 确认按钮的回调函数
/>
)
},
}
}
3. 引入 hooks
import { waybillConfirmService } from '@/request' // 引入运单确认服务
import { defineFormConfig, defineTableConfig } from '@common/hooks' // 引入定义表单和表格配置的函数
import { useBindWaybill } from './hooks' // 引入绑定运单的自定义hook
import { ref } from 'vue' // 引入Vue的ref函数,用于创建响应式引用
export default {
setup() {
const pageListRef = ref() // 创建一个响应式引用,用于引用PageList组件
const { openBindWaybill, renderBindWaybill } = useBindWaybill(pageListRef) // 使用绑定运单的hook,获取打开对话框和渲染对话框的函数
// 定义表格配置
const tableConfig = defineTableConfig({
columns: [ // 列配置
{ label: '合同编号', prop: 'contractNo', width: 160 },
{ label: '产废企业', prop: 'cfqymc', width: 200 },
{ label: '废物代码', prop: 'wasteCode', width: 160 },
{ label: '废物类型', prop: 'wasteItemType' },
{ label: '废物名称', prop: 'wasteName' },
{ label: '联单编号', prop: 'waybillId', width: 160, showOverflowTooltip: true },
{ label: '废物产生单位/单元', prop: 'unit', width: 160 },
{
label: '重量(KG)', prop: 'quantity', width: 120, render({ row }) {
return row.quantity ? row.quantity * 1000 : '-' // 如果有重量数据,显示重量乘以1000,否则显示'-'
}
},
{ label: '设备类型', prop: 'emergencyEquipment' },
{ label: '有害成分', prop: 'harmfulComponents' },
{ label: '主要成分', prop: 'mainComponents' },
{ label: '危害性', prop: 'wftx' },
{ label: '物理形态', prop: 'wfxt' },
{ label: '运输人员', prop: 'driver' },
],
handlerWidth: 100, // 操作列的宽度
handlerSlot({ row, renderBtns, editRow, deleteRow }) { // 操作列的自定义插槽
return <el-button link type="primary" onClick={() => openBindWaybill(row)}>编辑联单编号</el-button> // 点击按钮打开编辑联单编号对话框
},
})
// 渲染函数
return () => <PageList
ref={pageListRef} // 绑定PageList组件的引用
tableConfig={tableConfig} // 绑定表格配置
dialogConfig={{ width: 500 }} // 对话框配置
operateConfig={{ hiddenDelete: true, hiddenAdd: true }} // 隐藏删除和添加按钮
api={{ ...waybillConfirmService }} // 绑定API服务
>
{{
operate: ({ renderBtns, search, checkedList }) => <>
{/* checkedList: 多选中的数据 */}
{renderBtns()} // 渲染操作按钮
</>,
default: () => renderBindWaybill() // 渲染绑定运单对话框
}}
</PageList>
}
}