第三類 引用數據的操作
引用數據類型 又叫復雜數類型, 典型的代表是對象和數組,而數組和對象往往又嵌套到到一起
普通數組和對象的使用
vue中使用for循環遍歷
<template><div>我是關于頁面<div v-for="item in arr">{{ item.id }}</div></div>
</template><script setup lang="ts">
import { ref } from "vue"let arr = ref([{id:1},{id:2},{id:3}])</script>
思考1: v-for循環的時候 ,不指定key,是否會報錯,我這里沒有寫,也沒有報錯,大家的項目為什么不寫就報錯!
? ? ? ? 2:v-for 和v-if的優先級 誰高誰低? (需要分版本,不然怎么回答都是錯的)
Harmony中使用ForEach循環遍歷
@Entry
@Component
struct MyString {@State list:ESObject[] = [{id:1},{id:2},{id:2}];build() {Column() {ForEach(this.list,(item:ESObject)=>{Row(){Text(`${item.id}`)}})}.height('100%').width('100%')}
}
思考: ForEach中有幾個參數,分別表示什么意思
嵌套數組和對象的使用
vue中使用雙重for循環遍歷
<template><div>我是關于頁面<ul v-for="item in arr"><li><span>{{ item.id }}</span> <ul><li v-for="info in item.list">{{ info.id }}</li></ul></li> </ul></div>
</template><script setup lang="ts">
import { ref } from "vue"let arr = ref([{ id: 1, list: [{ id: 1.1 }] }, { id: 2, list: [{ id: 2.1 }] }, { id: 3, list: [{ id: 3.1 }] }
])</script>
效果
Harmony中使用雙重ForEach處理
interface ListModel{id:number,list?:ListModel[]
}
@Entry
@Component
struct MyString {@State list: ListModel[]= [{id:1,list:[{id:1.1}]},{id:2,list:[{id:2.1}]},{id:3,list:[{id:3.1}]}];build() {Column() {ForEach(this.list,(item:ESObject)=>{Column(){Text(`${item.id}`)ForEach(item.list,(info:ListModel)=>{Column(){Text(""+info.id)}})}})}.height('100%').width('100%')}
}
效果
思考:數據類型為什么要這么設計??
遞歸組件的使用
vue中使用遞歸組件
先聲明一個組件(注意處理遞歸的出口)
<template><!-- 樹節點組件 --><div class="tree-node"><!-- 當前節點 --><div class="node-label" @click="toggleExpand":style="{ paddingLeft: depth * 20 + 'px' }"><span v-if="hasChildren" class="toggle-icon">{{ isExpanded ? '▼' : '?' }}</span>{{ node.name }}</div><!-- 遞歸渲染子節點 --><div v-show="isExpanded && hasChildren" class="children"><TreeNodev-for="child in node.children":key="child.id":node="child":depth="depth + 1"/></div></div>
</template><script setup>
import { ref, computed } from 'vue';const props = defineProps({node: {type: Object,required: true},depth: {type: Number,default: 0}
});// 控制展開/折疊狀態
const isExpanded = ref(props.depth === 0); // 默認展開第一級// 計算是否有子節點
const hasChildren = computed(() => {return props.node.children && props.node.children.length > 0;
});// 切換展開狀態
function toggleExpand() {if (hasChildren.value) {isExpanded.value = !isExpanded.value;}
}
</script><style scoped>
.tree-node {font-family: 'Segoe UI', sans-serif;cursor: pointer;user-select: none;
}.node-label {padding: 8px 12px;border-radius: 4px;transition: background-color 0.2s;
}.node-label:hover {background-color: #f0f7ff;
}.toggle-icon {display: inline-block;width: 20px;font-size: 12px;
}.children {margin-left: 8px;border-left: 1px dashed #e0e0e0;transition: all 0.3s;
}
</style>
父頁面中使用
<template><div class="tree-container"><TreeNode v-for="item in treeData" :key="item.id" :node="item" /></div>
</template><script setup>
import TreeNode from '../components/TreeNode.vue';
import {ref} from "vue"// 樹形數據
const treeData = ref([{id: 1,name: '前端技術',children: [{id: 2,name: 'JavaScript',children: [{ id: 3, name: 'ES6 特性' },{ id: 4, name: '異步編程' }]},{id: 5,name: 'Vue.js',children: [{ id: 6, name: 'Vue 3 新特性' },{ id: 7, name: '高級用法',children: [{ id: 8, name: '遞歸組件' },{ id: 9, name: '渲染函數' }]}]}]},{id: 10,name: '后端技術',children: [{ id: 11, name: 'Node.js' },{ id: 12, name: 'Python' }]}
]);
</script><style>
.tree-container {max-width: 400px;margin: 20px;padding: 15px;border: 1px solid #eaeaea;border-radius: 8px;
}
</style>
效果
Harmony中使用遞歸組件
第一步聲明一個遞歸的數據格式(特別重要)
interface TreeNode {id: number;name: string;children?: TreeNode[];
}
第二步聲明組件
@Component
struct TreeNodeComponent {@Prop node: TreeNode;@Prop expand: boolean = false;build() {Column() {Row({ space: 5 }) {if (this.node.children && this.node.children.length > 0) {Image(this.expand ? $r('app.media.open') : $r('app.media.close')).width(20).height(20).onClick(() => {this.expand = !this.expand;});} else {Image($r('app.media.open')).width(20).height(20);}Text(this.node.name).fontSize(16).fontWeight(500).layoutWeight(1);}.width('100%').padding({ left: 10, right: 10, top: 5, bottom: 5 }).backgroundColor('#f5f5f5').borderRadius(5).margin({ bottom: 5 });if (this.node.children && this.node.children.length > 0 && this.expand) {ForEach(this.node.children, (childNode: TreeNode) => {TreeNodeComponent({ node: childNode });});}}.width('100%').padding({ left: 20 });}
}
第三步使用使用該組件
@Entry
@Component
struct TreeView {@State data: TreeNode[] = [{id: 1,name: '前端技術',children: [{id: 2,name: 'JavaScript',children: [{ id: 3, name: 'ES6 特性' },{ id: 4, name: '異步編程' }]},{id: 5,name: 'Vue.js',children: [{ id: 6, name: 'Vue 3 新特性' },{id: 7,name: '高級用法',children: [{ id: 8, name: '遞歸組件' },{ id: 9, name: '渲染函數' }]}]}]},{id: 10,name: '后端技術',children: [{ id: 11, name: 'Node.js' },{ id: 12, name: 'Python' }]}];build() {Column() {ForEach(this.data, (node: TreeNode) => {TreeNodeComponent({ node: node });});}.width('100%').height('100%').padding(20).backgroundColor('#ffffff');}
}
效果
列表懶加載
vue中使用滾動事件處理判斷使用
<template><div class="container"><header><h1>Vue 列表懶加載</h1><div class="subtitle">滾動到底部自動加載更多內容</div></header><div class="controls"><select v-model="pageSize"><option value="10">每頁 10 項</option><option value="20">每頁 20 項</option><option value="30">每頁 30 項</option></select><input type="number" v-model="scrollThreshold" min="50" max="500" step="50"><label>加載閾值(px)</label></div><div class="list-container" ref="listContainer" @scroll="handleScroll"><!-- 虛擬滾動容器 --><div class="virtual-list" :style="{ height: `${totalItems * itemHeight}px` }"><div v-for="item in visibleItems" :key="item.id"class="item":style="{ position: 'absolute', top: `${item.index * itemHeight}px`,width: 'calc(100% - 20px)'}"><div class="item-index">#{{ item.id }}</div><div class="item-content"><div class="item-title">項目 {{ item.id }} - {{ item.title }}</div><div class="item-description">{{ item.description }}</div></div></div></div><!-- 加載提示 --><div v-if="loading" class="loading"><div class="loader"></div><span>正在加載更多項目...</span></div><!-- 完成提示 --><div v-if="allLoaded" class="end-message">已加載全部 {{ totalItems }} 個項目</div></div><!-- 底部統計信息 --><div class="stats"><div>已加載項目: {{ items.length }} / {{ totalItems }}</div><div>可視項目: {{ visibleItems.length }}</div><div>滾動位置: {{ scrollPosition }}px</div></div><!-- 返回頂部按鈕 --><div class="scroll-top" @click="scrollToTop" v-show="scrollPosition > 500"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg></div></div>
</template><script setup>
import { ref, computed, onMounted, watch } from 'vue';// 基礎數據
const items = ref([]);
const loading = ref(false);
const allLoaded = ref(false);
const scrollPosition = ref(0);
const listContainer = ref(null);
const totalItems = 200;
const pageSize = ref(20);
const scrollThreshold = ref(200);
const itemHeight = 100; // 每個項目的高度// 生成隨機項目數據
const generateItems = (start, count) => {const newItems = [];const titles = ["前端開發", "后端架構", "數據庫設計", "UI/UX設計", "移動應用", "DevOps", "測試案例", "項目管理"];const descriptions = ["這是一個重要的項目,需要仔細規劃和執行","創新性解決方案,改變了我們處理問題的方式","使用最新技術棧實現的高性能應用","用戶友好界面,提供無縫體驗","優化了工作流程,提高了團隊效率","解決了長期存在的技術難題","跨平臺兼容性優秀的實現方案","獲得了用戶高度評價的產品功能"];for (let i = start; i < start + count; i++) {newItems.push({id: i + 1,index: i,title: titles[Math.floor(Math.random() * titles.length)],description: descriptions[Math.floor(Math.random() * descriptions.length)]});}return newItems;
};// 加載初始數據
const loadInitialData = () => {items.value = generateItems(0, pageSize.value);
};// 加載更多數據
const loadMore = () => {if (loading.value || allLoaded.value) return;loading.value = true;// 模擬API請求延遲setTimeout(() => {const startIndex = items.value.length;const remaining = totalItems - startIndex;const count = Math.min(pageSize.value, remaining);items.value = [...items.value, ...generateItems(startIndex, count)];if (items.value.length >= totalItems) {allLoaded.value = true;}loading.value = false;}, 800);
};// 處理滾動事件
const handleScroll = () => {if (!listContainer.value) return;const container = listContainer.value;scrollPosition.value = container.scrollTop;// 距離底部還有 scrollThreshold 像素時加載const fromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;if (fromBottom <= scrollThreshold.value) {loadMore();}
};// 計算可視區域的項目
const visibleItems = computed(() => {if (!listContainer.value) return [];const container = listContainer.value;const scrollTop = container.scrollTop;const visibleHeight = container.clientHeight;// 計算可視區域的起始和結束索引const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - 5);const endIndex = Math.min(totalItems - 1, Math.ceil((scrollTop + visibleHeight) / itemHeight) + 5);return items.value.filter(item => item.index >= startIndex && item.index <= endIndex);
});// 滾動到頂部
const scrollToTop = () => {if (listContainer.value) {listContainer.value.scrollTo({top: 0,behavior: 'smooth'});}
};// 重置并重新加載
const resetList = () => {items.value = [];allLoaded.value = false;loadInitialData();scrollToTop();
};// 監聽pageSize變化
watch(pageSize, resetList);// 初始化
onMounted(() => {loadInitialData();
});
</script><style scoped>
* {margin: 0;padding: 0;box-sizing: border-box;
}body {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);min-height: 100vh;display: flex;justify-content: center;align-items: center;padding: 20px;
}.container {max-width: 800px;width: 100%;background: white;border-radius: 16px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);overflow: hidden;margin: 20px auto;
}header {background: #4a69bd;color: white;padding: 20px;text-align: center;
}h1 {font-size: 2.2rem;margin-bottom: 10px;
}.subtitle {opacity: 0.8;font-weight: 300;
}.controls {display: flex;gap: 15px;padding: 20px;background: #f1f5f9;border-bottom: 1px solid #e2e8f0;flex-wrap: wrap;align-items: center;
}.controls select, .controls input {padding: 10px 15px;border: 1px solid #cbd5e1;border-radius: 8px;background: white;font-size: 1rem;outline: none;
}.controls input {width: 100px;
}.list-container {height: 500px;overflow-y: auto;position: relative;border-bottom: 1px solid #e2e8f0;
}.virtual-list {position: relative;
}.item {padding: 20px;margin: 10px;background: white;border-radius: 10px;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);transition: all 0.3s ease;display: flex;align-items: center;border-left: 4px solid #4a69bd;
}.item:hover {transform: translateY(-3px);box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
}.item-index {font-size: 1.5rem;font-weight: bold;color: #4a69bd;min-width: 50px;
}.item-content {flex: 1;
}.item-title {font-size: 1.2rem;margin-bottom: 8px;color: #1e293b;
}.item-description {color: #64748b;line-height: 1.5;
}.loading {padding: 30px;text-align: center;color: #64748b;font-size: 1.1rem;display: flex;flex-direction: column;align-items: center;gap: 15px;
}.loader {display: inline-block;width: 40px;height: 40px;border: 4px solid rgba(74, 105, 189, 0.3);border-radius: 50%;border-top-color: #4a69bd;animation: spin 1s ease-in-out infinite;
}@keyframes spin {to { transform: rotate(360deg); }
}.end-message {padding: 30px;text-align: center;color: #94a3b8;font-style: italic;font-size: 1.1rem;
}.stats {padding: 15px 20px;background: #f1f5f9;color: #475569;display: flex;justify-content: space-between;flex-wrap: wrap;gap: 10px;
}.stats div {min-width: 150px;
}.scroll-top {position: fixed;bottom: 30px;right: 30px;width: 50px;height: 50px;background: #4a69bd;color: white;border-radius: 50%;display: flex;align-items: center;justify-content: center;cursor: pointer;box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);transition: all 0.3s ease;z-index: 100;
}.scroll-top:hover {background: #3d56a0;transform: translateY(-3px);
}.scroll-top svg {width: 24px;height: 24px;fill: white;
}/* 響應式設計 */
@media (max-width: 768px) {.container {margin: 10px;}.list-container {height: 400px;}.stats {flex-direction: column;gap: 5px;}.controls {flex-direction: column;align-items: flex-start;}.controls input {width: 100%;}
}@media (max-width: 480px) {h1 {font-size: 1.8rem;}.list-container {height: 350px;}.item {padding: 15px;flex-direction: column;align-items: flex-start;}.item-index {margin-bottom: 10px;}
}
</style>
需要的地方使用
<template><div><LazyLoadedList /></div>
</template><script setup>
import LazyLoadedList from '../components/LazyList.vue';
</script>
效果
Harmony中使用LazyForEach
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: StringData[] = [];public totalCount(): number {return 0;}public getData(index: number): StringData {return this.originDataArray[index];}registerDataChangeListener(listener: DataChangeListener): void {if (this.listeners.indexOf(listener) < 0) {console.info('add listener');this.listeners.push(listener);}}unregisterDataChangeListener(listener: DataChangeListener): void {const pos = this.listeners.indexOf(listener);if (pos >= 0) {console.info('remove listener');this.listeners.splice(pos, 1);}}notifyDataReload(): void {this.listeners.forEach(listener => {listener.onDataReloaded();})}notifyDataAdd(index: number): void {this.listeners.forEach(listener => {listener.onDataAdd(index);})}notifyDataChange(index: number): void {this.listeners.forEach(listener => {listener.onDataChange(index);})}notifyDataDelete(index: number): void {this.listeners.forEach(listener => {listener.onDataDelete(index);})}notifyDataMove(from: number, to: number): void {this.listeners.forEach(listener => {listener.onDataMove(from, to);})}
}class MyDataSource extends BasicDataSource {private dataArray: StringData[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): StringData {return this.dataArray[index];}public addData(index: number, data: StringData): void {this.dataArray.splice(index, 0, data);this.notifyDataAdd(index);}public pushData(data: StringData): void {this.dataArray.push(data);this.notifyDataAdd(this.dataArray.length - 1);}public reloadData(): void {this.notifyDataReload();}
}class StringData {message: string;imgSrc: Resource;constructor(message: string, imgSrc: Resource) {this.message = message;this.imgSrc = imgSrc;}
}@Entry
@Component
struct MyComponent {private data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 9; i++) {// 此處'app.media.icon'僅作示例,請開發者自行替換,否則imageSource創建失敗會導致后續無法正常執行。this.data.pushData(new StringData(`Click to add ${i}`, $r('app.media.icon')));}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: StringData, index: number) => {ListItem() {Column() {Text(item.message).fontSize(20).onAppear(() => {console.info("text appear:" + item.message);})Image(item.imgSrc).width(100).height(100).onAppear(() => {console.info("image appear");})}.margin({ left: 10, right: 10 })}.onClick(() => {item.message += '0';this.data.reloadData();})}, (item: StringData, index: number) => JSON.stringify(item))}.cachedCount(5)}
}
?使用原理:LazyForEach從數據源中按需迭代數據,并在每次迭代時創建相應組件。當在滾動容器中使用了LazyForEach,框架會根據滾動容器可視區域按需創建組件,當組件滑出可視區域外時,框架會銷毀并回收組件以降低內存占用。
總結
本文介紹了引用數據類型(對象和數組)在不同框架中的操作方式。在Vue中,通過v-for循環遍歷數組和嵌套對象,討論了key屬性的重要性及v-for與v-if的優先級問題。在Harmony中,使用ForEach處理類似數據結構,并詳細說明了參數含義。文章還展示了遞歸組件的實現,包括Vue的樹形組件和Harmony的遞歸組件定義。最后,對比了兩種框架下的列表懶加載實現:Vue通過滾動事件處理,Harmony使用LazyForEach和DataSource機制。這些示例展示了不同框架對復雜數據結構的處理方式及其特有的語法
鴻蒙學習班級