You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RxJS无限滚动加载帖子竞态问题:如何替代同步延迟方案?

解决RxJS无限滚动的竞态问题,替代同步延迟方案

问题根源

你的代码出现竞态条件,核心原因是:

  • 滚动触发请求时,未限制同时进行的请求数量,旧请求可能晚于新请求返回,导致数据顺序混乱
  • 混合使用Observable和async/await,加上同步sleep阻塞主线程,无法从根本上解决请求顺序问题
  • 分页管理逻辑分散(组件page和服务myPage分离),容易出现状态不一致

规范解决方案

以下是基于RxJS重构的完整代码,彻底消除竞态和同步延迟:

重构后的服务(BlogsService)

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, switchMap, take, tap } from 'rxjs';
import { Blog } from './blog';

@Injectable({
  providedIn: 'root'
})
export class BlogsService {
  private blogUrl = 'http://thedietician.net/the-dietician/api/blogs?_format=json';
  private currentPage = 0;
  private hasMore$ = new BehaviorSubject<boolean>(true);

  constructor(private http: HttpClient) {}

  // 获取下一页博客,自动管理分页和是否还有更多数据
  getNextPage(): Observable<Blog[]> {
    return this.hasMore$.pipe(
      take(1),
      switchMap(hasMore => {
        if (!hasMore) return [];
        return this.http.get<Blog[]>(`${this.blogUrl}&page=${this.currentPage}`).pipe(
          tap(response => {
            if (response.length === 0) {
              this.hasMore$.next(false);
            } else {
              this.currentPage++;
            }
          })
        );
      })
    );
  }

  // 重置分页状态(用于刷新场景)
  resetPagination(): void {
    this.currentPage = 0;
    this.hasMore$.next(true);
  }
}

重构后的组件(BlogsComponent)

import { Component, OnInit, HostListener, OnDestroy } from '@angular/core';
import { BlogsService } from './blogs.service';
import { Blog } from './blog';
import { Observable, Subject, debounceTime, switchMap, takeUntil, tap } from 'rxjs';

@Component({
  selector: 'app-blogs',
  templateUrl: "./blogs.component.html",
})
export class BlogsComponent implements OnInit, OnDestroy {
  blogs: Blog[] = [];
  isLoading = false;
  private destroy$ = new Subject<void>();
  private scrollTrigger$ = new Subject<void>();

  constructor(private blogsService: BlogsService) {}

  ngOnInit(): void {
    // 初始化加载第一页
    this.loadNextPage();

    // 处理滚动触发的加载,添加防抖避免频繁请求
    this.scrollTrigger$.pipe(
      debounceTime(200), // 滚动停止200ms后再触发请求
      switchMap(() => this.loadNextPage()),
      takeUntil(this.destroy$)
    ).subscribe();
  }

  // 监听全局滚动事件
  @HostListener('window:scroll', [])
  onScroll(): void {
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    const windowHeight = window.innerHeight;
    const documentHeight = document.documentElement.scrollHeight;

    // 距离底部200px时触发加载,同时避免加载中重复触发
    if (scrollTop + windowHeight >= documentHeight - 200 && !this.isLoading) {
      this.scrollTrigger$.next();
    }
  }

  // 加载下一页数据的核心方法
  private loadNextPage(): Observable<Blog[]> {
    if (this.isLoading) return [];
    this.isLoading = true;

    return this.blogsService.getNextPage().pipe(
      tap(newBlogs => {
        this.blogs.push(...newBlogs);
        this.isLoading = false;
      })
    );
  }

  ngOnDestroy(): void {
    // 清理订阅,避免内存泄漏
    this.destroy$.next();
    this.destroy$.complete();
  }
}

对应的模板(blogs.component.html)

<div class="blog-container">
  <!-- 用Angular模板渲染数据,替代手动拼接HTML -->
  <div *ngFor="let blog of blogs" class="blog-item">
    <h3><strong>{{ blog.field_title }}</strong></h3>
    <p>{{ blog.body }}</p>
    <br>
  </div>

  <!-- 加载状态提示 -->
  <div *ngIf="isLoading" class="loading-text">加载中...</div>
</div>

关键优化点

  1. 竞态消除:使用switchMap操作符,新请求触发时自动取消未完成的旧请求,确保数据顺序正确
  2. 请求节流:通过isLoading状态控制同一时间只允许一个请求,避免重复触发
  3. 滚动防抖:debounceTime减少滚动过程中的请求次数,优化性能
  4. 统一数据流:全程用Observable管理请求和状态,抛弃同步sleep和async/await混合模式,避免主线程阻塞
  5. 分页自动化:服务内部维护分页状态,组件无需手动计算页码,降低耦合
  6. 安全渲染:用*ngFor替代手动拼接HTML,避免XSS风险和DOM操作混乱

内容的提问来源于stack exchange,提问作者sp2012

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 23:05:27