值得记录的bug 异步回调时序导致的setState未更新state

225 阅读2分钟

出问题的代码:

  console.log('rerender')

  useEffect(() => {
    if (Array.isArray(selectedIds)) {
      // 只有初始化时会实际请求接口,之后通过人员选择组件的回调更新
      // 直接拉取缓存数据时,value + 异步设置 Users ,共触发了两次rerender,可以接受
      userBaseInfoModel.getUsers(selectedIds).then((res)=>{
        setUsers([...res]);
        console.log(JSON.stringify(res));
      });
    }
  }, [value]);Ï

userBaseInfoModel.getUsers是一个可缓存的数据访问接口(最后确实是这里出了问题)

体现

输出、vsc debug都没有发现问题

  1. useEffect触发了3+次
  2. log最后一次数据返回,没有rerender
  • []
  • rerender
  • []
  • rerender
  • [有数据]
  1. devtools中组件最终state的值是 []

setState没有正确更新state的值,这冲击了我对react基本原理的认知

定位

经过对外部的排查,没有发现问题,最终将范围缩小到了:

  1. antD table columns render的实现问题
  2. userBaseInfoModel.getUsers的返回有问题

搜索1发现了一些似是而非的issues,基本上都是数据依赖的基础使用问题,没啥用
想办法调试2,发现了一些端倪

      const promise = userBaseInfoModel.getUsers(selectedIds);
      console.log(promise);
      // ......

最终输出的结果发现log:

  • Promise {<pending>}
  • Promise {<fulfilled>: Array(0)}
  • Promise {<fulfilled>: Array(0)}
  • Promise {<fulfilled>: Array(0)}
  • ...

在一堆log里面隐约发现了问题,有数据的返回虽然最后调用了setState设置了正确数据,但是他的 useState 出发在更早以前,综合之前的发现,可能和组件刷新复用有关,类似于 useEffect 应该在 return 里 cancel 异步,这里因为没有实现 cancel 导致 异步中的 log 输出混乱影响了判断

  • useEffect cancel订阅很重要
  • Suspense 在无其他库依赖时的使用还需要测试

解决

最终修改 userBaseInfoModel.getUsers 内部逻辑,正确解决了问题

对比

// 修改前
  getUsers(ids: number[]) {
    let anyFetch = false;
    for (const id of ids) {
      if (id && !this.pushedIdSet.has(id)) {
        this.requestQueue.add(id);
        this.pushedIdSet.add(id);
        anyFetch = true;
      }
    }

    // 需要return实际请求这批数据的promise
    if (anyFetch) {
      return this.trigger().then(() => this.getListWithIds(ids));
    } else {
      return Promise.resolve(this.getListWithIds(ids));
    }
  }

// 修改后
  async getUsers(ids: number[]) {
    let anyFetch = false;
    for (const id of ids) {
      if (id && !this.pushedIdSet.has(id)) {
        this.requestQueue.add(id);
        this.pushedIdSet.add(id);
      }

      if (!this.store.has(id)) {
        anyFetch = true;
      }
    }

    // 需要return实际请求这批数据的promise
    // 存在重复渲染的情况,短时间内多次请求了相同id,每一次都要将当前任务返回
    if (anyFetch) {
      await this.trigger();
    }
    return this.getListWithIds(ids);
  }