$ .Deferredを使用してネストを再帰的に保存します

画像

habrを歓迎します。最近、調査サービスを書く機会がありました。 このサービスの管理パネルには、質問とメモが添付されたフォームがありました。 そして質問を保存したとき、編集のためにすべてのネストを開いたままにしておく必要がありました。jQuery$ .Deferredは私を非常に助けてくれました。



右のスクリーンショットに示されているように、質問とメモの構造があると仮定して、分析します。 私はデザイナーではありませんが、この記事のためだけに定型化されているので、すみません。



順番に行きましょう。

最初に、条件が何であったかを説明します。



質問があります、それらの中にメモがあるかもしれません。 [編集]または[保存]をクリックすると、質問/メモのレイアウトがサーバーから返され、テンプレートで置き換えられます。 問題は、質問と添付されたノートの両方が編集された場合、質問を保存するときにノートへの変更を失わないことです。



この問題を解決するためのいくつかのオプションがあります。すべてを一度にサーバーに送信し、すでに分解することは可能ですが、これには構造の変更が必要です。

保存する子がいる場合、親の質問の保存を延期するオプションが気に入りました。 このような動作はさまざまな状況で必要になることがありますが、私の短い練習でも、すでに何度か必要になっています。



せっかちな人のために、記事の最後にデモへのリンクがあります。



最初の質問、ネストなし。





レイアウトは次のようになります
<ul class="questions"> <li> <div class="question" id="id1"> <span class="text"> ,  </span> <span class="edit">edit</span> </div> </li> </ul>
      
      







「編集」をクリックすると、質問IDがサーバーに転送され、「編集モード」でレイアウトが取得されます。これを置き換えると、次のようになります。



編集モードでのレイアウト:
 <ul class="questions"> <li> <div class="question" id="id1"> <input type="text" name="title" value=" ,  "> <span class="save">save</span> </div> </li> </ul>
      
      







テキストを変更し、「保存」をクリックします-質問のID、変更したテキストを渡し、「通常モード」のレイアウトを取得します(最後から2番目のスクリーンショット)。



最も単純な形式では、質問を保存するためのロジックは次のようになります。
 $('.questions').on('click', '.save', function() { var $li = $(this).closest('li'); saveData($li); }); function saveData($li) { var $item = $li.children('.question'), id = $item.id, $input = $item.children('input'); $.ajax({ type: 'POST', url: '/save', dataType: 'json', data: $input.serialize(), success: function(response) { if ( ! response.errors) { $li.replaceWith(response.data); } } }); }
      
      







ここではすべてが簡単です。もう一度、サーバーを保存した後、質問全体のレイアウトが返されるという事実に注目したいと思います。



2番目の質問は、最初のレベルのネストです。





レイアウト:
 <ul class="questions"> <li> <div class="question" id="id2"> <span class="text">   </span> <span class="edit">edit</span> </div> <ul> <li> <div class="note" id="id1"> <span class="text">1 </span> <span class="edit">edit</span> </div> </li> <li> <div class="note" id="id2"> <span class="text">2 </span> <span class="edit">edit</span> </div> </li> </ul> </li> </ul>
      
      







ここで興味深いのは、質問を保存/編集すると、質問のレイアウトが置き換えられるためです。つまり、すべてのネストが置き換えられます。 ノート。 質問の保存中にメモも編集された場合はどうなりますか?

それが私が意味することです



ユーザーがノートで「保存」をクリックせず、すぐに「保存」質問をクリックした場合-変更はノートに保存されず、レイアウトはサーバーから返されるレイアウトに単純に置き換えられます。 質問を保存するとき、編集用に開いているメモがあるかどうかを確認する必要があります。開いている場合は、最初にメモを保存してから、質問を保存します。 したがって、添付されたすべてのメモが保存された瞬間を追跡する必要があります。この場所に$ .Deferredが役立ちます。



最初に、上の図に示す例に従って、理論上どのように機能するかを説明し、次にコードを説明します。

