Redux工具包的createListenerMiddleware导致内存泄漏

whitzsjs  于 2022-12-23  发布在  其他
关注(0)|答案(2)|浏览(247)

我使用Redux Toolkit,特别是新的侦听器api,来执行类似于Redux-Saga的任务。
不幸的是,几天以来,我遇到了内存泄漏,找不到原因。
我已经复制了一个产生此内存泄漏的代码的最小示例,链接到该示例:https://github.com/MrSquaare/rtk-memory-leak
要观察此内存泄漏:

  • 我使用Chromium、DevTools内存工具
  • 我触发了一个垃圾收集器
  • 我创建了堆内存快照
  • 我分派实体/加载(通过UI按钮)
  • 我每隔2 - 3秒创建几个堆内存快照
  • 我使用比较工具,注意到数组分配大小无限增长

并且在分派实体/卸载之后,创建一个快照堆内存,我们可以观察到分配消失了...
有没有人观察到类似的行为?或者有没有人知道原因?谢谢!
编辑1:
我做了一个只使用侦听器中间件(only-middleware branch)的示例,并将其与不同的方法进行比较:

  • 对于forkApi.pause:重大泄漏,尤其是生成的实体
  • 没有forkApi.pause:我直接使用api.dispatch,不再泄漏生成的实体,一些其他类型的泄漏,但可能是正常的事情(我没有足够的资格对这一点发表意见)
  • 没有api.dispatch:我直接调用生成实体的函数,结果与api.dispatch相同

看起来泄漏与forkApi.pause有关,但同样我没有足够的资格知道真正的原因...

00jrzges

00jrzges1#

可能是promises.forEach,每隔1000 ms,你就创建一堆新的承诺,并为它们安排日程,你从来没有等到最后一批承诺完成,所以它们就积累起来了。
promises.forEach替换为await Promise.all(promises.map,看看会发生什么。
在更仔细地阅读了您的解决方案之后,我相信您可以通过更多地使用reducer而更少地使用listenerMiddleware来减少问题。
我建议作出以下修改:

export const entitySlice = createSlice({
    name: "entity",
    initialState: entityAdapter.getInitialState({ acceptingEntities: false }),
    reducers: {
        upsertOne: (state, action) => {
            entityAdapter.upsertOne(state, action.payload);
        },
        removeAll: (state) => {
            entityAdapter.removeAll(state);
        },
        load(state) { state.acceptingEntities = true },
        unload(state) { state.acceptingEntities = false },
    },
    extraReducers: builder => {
        builder.addCase(getEntity.fulfilled, (state, action) => {
            if (!state.acceptingEntities) return;
            const prevEntity = entitySelectors.selectById(state.entity, id);
            entityAdapter.upsertOne(state,
                prevEntity
                    ? mergeEntityData(prevEntity.data, action.payload.data)
                    : action.payload.data
            )
        })
    }
});

以及

entityMiddleware.startListening({
    actionCreator: api.actions.load,
    effect: async (action, api) => {
        const task = api.fork(async (forkApi) => {
            while (!forkApi.signal.aborted) {
                for (const id of entityIds) {
                    api.dispatch(getEntity(id))
                }
                await api.delay(1000);
            }
        });

        await api.condition(api.actions.unload.match);

        task.cancel();
    },
});

一般情况:

  • 像计算一个新值这样的逻辑属于Reducer,而不是外部。在外部做这样的事情总是有竞争条件的风险,而在Reducer中你总是有所有可用的信息(同样,没有因为保存过时的值引用而占用内存的风险)
  • 在thunk之后直接分派另一个操作只会增加更多的工作量--在每个reducer之后,每个selector都会重新运行,UI可能会重新呈现。
  • 我只是添加了一个布尔值acceptingEntities来指示当前是否应该进行更新
  • 这大大降低了侦听器的复杂性
txu3uszq

txu3uszq2#

相关问题