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