38

次のような構造のバックエンド サーバーからデータを受け取ります。

{
  name : "Mc Feast",
  owner :  "Mc Donalds"
}, 
{
  name : "Royale with cheese",
  owner :  "Mc Donalds"
}, 
{
  name : "Whopper",
  owner :  "Burger King"
}

私の見解では、リストを「反転」したいと思います。つまり、各所有者を一覧表示し、その所有者のすべてのハンバーガーを一覧表示します。groupByこれを実現するには、フィルターでunderscorejs 関数を使用し、それをng-repeatディレクティブと共に使用します。

JS:

app.filter("ownerGrouping", function() {
  return function(collection) {
    return _.groupBy(collection, function(item) {
      return item.owner;
    });
  }
 });

HTML:

<li ng-repeat="(owner, hamburgerList) in hamburgers | ownerGrouping">
  {{owner}}:  
  <ul>
    <li ng-repeat="burger in hamburgerList | orderBy : 'name'">{{burger.name}}</li>
  </ul>
</li>

これは期待どおりに機能しますが、「10 $digest iterations reached」というエラー メッセージでリストがレンダリングされると、大量のエラー スタック トレースが表示されます。このメッセージが示すように、自分のコードが無限ループを作成する方法を理解するのに苦労しています。誰でも理由を知っていますか?

コード付きのプランクへのリンクは次のとおりです: http://plnkr.co/edit/8kbVuWhOMlMojp0E5Qbs?p=preview

4

7 に答える 7

56

これは、が実行されるたびに新しい_.groupByオブジェクトのコレクションを返すために発生します。Angularは、 identityによってそれらを追跡するため、これらのオブジェクトが等しいことを認識しません。新しいものは新しいアイデンティティにつながります。これにより、Angular は最後のチェック以降に何かが変更されたと見なされます。つまり、Angular は別のチェック (別名ダイジェスト) を実行する必要があります。次のダイジェストは、さらに別の新しいオブジェクトのセットを取得することになるため、別のダイジェストがトリガーされます。Angular があきらめるまで繰り返します。ngRepeatngRepeat

エラーを取り除く簡単な方法の 1 つは、フィルターが毎回同じオブジェクトのコレクションを返すようにすることです (もちろん変更されていない限り)。を使用してアンダースコアを使用すると、これを非常に簡単に行うことができます_.memoize。memoize でフィルター関数をラップするだけです。

app.filter("ownerGrouping", function() {
  return _.memoize(function(collection, field) {
    return _.groupBy(collection, function(item) {
      return item.owner;
    });
  }, function resolver(collection, field) {
    return collection.length + field;
  })
});

フィルターに異なるフィールド値を使用する場合は、リゾルバー関数が必要です。上記の例では、配列の長さが使用されます。コレクションを一意の md5 ハッシュ文字列に減らすことをお勧めします。

プランカーフォークはこちら. Memoize は特定の入力の結果を記憶し、入力が以前と同じであれば同じオブジェクトを返します。ただし、値が頻繁に変更される場合は、_.memoize時間の経過とともにメモリ リークが発生しないように、 が古い結果を破棄するかどうかを確認する必要があります。

もう少し調べてみるとngRepeat 、拡張された構文がサポートされていることがわかりました。これは、Angular にオブジェクトの ID ではなくレストランの... track by EXPRESSIONを表示するように指示できるようにすることで、何らかの形で役立つ可能性があります。ownerこれは、上記のメモ化トリックの代替手段になりますが、plunker でテストすることはできませんでした (以前の Angular の古いバージョンtrack byが実装された可能性がありますか?)。

于 2013-05-12T23:23:22.917 に答える
13

わかりました、私はそれを理解したと思います。まず、 ngRepeatのソース コードを見てみましょう。199 行目に注意してください: これは、反復する配列/オブジェクトに監視を設定する場所です。そのため、その要素またはその要素が変更された場合にダイジェスト サイクルがトリガーされます。