質問を「保存」をクリックすると、$ .ajax beforeSendメソッドで、編集用に開いている添付ファイルがあるかどうかが確認されます。存在する場合は、質問の保存を中断し、$。



コードを表示:
 $('.question').on('click', '.save', function() { saveData.call(this); }); function saveData() { // this     (   ,   ) var self = this, //     / $button = $(this), $item = $button.closest('div'), $li = $item.closest('li'), //        id = $item.attr('id').replace(/[^0-9.]/g, ""), inputs = $item.find(':input'), type = $item.attr('class'); //       (  ) return $.Deferred(function() { var def = this; $.ajax({ type: 'POST', url: '/save', dataType: 'json', data: inputs.serialize() + '&id=' + id + '&type=' + type, beforeSend: function(xhr){ //      ,       .ignore // .ignore      ,        // (              //     ) var $inner_notes = $li.find('ul .save').not('.ignore'); //    .. if($inner_notes.length) { //         var deferreds = []; $inner_notes.each(function() { //    ,         //  this  .save   deferreds.push(saveData.call(this)); }); //       $.when.apply(null, deferreds).always(function() { //      -    . // self         .save   saveData.call(self); }); //    xhr.abort(); } }, success: function(response){ if ( ! response.errors) { //    ,   $li.replaceWith(response.data); } else { //    ,       $button.addClass('ignore'); } }, error: function() { //    ,       $button.addClass('ignore'); } }).complete(function() { //       -    resolve() def.resolve(); }); }); }
      
      







確かに「WTF? そのため、私が念頭に置いていたものをより詳細に記述します。



  1. saveData内部のこれは何ですか?

    答えは:
    これは、保存された質問/メモの保存要素と常に同じです。

    単に要素をナビゲートする方が便利だったというだけです。

     //   saveData: function() { var self = this, $button = $(this), $item = $button.closest('div'), $li = $item.closest('li'); } //      $('.question').on('click', '.save', function() { saveData.call(this); }); //   var $inner_notes = $li.find('ul .save').not('.ignore'); $inner_notes .each(function() { deferreds.push(saveData.call(this)); }); //   saveData: function() { var self = this; .... $.when.apply(null, deferreds).always(function() { saveData.call(self); }); }
          
          







  2. すべてのネストの保存の完了をどのように追跡しますか?

    答えは:
    延期の助けを借りて。

     var deferreds = []; $children_notes.each(function() { deferreds.push(app.saveData.call(this)); }); $.when.apply(null, deferreds).always(function() { saveData.call(self); });
          
          





    • 配列を作成し、保存関数(同じ関数)への呼び出しを追加しますが、編集のために開いているすべてのネストされたノートのコンテキストで。
    • $ .when(func1、func2).done(func3)

      構文は、func1およびfunc2を実行するときにfunc3を実行します。

      関数の配列があるため、.apply()を使用してそれらを渡します。
    • ここで、deferredは彼に渡したすべての機能の実行を開始し、それらが実行されるのを待ちます。

      ネストされた各関数は、.resolve()で完了を通知します

       saveData: function() { ... return $.Deferred(function() { var def = this; ... $.ajax({...}).complete(function() { def.resolve(); }); } }
            
            





    • これらすべてが実行されるとすぐに、func3が起動します。この場合、.always(function(){saveData.call(self);})で、selfは質問の.save要素を指します。




  3. なぜ$ .Deferred(function(){});を返すのですか?

    答えは:
    deferredは、複数のajaxリクエストを同時にサブスクライブし、そのうちの1つが失敗(エラーを返す)した場合、deferredへの関数呼び出しのさらなるチェーンが中断し、残りのすべての関数/リクエストが実行されないように機能します。 Ajaxには独自の遅延ビルトインがあり、その動作を変更することはできませんが、リクエストの成功にかかわらず、完了時にAjaxリクエストを何らかのラッパーでラップすることができます。

     return $.Deferred(function() { var def = this; $.ajax({...}).complete(function() { def.resolve(); }); });
          
          







  4. 正確に$ .Deferred(function(){});を返す理由

    答えは:
    $ .Deferredは2つの方法で作成できます...

     //    function someFunc(){ //  var def = $.Deferred(); setTimeout(function(){ //    def.resolve(); }, 1000); //  return def.promise(); } function someFunc(){ //    return $.Deferred(function() { var def = this; setTimeout(function(){ //    def.resolve(); }, 1000); })/* .promise() */; //    ,    .promise() , deferred    . } //someFunc.done(function() {});
          
          





    ...しかし、最初の方法に従って行った場合、ajaxは別の関数として取り出す必要がありますが、私はしたくなかったので、これは純粋に美学の問題です。



  5. なぜ.ignoreクラスなのか?

    答えは:
    簡単な答えは、永遠のサイクルに陥らないことです。

    拡張回答:コードの実行方法を行ごとに説明する方が簡単です。

    編集用の未解決の質問と2つの内部メモがあるとします。[質問を保存]をクリックします。

    1. 質問の.saveをクリックした後、ネストされたすべてのノートの.saveを探します。 見つかった2、質問の保存を停止しました。

       beforeSend: function(xhr){ xhr.abort(); }
            
            





    2. 最初のメモが正常に保存され、レイアウトが置き換えられました。
    3. 2番目のメモを保存すると、何らかのエラーが発生し、レイアウトが置き換えられず、.saveボタンが残りました。
    4. 配列にはノートが残っていないため、質問が呼び出されます。

       $.when.apply(null, deferreds).always(function() { saveData.call(self); });
            
            





    5. beforeSendに再び入り、保存されていないネストを確認します

       var $inner_notes = $li.find('ul .save')/*.not('.ignore')*/; //  .not()  if($inner_notes.length) {}
            
            





    6. メモの1つが保存されていないため、それを見つけました。ここで、2つのことが発生します。

      • または、何らかの理由で、ノートが正常に保存され、その後、質問が正常に保存されます。
      • または再び失敗し、無限のサイクルに陥ります。


      .ignoreクラスのおかげで、このような場合から自分自身を守ることができます。

      メモを保存できませんでしたか? さて、私たちにとっての主な質問は保存することです。









3番目の質問は、マルチレベルのネストです。





レイアウト:
 <ul class="questions"> <li> <div class="question" id="id3"> <span class="text"> c  </span> <span class="edit">edit</span> </div> <ul> <li> <div class="note" id="id1"> <span class="text">1 </span> <span class="edit">edit</span> </div> </li> <li> <div class="note" id="id2"> <span class="text">2 </span> <span class="edit">edit</span> </div> <ul> <li> <div class="note" id="id4"> <span class="text">4 </span> <span class="edit">edit</span> </div> <ul> <li> <div class="note" id="id7"> <span class="text">7 </span> <span class="edit">edit</span> </div> </li> <li> <div class="note" id="id8"> <span class="text">8 </span> <span class="edit">edit</span> </div> </li> </ul> </li> <li> <div class="note" id="id5"> <span class="text">5 </span> <span class="edit">edit</span> </div> <ul> <li> <div class="note" id="id6"> <span class="text">6 </span> <span class="edit">edit</span> </div> </li> </ul> </li> </ul> </li> <li> <div class="note" id="id3"> <span class="text">3 </span> <span class="edit">edit</span> </div> </li> </ul> </li> </ul>
      
      







ここではコードが非常に怖くなると思われますが、実際には、以前の実装はすでにこれに実際に適しています。 関数を1つだけ追加する必要があります。

事実は..

 var $inner_notes = $li.find('ul .save').not('.ignore')
      
      





...は、ネストのすべてのレベルでメモを収集します。親のメモが子の前に保存されている場合、子には保存する時間がありません。 問題が繰り返されます。 私たちがする必要があるのは、それぞれの親のノートを子に関する質問のように振舞わせることです。

つまり メモを保存し、その中にまだ保存されていないメモがあることをbeforeSendが明らかにした場合、親メモの保存を一時停止し、すべての子が実行されるまで待ちます。

多数のネストを使用すると、このような深い再帰が得られます。



ユーザーが完全にクレイジーで、編集のためにそのようなブランチを開いているときに質問を保存することにしたとしましょう。



「2番目のメモ」をすぐに保存することはできません。 他のすべてのノートの編集は失われます。

そのため、下から上に移動する必要があります。 何も見逃さないようにノートを保存するための正しいシーケンスは何だと思いますか? 8.6、4、2、そして質問。

ただし、問題を保存することを押します。つまり、.closest()メソッドなど、最も近い子を検索する関数が必要ですが、その逆も同様です。



機能の実装と適用
 //      beforeSend ,   - getClosestChildrens saveData: function() { .... beforeSend: function(xhr){ var $inner_notes = $li.find('ul .save').not('.ignore'), //          $children_notes = getClosestChildrens($inner_notes); if($children_notes.length) { var deferreds = []; $children_notes.each(function() { deferreds.push(app.saveData.call(this)); }); //          $.when.apply(null, deferreds).always(function() { //       //      ,   . // self        . app.saveData.call(self); }); //   / xhr.abort(); } }, .... } //      (    ). //            //        ,    . // ..   " ",     . function getClosestChildrens($inner_notes) { var children_notes = $.grep($inner_notes, function(value, key) { var is_child_of = false, $btn = $(value), $parent_li = $btn.closest('li'); $inner_notes.not($btn).each(function(key,v) { if($(this).closest($parent_li).length) { is_child_of = true; } }); return is_child_of ? false : true; }); return $(children_notes); }
      
      







実行ロジックは次のようになります。

  1. 質問を「保存」をクリックします
  2. beforeSendですべてのノートを見つけ、最も近い子以外のすべてを破棄します。 2番目のメモのままです。 質問の保存を停止し、2番目のメモの保存の完了にサブスクライブします。
  3. 2番目のメモの保存を開始し、ネストがあることがわかります(4、8、6)。それらを見つけ、最も近いものを除くすべてを破棄します。 4と6のままです。2番目のノートの保存を停止し、4番目と6番目の保存の完了にサブスクライブします。
  4. 6番目のノートの保存を開始します。ネストなしで保存します。
  5. 4番目のノートの保存を開始すると、ネストが見つかります(8e)。 4番目のメモの保存を停止し、8番目の保存の完了にサブスクライブします。
  6. ネストなしで8番目のノートの保存を開始し、保存します。
  7. 8日の親メモの保存を開始します。 4番目のメモ。 編集可能なネストを左に保存します。
  8. 8番目と6番目の親ノートの保存を開始します。 セカンドノート。 編集可能なネストを左に保存します。
  9. 質問の保存を開始します。 編集可能なネストを左に保存します。




ps1すべての例で、質問から始まる保存について見てきましたが、これは、質問がいわば、最も難しい選択肢であるためです。 もちろん、最後の例に示すような状況で、メモの1つ(2mなど)で[保存]をクリックすると、それに添付されているすべてのメモも正常に保存されます。



ps2すべての例で、項目が保存されるときに呼び出されるsaveData関数を示しました。 また、beforeSend関数をeditData関数に追加する必要があります。これは、編集要素をクリックしたときに呼び出されます。 結局のところ、[編集]をクリックして、まだ編集可能なメモが残っている場合-それらも保存する必要がありますが、これはすでにデモで確認できます。



したがって、編集可能なデータを失うことなく、ネストの構造を保存できます。



デモンストレーションには、サーバーからの応答用に小さなphpを使用する必要があったため、jsFiddleでデモをデモンストレーションできません。

Githubに完全に動作する例をアップロードしたので、興味のある人は誰でもダウンロードして遊ぶことができます。

また、過負荷ホスティングの1つにデモをアップロードしました。デモを参照してください



レイアウトがどのように置き換えられるかを確認できるように、1秒の遅延を追加しました。 質問/メモが編集または保存されるたびに、レイアウトを置き換えるときに現在のブロックの背景が点滅します。 何が起こっているかを理解するのは簡単です。



それだけです、コメントの質問に答えてうれしいです。



All Articles