I have a table of canvas elements that emulates tiled image. I need to populate only those ti开发者_如何学Pythonles that are currently visible in the viewport. Here's the code I use
for (var ix = visibleRange.firstX; ix <= visibleRange.lastX; ix++) {
for (var iy = visibleRange.firstY; iy <= visibleRange.lastY; iy++) {
var canvasDispJq = $("canvas#" + frameResId + "-" + ix + "-" + iy + "- disp");
var canvasDisp = canvasDispJq.get(0);
var image = new Image();
var context = canvasDisp.getContext("2d");
image.onload = function() {
context.drawImage(image, 0, 0);
}
image.src = self.baseURL + '/' + canvasDispJq.attr("id");
}
}
};
But the problem is that only the last tile from the range is loaded. The same thing but implemented in this way is working fine:
$("table#canvasTable-" + frameResId + " canvas.display").each(function() {
var canvasDispJq = $(this);
if ((canvasDispJq.position().top + self.tileSize + imagePosition.y) > 0 &&
(canvasDispJq.position().left + self.tileSize + imagePosition.x) > 0 &&
(canvasDispJq.position().top + imagePosition.y) < self.containerHeight &&
(canvasDispJq.position().left + imagePosition.x) < self.containerWidth) {
var canvasDisp = canvasDispJq.get(0);
var image = new Image();
var context = canvasRaw.getContext("2d");
image.onload = function() {
context.drawImage(image, 0, 0);
}
image.src = self.baseURL + '/' + canvasDispJq.attr("id");
}
});
Difference in how the visible range is calculated shouldn't matter because I checked in Firebug and for both approaches tiles to render are selected correctly and actual tile images are downloaded from server but for first approach only last tile is displayed, while for second one all the tiles are rendered correctly.
Thanks in advance for any suggestions.
The reason for the difference is primarily here:
var image = new Image();
var context = canvasDisp.getContext("2d");
image.onload = function() {
context.drawImage(image, 0, 0);
}
What that does is create a closure and assign it to the load
event of the image. The closure closes over the context
and image
variables (and several others). The key thing here is that it has an enduring reference to those variables, not a copy of their values when the function was created. Since the above code is in a loop, all of the functions (closures) that get created will refer to the same context
and image
— the last ones created by the loop.
It doesn't happen in the each
version because in that version, the closure closes over a variable that doesn't change, the one context
and image
created by the call to the iterator function you're passing into each
.
More about closures here: Closures are not complicated
Perhaps slightly off-topic, perhaps not: This may have been more clear if your var
s were in a different place. var
is sometimes misunderstood. In particular, it's not the same as variable declarations in some other languages (C, C++, Java, and C# for instance), partially because JavaScript doesn't have block-level scope (only function-level scope and the global scope).
This is how the JavaScript interpreter sees your first version:
var ix, iy, canvasDispJq, canvasDisp, image, context;
for (ix = visibleRange.firstX; ix <= visibleRange.lastX; ix++) {
for (iy = visibleRange.firstY; iy <= visibleRange.lastY; iy++) {
canvasDispJq = $("canvas#" + frameResId + "-" + ix + "-" + iy + "- disp");
canvasDisp = canvasDispJq.get(0);
image = new Image();
context = canvasDisp.getContext("2d");
image.onload = function() {
context.drawImage(image, 0, 0);
};
image.src = self.baseURL + '/' + canvasDispJq.attr("id");
}
}
}
Note that all the var
statements have moved to the top, because that's how they'll be treated. Regardless of where var
appears in a function, it takes effect from the very beginning of the function and only happens once. If the var
has an initializer, that becomes an assignment where the var
is. So var x = 2;
is really two completely separate things that are handled at separate times by the interpreter: var x
, which happens upon entry to the function before anything else happens, and x = 2;
, which happens where it appears in the step-by-step execution of the function's code.
(Also, off-topic: There's no need for a ;
after the closing }
of a for
loop; but you do want one after the assignment to the onload
handler. I've made both of those mods aboev as well.)
Writing the code the way the interpreter will see it, now (to me, anyway) it's clearer that each of the closures you're assigning to onload
will be referring to the same context
and image
variables, and so they'll all see the last ones.
If you want to use the non-each
version, you just have to change it slightly so the onload
closures close over their own copy of context
and image
. That looks like this:
var ix, iy, canvasDispJq, canvasDisp, image, context;
for (ix = visibleRange.firstX; ix <= visibleRange.lastX; ix++) {
for (iy = visibleRange.firstY; iy <= visibleRange.lastY; iy++) {
canvasDispJq = $("canvas#" + frameResId + "-" + ix + "-" + iy + "- disp");
canvasDisp = canvasDispJq.get(0);
image = new Image();
context = canvasDisp.getContext("2d");
image.onload = createOnloadHandler(context);
image.src = self.baseURL + '/' + canvasDispJq.attr("id");
}
}
}
function createOnloadHandler(ct, img) {
return function() {
context.drawImage(img, 0, 0);
};
}
Now the closure created by the createOnloadHandler
function closes over ct
and img
, not context
and image
. Each closure gets its own copy (the one passed into the createOnloadHandler
call that created that closure).
精彩评论