How to use jQuery promise on multiple change event

2019-09-15 18:02发布

问题:

I've got the following code which shows and hides select menus using a basic options chain parent/child relationship. Once there is a change I need to call a function.

Currently I am doing it as demonstrated below, triggering an event on change for each <select>, which then calls the function. In real life this is an ajax call but that is irrelevant to the question.

The code I have below works. The problem I would like to solve is how I can accomplish this without relying on a change event since it triggers the function multiple times unnecessarily. I'd like to simply place a function call at the end of the $.each() loop when the parent is changed but because of the animation involved it will fail since the elements in question are sometimes still visible/hidden. I solved this by placing a callback inside each slideUp() and slideDown() but this is the exact reason I end up triggering things multiple times.

I have a strong feeling that the answer I am looking for uses jQuery promise() which states:

The .promise() method returns a dynamically generated Promise that is resolved once all actions of a certain type bound to the collection, queued or not, have ended.

That is exactly what I want to do but I am utterly confused by promise() and it's proper usage. I get the concept, but I could use some help understanding how I can queue all those change events into one promise() and then call my function when they are all complete.

$('select[data-parent="true"]').change(function() {
  var parentValue = +this.value;
  var parentId = $(this).data('parent_id');
  $('div[data-parent_id="' + parentId + '"]').each(function() {
    var parentValues = $(this).data('parent_values');
    if (parentValues.indexOf(parentValue) !== -1) {
      $(this).slideDown(1000, function() {
        $(this).find('select').change();
      });
    } else {
      $(this).slideUp(1000, function() {
        $(this).find('select').change();
      });
    }
  });
});
$('select').on('change', function() {
  var results = '';
  $('div.option').each(function() {
    results += $(this).find('select').prop('name') + ' IS VISIBLE: ' + +$(this).is(':visible') + '<br>';
  });
  $('#results').html(results);
});
div {
  padding: 5px 10px;
}

div.child {
  display: none
}

select {
  width: 200px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<div class="option">
  <select name="container" data-parent="true" data-parent_id="10">
  <option value="">--- Select a container---</option>
		<option value="1">Box</option>
		<option value="2">Bag</option>
		<option value="3">Pallet</option>
	</select>
</div>
<div class="child option" data-parent_id="10" data-parent_values="[1]">
  <select name="box color">
		<option value="100">Red</option>
		<option value="200">Blue</option>
	</select>
</div>
<div class="child option" data-parent_id="10" data-parent_values="[2]">
  <select name="bag color">
		<option value="300">Green</option>
		<option value="400">Yellow</option>
	</select>
</div>
<div class="child option" data-parent_id="10" data-parent_values="[3]">
  <select name="pallet color">
		<option value="500">Black</option>
		<option value="600">Yellow</option>
	</select>
</div>
<div id="results">
</div>

回答1:

As jQuery animations can return promises .promise() and using .map instead of .each - the code can be simply:

$('select[data-parent="true"]').change(function() {
    var parentValue = +this.value;
    var parentId = $(this).data('parent_id');
    $.when.apply($, $('div[data-parent_id="' + parentId + '"]').map(function(i, v) {
        var parentValues = $(this).data('parent_values');
        if (parentValues.indexOf(parentValue) !== -1) {
            return $(this).slideDown(1000).promise();
        } else {
            return $(this).slideUp(1000).promise();
        }
    })).done(function() {
        var results = '';
        $('div.option').each(function() {
            results += $(this).find('select').prop('name') + ' IS VISIBLE: ' + +$(this).is(':visible') + '<br>';
        });
        $('#results').html(results);
    });
});
div {
  padding: 5px 10px;
}

div.child {
  display: none
}

select {
  width: 200px;
  margin: 4px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<div class="option">
  <select name="container" data-parent="true" data-parent_id="10">
  <option value="">--- Select a container---</option>
		<option value="1">Box</option>
		<option value="2">Bag</option>
		<option value="3">Pallet</option>
	</select>
</div>
<div class="child option" data-parent_id="10" data-parent_values="[1]">
  <select name="box color">
		<option value="100">Red</option>
		<option value="200">Blue</option>
	</select>
</div>
<div class="child option" data-parent_id="10" data-parent_values="[2]">
  <select name="bag color">
		<option value="300">Green</option>
		<option value="400">Yellow</option>
	</select>
</div>
<div class="child option" data-parent_id="10" data-parent_values="[3]">
  <select name="pallet color">
		<option value="500">Black</option>
		<option value="600">Yellow</option>
	</select>
</div>
<div id="results">
</div>



回答2:

An hour of experimenting has yielded the following solution. Create an array of $.Deferred() objects and use $.when to resolve them. The tricky part came from this answer which explains how to pass an array to $.when. Hope this helps someone struggling with jQuery deferred objects like I was:

$('select[data-parent="true"]').change(function() {
  var deferred = [];
  var parentValue = +this.value;
  var parentId = $(this).data('parent_id');
  $('div[data-parent_id="' + parentId + '"]').each(function(i, v) {
    deferred[i] = $.Deferred();
    var parentValues = $(this).data('parent_values');
    if (parentValues.indexOf(parentValue) !== -1) {
      $(this).slideDown(1000, function() {
        deferred[i].resolve();
      });
    } else {
      $(this).slideUp(1000, function() {
        deferred[i].resolve();
      });
    }
  });
  $.when.apply($, deferred).done(function() {
    var results = '';
    $('div.option').each(function() {
      results += $(this).find('select').prop('name') + ' IS VISIBLE: ' + +$(this).is(':visible') + '<br>';
    });
    $('#results').html(results);
  });
});
div {
  padding: 5px 10px;
}

div.child {
  display: none
}

select {
  width: 200px;
  margin: 4px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<div class="option">
  <select name="container" data-parent="true" data-parent_id="10">
  <option value="">--- Select a container---</option>
		<option value="1">Box</option>
		<option value="2">Bag</option>
		<option value="3">Pallet</option>
	</select>
</div>
<div class="child option" data-parent_id="10" data-parent_values="[1]">
  <select name="box color">
		<option value="100">Red</option>
		<option value="200">Blue</option>
	</select>
</div>
<div class="child option" data-parent_id="10" data-parent_values="[2]">
  <select name="bag color">
		<option value="300">Green</option>
		<option value="400">Yellow</option>
	</select>
</div>
<div class="child option" data-parent_id="10" data-parent_values="[3]">
  <select name="pallet color">
		<option value="500">Black</option>
		<option value="600">Yellow</option>
	</select>
</div>
<div id="results">
</div>