跳到主要内容

模块加载副作用及优化

概述

当使用ArkTS模块化时,模块的加载和执行可能会引发副作用。副作用是指在模块导入时除了导出功能或对象之外,额外的行为或状态变化,这些行为可能影响程序的其他部分,并导致产生非预期的顶层代码执行、全局状态变化、原型链修改、导入内容未定义等问题

ArkTS模块化导致副作用的场景及优化方式

模块执行顶层代码

副作用产生场景

模块在被导入时,整个模块文件中的顶层代码会立即执行,而不仅仅是导出的部分。这意味着,即使只想使用模块中的某些导出内容,任何在顶层作用域中执行的代码也会运行,从而产生副作用。

// ModulePartOne.ets
console.info('Module loaded!'); // 这段代码在导入时会立即执行,可能会导致副作用。
export const data = 1;
// PageOne.ets
import { data } from './ModulePartOne'; // 导入时,ModulePartOne.ets中的console.info会执行,产生输出。
console.info('data is ', data);

输出内容:

Module loaded!
data is 1

产生的副作用

即使只需要data,console.info("Module loaded!") 仍会运行,导致开发者可能预期只输出data的值,但却额外输出了“Module loaded!”,影响输出内容

优化方式

优化方式1:去除顶层代码,只导出需要的内容,避免不必要的代码执行。

// ModulePartTwo.ets
export const data = 1;
// PageTwo.ets
import { data } from './ModulePartTwo';
console.info('data is ', data);

输出内容:

data is 1

优化方式2:将可能引发副作用的代码放在函数或方法内部,只有在需要时再执行,而不是在模块加载时立即执行。

// ModulePartThree.ets
export function initialize() {
console.info('Module loaded!');
}
export const data = 1;
// PageThree.ets
import { data } from './ModulePartThree';
console.info('data is ', data);

输出内容:

data is 1

修改全局对象

副作用产生场景

顶层代码或导入的模块可能会直接操作全局变量,改变全局状态,引发副作用。

// ModulePartFour.ets
export let data1 = 'data from module';
globalThis.someGlobalVar = 100; // 改变了全局状态
// SideEffectModuleFour.ets
export let data2 = 'data from side effect module';
globalThis.someGlobalVar = 200; // 也改变了全局状态
// ModuleUseGlobalVarFour.ets
import { data1 } from './ModulePartFour'; // 此时可能预期全局变量someGlobalVar的值为100
export function useGlobalVar() {
console.info('data1 is ', data1);
console.info('globalThis.someGlobalVar is ', globalThis.someGlobalVar); // 此时由于PageFour.ets中加载了SideEffectModuleFour模块,someGlobalVar的值已经被改为200
}
// PageFour.ets(执行入口)
import { data1 } from './ModulePartFour'; // 将全局变量someGlobalVar的值改为100
import { data2 } from './SideEffectModuleFour'; // 又将全局变量someGlobalVar的值改为200
import { useGlobalVar } from './ModuleUseGlobalVarFour';

useGlobalVar();
function maybeNotCalledAtAll() {
console.info('data1 is ', data1);
console.info('data2 is ', data2);
}

输出内容:

data1 is data from module
globalThis.someGlobalVar is 200

产生的副作用

模块加载时直接修改全局变量 globalThis.someGlobalVar 的值,会影响其他依赖该变量的模块或代码

优化方式

将可能引发副作用的代码放在函数或方法内部,只有在需要时再执行,而不是在模块加载时立即执行。

// ModulePartFive.ets
export let data1 = 'data from module';
export function changeGlobalVar() {
globalThis.someGlobalVar = 100;
}
// SideEffectModuleFive.ets
export let data2 = 'data from side effect module';
export function changeGlobalVar() {
globalThis.someGlobalVar = 200;
}
// ModuleUseGlobalVarFive.ets
import { data1, changeGlobalVar } from './ModulePartFive';
export function useGlobalVar() {
console.info('data1 is ', data1);
changeGlobalVar(); // 在需要的时候执行代码,而不是模块加载时执行。
console.info('globalThis.someGlobalVar is ', globalThis.someGlobalVar);
}
// PageFive.ets(执行入口)
import { data1 } from './ModulePartFive';
import { data2 } from './SideEffectModuleFive';
import { useGlobalVar } from './ModuleUseGlobalVarFive';

useGlobalVar();
function maybeNotCalledAtAll() {
console.info('data1 is ', data1);
console.info('data2 is ', data2);
}

输出内容:

data1 is data from module
globalThis.someGlobalVar is 100

修改应用级ArkUI组件的状态变量信息

副作用产生场景

顶层代码或导入的模块可能会直接修改应用级ArkUI组件的状态变量信息,改变全局状态,引发副作用。

// ModulePartSix.ets
export let data = 'data from module';
AppStorage.setOrCreate('SomeAppStorageVar', 200); // 修改应用全局的UI状态
// PageSix.ets
import { data } from './ModulePartSix'; // 将AppStorage中的SomeAppStorageVar改为200

