📢 DevUI团队重磅推出MateChat,前端智能化场景AI解决方案,为你的项目快速添加一个智能化助手,目前已在华为内部及外部多个服务应用落地实践,欢迎前来体验~
源码:gitcode.com/DevCloudFE/…(欢迎star~)
前端框架提供的内存占用优化
感谢我们社区贡献者Aubur22提供的优质文章!期待大家点在收藏~
在 JS 开发中,很多时候我们不需要关注内存占用情况。
比如,Vue
或者 React
中,监听事件后,组件销毁时会自动取消监听,事件监听的内存占用被自动释放,如上图左边的情况。
// Vue
export default defineComponent({
name: 'Button',
setup() {
const count = ref(0);
return () => (
// 组件销毁时自动取消监听 click
<button onClick={() => count.value++}>count: { count.value }</button>
);
}
})
// React
export default function Button() {
const [count, setCount] = useState(0);
return (
// 组件销毁时自动取消监听 click
<button onClick={() => setCount(count + 1)}>count: { count }</button>
);
}
虽然前端框架帮助我们做了很多事情,减少了对内存占用的关注。但是框架文档仍会提醒我们──在组件销毁时应及时清理不需要的数据。
如:组件中渲染 ECharts 图后,要在组件销毁阶段销毁图;EventBus/EventEmitter 订阅事件后,要在销毁时取消订阅;原生 JS 监听事件后,要及时取消监听;setInterval 及时清理等等。但手动完成这些事情往往意味着重复、遗漏、不够可靠。
为什么需要自动取消订阅
本文将以
Angular
为例,介绍“组件销毁时自动取消订阅”的一种实现思路,其他框架也可参考同样的思路。
Angular 的状态管理和响应式模型非常依赖 RxJS,尤其是在 Angular Signals 普及开来之前。
在 RxJS 中订阅 Observable 之后,如果无需全运行时生效,则需要在适当时机取消订阅。若未及时取消订阅,会导致各种各样的问题:内存占用高、执行上下文变化导致程序执行异常等。
在组件中,我们通常会选择在组件销毁的时候手动取消订阅。手动取消订阅,1 个直接取消不麻烦,2 个以上通过数组批量取消也还行。
@Component({ selector: 'app-test', template: '' })
export class TestComponent {
// 💡单个订阅
scrollSubscription: Subscription;
// 💡批量订阅
subscriptionList: Subscription[];
ngAfterViewInit() {
this.scrollSubscription = fromEvent(document.documentElement, 'scroll').subscribe(console.log);
this.subscriptionList.push(
fromEvent(document, 'click').subscribe(console.log),
fromEvent(document, 'mousemove').subscribe(console.log),
);
// 💡执行后自动结束,无需取消订阅
http.get('/api/user/devui').subscribe(console.log)
}
ngOnDestroy() {
this.scrollSubscription.unsubscribe();
this.subscriptionList.forEach(item => item.unsubscribe());
}
}
这时,同事引入了一种新的取消订阅形式。完成 1 件事情,就有了 3 种方式,可读性困难度增加。
@Component({ selector: 'app-test', template: '' })
export class TestComponent {
// 💡takeUntil 集中取消订阅
destroySubject = new Subject<void>();
ngAfterViewInit() {
// 省略单个订阅、批量订阅 ...
fromEvent(document, 'keydown').pipe(takeUntil(this.destroySubject)).subscribe(console.log);
}
ngOnDestroy() {
this.destroySubject.next();
this.destroySubject.complete();
}
}
在多人协作的情况下,一个组件中多种多样的订阅及其取消方式越来越复杂。甚至,可能有些时候忘记取消订阅,程序不可控程度增加了。
而且我们还有很多别的组件,还会把上面各种订阅、取消订阅方式一遍遍重复地写。每次发现新组件中没有 destroySubject
,还得手动创建一个变量,写 2 行销毁方法,真费劲。
太麻烦了,我们需要一种新的方式,来简化这样一件事 “组件销毁时,自动取消订阅”。
如何实现自动销毁
基于上述 3 种订阅取消方式,自动销毁整体思路如下:
- 自动获取单个订阅,遍历组件
Subscription
类型属性。浅层遍历组件属性对性能影响可控。 - 自动获取指定的批量订阅,指定组件某个
Subscription[]
类型属性名。涉及深层遍历组件属性,对性能影响较大,此处直接指定一个属性名即可。 - 自动生成集中取消订阅所需要的
Subject
,提供 RxJS 操作符自动关联Subject
到takeUntil
- 扩展组件的
ngOnDestroy
原型方法,自动获取前 3 步的订阅,并取消订阅。
代码实现
○ 使用方式
// AutoDestroy 自动扩展了组件的 ngOnDestroy 方法
// 配合 checkProperties 属性,选择性开启是否在组件销毁时自动取消单个订阅,默认 true
// 配合 arrayName 属性,在组件销毁时,遍历 arrayName 指定的属性,批量取消订阅,默认空
// 自动执行结束 takeUntil(Subject) 中的 Subject
@AutoDestroy({ checkProperties: true, arrayName: 'subscriptionList' })
@Component({ selector: 'app-test', template: '' })
export class TestComponent {
// 💡单个订阅
scrollSubscription: Subscription;
// 💡批量订阅
subscriptionList: Subscription[];
ngAfterViewInit() {
this.scrollSubscription = fromEvent(document.documentElement, 'scroll').subscribe(console.log);
this.subscriptionList.push(
fromEvent(document, 'click').subscribe(console.log),
fromEvent(document, 'mousemove').subscribe(console.log),
);
// 💡配合 autoDestroyPipe 自动添加 takeUntil(Subject) 的逻辑,注意 this 需为当前组件实例
fromEvent(document, 'keydown').pipe(autoDestroyPipe(this)).subscribe(console.log);
}
// 💡ngOnDestroy 可直接移除,由 AutoDestroy 接管。特殊情况可保留,以处理其他销毁逻辑。
}
最佳实践
- 组件上方添加
@AutoDestroy()
,不需要传参,使用默认配置- 订阅中添加操作符
autoDestroyPipe(this)
,不使用单个订阅、批量订阅- 一次性执行的订阅,不确定是否会自动结束时,添加操作符
take(1)
,无需使用本文的自动取消订阅方案
○ 实现
import { Observable, Subject, Subscription, takeUntil } from 'rxjs';
type SamePipeFunc = <SourceValue>(source: Observable<SourceValue>) => Observable<SourceValue>;
interface AutoDestroyOption {
checkProperties?: boolean;
arrayName?: string;
excludeList?: string[];
}
const autoDestroyKey = {
destroyMethodName: 'ngOnDestroy',
active: Symbol('autoDestroy.active'), // 标识当前组件是否启用了 AutoDestroy
subject: Symbol('autoDestroy.subject'), // 存储组件中用于 takeUntil 的 Subject
};
const unsubscribe = (subscription: Subscription) => {
if (subscription instanceof Subscription) {
subscription.unsubscribe();
}
};
/**
* 自动销毁时,添加的销毁内容
*/
const extendedDestroy = (instance: any, option: AutoDestroyOption) => {
if (instance[autoDestroyKey.subject]) {
const subject = instance[autoDestroyKey.subject] as Subject<void>;
subject.next();
subject.complete();
instance[autoDestroyKey.subject] = null;
}
if (option.arrayName) {
const list = (instance[option.arrayName] ?? []) as Subscription[];
list.forEach(item => unsubscribe(item));
}
// 默认检查单个订阅
if (option.checkProperties !== false) {
const excludeList = new Set(option.excludeList ?? []);
Object.keys(instance).forEach(key => {
if (excludeList.has(key)) {
return;
}
const value = instance[key];
unsubscribe(value);
});
}
};
/**
* 类装饰器,装饰于组件 class 上。可在组件销毁时,统一取消订阅
* 注意区分 target(组件 class)和 this(组件实例,等同于 instance)
*/
export const AutoDestroy = (option: AutoDestroyOption = {}) => {
return (target: any) => {
const _destroy = target.prototype[autoDestroyKey.destroyMethodName];
target.prototype[autoDestroyKey.destroyMethodName] = function (this: any, ...args: any[]) {
if (typeof _destroy === 'function') {
_destroy.apply(this, args);
}
extendedDestroy(this, option);
};
target.prototype[autoDestroyKey.active] = true;
};
};
/**
* 装饰了 AutoDestroy 的组件中,添加 autoDestroyPipe 操作符的 Observable,可在组件销毁时自动走到完成状态。
* @example
* // this 为组件实例,
* from(document, 'click').pipe(autoDestroyPipe(this)).subscribe(console.log);
*/
export const autoDestroyPipe = (instance: any): SamePipeFunc => {
if (!instance[autoDestroyKey.subject]) {
instance[autoDestroyKey.subject] = new Subject<void>();
}
return source => {
const subject = instance[autoDestroyKey.subject];
return source.pipe(takeUntil(subject));
};
};
参考
- until-destroy,
AutoDestroy
实现过程中,部分采用了until-destroy
的配置 api,便于互相替换。until-destroy
更全面,AutoDestroy
更轻量。
🔥 加入我们
感谢我们社区贡献者Aubur22提供的优质文章!内容觉得不错的话大家也给我们的小伙伴点点关注哦~
DevUI是面向企业中后台产品的开源前端解决方案,其设计价值观基于高效、开放、可信、乐趣四种自然与人文相结合的理念,是一款企业级开箱即用的产品。
MateChat仓库地址:gitcode.com/DevCloudFE/…
MateChat官网:matechat.gitcode.com
DevUI社区仓库地址: gitcode.com/DevCloudFE
DevUI Design 官网: devui.design/home
如果你对我们的开源项目感兴趣,并希望参与共建,欢迎加入我们的开源社区,关注DevUI微信公众号:DevUI 。我们还为大家准备了特有的开源活动 ✨DevUI 点亮计划启动~ 快来参与吧~