角度异步管道未更新视图

时间:2018-07-14 20:10:36

标签: angular ngrx

我的问题可以通过类似于ngrx文档的选择器示例来最好地描述,以使事情变得简单(https://github.com/ngrx/platform/blob/master/docs/store/selectors.md#using-selectors-for-multiple-pieces-of-state)。

我使用异步管道来订阅我使用选择器选择的某些状态片,例如

this.visibleBooks$ = this.store$.select(selectVisibleBooks)

问题是,如果allBooks数组是“ small”(小于100个项目),则我的视图会立即更新。但是,当它大于100时,我的视图仅在下次触发更改检测(例如通过滚动)时才更新。这是非常糟糕的用户体验,仅在滚动列表后才能看到书籍。

我查看了异步管道(https://github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts)的来源,实际上_updateLatestValue方法调用了ChangeDetectorRef.markForCheck(),据我所知,它标记了要检查组件的更改< strong>下次更改检测被触发。

目前,我的解决方法是通过手动订阅顶级组件

this.store$.select(selectVisibleBooks).subscribe(cb)

,然后在回调中手动调用ChangeDetectorRef.detectChanges()

我发现这不尽人意,并且无论Book[]数组有多大,都希望异步管道始终运行。有人对我可以做一些建议或纠正吗?


根据请求进行编辑

如上所述,上述“书店”案例只是我为简化程序而编写的应用程序的类比。实际上,我的应用程序渲染图的节点和边缘,其中节点和边缘还附加有一个称为“ vnode”的版本,该版本与“ vedge”一起跨越了一个版本树。因此,任何图元素都有其自己的版本树。

我当前正在开发的是一个搜索表单,我们在其中向后端发送特定请求,以向其询问与一组特定搜索键/值对匹配的任何节点。

因此,这些节点将随后呈现在组件<nodes-list>中,我们通过输入绑定传递节点

<nodes-list [nodes]="nodes$ | async"></nodes-list>

nodes-list具有更改检测功能,而顶级<search>组件则具有默认策略。

nodes$ngOnInit()中设置为

this.nodes$ = this.store$.select(selectFullNodesList)

selectFullNodesList看起来像这样:

export const fullNodesSelector = getFullNodesSelector(createSelector(selectSearchState, s => {
    if (s.currentId) {
        const nodes = s.queries.get(s.currentId).nodes;
        if (nodes) {
            return [...nodes];
        }
    }
    return null;
}))

export const selectFullNodesList = createSelector(
    fullNodesSelector,
    (global: GlobalState) => global.data.counts,
    createSelector(selectSearchState, s => s.sort),
    (nodes, counts, sorting) => {
        if (!nodes || !counts || !sorting) return null;
        return [...nodes.sort(sorting.sortCbFactory(counts))];
    }
)

让我解释一下:

  • getFullNodesSelector(...)我将在下面显示,它位于顶层库中,因为我们可以在许多功能中重复使用它。但是它的作用是,它以另一个选择器作为参数,该选择器指向节点和vnode键对{key: number, vKey: number}[]的数组,并将该数组转换为附加了vnode的节点数组(请参见下面的方法)。
  • 因此,您可以看到,我们传递给它的选择器选择了search功能的状态,如果有currentId(这是当前请求到后端的ID),那么我们选择我们当前请求的结果节点。
  • s.queries是一个围绕Javascript对象的包装器,它使我可以轻松获取/设置值,进行克隆或向克隆添加新项目。当在NGRX中使用键/值存储时,这对我很有帮助。因此,s.queries.get(s.currentId).nodes
  • global.data.counts只是每个节点有多少个邻居的列表。我想知道这一点,因为我想按“计数”对节点列表进行排序。
  • s.sort是当前选择列表的排序方式。
  • 请注意使用sortCbFactory,该工厂仅返回正确的回调以传递给Array.sort,但我需要counts出现在回调的本地范围中,否则我无法按计数排序。
  • 因此,每当节点发生更改(例如,在该节点上引用了新版本),计数发生更改(邻居添加到节点)或排序发生更改时,就会调用投影函数,并发出新的节点列表。
  • 请注意,我们在排序后返回了一个新的数组。

selectSearchState仅仅是功能选择器

export const selectSearchState = createFeatureSelector<SearchState>('search');

getFullNodesSelector(...)看起来像这样:

function getFullNodesSelector(keyPairsSelector: MemoizedSelector<object, GraphElementKeyPair[]>): MemoizedSelector<object, INodeJSON<IVNodeJSON>[]> {
    return createSelector(
        keyPairsSelector,
        (s: GlobalState) => s.data.nodes,
        (s: GlobalState) => s.data.vnodes,
        (pairs, nodes, vnodes) => {
            if (!pairs || !nodes || !vnodes) return null;
            return pairs.map(pair => ({
                ...nodes.get(pair.key),
                _SUB: {
                    ...vnodes.get(pair.vKey)
                }
            }));
        })
}

再次评论:

  • 如您所见,我们传递了一个选择器,该选择器指向GraphElementKeyPair{key: number, vKey: number})数组
  • 我们询问节点存储和vnodes存储的全局状态
  • 我们将所有对映射到一个新对象。
  • 请注意,节点和边缘还是前面提到的包装器对象,它具有get方法。

因此,由于我们已经使用异步管道订阅了this.nodes$,因此每次在流<nodes-list>上有新事件时都应进行更新。但是,实际上,这似乎取决于INodeJSON<IVNodeJSON>[]的大小,并且如果数组的长度>〜80,我们必须通过单击某个地方或滚动来手动触发更改检测。对于较小的数组,应该自动刷新节点列表。

1 个答案:

答案 0 :(得分:1)

大型数据集不会有任何问题。您的选择器应该是同步的。这意味着选择器在运行时,在后台没有其他任何事情发生。不管在选择器中计算所有内容需要花费多少时间,都可以。如果花费的时间太长,您的浏览器可能会死机,但仅此而已。

当你说

  

我使用的是ngrx-store-freeze,应该防止出现这种情况

It is not true.

从商店的角度来看,这是正确的。但是,让我们想象一下:

您的商店有一个ID数组(比方说用户ID)。

您有一个名为getAllUsers的第一个选择器。
这只是映射用户ID并检索正确的用户,对吗?签名为(usersIds: string[]): User[]

当然,在这里,您将创建一个新的数组引用,并且(不应该)使usersIds数组发生突变。

但是,您还有另一个选择器。 getUsersResolved基本上可以“解析”异物。假设用户有动物列表。从第一个选择器中,您将获得一个用户列表,每个用户都有一个animalsIds属性。但是,您想要的是拥有一个animals数组。如果您从此选择器中更改了原始数组(来自第一个选择器的数组),则ngrx-store-freeze将不会引发任何错误,这是有道理的:您正在更改一个数组,但不是存储中的一个数组。 / p>

那怎么可能是个问题?

  • 您的组件订阅getUsersResolved,将其分配给一个变量,然后使用async管道从视图中订阅该变量(假设这是您在整个应用程序中首次订阅做到了!)
  • 然后您的第一个选择器getAllUsersgetUsersResolved(也是第一次调用)(第一次)
  • getAllUsers按预期创建一个新数组,并将其传递给getUsersResolved。由于这是第一次,即使您将该数组修改为getUsersResolved,也不会有任何问题:更改检测将在第一次接收到该数组时完成
  • 现在想像一下,您的用户列表**不会*更改,但是动物列表会更改。您的选择器getUsersResolved将被触发,但是如果您不尊重不变性并修改来自getAllUsers的第一个数组,则该数组引用不会更改,并且不会进行更改检测。而且,因为该数组不是商店的一部分,它是从选择器创建的数组,所以完全没问题

所以我不确定您的问题是否来自那里,但您可能要仔细检查一下您是否尊重选择器中的不变性。

最终,如果不确定,请共享selectVisibleBooks及其使用的每个选择器的代码