



引言
在HarmonyOS应用开发中,工具函数是提升开发效率、保证代码质量的重要组成部分。无论是数据校验、性能优化还是数据处理,优秀的工具函数都能极大简化开发流程。本文将深入探讨ArkTS中最常用的工具函数,包括isEmpty、debounce、throttle、deepClone等,从原理、实现到应用场景进行全方位解析。
一、工具函数的设计原则
在开始具体实现之前,我们需要明确工具函数的设计原则:
1.1 单一职责原则
每个工具函数应该只负责一个功能,避免"瑞士军刀"式的万能函数。
1.2 类型安全
ArkTS作为静态类型语言,工具函数必须保证严格的类型检查,避免使用any或unknown类型。
1.3 无副作用
纯函数是工具函数的最佳实践,避免修改传入参数或产生全局状态变化。
1.4 防御性编程
处理边界情况,对异常输入进行合理处理,提供清晰的错误信息。
二、isEmpty - 空值判断函数
2.1 函数概述
isEmpty函数用于判断一个值是否为空,支持多种数据类型的判断。
2.2 实现原理
空值判断需要考虑多种情况:
null和undefined- 空字符串(包括仅包含空格的字符串)
NaN(非数字)- 空数组
- 空对象
2.3 ArkTS实现
typescript
export function isEmpty(value: string | number | boolean | object | undefined | null): boolean {
// 处理 null 和 undefined
if (value === null || value === undefined) {
return true;
}
// 处理字符串类型
if (typeof value === 'string') {
return value.trim().length === 0;
}
// 处理数字类型(NaN判断)
if (typeof value === 'number') {
return isNaN(value);
}
// 处理数组类型
if (Array.isArray(value)) {
return value.length === 0;
}
// 处理对象类型
if (typeof value === 'object') {
return Object.keys(value).length === 0;
}
// 其他类型默认为非空
return false;
}
export function isNotEmpty(value: string | number | boolean | object | undefined | null): boolean {
return !isEmpty(value);
}
2.4 关键技术点解析
2.4.1 类型联合声明
typescript
value: string | number | boolean | object | undefined | null
ArkTS支持联合类型,通过|连接多种类型,使函数能够接受不同类型的参数。
2.4.2 typeof 类型守卫
typescript
if (typeof value === 'string') {
return value.trim().length === 0;
}
typeof操作符在ArkTS中是类型守卫,可以在条件分支中缩小类型范围。
2.4.3 Array.isArray 检测
typescript
if (Array.isArray(value)) {
return value.length === 0;
}
由于typeof []返回'object',需要使用Array.isArray专门检测数组类型。
2.4.4 对象空值检测
typescript
if (typeof value === 'object') {
return Object.keys(value).length === 0;
}
使用Object.keys()获取对象的所有键名,通过判断键名数量来确定对象是否为空。
2.5 使用场景
场景1:表单验证
typescript
interface FormData {
username: string;
password: string;
email: string;
}
function validateForm(form: FormData): boolean {
if (isEmpty(form.username)) {
console.error('用户名不能为空');
return false;
}
if (isEmpty(form.password)) {
console.error('密码不能为空');
return false;
}
if (isEmpty(form.email)) {
console.error('邮箱不能为空');
return false;
}
return true;
}
场景2:API请求参数校验
typescript
interface ApiParams {
page?: number;
size?: number;
keyword?: string;
}
function buildRequest(params: ApiParams): Record<string, unknown> {
const query: Record<string, unknown> = {};
if (isNotEmpty(params.page)) {
query['page'] = params.page;
}
if (isNotEmpty(params.size)) {
query['size'] = params.size;
}
if (isNotEmpty(params.keyword)) {
query['keyword'] = params.keyword;
}
return query;
}
场景3:状态管理中的空值判断
typescript
@Local userInfo: User | null = null;
build() {
Column() {
if (isEmpty(this.userInfo)) {
Text('请先登录')
.fontSize(16)
.fontColor('#999999');
} else {
Text(`欢迎, ${this.userInfo.name}`)
.fontSize(16)
.fontColor('#333333');
}
}
}
2.6 边界情况处理
| 测试用例 | 预期结果 | 说明 |
|---|---|---|
isEmpty(null) |
true |
null值 |
isEmpty(undefined) |
true |
undefined值 |
isEmpty('') |
true |
空字符串 |
isEmpty(' ') |
true |
空白字符串 |
isEmpty(0) |
false |
数字0 |
isEmpty(NaN) |
true |
非数字 |
isEmpty([]) |
true |
空数组 |
isEmpty({}) |
true |
空对象 |
isEmpty(false) |
false |
布尔值false |
isEmpty(true) |
false |
布尔值true |
三、debounce - 防抖函数
3.1 函数概述
防抖函数用于限制函数的执行频率,确保在指定时间内只有最后一次调用生效。
3.2 应用场景
防抖常用于以下场景:
- 搜索框输入联想
- 窗口resize事件
- 按钮重复点击防止重复提交
- 滚动事件处理
3.3 实现原理
防抖的核心思想是:
- 当事件触发时,设置一个定时器
- 如果在定时器触发前再次触发事件,则清除定时器并重新设置
- 只有在指定时间内没有再次触发,才执行目标函数
3.4 ArkTS实现
typescript
type SimpleFunction = () => void;
export function debounce(fn: SimpleFunction, delay: number): SimpleFunction {
let timer: number | null = null;
return (): void => {
// 如果存在定时器,清除它
if (timer !== null) {
clearTimeout(timer);
}
// 设置新的定时器
const timerValue = setTimeout(() => {
fn();
}, delay);
timer = timerValue as number;
};
}
3.5 关键技术点解析
3.5.1 闭包的使用
typescript
return (): void => {
if (timer !== null) {
clearTimeout(timer);
}
// ...
};
防抖函数利用闭包特性,使内部函数能够访问外部函数的timer变量,从而实现定时器的持久化和清除。
3.5.2 定时器管理
typescript
let timer: number | null = null;
// ...
const timerValue = setTimeout(() => {
fn();
}, delay);
timer = timerValue as number;
使用setTimeout创建定时器,并将返回值保存,以便后续清除。
3.5.3 类型安全设计
typescript
type SimpleFunction = () => void;
定义SimpleFunction类型别名,确保传入的函数符合预期的签名。
3.6 使用示例
示例1:搜索框防抖
typescript
@Local searchKeyword: string = '';
@Local searchResults: string[] = [];
private debouncedSearch: () => void = debounce(() => {
this.performSearch();
}, 300);
performSearch(): void {
if (isEmpty(this.searchKeyword)) {
this.searchResults = [];
return;
}
// 模拟API请求
console.log(`搜索: ${this.searchKeyword}`);
// this.searchResults = await api.search(this.searchKeyword);
}
build() {
Column() {
TextInput({ placeholder: '请输入搜索关键词' })
.onChange((value: string) => {
this.searchKeyword = value;
this.debouncedSearch();
})
List() {
ForEach(this.searchResults, (item: string) => {
ListItem() {
Text(item)
.fontSize(14)
}
})
}
}
}
示例2:窗口resize处理
typescript
@Local windowWidth: number = 0;
@Local windowHeight: number = 0;
private debouncedResize: () => void = debounce(() => {
this.updateWindowSize();
}, 200);
updateWindowSize(): void {
// 获取窗口尺寸
// this.windowWidth = getWindowWidth();
// this.windowHeight = getWindowHeight();
console.log(`窗口尺寸: ${this.windowWidth} x ${this.windowHeight}`);
}
aboutToAppear() {
// 监听窗口resize事件
// window.addEventListener('resize', this.debouncedResize);
}
aboutToDisappear() {
// 移除事件监听
// window.removeEventListener('resize', this.debouncedResize);
}
3.7 防抖与节流的区别
| 特性 | 防抖(debounce) | 节流(throttle) |
|---|---|---|
| 执行时机 | 最后一次触发后延迟执行 | 固定时间间隔执行 |
| 触发频率高时 | 只执行最后一次 | 按固定频率执行 |
| 适用场景 | 搜索输入、窗口resize | 滚动事件、按钮点击 |
| 实现复杂度 | 较简单 | 较复杂 |
四、throttle - 节流函数
4.1 函数概述
节流函数用于限制函数的执行频率,确保在指定时间间隔内只执行一次。
4.2 应用场景
节流常用于以下场景:
- 滚动事件处理
- 按钮点击防重复
- 鼠标移动事件
- 游戏中的帧率控制
4.3 实现原理
节流的核心思想是:
- 记录上次执行的时间戳
- 当事件触发时,检查当前时间与上次执行时间的差值
- 如果差值大于等于指定间隔,则执行函数并更新时间戳
- 否则忽略本次触发
4.4 ArkTS实现
typescript
type SimpleFunction = () => void;
export function throttle(fn: SimpleFunction, delay: number): SimpleFunction {
let lastTime: number = 0;
return (): void => {
const now: number = Date.now();
// 如果当前时间与上次执行时间的差值大于等于delay,则执行
if (now - lastTime >= delay) {
lastTime = now;
fn();
}
};
}
4.5 关键技术点解析
4.5.1 时间戳记录
typescript
let lastTime: number = 0;
// ...
const now: number = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn();
}
使用Date.now()获取当前时间戳,通过时间差判断是否执行函数。
4.5.2 首次执行策略
初始lastTime设为0,确保第一次触发时必定执行(因为Date.now() - 0 >= delay在delay不为0时成立)。
4.6 使用示例
示例1:滚动加载更多
typescript
@Local pageIndex: number = 1;
@Local loading: boolean = false;
private throttledLoadMore: () => void = throttle(() => {
this.loadMoreData();
}, 1000);
loadMoreData(): void {
if (this.loading) return;
this.loading = true;
// 模拟加载数据
console.log(`加载第 ${this.pageIndex} 页`);
this.pageIndex++;
setTimeout(() => {
this.loading = false;
}, 1000);
}
build() {
Scroll() {
Column() {
// 列表内容
}
.onScroll((event: ScrollEvent) => {
// 判断是否滚动到底部
if (event.offsetDistance >= event.scrollableDistance - 100) {
this.throttledLoadMore();
}
})
}
}
示例2:按钮点击节流
typescript
@Local clickCount: number = 0;
private throttledClick: () => void = throttle(() => {
this.handleClick();
}, 500);
handleClick(): void {
this.clickCount++;
console.log(`点击次数: ${this.clickCount}`);
}
build() {
Button('点击测试')
.onClick(() => {
this.throttledClick();
})
Text(`点击次数: ${this.clickCount}`)
.fontSize(14)
}
五、deepClone - 深拷贝函数
5.1 函数概述
深拷贝函数用于创建一个对象的完全独立副本,确保修改副本不会影响原对象。
5.2 浅拷贝与深拷贝的区别
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 基本类型 | 值复制 | 值复制 |
| 对象类型 | 引用复制 | 值复制 |
| 嵌套对象 | 共享引用 | 递归复制 |
| 修改副本 | 影响原对象 | 不影响原对象 |
5.3 实现原理
深拷贝需要处理多种情况:
- 基本类型(直接返回)
- null值(直接返回)
- 数组类型(递归复制每个元素)
- 对象类型(递归复制每个属性)
5.4 ArkTS实现
typescript
export function deepClone(obj: object | null): object | null {
// 处理null和非对象类型
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理数组类型
if (Array.isArray(obj)) {
const arrLen = (obj as []).length;
const result: object[] = [];
for (let i = 0; i < arrLen; i++) {
const item = (obj as []).slice(i, i + 1)[0];
if (item === null || typeof item !== 'object') {
result.push(item as object);
} else {
const cloned = deepClone(item as object);
if (cloned !== null) {
result.push(cloned);
} else {
result.push(item as object);
}
}
}
return result;
}
// 处理对象类型
const keys = Object.keys(obj);
const keyLen = keys.length;
const clone: Record<string, object | null> = {};
for (let i = 0; i < keyLen; i++) {
const key = keys[i];
const value = (obj as Record<string, object | null>)[key];
if (value === null || typeof value !== 'object') {
clone[key] = value;
} else {
clone[key] = deepClone(value);
}
}
return clone;
}
5.5 关键技术点解析
5.5.1 递归实现
typescript
if (item === null || typeof item !== 'object') {
result.push(item as object);
} else {
const cloned = deepClone(item as object);
// ...
}
通过递归调用处理嵌套对象,确保所有层级的对象都被复制。
5.5.2 数组处理
typescript
if (Array.isArray(obj)) {
const arrLen = (obj as []).length;
const result: object[] = [];
for (let i = 0; i < arrLen; i++) {
const item = (obj as []).slice(i, i + 1)[0];
// ...
}
return result;
}
使用Array.isArray检测数组类型,然后逐个处理数组元素。
5.5.3 对象处理
typescript
const keys = Object.keys(obj);
const keyLen = keys.length;
const clone: Record<string, object | null> = {};
for (let i = 0; i < keyLen; i++) {
const key = keys[i];
const value = (obj as Record<string, object | null>)[key];
// ...
}
使用Object.keys()获取对象的所有键,然后逐个处理每个属性。
5.6 使用示例
示例1:状态管理中的深拷贝
typescript
interface User {
name: string;
age: number;
address: {
city: string;
street: string;
};
}
@Local originalUser: User = {
name: '张三',
age: 25,
address: {
city: '北京',
street: '朝阳区'
}
};
@Local clonedUser: User | null = null;
cloneUser(): void {
this.clonedUser = deepClone(this.originalUser) as User;
}
modifyClonedUser(): void {
if (this.clonedUser) {
this.clonedUser.name = '李四';
this.clonedUser.address.city = '上海';
}
}
build() {
Column() {
Button('克隆用户')
.onClick(() => {
this.cloneUser();
})
Button('修改克隆用户')
.onClick(() => {
this.modifyClonedUser();
})
Text(`原始用户: ${JSON.stringify(this.originalUser)}`)
.fontSize(12)
Text(`克隆用户: ${JSON.stringify(this.clonedUser)}`)
.fontSize(12)
}
}
示例2:表单数据备份
typescript
interface FormData {
username: string;
password: string;
preferences: {
darkMode: boolean;
notifications: boolean;
};
}
@Local formData: FormData = {
username: '',
password: '',
preferences: {
darkMode: false,
notifications: true
}
};
@Local savedData: FormData | null = null;
saveForm(): void {
this.savedData = deepClone(this.formData) as FormData;
console.log('表单已保存');
}
resetForm(): void {
if (this.savedData) {
this.formData = deepClone(this.savedData) as FormData;
console.log('表单已重置');
}
}
5.7 性能优化考虑
5.7.1 循环引用检测
当前实现不支持循环引用检测,如果对象中存在循环引用会导致无限递归。可以通过WeakSet来检测:
typescript
export function deepCloneWithCycle(obj: object | null, seen: WeakSet<object> = new WeakSet()): object | null {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 检测循环引用
if (seen.has(obj)) {
return obj; // 返回已处理的对象,避免无限递归
}
seen.add(obj);
if (Array.isArray(obj)) {
const arrLen = (obj as []).length;
const result: object[] = [];
for (let i = 0; i < arrLen; i++) {
const item = (obj as []).slice(i, i + 1)[0];
if (item === null || typeof item !== 'object') {
result.push(item as object);
} else {
const cloned = deepCloneWithCycle(item as object, seen);
if (cloned !== null) {
result.push(cloned);
}
}
}
return result;
}
const keys = Object.keys(obj);
const keyLen = keys.length;
const clone: Record<string, object | null> = {};
for (let i = 0; i < keyLen; i++) {
const key = keys[i];
const value = (obj as Record<string, object | null>)[key];
if (value === null || typeof value !== 'object') {
clone[key] = value;
} else {
clone[key] = deepCloneWithCycle(value, seen);
}
}
return clone;
}
六、generateId - 唯一ID生成函数
6.1 函数概述
generateId函数用于生成唯一标识符,常用于数据库记录、组件key等场景。
6.2 ArkTS实现
typescript
export function generateId(): string {
const timestamp: number = Date.now();
const random: number = Math.floor(Math.random() * 10000);
return `${timestamp}${random}`;
}
6.3 实现原理
- 时间戳:保证ID的唯一性和递增性
- 随机数:避免同一毫秒内生成重复ID
6.4 使用场景
typescript
interface TodoItem {
id: string;
title: string;
completed: boolean;
}
@Local todos: TodoItem[] = [];
addTodo(title: string): void {
const newTodo: TodoItem = {
id: generateId(),
title: title,
completed: false
};
this.todos.push(newTodo);
}
build() {
List() {
ForEach(this.todos, (item: TodoItem) => {
ListItem() {
Text(item.title)
.fontSize(16)
}
.key(item.id)
})
}
}
七、formatFileSize - 文件大小格式化函数
7.1 函数概述
formatFileSize函数用于将字节数转换为可读的文件大小格式。
7.2 ArkTS实现
typescript
export function formatFileSize(bytes: number): string {
if (bytes < 1024) {
return `${bytes} B`;
} else if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(2)} KB`;
} else if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
} else {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
}
7.3 使用场景
typescript
interface FileInfo {
name: string;
size: number;
type: string;
}
@Local files: FileInfo[] = [
{ name: 'document.pdf', size: 2048000, type: 'pdf' },
{ name: 'image.jpg', size: 153600, type: 'jpg' },
{ name: 'video.mp4', size: 52428800, type: 'mp4' }
];
build() {
List() {
ForEach(this.files, (file: FileInfo) => {
ListItem() {
Row() {
Text(file.name)
.fontSize(14)
Text(formatFileSize(file.size))
.fontSize(12)
.fontColor('#999999')
}
}
})
}
}
八、formatDuration - 时长格式化函数
8.1 函数概述
formatDuration函数用于将秒数转换为可读的时长格式。
8.2 ArkTS实现
typescript
export function formatDuration(seconds: number): string {
const hours: number = Math.floor(seconds / 3600);
const minutes: number = Math.floor((seconds % 3600) / 60);
const secs: number = seconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
8.3 使用场景
typescript
@Local videoDuration: number = 3661; // 1小时1分1秒
@Local progress: number = 120; // 2分钟
build() {
Column() {
Text(`总时长: ${formatDuration(this.videoDuration)}`)
.fontSize(14)
Text(`已播放: ${formatDuration(this.progress)}`)
.fontSize(14)
}
}
九、工具函数的模块化组织
9.1 目录结构
common/
├── utils/
│ ├── common.ets # 通用工具函数
│ ├── formatter.ets # 格式化工具函数
│ ├── converter.ets # 类型转换工具函数
│ ├── validator.ets # 验证工具函数
│ └── index.ets # 统一导出入口
├── ui/
│ ├── components.ets # UI组件
│ ├── layouts.ets # 布局组件
│ └── styles.ets # 样式定义
├── network/
│ ├── http.ets # HTTP请求封装
│ └── aiClient.ets # AI接口封装
├── data/
│ ├── parser.ets # 数据解析
│ └── storage.ets # 数据存储
└── state/
└── store.ets # 状态管理
9.2 统一导出入口
typescript
// common/utils/index.ets
export * from './common';
export * from './formatter';
export * from './converter';
export * from './validator';
9.3 使用方式
typescript
import {
isEmpty,
debounce,
throttle,
deepClone,
generateId,
formatFileSize,
formatDuration
} from '../common/utils';
十、性能优化与最佳实践
10.1 避免重复创建函数
typescript
// 错误:每次渲染都会创建新的防抖函数
@Builder
buildSearchInput() {
TextInput()
.onChange(() => {
debounce(() => this.search(), 300)();
})
}
// 正确:在组件初始化时创建一次
@Local debouncedSearch: () => void = debounce(() => {
this.search();
}, 300);
@Builder
buildSearchInput() {
TextInput()
.onChange(() => {
this.debouncedSearch();
})
}
10.2 及时清理定时器
typescript
@Local timer: number | null = null;
someFunction() {
if (this.timer !== null) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
// 执行操作
}, 1000);
}
aboutToDisappear() {
if (this.timer !== null) {
clearTimeout(this.timer);
}
}
10.3 深拷贝的性能考虑
对于大型对象,深拷贝可能会影响性能。可以考虑:
- 使用不可变数据结构
- 只复制需要修改的部分
- 使用缓存机制避免重复拷贝
十一、总结
本文详细介绍了HarmonyOS ArkTS中常用工具函数的实现与应用,包括:
- isEmpty - 空值判断,支持多种类型
- debounce - 防抖函数,限制高频事件的执行
- throttle - 节流函数,固定频率执行
- deepClone - 深拷贝函数,创建独立副本
- generateId - 唯一ID生成
- formatFileSize - 文件大小格式化
- formatDuration - 时长格式化
这些工具函数是HarmonyOS应用开发中不可或缺的基础组件,掌握它们的原理和使用场景,能够显著提升开发效率和代码质量。
在实际项目中,建议将这些工具函数按照模块化方式组织,便于维护和复用。同时,根据具体业务需求进行适当的扩展和优化,以满足不同场景的需求。
附录:工具函数完整代码
typescript
// common/utils/common.ets
export function isEmpty(value: string | number | boolean | object | undefined | null): boolean {
if (value === null || value === undefined) {
return true;
}
if (typeof value === 'string') {
return value.trim().length === 0;
}
if (typeof value === 'number') {
return isNaN(value);
}
if (Array.isArray(value)) {
return value.length === 0;
}
if (typeof value === 'object') {
return Object.keys(value).length === 0;
}
return false;
}
export function isNotEmpty(value: string | number | boolean | object | undefined | null): boolean {
return !isEmpty(value);
}
export function generateId(): string {
const timestamp: number = Date.now();
const random: number = Math.floor(Math.random() * 10000);
return `${timestamp}${random}`;
}
type SimpleFunction = () => void;
export function debounce(fn: SimpleFunction, delay: number): SimpleFunction {
let timer: number | null = null;
return (): void => {
if (timer !== null) {
clearTimeout(timer);
}
const timerValue = setTimeout(() => {
fn();
}, delay);
timer = timerValue as number;
};
}
export function throttle(fn: SimpleFunction, delay: number): SimpleFunction {
let lastTime: number = 0;
return (): void => {
const now: number = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn();
}
};
}
export function formatFileSize(bytes: number): string {
if (bytes < 1024) {
return `${bytes} B`;
} else if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(2)} KB`;
} else if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
} else {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
}
export function formatDuration(seconds: number): string {
const hours: number = Math.floor(seconds / 3600);
const minutes: number = Math.floor((seconds % 3600) / 60);
const secs: number = seconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
export function deepClone(obj: object | null): object | null {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
const arrLen = (obj as []).length;
const result: object[] = [];
for (let i = 0; i < arrLen; i++) {
const item = (obj as []).slice(i, i + 1)[0];
if (item === null || typeof item !== 'object') {
result.push(item as object);
} else {
const cloned = deepClone(item as object);
if (cloned !== null) {
result.push(cloned);
} else {
result.push(item as object);
}
}
}
return result;
}
const keys = Object.keys(obj);
const keyLen = keys.length;
const clone: Record<string, object | null> = {};
for (let i = 0; i < keyLen; i++) {
const key = keys[i];
const value = (obj as Record<string, object | null>)[key];
if (value === null || typeof value !== 'object') {
clone[key] = value;
} else {
clone[key] = deepClone(value);
}
}
return clone;
}