可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I'm working with the google maps api and this piece of code is returning a list of places asynchronously. How can I call this function and have it trigger something when all the data is collected? This has been what i've tried so far -
$.search = function(boxes) { function findNextPlaces(place_results, searchIndex) { var dfd = $.Deferred(); if (searchIndex < boxes.length) { service.radarSearch({ bounds: boxes[searchIndex], types: ["food"] }, function (results, status) { if (status != google.maps.places.PlacesServiceStatus.OK) { return dfd.reject("Request["+searchIndex+"] failed: "+status); } console.log( "bounds["+searchIndex+"] returns "+results.length+" results" ); for (var i = 0, result; result = results[i]; i++) { var marker = createMarker(result); place_results.push(result.reference); // marker? } }); return dfd.then(findNextPlaces); } else { return dfd.resolve(place_results).promise(); } } return findNextPlaces([], 0); };
回答1:
To answer the question implied by the title, "Turn callback into promise", the simple answer is to use a really simple "promisifier pattern" (my term), in which a Deferred's .resolve()
method is established as a callback :
Original call with callback :
obj.method(param1, param2, ... paramN, callbackFn);
Converted call, with Deferred wrapper :
$.Deferred(function(dfd) { obj.method(param1, param2, ... paramN, dfd.resolve); }).promise();
This can be done whether or not obj.method
is asynchronous. The advantage is that you now have the full chainability of Deferreds/promises available to you either in the same block of code or, more typically, elsewhere having assigned or returned the generated Promise.
Here's a way in which the pattern might be used in the case of this question ...
$.search = function(boxes, types) { function findPlaces(box) { var request = { bounds: box, types: types }; //*********************** // Here's the promisifier //*********************** return $.Deferred(function(dfd) { service.radarSearch(request, dfd.resolve); }).promise(); //*********************** //*********************** //*********************** } function handleResults(results, status, searchIndex) { var message, marker; if (status != google.maps.places.PlacesServiceStatus.OK) { message = "bounds[" + searchIndex + "] failed : " + status; } else { message = "bounds[" + searchIndex + "] returns " + results.length + " results"; for (var i = 0, result; result = results[i]; i++) { marker = createMarker(result); place_Results.push(result.reference); } } return message; } var place_Results = [], p = $.when();//resolved starter promise //This concise master routine comprises a loop to build a `.then()` chain. $.each(boxes, function(i, box) { p = p.then(function() { return findPlaces(box).done(function(results, status) { // This function's arguments are the same as the original callback's arguments. var message = handleResults(results, status, i); $('#side_bar').append($("<div/>").append(message)); }); }); }); //Now, chain a final `.then()` in order to make the private var `place_Results` available via the returned promise. For good measure, the `types` array is also repeated back. return p.then(function() { return { types: types, results: place_Results }; }); };
$.search()
can now be used as follows :
$.search(boxes, ["food"]).done(function(obj) { alert(obj.results.length + ' markers were a added for: "' + obj.types.join() + '"'); });
DEMO - Note: jQuery 1.8.3+ is required due to major revision of jQuery.Deferred.then()
at jQuery 1.8.3 .
This is not exactly equivalent to the code in the question but may be good for diagnosis of the issues you report. In particular, :
- it won't stop on error
- it will put success and error messages in the '#side_bar'.
It should be simple enough to adjust the code to do what you want.
回答2:
... jQuery-Deferred should always be resolved. So after finishing your tasks in your function call "dfd.resolve()" with your parameters you have to work with in your then-callback-function.
回答3:
Current JavaScript:
var map = null; var boxpolys = null; var directions = null; var routeBoxer = null; var distance = null; // km var service = null; var gmarkers = []; var infowindow = new google.maps.InfoWindow(); var promises = []; function MyPromise() { return this; }; MyPromise.prototype.promise = function () { var p = promises[this] || { then: [] }; promises[this] = p; return this; }; MyPromise.prototype.then = function (func) { this.promise(); promises[this].then.push(func); return this; }; MyPromise.prototype.resolve = function () { this.promise(); var then = promises[this].then; for (var promise in then) { then[promise].apply(this, arguments); } return this; }; function initialize() { // Default the map view to the continental U.S. var mapOptions = { center: new google.maps.LatLng(40, -80.5), mapTypeId: google.maps.MapTypeId.ROADMAP, zoom: 8 }; map = new google.maps.Map(document.getElementById("map"), mapOptions); service = new google.maps.places.PlacesService(map); routeBoxer = new RouteBoxer(); directionService = new google.maps.DirectionsService(); directionsRenderer = new google.maps.DirectionsRenderer({ map: map }); } function route() { var dfd = new MyPromise().promise(); // Clear any previous route boxes from the map clearBoxes(); // Convert the distance to box around the route from miles to km distance = parseFloat(document.getElementById("distance").value) * 1.609344; var request = { origin: document.getElementById("from").value, destination: document.getElementById("to").value, travelMode: google.maps.DirectionsTravelMode.DRIVING } // Make the directions request directionService.route(request, function (result, status) { if (status == google.maps.DirectionsStatus.OK) { directionsRenderer.setDirections(result); // Box around the overview path of the first route var path = result.routes[0].overview_path; var boxes = routeBoxer.box(path, distance); // alert(boxes.length); drawBoxes(boxes); // findPlaces(boxes,0); $.search(boxes, 0).then(function (p) { console.log("done", p); }).then(dfd.resolve); } else { alert("Directions query failed: " + status); } }); // $.when(findPlaces()).done(function(){ // console.log("done"); // }); return dfd; } // Draw the array of boxes as polylines on the map function drawBoxes(boxes) { boxpolys = new Array(boxes.length); for (var i = 0; i < boxes.length; i++) { boxpolys[i] = new google.maps.Rectangle({ bounds: boxes[i], fillOpacity: 0, strokeOpacity: 1.0, strokeColor: '#000000', strokeWeight: 1, map: map }); } } $.search = function findPlaces(boxes, searchIndex) { var dfd = new MyPromise().promise(); var request = { bounds: boxes[searchIndex], types: ["food"] }; window.place_Results = []; service.radarSearch(request, function (results, status) { if (status != google.maps.places.PlacesServiceStatus.OK) { alert("Request[" + searchIndex + "] failed: " + status); return; } document.getElementById('side_bar').innerHTML += "bounds[" + searchIndex + "] returns " + results.length + " results<br>" for (var i = 0, result; result = results[i]; i++) { var marker = createMarker(result); place_Results.push(result.reference); } searchIndex++; if (searchIndex < boxes.length) findPlaces(boxes, searchIndex); if (place_Results.length > 0) { dfd.resolve(place_Results); } }); return dfd; } // Clear boxes currently on the map function clearBoxes() { if (boxpolys != null) { for (var i = 0; i < boxpolys.length; i++) { boxpolys[i].setMap(null); } } boxpolys = null; } function createMarker(place) { var placeLoc = place.geometry.location; if (place.icon) { var image = new google.maps.MarkerImage( place.icon, new google.maps.Size(71, 71), new google.maps.Point(0, 0), new google.maps.Point(17, 34), new google.maps.Size(25, 25)); } else var image = null; var marker = new google.maps.Marker({ map: map, icon: image, position: place.geometry.location }); var request = { reference: place.reference }; google.maps.event.addListener(marker, 'click', function () { service.getDetails(request, function (place, status) { // console.log(place); if (status == google.maps.places.PlacesServiceStatus.OK) { var contentStr = '<h5>' + place.name + '</h5><p>' + place.formatted_address; if ( !! place.formatted_phone_number) contentStr += '<br>' + place.formatted_phone_number; if ( !! place.website) contentStr += '<br><a target="_blank" href="' + place.website + '">' + place.website + '</a>'; contentStr += '<br>' + place.types + '</p>'; infowindow.setContent(contentStr); infowindow.open(map, marker); } else { var contentStr = "<h5>No Result, status=" + status + "</h5>"; infowindow.setContent(contentStr); infowindow.open(map, marker); } }); }); gmarkers.push(marker); var side_bar_html = "<a href='javascript:google.maps.event.trigger(gmarkers[" + parseInt(gmarkers.length - 1) + "],\"click\");'>" + place.name + "</a><br>"; document.getElementById('side_bar').innerHTML += side_bar_html; } initialize(); document.getElementById('route').onclick = route;
See http://jsfiddle.net/zsKnK/7/ for the full working document.
回答4:
You're resolving your deferred after the first request, not waiting for the results of the recursive calls. To do that, you'll need to chain them. Also, you shouldn't use a global variable for the place_Results
.
$.search = function(boxes) { function findNextPlaces(place_results, searchIndex) { var dfd = $.Deferred(); if (searchIndex < boxes.length) { service.radarSearch({ bounds: boxes[searchIndex], types: ["food"] }, function (results, status) { if (status != google.maps.places.PlacesServiceStatus.OK) { return dfd.reject("Request["+searchIndex+"] failed: "+status); } console.log( "bounds["+searchIndex+"] returns "+results.length+" results" ); for (var i = 0, result; result = results[i]; i++) { var marker = createMarker(result); place_results.push(result.reference); // marker? } dfd.resolve(place_results, searchIndex+1); }); return dfd.then(findNextPlaces); } else { return dfd.resolve(place_results).promise(); } } return findNextPlaces([], 0); };
$.search(boxes,0).then(function(results) { console.log("done", results); }, function(err) { alert(err); });