在 Angular 中处理 API 请求的订阅内存泄漏

问题描述 投票:0回答:1

当我开始使用 Angular 时,我工作场所的某人告诉我在使用

RxJs
中的 observables 执行 API 调用时使用 take(1),声称它会在一次 API 调用后自动取消订阅。

所以我相信这个说法,并继续使用它,直到我发现组件加载时存在大量渲染延迟,尤其是在页面之间导航时。

让我们看一下我的例子:

这是一个

UserRepository
,它与 API 对话以向应用程序授予权限:

@Injectable({
    providedIn: 'root'
})
export class UserRepository {

    private apiUrl: string = environment.apiUrl;

    constructor(private readonly httpRequestService: HttpRequestService) {
        super();
    }

    @UnsubscribeOnDestroy()
    getPermissions() {
        return this.httpRequestService.get(`${this.apiUrl}/user/permissions`).pipe(map((res: any) => res.user));
    }
}

用法:

userRepository.getPermissions().pipe(take(1)).subscribe(data => ...);

我用谷歌搜索了一下,发现

take(1)
不会自动取消订阅,而且它根本不是目的,它所做的只是从管道中获取一个可观察值并忽略其余部分。

要解决这个问题,我有两个选择:

  1. takeUntil
    unsubscriptionSubject
    结合使用,致电
    unsubscriptionSubject.next
    触发取消订阅
    ngOnDestroy
  2. 使用
    methodName => Subject
    字典创建一个 BaseRepository,这样对于每个 API 调用,我都可以保存它自己的主题,并创建一个装饰器,自动取消订阅之前的 API 调用,并确保不会有超过一个订阅API 调用的类型。

装饰器和用法示例:

基础存储库

export abstract class BaseRepository {
    protected unsubscriptionSubjects: {[key: string]: Subject<void>} = {};
}

实施:

@Injectable({
    providedIn: 'root'
})
export class UserRepository extends BaseRepository {

    private apiUrl: string = environment.apiUrl;

    constructor(private readonly httpRequestService: HttpRequestService) {
        super();
    }

    @UnsubscribeOnDestroy()
    getPermissions() {
        return this.httpRequestService.get(`${this.apiUrl}/user/permissions`).pipe(map((res: any) => res.user));
    }
}

装饰者:

export function UnsubscribeOnDestroy() {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;

        descriptor.value = function (...args: any[]) {
            // Access the instance of the class
            const instance = this as any; // Cast to any to access custom properties

            // Create a new Subject for the method
            const methodName = propertyKey;
            instance.unsubscriptionSubjects = instance.unsubscriptionSubjects || {};

            if (instance.unsubscriptionSubjects[methodName]) {
                instance.unsubscriptionSubjects[methodName].next();
            } else {
                instance.unsubscriptionSubjects[methodName] = new Subject();
            }

            // Invoke the original method
            const result = originalMethod.apply(this, args);

            // Return the original result
            return result.pipe(takeUntil(instance.unsubscriptionSubjects[methodName]));
        };

        return descriptor;
    };
}

我选择了选项#2,从那以后一切都得到了修复,渲染流畅,没有延迟。

我选择这个选项是因为我在同一页面加载大量数据而不刷新,所以当使用它几分钟执行大量搜索时,您可以看到加载状态慢慢变得滞后,并且一切都需要更多时间加载。因此,据我了解,理想的做法是在再次使用订阅时始终清除订阅。

我想问:

  1. 为什么 rxjs 不像 axios 中使用
    then
    catch
    那样内置只接受一个请求并自动取消订阅?
  2. 我的方法真的能解决这个问题吗?还是会导致我没有看到的任何其他问题?
  3. 为什么大多数使用 Angular 的前端开发人员使用 observables 而不是使用 axios 的 then/catch ?
angular typescript rxjs memory-leaks
1个回答
0
投票

对于你的问题2。你的解决方案非常好,我们必须这样做。 我们知道HTTP请求(使用HttpClient)是自动完成的(因为HTTP请求只发出一次然后完成)

所以几乎所有开发者都说不需要取消订阅/或者不需要添加take(1)。

但是,如果HTTP请求需要很长时间才能完成。你的做法很好。它帮助我们在 HTTP 请求完成之前取消订阅它。

检查用户组件中的示例

导航至 /users,5 秒后离开。 正如您在离开用户组件 10 秒后所看到的。消息

Http 2 Response
Execute logic when HTTP 2  response 
仍会写入控制台。

以我的经验。我建议我们应该始终取消订阅以避免意想不到的副作用

ngOnInit(): void {
    this.http.get('https://jsonplaceholder.typicode.com/users').subscribe({
      next: (res) => {
        console.log('Http 1 Response ', res);
      },
      complete: () => {
        console.log('Http 1 complete ');
      },
    });

    this.http
      .get('https://jsonplaceholder.typicode.com/users')
      .pipe(delay(10000))
      .subscribe({
        next: (res) => {
          console.log('Http 2 Response ', res);
          console.log('Execute logic when http 2  response ');
        },
        complete: () => {
          console.log('Http 2 complete ');
        },
      });

    this.http
      .get('https://jsonplaceholder.typicode.com/users')
      .pipe(delay(10000), takeUntil(this.destroyed$))
      .subscribe({
        next: (res) => {
          console.log('Http 3 Response ', res);
          console.log('Execute logic when HTTP 3  response ');
        },
        complete: () => {
          console.log('Http 3 complete ');
        },
      });
  }
© www.soinside.com 2019 - 2024. All rights reserved.