将回调函数转换为Promise

6
我正在使用谷歌地图API,并使用以下代码异步返回地点列表。当所有数据收集完成后,如何调用此函数并触发某些操作?以下是我尝试过的内容 -
$.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);
};
4个回答

5
为了回答标题中暗示的问题“将回调函数转换为Promise”,简单的答案是使用非常简单的“Promisifier模式”(我自己的术语),其中一个Deferred对象的`.resolve()`方法被建立为回调函数:
原始带回调函数的调用:
obj.method(param1, param2, ... paramN, callbackFn);

使用延迟包装的转换呼叫:

$.Deferred(function(dfd) {
    obj.method(param1, param2, ... paramN, dfd.resolve);
}).promise();

无论obj.method是否是异步的,都可以完成此操作。优点是现在您可以完全使用Deferreds / promises的链式调用功能,无论是在同一代码块中还是更通常地在分配或返回生成的Promise后的其他位置。

以下是在此问题情况下可能使用该模式的方式...

$.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() 现在可以这样使用:

$.search(boxes, ["food"]).done(function(obj) {
    alert(obj.results.length + ' markers were a added for: "' + obj.types.join() + '"');
});
示例 - 注意:由于在jQuery 1.8.3中对jQuery.Deferred.then()进行了重大修订,因此需要使用jQuery 1.8.3+。

这段代码与问题中的代码并不完全相同,但可以用于诊断您报告的问题。特别是:

  • 它不会在出现错误时停止
  • 它会将成功和错误消息都放在“#side_bar”中。

调整代码以实现您想要的功能应该很简单。


1

当前的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;

请访问http://jsfiddle.net/zsKnK/7/以查看完整的可用文档。


.notify(...) 触发延迟对象的 "progress" 回调。如果你想触发延迟对象的 "done" 回调,那么请使用 .resolve(...) - Shai
@seasick 真的吗?你能否创建一个 JSFiddle 并提供完整的代码,以便我们查看一下哪里出了问题? - Toothbrush
@seasick 试一下http://jsfiddle.net/zsKnK/4/。我不明白为什么它不起作用...我已经尝试了很多不同的方法... - Toothbrush
@seasick 你需要使用jQuery的Promise吗?还是自定义的Promise函数可以胜任? - Toothbrush
@seasick 请看我的回答。现在它完全可以正常工作并且实现了你想要的所有功能。 - Toothbrush
@seasick 让我们在聊天室里继续讨论 - Toothbrush

1
你在第一个请求后才解决了延迟,没有等待递归调用的结果。为了做到这一点,你需要链接它们。另外,你不应该使用全局变量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);
});

你具体尝试了什么(可以提供fiddle链接吗?)以及你遇到了什么问题? - Bergi

1

...jQuery-Deferred 应该始终被解决。因此,在您的函数中完成任务后,请使用您需要在 then-callback-function 中使用的参数调用 "dfd.resolve()"。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接