简体   繁体   English

角度异步管道不更新视图

[英]angular async pipe not updating the view

My problem can be best described by analogy with the selectors example of the ngrx documentation to keep things simple ( https://github.com/ngrx/platform/blob/master/docs/store/selectors.md#using-selectors-for-multiple-pieces-of-state ). 我的问题可以通过与ngrx文档的选择器示例进行类比来最好地描述,以使事情保持简单( https://github.com/ngrx/platform/blob/master/docs/store/selectors.md#using-selectors-for多件状态 )。

I use the async pipe to subscribe to certain slices of state which I select using selectors, for instance 我使用异步管道来订阅某些状态切片,例如使用选择器进行选择

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

The thing is that, if the allBooks array is "small", <100 items, my view gets updated instantly. 问题是,如果allBooks数组是“ small”(小于100个项目),则我的视图会立即更新。 But when it is large, >100, my view gets only updated next time change detection is triggered, for instance by scrolling. 但是,当它大于100时,我的视图仅在下次触发更改检测(例如通过滚动)时才更新。 This is quite a bad user experience, to only see books once you scroll the list. 这是非常糟糕的用户体验,仅在滚动列表后才能看到书籍。

I looked at the source for async pipe ( https://github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts ), and indeed the _updateLatestValue method calls ChangeDetectorRef.markForCheck() , which as far as I understand marks the component to be checked for changes the next time change detection is triggered. 我看了异步管道的源代码( https://github.com/angular/angular/blob/master/packages/common/src/pipes/async_pipe.ts ),实际上_updateLatestValue方法调用_updateLatestValue ChangeDetectorRef.markForCheck() ,据我所知,它会在下次触发更改检测将要检查的组件标记为更改。

My current way around this is by subscribing manually within the top-level component 我目前的解决方法是在顶级组件中手动订阅

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

and calling ChangeDetectorRef.detectChanges() manually within the callback. 并在回调中手动调用ChangeDetectorRef.detectChanges()

I find this however unsatisfactory and would simply like async pipe to always work, no matter how large the Book[] array. 我发现这并不令人满意,并且无论Book[]数组有多大,都希望异步管道始终运行。 Does anybody have some suggestions or a correction with which I could make things work? 有人对我可以做一些建议或纠正吗?


edit as per request 根据要求编辑

The "books store" case above, as said, was just an analogy for the app I'm writing to keep things simple. 如上所述,上面的“书店”案例只是我为简化程序而编写的应用程序的类比。 In reality, my app renders nodes and edges of a graph, where nodes and edges also have a version attached, denoted "vnode", which together with "vedge"s span a version tree. 实际上,我的应用程序渲染图的节点和边缘,其中节点和边缘还附加有一个称为“ vnode”的版本,该版本与“ vedge”一起跨越了一个版本树。 So any graph element has its own version tree. 因此,任何图元素都有其自己的版本树。

What I am developing currently is a search form, where we send a certain request to the backend, asking it for any nodes which match a certain set of search key/value pairs. 我当前正在开发的是一个搜索表单,我们在其中向后端发送特定请求,以向其询问与一组特定搜索键/值对匹配的任何节点。

So, those nodes would then be rendered in a component <nodes-list> , which we pass nodes by input binding 因此,这些节点将随后呈现在组件<nodes-list> ,我们通过输入绑定传递节点

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

nodes-list has change detection "on push", while the top-level <search> component has default strategy. nodes-list具有“按入”更改检测功能,而顶级<search>组件具有默认策略。

nodes$ is set within ngOnInit() as ngOnInit()中将nodes$设置为

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

selectFullNodesList looks like this: 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))];
    }
)