$scope.$watchCollection(rhs, function ngRepeatAction(collection){

ここで、 rootScope.js$watchCollectionの 360 行目から始まるの定義を見つける必要があります。この関数は、配列またはオブジェクト式 (この場合は ) で渡されます。365 行目で、この文字列式はサービスを使用する関数に変換されます。この関数は後で呼び出され、このウォッチャーが実行されるたびに呼び出されます。hamburgers | ownerGrouping$parse

var objGetter = $parse(obj);

フィルターを評価して結果の配列を取得する新しい関数は、数行下で呼び出されます。

newValue = objGetter(self);

groupBynewValueが適用された後、フィルタリングされたデータの結果を保持します。

次に、408 行目までスクロールして、次のコードを見てください。

        // copy the items to oldValue and look for changes.
        for (var i = 0; i < newLength; i++) {
          if (oldValue[i] !== newValue[i]) {
            changeDetected++;
            oldValue[i] = newValue[i];
          }
        }

初めて実行するときは、oldValue は単なる空の配列 (上記で「internalArray」として設定) であるため、変更が検出されます。ただし、その各要素は newValue の対応する要素に設定されるため、次回の実行時にすべてが一致し、変更が検出されないことが予想されます。したがって、すべてが正常に機能している場合、このコードは 2 回実行されます。最初の null 状態からの変更を検出するセットアップ用に 1 回、検出された変更により新しいダイジェスト サイクルが強制的に実行されるため、もう一度。通常、この 2 回目の実行中に変更は検出されません。これは、その時点(oldValue[i] !== newValue[i])ですべての i が false になるためです。これが、実際の例で 2 つの console.log 出力が表示された理由です。

しかし、失敗した場合、フィルター コードは実行されるたびに新しい要素を含む新しい配列を生成しています。この新しい配列の要素は、古い配列の要素と同じ値 (完全なコピー) を持ちますが、実際の要素は同じではありません。つまり、たまたま同じプロパティと値を持つメモリ内の異なるオブジェクトを参照します。したがって、あなたの場合oldValue[i] !== newValue[i]は常に true になります。同じ理由で、たとえば、{x: 1} !== {x: 1}常に true です。そして、変化は常に検出されます。

したがって、本質的な問題は、フィルターが実行されるたびに配列の新しいコピーを作成し、元の配列の elments のコピーである新しい要素で構成されることです。そのため、ngRepeat によるウォッチャーのセットアップは、本質的に無限再帰ループに陥り、常に変更を検出して新しいダイジェスト サイクルをトリガーします。

同じ問題を再現するコードのより単純なバージョンを次に示します: http://plnkr.co/edit/KiU4v4V0iXmdOKesgy7t?p=preview

フィルターが実行されるたびに新しい配列の作成を停止すると、問題はなくなります。

于 2013-05-12T20:59:14.373 に答える
2

メモ化ソリューションをありがとう、それはうまくいきます。

ただし、 _. memoizeは、最初に渡されたパラメーターをキャッシュのデフォルト キーとして使用します。これは、特に最初のパラメーターが常に同じ参照である場合は便利ではありません。うまくいけば、この動作はresolverパラメーターを介して構成可能です。

以下の例では、最初のパラメーターは常に同じ配列になり、2 番目のパラメーターはグループ化するフィールドを表す文字列になります。

return _.memoize(function(collection, field) {
    return _.groupBy(collection, field);
}, function resolver(collection, field) {
    return collection.length + field;
});
于 2014-08-26T16:41:24.703 に答える
0

価値のあるものとして、もう 1 つの例と解決策を追加するために、次のような単純なフィルターを用意しました。

.filter('paragraphs', function () {
    return function (text) {
        return text.split(/\n\n/g);
    }
})

と:

<p ng-repeat="p in (description | paragraphs)">{{ p }}</p>

で説明されている無限再帰を引き起こしました$digest。次の方法で簡単に修正できました。

<p ng-repeat="(i, p) in (description | paragraphs) track by i">{{ p }}</p>

ngRepeat逆説的に繰り返しを好まないため、これも必要です。つまり"foo\n\nfoo"、2 つの同一の段落のためにエラーが発生します。この解決策は、段落の内容が実際に変化していて、消化し続けることが重要な場合には適切ではないかもしれませんが、私の場合は問題ありません。

于 2014-06-25T14:26:47.917 に答える
0

このエラーが発生する理由はわかりませんが、論理的には、配列の各要素に対してフィルター関数が呼び出されます。

あなたの場合、作成したフィルター関数は、配列の各要素ではなく、配列が更​​新されたときにのみ呼び出される関数を返します。関数によって返された結果は、html にバインドできます。

私はプランカーをフォークし、ここで独自の実装を作成しましたhttp://plnkr.co/edit/KTlTfFyVUhWVCtX6igsn

フィルタは使用しません。groupBy基本的な考え方は、開始時と要素が追加されるたびにを呼び出すことです

$scope.ownerHamburgers=_.groupBy(hamburgers, function(item) {
      return item.owner;
    });



$scope.addBurger = function() {
    hamburgers.push({
      name : "Mc Fish",
      owner :"Mc Donalds"
    });
    $scope.ownerHamburgers=_.groupBy(hamburgers, function(item) {
      return item.owner;
    });
  }
于 2013-05-12T14:31:23.617 に答える