前言
'Talk Is Cheap, Show Me The Code' 代码规范是我们老生常谈的一个话题,我这里想聊的并不是Pretty
与EsLint
这些能表面规范代码工具的作用,而是我们开发者在开发中无意间导致性能额外损耗的代码规范。虽然优化的性能较少,但是庞大的项目而言这也将是比较大的优化之一,同时代码的可维护性也将有所提高🚀🚀🚀
main
优化 localStorage 遍历性能
计算 localStorage 已使用空间大小的函数。主要关注点在于如何高效地遍历和访问 localStorage,避免重复和低效的 API 调用。
function getUseSize() {
let size = 0;
// 问题:每次循环都多次访问 localStorage
for (const item in window.localStorage) {
// 问题:使用 for...in 遍历和 hasOwnProperty 检查
if (window.localStorage.hasOwnProperty(item)) {
size += window.localStorage.getItem(item)?.length;
}
}
return (size / 1024).toFixed(2);
}
- 使用 for...in 遍历会包含原型链属性
- 每次循环重复调用 localStorage API
- 性能效率低
function getUseSize() {
// 一次性获取所有键
const keys = Object.keys(window.localStorage);
let size = 0;
for (const key of keys) {
const item = window.localStorage.getItem(key);
if (item) {
size += item.length;
}
}
return (size / 1024).toFixed(2);
}
// 如果需要考虑性能优化,可以添加缓存机制
const CACHE_DURATION = 5000; // 5秒缓存
let cachedSize: string | null = null;
let lastUpdateTime = 0;
function getUseSizeWithCache() {
const now = Date.now();
if (cachedSize && now - lastUpdateTime < CACHE_DURATION) {
return cachedSize;
}
const size = getUseSize();
cachedSize = size;
lastUpdateTime = now;
return size;
}
优化点:
- 使用 Object.keys() 一次性获取所有键
- 避免重复调用 localStorage API
- 可选的缓存机制提高性能
- 代码更清晰可维护
这个优化既提高了代码的性能,也提升了代码的可维护性和可读性。对于频繁调用的场景,缓存机制可以进一步提升性能。
RN标签的嵌套层级
考虑将不需要样式和布局的纯包装用途的 <View>
替换为 fragment (<>
),
以减少不必要的原生视图节点。但注意保留需要应用样式、布局或处理触摸事件的 View 组件。
// Bad Case: 不必要的 View
const UnnecessaryView = () => (
<View> // 可以替换为 Fragment
<Header />
<MainContent />
<Footer />
</View>
);
// Good Case: 使用 Fragment 的正确场景
const GoodUseOfFragment = () => (
<>
<Header />
<MainContent />
<Footer />
</>
);
// Good Case: 需要 View 的场景
const NeedsView = () => (
<View style={styles.container}>
<Text>This needs a container with layout</Text>
</View>
);
Fragment节点的优点: 使用 Fragment 可以减少不必要的原生视图节点,可以潜在的提升性能 减少内存使用 在某些情况下可以优化渲染性能
限制和注意事项: Fragment 不能设置样式 Fragment 不能应用布局属性 Fragment 不能添加事件处理器 Fragment 不支持 ref
Vue3静态值处理最佳实践
// Bad Case: 不必要地将静态值包含在 setup 返回值中
export default defineComponent({
name: 'progress-bar',
setup() {
// 错误示范:静态值不需要放在 setup 中返回
const showProgressAnimation = true;
const state = reactive({
curPercentage: 0,
});
return {
state,
showProgressAnimation, // 不必要地暴露给模板
};
},
});
问题:
- 将不变的静态值包含在 setup 的返回值中
- 增加了响应式系统的负担
- 代码意图不够清晰
- 可能误导其他开发者认为这个值是可变的
// Good Case 1: 如果真的是静态值,可以直接作为组件属性
export default defineComponent({
name: 'progress-bar',
// 方案1:直接作为组件属性
showProgressAnimation: true,
setup() {
const state = reactive({
curPercentage: 0,
});
return {
state,
};
},
});
// Good Case 2: 如果需要基于条件计算,使用 computed
export default defineComponent({
name: 'progress-bar',
setup() {
// 方案2:使用计算属性(当值需要基于其他条件计算时)
const showProgressAnimation = computed(() => {
return someCondition;
});
const state = reactive({
curPercentage: 0,
});
return {
state,
showProgressAnimation,
};
},
});
优点:
- 代码意图更清晰
- 提高了性能
- 更好的可维护性
- 符合 Vue3 的最佳实践
最佳实践建议
- 对于完全静态的值,优先考虑使用常量
- 如果值需要基于条件计算,使用 computed
- 如果值确实是组件的静态特性,可以直接作为组件属性
- 避免将不需要响应式的值包含在响应式系统中
按需加载优化:减少打包体积
// Bad Case
import { ProTooltip } from '@kibt/pro-render';
这种方式会将整个 @kibt/pro-render 包中的所有组件都打包进来,导致打包体积增大,加载时间变长。即使开启了 Tree Shaking,这种方式仍可能会将一些未使用的代码打包进来,特别是如果库的结构不支持有效的 Tree Shaking。当然这种方法引入也可以使用babel插件可以自动转换成按需加载。
// Good Case
import ProTooltip from '@kibt/pro-render/components/ProTooltip.vue';
这种方式只会加载 ProTooltip 组件,避免了不必要的代码打包,从而减少了打包体积,提高了页面加载速度。
解释: 按需加载通过仅在需要时加载特定组件,避免了将整个库的所有组件都打包进来,从而显著减少了打包体积。这不仅优化了初始加载时间,还提高了应用的性能和用户体验。
避免不必要的序列化
// Bad Case
extParamsForPreReq: JSON.stringify({
buyParams: {
sellerMerchantId: String(payload.sellerMerchantId) ?? '',
sellerUserId: payload?.sellerUserId ?? '',
scene: payload?.scene ?? '',
cT: payload?.extParams?.cT ?? '',
cS: payload?.extParams?.cS ?? '',
cId: payload?.extParams?.cId ?? '',
},
}),
冗余数据:?? '' 操作符用于在左侧值为 null 或 undefined 时提供一个默认值。在这种情况下,默认值是空字符串 ''。但是,如果已经将值转换为字符串(例如 String(payload.sellerMerchantId)),那么即使原始值是 null 或 undefined,转换后的值也会是字符串 'null' 或 'undefined',而不是空字符串。
不必要的操作:由于已经将值转换为字符串,?? '' 操作符变得多余。移除它可以简化代码,减少不必要的操作。
一个好的例子是直接使用字符串转换后的值,而不使用 ?? '' 操作符:
extParamsForPreReq: JSON.stringify({
buyParams: {
sellerMerchantId: String(payload.sellerMerchantId),
sellerUserId: String(payload?.sellerUserId),
scene: String(payload?.scene),
cT: String(payload?.extParams?.cT),
cS: String(payload?.extParams?.cS),
cId: String(payload?.extParams?.cId),
},
}),
简化代码:移除冗余的 ?? '' 操作符,使代码更简洁。
减少冗余数据:避免在 JSON 中生成不必要的空字符串字段。
url参数拼接
建议使用 URLSearchParams 或 axios 的 params 配置来处理查询参数,
以确保正确的 URL 编码和参数处理。这样可以避免手动拼接 URL 可能带来的问题
// Bad Case
const groupId = "123&hack=1";
const source = "test=dangerous";
const isTiny = "true;dangerous";
// 直接拼接可能导致URL结构被破坏
const badUrl = `/rest/w/promotion/dailyCashLadder/assist?groupId=${groupId}&source=${source}&isTiny=${isTiny}`;
// 结果: /rest/w/promotion/dailyCashLadder/assist?groupId=123&hack=1&source=test=dangerous&isTiny=true;dangerous
正确的处理方式
// Good Case 1: 使用 URLSearchParams
const params = new URLSearchParams({
groupId,
source,
isTiny: String(isTiny)
});
const goodUrl = `/rest/w/promotion/dailyCashLadder/assist?${params.toString()}`;
// Good Case 2: 如果使用 axios,可以使用其内置的 params 选项
const response = await axios.post('/rest/w/promotion/dailyCashLadder/assist', null, {
params: {
groupId,
source,
isTiny
}
});
避免或逻辑意外覆盖变量
在 JS 代码中,使用 || 运算符可能会导致假值(如 0、空字符串 ""、null、undefined 或 false)被意外覆盖。为了避免这种情况,可以使用 ??(空值合并运算符),它只会在左侧操作数为 null 或 undefined 时才返回右侧操作数。这可以确保假值不会被意外覆盖。
// Bad Case
const data = { defaultValue: 0 };
const value = 10;
// 使用 || 运算符
<input defaultValue={data.defaultValue || value} />
在这个Case中,data.defaultValue 是 0,这是一个假值。由于 || 运算符会返回第一个真值,因此 defaultValue 将被设置为 value,即 10,而不是 0。
const data = { defaultValue: 0 };
const value = 10;
// Good Case 1 使用三元运算符
<input defaultValue={data.defaultValue !== undefined ? data.defaultValue : value} />
// Good Case 2 使用 ?? 运算符
<input defaultValue={data.defaultValue ?? value} />
在Case1中,data.defaultValue 是 0。由于我们明确检查 data.defaultValue 是否为 undefined,defaultValue 将被正确地设置为 0,而不是 10。
Case2中,使用 ?? 运算符可以避免假值被意外覆盖,因为它只会在左侧操作数为 null 或 undefined 时才返回右侧操作数。这使得代码更加健壮和易于维护。
在 switch
语句中添加一个 default case 的处理,避免异常 case
通过添加 default,可以增加代码的
- 健壮性:确保即使输入值不在已知的 case 中,程序仍能处理,而不会导致意外行为或崩溃。
- 可维护性:为未来的代码更新提供一个明确的处理路径,减少遗漏处理新情况的风险。
- 调试便利性:可以在 default 中添加日志或错误处理,帮助识别和调试未预见的输入。
switch (xxx) {
case A: // 业务逻辑 A
case B: // 业务逻辑 B
// ... 业务逻辑
default: // 兜底逻辑
}
vue中watch方法的第二个参数
注意,watch在监听某一变量的变化后,其第二个参数回调函数的入参为该监听变量变化的最新值,以下错误的写法仍旧使用了旧变量的值,导致获取到的pageVisible 不是最新结果,进而导致判断错误
// Bad case
watch(() => pageVisible, async () => {
if (!context.isInited || !pageVisible.value) {
return;
}
// ...
}, { immediate: true });
这里正确的写法应为,其中newVal 为pageVisible 变化后的最新订阅的结果
// Good case
watch(() => pageVisible.value, async (newVal) => {
if (!context.isInited || !newVal) {
return;
}
// ...
}, { immediate: true });
数组映射操作在JSX中的最佳实践
// Bad Case
<IndicatorScrollView>
{intAbTest.floorLevel === 2
? items.slice(0, sliceLength).map(renderItem)
: items.map(renderItem)}
</IndicatorScrollView>
// Good Case
const renderedItems = React.useMemo(() => {
return intAbTest.floorLevel === 2
? items.slice(0, sliceLength).map(renderItem)
: items.map(renderItem);
}, [items, sliceLength, intAbTest.floorLevel, renderItem]);
return (
<IndicatorScrollView>
{renderedItems}
</IndicatorScrollView>
优化原因分析
- 性能优化
Bad Case中,每次组件重新渲染时都会执行数组映射操作,即使依赖项没有发生变化
Good Case通过useMemo将计算结果缓存,只在依赖项变化时才重新计算,避免了不必要的运算
- 代码可读性
Bad Case将业务逻辑和渲染逻辑混在一起,降低了代码的可读性
Good Case实现了关注点分离,计算逻辑和渲染逻辑清晰分开
- 可维护性
Good Case的结构更利于后续维护和调试
如果需要添加新的条件判断或处理逻辑,可以直接在useMemo中修改,而不会影响渲染部分的代码
这个优化虽然在小规模应用中可能效果不明显,但在大型应用或者列表项较多的情况下,能带来可观的性能提升。同时,这种代码组织方式也更符合React的开发理念和最佳实践。
使用find、findIndex代替for循环
原代码使用传统的 for 循环来遍历数组并查找元素,这种方式不仅代码冗长,而且需要手动管理循环索引和终止条件。虽然使用了可选链操作符,但整体代码结构仍显得比较臃肿。同时,使用 for 循环的方式在处理大型数组时可能会有性能影响,因为它需要手动遍历每个元素直到找到目标。
// Bad Case
for (let i = 0; i < props?.awardList?.length; i++) {
const item = props.awardList[i];
if (item.state === 1) {
percentage.value = (i / props.awardList?.length) * 100;
break;
}
}
// Good Case1 使用 findIndex
const index = props?.awardList?.findIndex(item => item.state === 1) ?? -1;
if (index !== -1) {
percentage.value = (index / props.awardList.length) * 100;
}
// 方案2:使用可选链配合更简洁的写法
percentage.value = (props?.awardList?.findIndex(item => item.state === 1) / props.awardList?.length) * 100 || 0;
结语
目前总结如此为止,续后总结发现还会更新!!