Let me explain: 让我解释:

  • getFullNodesSelector(...) I will show below, it sits in a top-level library because we may reuse it in many features. getFullNodesSelector(...)我将在下面显示,它位于顶级库中,因为我们可能会在许多功能中重复使用它。 But what it does is, it takes as an argument another selector which points to an array of node & vnode key pairs {key: number, vKey: number}[] , and turns that array into an array of nodes with their vnodes attached (see below how). 但是它的作用是,它以另一个选择器作为参数,该选择器指向节点和vnode密钥对{key: number, vKey: number}[]数组,并将该数组转换为附加了vnode的节点数组(请参阅下面的方法)。
  • So as you can see, the selector we pass it selects the state of our search feature, if there is a currentId , which is the id of the current request to the backend, then we select the nodes which were the result of our current request. 因此,您可以看到,传递给它的选择器选择了search功能的状态,如果存在currentId ,这是当前请求到后端的ID,那么我们选择作为当前请求结果的节点。
  • s.queries is a light wrapper around a Javascript object, which allows me easily get/set values, clone, or add new items to a clone. s.queries是一个围绕Javascript对象的轻量包装程序,它使我可以轻松获取/设置值,克隆或向克隆添加新项目。 This I find helpful when working with key/value stores in NGRX. 当在NGRX中使用键/值存储时,这对我很有帮助。 Hence the s.queries.get(s.currentId).nodes . 因此, s.queries.get(s.currentId).nodes
  • global.data.counts is simply a list of how many neighbors each node has. global.data.counts只是每个节点有多少个邻居的列表。 This I want to know because I'd like to sort the nodes list by "count". 我想知道这一点,因为我想按“计数”对节点列表进行排序。
  • s.sort is which sorting of the list is currently selected. s.sort是当前选择列表的排序方式。
  • Note the use of sortCbFactory , this factory simply returns the correct callback to pass to Array.sort , but I need counts to be present in the local scope of the callback because otherwise I wouldn't be able to sort by counts. 请注意,使用sortCbFactory ,该工厂仅返回正确的回调以传递给Array.sort ,但我需要在回调的本地范围内显示counts ,因为否则我将无法按计数进行排序。
  • So, whenever nodes change (for instance a new version is referenced on the node), counts change (neighbors are added to a node) or sorting changes, the projection function is called, and a new nodes list is emitted. 因此,每当节点发生更改(例如,在该节点上引用了新版本),计数发生更改(邻居添加到节点)或排序发生更改时,都会调用投影函数,并发出新的节点列表。
  • Note that we return a fresh array after sorting. 请注意,我们在排序后返回了一个新的数组。

selectSearchState is simply a feature selector selectSearchState只是功能选择器

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

getFullNodesSelector(...) looks like this: 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)
                }
            }));
        })
}

Some comments again: 再次发表一些评论:

  • As you see, we pass a selector which points to an array of GraphElementKeyPair ( {key: number, vKey: number} ) 如您所见,我们传递了一个选择器,该选择器指向GraphElementKeyPair{key: number, vKey: number} )数组
  • We ask the global state for the nodes store and vnodes store 我们询问节点存储和vnodes存储的全局状态
  • We map all pairs to a fresh object. 我们将所有对映射到一个新对象。
  • Note that nodes and edges are again the wrapper object mentioned earlier, which has a get method. 请注意,节点和边缘仍然是前面提到的包装器对象,它具有get方法。

Thus, as we've subscribed to this.nodes$ with the async pipe, each time there is a new event on the stream <nodes-list> should be updated. 因此,由于我们已经使用异步管道订阅了this.nodes$ ,因此每次流<nodes-list>上有一个新事件都应进行更新。 However, in practice it appears that this depends on the size of INodeJSON<IVNodeJSON>[] , and that if the array has length > ~80, we've got to trigger change detection manually by clicking somewhere or scrolling. 但是,实际上,这似乎取决于INodeJSON<IVNodeJSON>[]的大小,并且如果数组的长度> INodeJSON<IVNodeJSON>[] ,我们必须通过单击某个地方或滚动来手动触发更改检测。 nodes-list is refreshed automatically, as should be the case, for smaller arrays. 对于较小的数组,应该自动刷新节点列表。

You'll not have any problem with large dataset. 大型数据集不会有任何问题。 You selectors are supposed to be synchronous. 您的选择器应该是同步的。 Which means that when the selector is running, nothing else is happening in the background. 这意味着选择器在运行时,在后台没有其他任何事情发生。 Not matter how much time it takes to compute everything in your selector, you'll be fine. 不管在选择器中计算所有内容需要花费多少时间,都可以。 If it's taking too long, you might have a freeze in the browser but that's it. 如果花费的时间太长,您的浏览器可能会死机,仅此而已。