@Entry
@Component
struct Index {
// 开发者可能预期该值为100,但是由于ModulePartSix模块导入,该值已经被修改为200,但开发者可能并不知道值已经被修改
@StorageLink('SomeAppStorageVar') message: number = 100;
build() {
Row() {
Column() {
Text('test' + this.message)
.fontSize(50)
}
.width('100%')
}
.height('100%')
}
}
function maybeNotCalledAtAll() {
console.info('data is ', data);
}

显示内容:

test200

产生的副作用

模块加载时直接修改AppStorage中SomeAppStorageVar的值,会影响其他依赖该变量的模块或代码

ArkUI组件的状态变量信息可以通过一些应用级接口修改,详见ArkUI状态管理接口文档

优化方式

将可能引发副作用的代码放在函数或方法内部,只有在需要时再执行,而不是在模块加载时立即执行。

// ModulePartSeven.ets
export let data = 'data from module';
export function initialize() {
AppStorage.setOrCreate('SomeAppStorageVar', 200);
}
// PageSeven.ets
import { data } from './ModulePartSeven';

@Entry
@Component
struct Index {
@StorageLink('SomeAppStorageVar') message: number = 100;
build() {
Row() {
Column() {
Text('test' + this.message)
.fontSize(50)
}
.width('100%')
}
.height('100%')
}
}
function maybeNotCalledAtAll() {
console.info('data is ', data);
}

显示内容:

test100

修改内置全局变量或原型链(ArkTS内禁止修改对象原型与内置方法)

副作用产生场景

为使现代JavaScript特性能够在旧版浏览器或运行环境中运行,第三方库或框架可能会修改内置的全局对象或原型链,从而影响其他代码的执行。

// ModifyPrototype.ts
export let data = 'data from modifyPrototype';
Array.prototype.includes = function (value) {
return this.indexOf(value) !== -1;
};
// PageEight.ets
import { data } from './ModifyPrototype'; // 此时修改了Array的原型链
let arr = [1, 2, 3, 4];
console.info('arr.includes(1) = ' + arr.includes(1)); // 此时调用的是ModifyPrototype.ts中的Array.prototype.includes方法
function maybeNotCalledAtAll() {
console.info('data is ', data);
}

产生的副作用

修改内置的全局对象或原型链,可能会影响其他代码运行。

优化方式

导入可能会修改内置的全局对象或原型链的第三方库时,确认该第三方库的行为是符合预期的。

循环依赖

副作用产生场景

ArkTS模块化支持循环依赖,即模块A依赖模块B,同时模块B又依赖模块A。在这种情况下,某些导入的模块可能尚未完全加载,从而导致部分代码在执行时行为异常,产生意外的副作用。

// ExportA.ets
import { b } from './ExportB';
console.info('Module A: ', b);
export const a = 'A';
// ExportB.ets
import { a } from './ExportA';
console.info('Module B: ', a);
export const b = 'B';

输出内容:

Error message: a is not initialized
Stacktrace:
at func_main_0 (b.ets:2:27)

产生的副作用

由于模块间相互依赖,模块的执行顺序可能导致导出的内容未定义,影响代码的逻辑流,具体报错信息为:“变量名 is not initialized”。

优化方式

尽量避免模块间的循环依赖,确保模块的加载顺序是明确和可控的,以避免产生意外的副作用。@security/no-cycle循环依赖检查工具 可以辅助检查循环依赖。

延迟加载(lazy import)改变模块执行顺序,可能导致预期的全局变量未定义

副作用产生场景

延迟加载特性可使待加载模块在冷启动阶段不被加载,直至应用程序实际运行过程中需要用到这些模块时,才按需同步加载相关模块,从而缩短应用冷启动耗时。但这也同时会改变模块的执行顺序。

// ModulePartNine.ets
export let data = 'data from module';
globalThis.someGlobalVar = 100;
// ModuleUseGlobalVarNine.ets
import lazy { data } from './ModulePartNine';
console.info('globalThis.someGlobalVar', globalThis.someGlobalVar); // 此时由于lazy特性,ModulePartNine模块还未执行,someGlobalVar的值为undefined
console.info('data is ', data); // 使用到ModulePartNine模块的变量,此时ModulePartNine模块执行,someGlobalVar的值变为100

输出内容:

globalThis.someGlobalVar undefined
data is data from module

产生的副作用

由于使用到延迟加载(lazy import)特性,会导致模块变量在使用到时再执行对应的模块,模块中的一些全局变量修改行为也会延迟,可能会导致运行结果不符合预期。

优化方式

将可能引发副作用的代码放在函数或方法内部,只有在需要时再执行,而不是在模块加载时立即执行。

