简体   繁体   中英

Continually iterating through promises results in excessively deep stack

LATER EDIT

This question should be deleted, because the problem I'm reporting isn't even real.

There was no 'long chain of closures', that was me misunderstanding the google chrome watch window.

Any memory leak may be caused by the video elements not cleaning up properly. This is a different problem which has been addressed in other questions.


I am writing Javascript code to continually loop through a playlist. The items on the playlist are images (which display for 10 seconds) or videos.

Here's some code to do this (handling images or videos)

 var IMAGE = 0; var VIDEO = 1; var mediaElements = [{ url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Rembrandt_van_Rijn_-_Self-Portrait_-_Google_Art_Project.jpg/180px-Rembrandt_van_Rijn_-_Self-Portrait_-_Google_Art_Project.jpg', mediaType: IMAGE }, { url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/English_Pok%C3%A9mon_logo.svg/269px-English_Pok%C3%A9mon_logo.svg.png', mediaType: IMAGE }, { url: 'http://www.w3schools.com/html/mov_bbb.mp4', mediaType: VIDEO }, { url: 'https://upload.wikimedia.org/wikipedia/en/thumb/3/34/Teuchitlan_scale_model_1_cropped.jpg/133px-Teuchitlan_scale_model_1_cropped.jpg', mediaType: IMAGE }, { url: 'https://upload.wikimedia.org/wikipedia/en/f/f7/Sugimoris025.png', mediaType: IMAGE } ]; $(function () { function displayMediaFile(mediaFile) { $('#imageTarget').empty(); if (mediaFile.mediaType === IMAGE) { $('#imageTarget').append($('<img />').attr('src', mediaFile.url)); return new Promise(function (resolve, reject) { window.setTimeout(resolve, 2000); }); } else { var videoElement = $('<video></video>').attr('autoplay', ''); var sourceElement = $('<source><source>') .attr('src', mediaFile.url) .attr('type', 'video/mp4') .appendTo(videoElement); videoElement = videoElement.appendTo('#imageTarget'); return new Promise(function (resolve, reject) { videoElement[0].onended = resolve; }); } }; function iterateThroughPlaylist(index) { if (index >= mediaElements.length) { index = 0; } console.log('displaying image ' + index); displayMediaFile(mediaElements[index]).then(function () { iterateThroughPlaylist(index + 1); }); } iterateThroughPlaylist(0); }); 

See this jsfiddle for a complete working example.

So the actual 'displayMediaFile' function returns a Promise. This promise resolves when the play is complete, and then we move on to the next image.

The problem is, it stops running after a while. I put a breakpoint in the iterateThroughImages method, then looked at the call stack. I could see an extremely long chain of closures.

Can I produce code with the same simplicity, but somehow avoid having the runtime keep the closures in memory?

Promises don't seem to be the best solution here.

var imageUrls = [ url1, url2, url3, url4  ];
(function play(index){
    console.log('displaying image ' + index);
    var url = imageUrls[index];
    $('#imageTarget').empty().append($('<img />').attr('src', url));
    window.setTimeout(play, 2000, (index+1)%imageUrls.length);
})(0);

You could still use promises inside if you have other operations to chain but don't build an infinite chain of promises.

Side note: you don't really have to delete and recreate the img element, you could simply change its src property.

This is a case where promises are not the best solution. As long term stability is an issue it's better to make iterateThroughPlaylist run again more directly with :

  • image: window.setTimeout(iterateThroughPlaylist, 2000);
  • video: videoElement[0].onended = iterateThroughPlaylist;

As with promises, there's no recursion because iterateThroughPlaylist will be called in a later event turn in both cases.

$(function () {
    var mediaTypes = { 'IMAGE':0, 'VIDEO':1 },
        mediaElements = [
            ...
            ...
            ...
        ],
        index = -1;
    function displayMediaFile(index, fn) {
        var mediaFile = mediaElements[index];
        $('#imageTarget').empty();
        switch(mediaFile.mediaType) {
            case mediaTypes.IMAGE:
                $('#imageTarget').append($('<img/>').attr('src', mediaFile.url));
                window.setTimeout(fn, 2000);
            break;
            case mediaTypes.VIDEO:
                var videoElement = $('<video/>')
                    .attr('autoplay', '')
                    .append($('<source/>').attr({ 'src': mediaFile.url, 'type': 'video/mp4')})
                    .appendTo('#imageTarget');
                videoElement[0].onended = fn;
            break;
            default:
                //do nothing and let the sequence lapse, or
                //eg. window.setTimeout(fn, 2000); to keep trying
        }
    };
    function iterateThroughPlaylist() {
        index = (index + 1) % mediaElements.length;
        displayMediaFile(index, arguments.callee);
    }
    iterateThroughPlaylist();
});

Notes

  • Passing arguments.callee as a callback is just a way of not hard-coding the function name iterateThroughPlaylist
  • there are no inner functions within displayMediaFile or iterateThroughPlaylist , so only the outermost $(function () {...} forms a closure as expected.
  • Containing everything in the $(function () {...} structure avoids the need to use the global namespace.
  • switch/case will make it easier to add further media types if necessary.
  • for even greater efficiency you could create the <img> and <video> elements up front and reuse them.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM