• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 模块加载副作用及优化
2<!--Kit: ArkTS-->
3<!--Subsystem: ArkCompiler-->
4<!--Owner: @wangchen965-->
5<!--Designer: @yao_dashuai-->
6<!--Tester: @kirl75; @zsw_zhushiwei-->
7<!--Adviser: @foryourself-->
8
9## 概述
10
11当使用[ArkTS模块化](module-principle.md)时,模块的加载和执行可能会引发**副作用**。副作用是指在模块导入时除了导出功能或对象之外,额外的行为或状态变化,**这些行为可能影响程序的其他部分,并导致产生非预期的顶层代码执行、全局状态变化、原型链修改、导入内容未定义等问题**。
12
13## ArkTS模块化导致副作用的场景及优化方式
14
15### 模块执行顶层代码
16
17**副作用产生场景**
18
19模块在被导入时,整个模块文件中的顶层代码会立即执行,而不仅仅是导出的部分。这意味着,即使只想使用模块中的某些导出内容,任何在顶层作用域中执行的代码也会运行,从而产生副作用。
20
21```typescript
22// module.ets
23console.info("Module loaded!"); // 这段代码在导入时会立即执行,可能会导致副作用。
24export const data = 1;
25
26// main.ets
27import { data } from './module' // 导入时,module.ets中的console.info会执行,产生输出。
28console.info("data is ", data);
29```
30
31输出内容:
32
33```typescript
34Module loaded!
351
36```
37
38**产生的副作用**
39
40即使只需要data,console.info("Module loaded!") 仍会运行,导致开发者可能预期只输出data的值,但却额外输出了“Module loaded!”,**影响输出内容**。
41
42**优化方式**
43
44优化方式1:去除顶层代码,只导出需要的内容,避免不必要的代码执行。
45
46```typescript
47// module.ets
48export const data = 1;
49
50// main.ets
51import { data } from './module'
52console.info("data is ", data);
53```
54
55输出内容:
56
57```typescript
581
59```
60
61优化方式2:将可能引发副作用的代码放在函数或方法内部,只有在需要时再执行,而不是在模块加载时立即执行。
62
63```typescript
64// module.ets
65export function initialize() {
66    console.info("Module loaded!");
67}
68export const data = 1;
69
70// main.ets
71import { data } from './module'
72console.info("data is ", data);
73```
74
75输出内容:
76
77```typescript
781
79```
80
81### 修改全局对象
82
83**副作用产生场景**
84
85顶层代码或导入的模块可能会直接**操作全局变量**,改变全局状态,引发副作用。
86
87```typescript
88// module.ets
89export let data1 = "data from module"
90globalThis.someGlobalVar = 100; // 改变了全局状态
91
92// sideEffectModule.ets
93export let data2 = "data from side effect module"
94globalThis.someGlobalVar = 200; // 也变了全局状态
95
96// moduleUseGlobalVar.ets
97import { data1 } from './module' // 此时可能预期全局变量someGlobalVar的值为100
98export function useGlobalVar() {
99    console.info("data1 is ", data1);
100    console.info("globalThis.someGlobalVar is ", globalThis.someGlobalVar); // 此时由于main.ets中加载了sideEffectModule模块,someGlobalVar的值已经被改为200
101}
102
103// main.ets(执行入口)
104import { data1 } from "./module" // 将全局变量someGlobalVar的值改为100
105import { data2 } from "./sideEffectModule" // 又将全局变量someGlobalVar的值改为200
106import { useGlobalVar } from './moduleUseGlobalVar'
107
108useGlobalVar();
109function maybeNotCalledAtAll() {
110    console.info("data1 is ", data1);
111    console.info("data2 is ", data2);
112}
113```
114
115输出内容:
116
117```text
118data from module
119200
120```
121
122**产生的副作用**
123
124模块加载时直接修改全局变量 `globalThis.someGlobalVar` 的值,**会影响其他依赖该变量的模块或代码**。
125
126**优化方式**
127
128将可能引发副作用的代码放在函数或方法内部,只有在需要时再执行,而不是在模块加载时立即执行。
129
130```typescript
131// module.ets
132export let data1 = "data from module"
133export function changeGlobalVar() {
134    globalThis.someGlobalVar = 100;
135}
136
137// sideEffectModule.ets
138export let data2 = "data from side effect module"
139export function changeGlobalVar() {
140    globalThis.someGlobalVar = 200;
141}
142
143// moduleUseGlobalVar.ets
144import { data1, changeGlobalVar } from './module'
145export function useGlobalVar() {
146    console.info("data1 is ", data1);
147    changeGlobalVar(); // 在需要的时候执行代码,而不是模块加载时执行。
148    console.info("globalThis.someGlobalVar is ", globalThis.someGlobalVar);
149}
150
151// main.ets(执行入口)
152import { data1 } from "./module"
153import { data2 } from "./sideEffectModule"
154import { useGlobalVar } from './moduleUseGlobalVar'
155
156useGlobalVar();
157function maybeNotCalledAtAll() {
158    console.info("data1 is ", data1);
159    console.info("data2 is ", data2);
160}
161```
162
163输出内容:
164
165```text
166data from module
167100
168```
169
170### 修改应用级ArkUI组件的状态变量信息
171
172**副作用产生场景**
173
174顶层代码或导入的模块可能会直接**修改应用级ArkUI组件的状态变量信息**,改变全局状态,引发副作用。
175
176```typescript
177// module.ets
178export let data = "data from module"
179AppStorage.setOrCreate("SomeAppStorageVar", 200); // 修改应用全局的UI状态
180
181// Index.ets
182import { data } from "./module" // 将AppStorage中的SomeAppStorageVar改为200
183
184@Entry
185@Component
186struct Index {
187    // 开发者可能预期该值为100,但是由于module模块导入,该值已经被修改为200,但开发者可能并不知道值已经被修改
188    @StorageLink("SomeAppStorageVar") message: number = 100;
189    build() {
190        Row() {
191            Column() {
192                Text("test" + this.message)
193                    .fontSize(50)
194            }
195            .width("100%")
196        }
197        .height("100%")
198    }
199}
200function maybeNotCalledAtAll() {
201    console.info("data is ", data);
202}
203```
204
205显示内容:
206
207```text
208test200
209```
210
211**产生的副作用**
212
213模块加载时直接修改AppStorage中SomeAppStorageVar的值,**会影响其他依赖该变量的模块或代码**。
214
215ArkUI组件的状态变量信息可以通过一些应用级接口修改,详见[ArkUI状态管理接口文档](../ui/state-management/arkts-state-management-overview.md)。
216
217**优化方式**
218
219将可能引发副作用的代码放在函数或方法内部,只有在需要时再执行,而不是在模块加载时立即执行。
220
221```typescript
222// module.ets
223export let data = "data from module"
224export function initialize() {
225    AppStorage.setOrCreate("SomeAppStorageVar", 200);
226}
227
228// Index.ets
229import { data } from "./module"
230
231@Entry
232@Component
233struct Index {
234    @StorageLink("SomeAppStorageVar") message: number = 100;
235    build() {
236        Row() {
237            Column() {
238                Text("test" + this.message)
239                    .fontSize(50)
240            }
241            .width("100%")
242        }
243        .height("100%")
244    }
245}
246function maybeNotCalledAtAll() {
247    console.info("data is ", data);
248}
249```
250
251显示内容:
252
253```text
254test100
255```
256
257### 修改内置全局变量或原型链(ArkTS内禁止修改对象原型与内置方法)
258
259**副作用产生场景**
260
261为使现代JavaScript特性能够在旧版浏览器或运行环境中运行,第三方库或框架可能会修改内置的全局对象或原型链,从而影响其他代码的执行。
262
263```typescript
264// modifyPrototype.ts
265export let data = "data from modifyPrototype"
266Array.prototype.includes = function (value) {
267    return this.indexOf(value) !== -1;
268};
269
270// main.ets
271import { data } from "./modifyPrototype" // 此时修改了Array的原型链
272let arr = [1, 2, 3, 4];
273console.info("arr.includes(1) = " + arr.includes(1)); // 此时调用的是modifyPrototype.ts中的Array.prototype.includes方法
274function maybeNotCalledAtAll() {
275    console.info("data is ", data);
276}
277```
278
279**产生的副作用**
280
281修改内置的全局对象或原型链,可能会影响其他代码运行。
282
283**优化方式**
284
285导入可能会修改内置的全局对象或原型链的第三方库时,确认该第三方库的行为是符合预期的。
286
287### 循环依赖
288
289**副作用产生场景**
290
291ArkTS模块化支持循环依赖,即模块A依赖模块B,同时模块B又依赖模块A。在这种情况下,某些导入的模块可能尚未完全加载,从而导致部分代码在执行时行为异常,产生意外的副作用。
292
293```typescript
294// a.ets
295import { b } from "./b"
296console.info('Module A: ', b);
297export const a = 'A';
298
299// b.ets
300import { a } from "./a"
301console.info('Module B: ', a);
302export const b = 'B';
303```
304
305输出内容:
306
307```text
308Error message: a is not initialized
309Stacktrace:
310    at func_main_0 (b.ets:2:27)
311```
312
313**产生的副作用**
314
315由于模块间相互依赖,模块的执行顺序可能导致导出的内容未定义,影响代码的逻辑流,具体报错信息为:“变量名 is not initialized”。
316
317**优化方式**
318
319尽量避免模块间的循环依赖,确保模块的加载顺序是明确和可控的,以避免产生意外的副作用。[@security/no-cycle循环依赖检查工具](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide_no-cycle) 可以辅助检查循环依赖。
320
321### 延迟加载(lazy import)改变模块执行顺序,可能导致预期的全局变量未定义
322
323**副作用产生场景**
324
325[延迟加载](arkts-lazy-import.md)特性可使待加载模块在冷启动阶段不被加载,直至应用程序实际运行过程中需要用到这些模块时,才按需同步加载相关模块,从而缩短应用冷启动耗时。但这也同时会改变模块的执行顺序。
326
327```typescript
328// module.ets
329export let data = "data from module"
330globalThis.someGlobalVar = 100;
331
332// moduleUseGlobalVar.ets
333import lazy { data } from "./module"
334console.info("globalThis.someGlobalVar", globalThis.someGlobalVar); // 此时由于lazy特性,module模块还未执行,someGlobalVar的值为undefined
335console.info("data is ", data); // 使用到module模块的变量,此时module模块执行,someGlobalVar的值变为100
336```
337
338输出内容:
339
340```text
341undefined
342data from module
343```
344
345**产生的副作用**
346
347由于使用到延迟加载(lazy import)特性,会导致模块变量在使用到时再执行对应的模块,模块中的一些全局变量修改行为也会延迟,可能会导致运行结果不符合预期。
348
349**优化方式**
350
351将可能引发副作用的代码放在函数或方法内部,只有在需要时再执行,而不是在模块加载时立即执行。
352
353```typescript
354// module.ets
355export let data = "data from module"
356export function initialize() {
357    globalThis.someGlobalVar = 100; // 延迟到函数调用时执行
358}
359
360// moduleUseGlobalVar.ets
361import lazy { data, initialize } from "./module"
362initialize(); // 执行初始化函数,初始化someGlobalVar
363console.info("globalThis.someGlobalVar is ", globalThis.someGlobalVar); // 此时someGlobalVar一定为预期的值
364console.info("data is ", data);
365```
366
367输出内容:
368
369```text
370100
371data from module
372```
373
374## 通过import路径展开优化性能
375
376### 原理
377
378在import语句中,跳过中间的依赖路径,直接依赖变量对应的模块,即为import路径展开。
379
380下文将通过示例说明import路径展开优化性能的原理。
381
382```typescript
383// main.ets
384import * as har from "har"
385console.info("har.One is ", har.One); // 这里的One变量是har/src/main/ets/NumberString.ets导出的
386
387// har/Index.ets
388export * from "./src/main/ets/OtherModule1"
389export * from "./src/main/ets/OtherModule2"
390export * from "./src/main/ets/Utils"
391console.info("har Index.ets execute.");
392
393// har/src/main/ets/Utils.ets
394export * from "./OtherModule3"
395export * from "./OtherModule4"
396export * from "./NumberString"
397console.info("har Utils.ets execute.");
398
399// har/src/main/ets/NumberString.ets
400export const One: string = "1";
401console.info("har NumberString.ets execute.");
402```
403
4041. 如果main.ets只需要依赖har中的NumberString模块,import xxx from "har"的写法会导致har整条链路上的模块被解析、执行,**导致模块解析及执行耗时增加**。上述例子中的har/Index、OtherModule1、OtherModule2、Utils、OtherModule3、OtherModule4、NumberString模块均会被解析、执行。
405
4062. 在模块解析阶段会通过深度优先遍历的方式建立变量的绑定关系,main.ets中使用的har.One变量是由har/src/main/ets/NumberString.ets导出的,由于使用了export *的写法,建立变量的绑定关系时需要递归去进行变量名的匹配,从而**导致模块解析耗时增加**。
407在上述例子中,会先查找 `har/Index.ets` 文件。该文件中有多个 `export *` 语句,因此会依次检查 `OtherModule1` 和 `OtherModule2` 是否导出 `One` 变量。接着,找到 `Utils` 模块,该模块也有 `export *` 语句,因此会继续检查 `OtherModule3` 和 `OtherModule4`,最终确定 `One` 变量是从 `NumberString` 模块导出的。
408
409优化方式:改为如下的代码写法,跳过中间的依赖路径,直接依赖变量对应的模块。
410
411```typescript
412// main.ets
413import { One } from "har/src/main/ets/NumberString"
414console.info("One is ", One);
415
416// har/src/main/ets/NumberString.ets
417export const One: string = "1";
418console.info("har NumberString.ets execute.");
419```
420
421### 副作用
422
423**副作用产生场景**
424
425由于import路径展开会跳过中间模块的执行,若业务依赖模块的执行顺序,修改后可能会导致业务异常。
426
427```typescript
428// main.ets
429import { serviceManager } from "har"
430
431serviceManager.print();
432
433// har/Index.ets
434import { serviceManager } from "./src/main/ets/ServiceManager"
435
436serviceManager.init();
437export { serviceManager }
438
439// har/src/main/ets/ServiceManager.ets
440class ServiceManager {
441    public inited: boolean = false;
442
443    public init() {
444        this.inited = true;
445    }
446    public print() {
447        if (this.inited) {
448            console.info("ServiceManager is inited.");
449        } else {
450            console.error("ServiceManager is not inited.");
451        }
452    }
453}
454export let serviceManager: ServiceManager = new ServiceManager();
455```
456
457运行的输出为:
458
459```text
460ServiceManager is inited.
461```
462
463如果进行import路径展开,展开后的代码为:
464
465```typescript
466// main.ets
467import { serviceManager } from "har/src/main/ets/ServiceManager"
468
469serviceManager.print();
470
471// har/src/main/ets/ServiceManager.ets
472class ServiceManager {
473    public inited: boolean = false;
474
475    public init() {
476        this.inited = true;
477    }
478    public print() {
479        if (this.inited) {
480            console.info("ServiceManager is inited.");
481        } else {
482            console.error("ServiceManager is not inited.");
483        }
484    }
485}
486export let serviceManager: ServiceManager = new ServiceManager();
487```
488
489运行的输出为:
490
491```text
492ServiceManager is not inited.
493```
494
495**产生的副作用**
496
497由于har/Index模块中存在顶层代码进行ServiceManager的初始化,如果在main模块中进行import路径展开,将不会执行har/Index模块,从而导致ServiceManager未初始化,可能引起业务异常。
498
499**优化方式**
500
501开发者需根据业务需要排查跳过执行顶层代码的影响,并进行相应的修改。
502
503对于上文的示例,可以进行如下修改:
504
505```typescript
506// main.ets
507import { serviceManager } from "har/src/main/ets/ServiceManager"
508
509serviceManager.print();
510
511// har/src/main/ets/ServiceManager.ets
512class ServiceManager {
513    public inited: boolean = false;
514
515    public init() {
516        this.inited = true;
517    }
518    public print() {
519        if (this.inited) {
520            console.info("ServiceManager is inited.");
521        } else {
522            console.error("ServiceManager is not inited.");
523        }
524    }
525}
526export let serviceManager: ServiceManager = new ServiceManager();
527// 在导出的模块执行对应的逻辑。
528serviceManager.init();
529```
530