// ModulePartTen.ets
export let data = 'data from module';
export function initialize() {
globalThis.someGlobalVar = 100; // 延迟到函数调用时执行
}
// ModuleUseGlobalVarTen.ets
import lazy { data, initialize } from './ModulePartTen';
initialize(); // 执行初始化函数,初始化someGlobalVar
console.info('globalThis.someGlobalVar is ', globalThis.someGlobalVar); // 此时someGlobalVar一定为预期的值
console.info('data is ', data);

输出内容:

globalThis.someGlobalVar is 100
data is data from module

通过import路径展开优化性能

原理

在import语句中,跳过中间的依赖路径,直接依赖变量对应的模块,即为import路径展开。

下文将通过示例说明import路径展开优化性能的原理。

// main.ets
import * as har from "har"
console.info("har.One is ", har.One); // 这里的One变量是har/src/main/ets/NumberString.ets导出的

// har/Index.ets
export * from "./src/main/ets/OtherModule1"
export * from "./src/main/ets/OtherModule2"
export * from "./src/main/ets/Utils"
console.info("har Index.ets execute.");

// har/src/main/ets/Utils.ets
export * from "./OtherModule3"
export * from "./OtherModule4"
export * from "./NumberString"
console.info("har Utils.ets execute.");
// har/src/main/ets/NumberString.ets
export const One: string = '1';
console.info('har NumberString.ets execute.');
  1. 如果main.ets只需要依赖har中的NumberString模块,import xxx from "har"的写法会导致har整条链路上的模块被解析、执行,导致模块解析及执行耗时增加。上述例子中的har/Index、OtherModule1、OtherModule2、Utils、OtherModule3、OtherModule4、NumberString模块均会被解析、执行。
  2. 在模块解析阶段会通过深度优先遍历的方式建立变量的绑定关系,main.ets中使用的har.One变量是由har/src/main/ets/NumberString.ets导出的,由于使用了export 的写法,建立变量的绑定关系时需要递归去进行变量名的匹配,从而*导致模块解析耗时增加。

在上述例子中,会先查找 har/Index.ets 文件。该文件中有多个 export * 语句,因此会依次检查 OtherModule1 和 OtherModule2 是否导出 One 变量。接着,找到 Utils 模块,该模块也有 export * 语句,因此会继续检查 OtherModule3 和 OtherModule4,最终确定 One 变量是从 NumberString 模块导出的。

优化方式:改为如下的代码写法,跳过中间的依赖路径,直接依赖变量对应的模块。

// PageEleven.ets
import { One } from 'staticlibrary/src/main/ets/components/NumberString';
console.info('One is ', One);
// har/src/main/ets/NumberString.ets
export const One: string = '1';
console.info('har NumberString.ets execute.');

副作用

副作用产生场景

由于import路径展开会跳过中间模块的执行,若业务依赖模块的执行顺序,修改后可能会导致业务异常。

// PageTwelve.ets
import { serviceManager } from 'staticlibrary';

serviceManager.print();
import { serviceManager } from './src/main/ets/ServiceManagerPartOne';

serviceManager.init();
export { serviceManager }
// har/src/main/ets/ServiceManagerPartOne.ets
class ServiceManager {
public inited: boolean = false;

public init() {
this.inited = true;
}
public print() {
if (this.inited) {
console.info('ServiceManager is inited.');
} else {
console.error('ServiceManager is not inited.');
}
}
}
export let serviceManager: ServiceManager = new ServiceManager();

运行的输出为:

ServiceManager is inited.

如果进行import路径展开,展开后的代码为:

// PageThirteen.ets
import { serviceManager } from 'staticlibrary/src/main/ets/ServiceManagerPartTwo';

serviceManager.print();
// har/src/main/ets/ServiceManagerPartTwo.ets
class ServiceManager {
public inited: boolean = false;

public init() {
this.inited = true;
}
public print() {
if (this.inited) {
console.info('ServiceManager is inited.');
} else {
console.error('ServiceManager is not inited.');
}
}
}
export let serviceManager: ServiceManager = new ServiceManager();

运行的输出为:

ServiceManager is not inited.

产生的副作用

由于har/Index模块中存在顶层代码进行ServiceManager的初始化,如果在main模块中进行import路径展开,将不会执行har/Index模块,从而导致ServiceManager未初始化,可能引起业务异常。

优化方式

开发者需根据业务需要排查跳过执行顶层代码的影响,并进行相应的修改。

对于上文的示例,可以进行如下修改:

// PageFourteen.ets
import { serviceManager } from 'staticlibrary/src/main/ets/ServiceManagerPartThree';

serviceManager.print();
// har/src/main/ets/ServiceManagerPartThree.ets
class ServiceManager {
public inited: boolean = false;

public init() {
this.inited = true;
}
public print() {
if (this.inited) {
console.info('ServiceManager is inited.');
} else {
console.error('ServiceManager is not inited.');
}
}
}
export let serviceManager: ServiceManager = new ServiceManager();
// 在导出的模块执行对应的逻辑。
serviceManager.init();