When you say 当你说

I use ngrx-store-freeze which should guard me against that 我使用了ngrx-store-freeze,它应该防止出现这种情况

It is not true.

It is true from the store point of view. 从商店的角度来看,这是事实。 But let's picture the following: 但是,让我们想象一下:

Your store has an array of IDs (let say users IDs). 您的商店有一个ID数组(比方说用户ID)。

You have a first selector called getAllUsers . 您有一个名为getAllUsers的第一个选择器。
This one is just mapping over the users IDs and retrieving the correct users, right? 这只是映射用户ID并检索正确的用户,对吗? Signature would be (usersIds: string[]): User[] . 签名为(usersIds: string[]): User[]

Of course here, you create a new array reference and you are not (supposed) to mutate the usersIds array. 当然,在这里,您将创建一个新的数组引用,并且(不应该)使usersIds数组发生突变。

But then, you've got an other selector. 但是,然后,您有了另一个选择器。 getUsersResolved which basically "resolve" foreign properties. getUsersResolved基本上可以“解析”外部属性。 Let say that a user has a list of animals. 假设用户有动物列表。 From the first selector you'll get a list of users and for each of them, an animalsIds properties. 从第一个选择器中,您将获得一个用户列表,每个用户都有一个animalsIds属性。 But what you want is to have an animals array instead. 但是,您想要的是改为具有animals数组。 If from this selector you mutate the original array (the one coming from the first selector), ngrx-store-freeze will not throw any error, which make sense: You're mutating an array, but not the one from the store. 如果从该选择器中ngrx-store-freeze了原始数组(来自第一个选择器的数组),则ngrx-store-freeze将不会引发任何错误,这是有道理的:您正在更改一个数组,但不是存储中的一个数组。

So how could that be a problem? 那怎么可能是个问题呢?

  • Your component subscribe to getUsersResolved , assign that to a variable which is then subscribed to from the view using the async pipe (let's say it's the first time in the whole app that you're subscribing to it!) 您的组件订阅getUsersResolved ,将其分配给一个变量,然后使用async管道从视图中订阅该变量(假设这是您第一次在整个应用程序中订阅它!)
  • Your first selector getAllUsers is then called (for the first time) by getUsersResolved (also called for the first time) 然后,第一个选择器getAllUsersgetUsersResolved调用(首次)(也首次调用)
  • getAllUsers creates a new array as intended and passes it to getUsersResolved . getAllUsers按预期创建一个新数组,并将其传递给getUsersResolved As it's the first time, even if you modify that array into the getUsersResolved , you wont have any problem: Change detection will be done as it's the first time receiving the array 因为这是第一次,即使您将该数组修改为getUsersResolved ,您也不会有任何问题:更改检测将在第一次接收该数组时完成
  • Now imagine that your list of users **does not* change but the animals list changes. 现在,假设您的用户列表**不会*更改,但是动物列表会更改。 Your selector getUsersResolved will be triggered but in the case where you're not respecting immutability and modify the first array coming from getAllUsers , the array reference does not change and the change detection won't happen. 您的选择器getUsersResolved将被触发,但是如果您不尊重不变性并修改来自getAllUsers的第一个数组,则该数组引用不会更改,并且更改检测也不会发生。 And it's totally fine as that array is not part of the store, it was an array created from a selector 而且,因为该数组不是商店的一部分,这是一个很好的选择,它是由选择器创建的数组

So I'm not sure whether your problem comes from there or not, but you might want to double check that you respect immutability within your selectors. 因此,我不确定您的问题是否来自那里,但是您可能要仔细检查一下您是否尊重选择器中的不变性。

Eventually if you're not sure, please share the code of selectVisibleBooks , and every selectors used by it . 最终,如果不确定,请共享selectVisibleBooks的代码, 以及它使用的每个选择器

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM