使用AJAX时JavaScript为何生成位置错误的棋子图像?
问题分析与解决方案
首先,我帮你定位了问题的核心原因:全局状态残留、旧DOM元素的异步加载干扰,这两个因素共同导致了点击"New Board"后棋子位置错乱的问题。下面是具体的分析和修复方案:
问题根源
- 全局状态未重置:
last_colour、colour_row、colour_column这三个全局变量在每次生成新棋盘时没有重置为初始值,状态会从上一次调用中继承,导致位置计算逻辑混乱。 - 旧图片元素残留:每次点击按钮都会创建新的
<img>元素添加到隐藏容器中,旧的元素并未被删除。这些旧元素的load事件可能延迟触发,在新的画布上绘制旧的棋子,造成画面错乱。 - 异步加载的闭包隐患:虽然当前棋子数组是固定的红车在前、黑车在后,但全局变量的设计本身就容易引发状态污染,后续如果数组顺序变化会直接导致位置错误。
修复方案
方案1:快速修复(最小改动)
只需要在每次生成新棋盘时重置全局状态,并清理旧的图片元素:
function displayPieces(context) { // 1. 重置全局状态,确保每次新棋盘从初始值开始计算 last_colour = ''; colour_row = 0; colour_column = 0; // 2. 清空旧的图片元素,避免残留的load事件干扰新绘制 const pieceImages = document.getElementById('pieceImages'); while (pieceImages.firstChild) { pieceImages.removeChild(pieceImages.firstChild); } // 原有代码保持不变 var red_count = document.getElementById('red-count'); var black_count = document.getElementById('black-count'); red_count.innerText = context.colour_length['red'] black_count.innerText = context.colour_length['black'] var max_colour_length = context.max_colour_length; var position_id = 'top-image' var ctx = prepareCanvas(position_id, max_colour_length) var piece_index = 0; context.pieces.forEach(piece_name => { createModelImage (position_id, piece_index) displayPieceImage (context, ctx, position_id, piece_name, piece_index) piece_index ++; } ) }
方案2:最优修复(消除全局变量)
将位置计算的状态封装为局部变量,彻底避免全局状态污染,同时优化图片加载逻辑:
// static/js/test.js // 'Constants' to define image cropping var RAW_WIDTH = 552 var RAW_HEIGHT = 640 var SCALE = 0.1 var PIECE_WIDTH = RAW_WIDTH * SCALE var PIECE_HEIGHT = RAW_HEIGHT * SCALE var CANVAS_HEIGHT = 3 * PIECE_HEIGHT; // 预加载棋子图片,避免重复创建DOM元素 const pieceCache = {}; $(document).ready(function () { var new_board_link = document.getElementById('new-board'); new_board_link.onclick = getNewBoard; getNewBoard(); }); function getNewBoard() { $.ajax( { type: "GET", url: 'new-board', cache: false, success: function (context) { // 缓存图片URL,后续直接使用 Object.keys(context.piece_images).forEach(key => { if (!pieceCache[key]) { const img = new Image(); img.src = context.piece_images[key]; pieceCache[key] = img; } }); displayPieces(context); } } ); } function displayPieces(context) { var red_count = document.getElementById('red-count'); var black_count = document.getElementById('black-count'); red_count.innerText = context.colour_length['red'] black_count.innerText = context.colour_length['black'] var max_colour_length = context.max_colour_length; var position_id = 'top-image' var ctx = prepareCanvas(position_id, max_colour_length) // 使用局部变量保存位置状态,替代全局变量 let lastColour = ''; let colourColumn = -1; context.pieces.forEach((pieceName, index) => { const { posX, posY } = calculatePiecePosition(pieceName, lastColour, colourColumn); // 更新局部状态 lastColour = pieceName.substring(0, 1); colourColumn = pieceName.substring(0, 1) === lastColour ? colourColumn + 1 : 0; // 绘制棋子 drawPiece(ctx, pieceName, posX, posY); }) } function prepareCanvas(position_id, max_colour_length) { const canvas = document.getElementById(position_id); canvas.width = max_colour_length * PIECE_WIDTH; canvas.height = CANVAS_HEIGHT; const ctx = canvas.getContext('2d'); // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); return ctx } // 纯函数:计算棋子位置,不依赖全局状态 function calculatePiecePosition(pieceName, currentLastColour, currentColumn) { const pieceColour = pieceName.substring(0, 1); let column = currentColumn; if (pieceColour !== currentLastColour) { column = 0; } else { column += 1; } // 黑车在顶行(y=0),红车在底行(y=PIECE_HEIGHT) const row = pieceColour === 'b' ? 0 : 1; return { posX: column * PIECE_WIDTH, posY: row * PIECE_HEIGHT }; } function drawPiece(ctx, pieceName, posX, posY) { const img = pieceCache[pieceName]; if (img.complete) { // 图片已加载完成,直接绘制 ctx.drawImage(img, 0, 0, RAW_WIDTH, RAW_HEIGHT, posX, posY, PIECE_WIDTH, PIECE_HEIGHT); } else { // 图片未加载完成,等待加载后绘制 img.addEventListener('load', () => { ctx.drawImage(img, 0, 0, RAW_WIDTH, RAW_HEIGHT, posX, posY, PIECE_WIDTH, PIECE_HEIGHT); }, { once: true }); } }
方案说明
- 方案1:适合快速修复现有代码,改动最小,能解决当前问题。
- 方案2:采用了更健壮的设计:
- 消除了全局状态,使用局部变量管理位置计算逻辑,避免状态污染。
- 预加载棋子图片,重复使用同一个
<img>元素,避免创建大量DOM元素。 - 使用
once: true确保load事件只触发一次,避免重复绘制。 - 位置计算函数改为纯函数,逻辑更清晰,易于维护和测试。
内容的提问来源于stack exchange,提问作者Psionman
相关产品推荐
相关产品推荐

