如何让函数等待事件发生?
Posted
技术标签:
【中文标题】如何让函数等待事件发生?【英文标题】:How to make the function wait for an event to occur? 【发布时间】:2019-06-19 07:43:53 【问题描述】:我正在制作井字游戏,我的问题是游戏功能不会等待用户选择他想在哪里下棋,它只是在我完成后立即运行 (gameOver) 功能按开始游戏按钮。
谁能告诉我我的代码有什么问题并帮助我修复它?
```
const start = document.getElementById('start');
const table = document.getElementById('table');
places = ["one", "two", "three", "four", "five", "six", "seven", 'eight', "nine"];
let move = 0;
start.addEventListener('click', function()
startGame();
);
function startGame()
console.log("Game started");
user();
function gameOver()
console.log("Game Over");
function computer()
let index = places[Math.floor(Math.random() * places.length)];
let pos = document.getElementById(index);
if (/[1-9]/.test(pos.innerhtml))
pos.innerHTML = "O";
move += 1;
places.splice(places.indexOf(pos), 1 );
if (move > 8)
gameOver();
else
user();
function user()
table.addEventListener('click', function(event)
let pos = event.target;
let Id = event.target.id;
if (/[1-9]/.test(pos.innerHTML))
pos.innerHTML = "X";
move += 1;
places.splice(places.indexOf(Id), 1 );
if (move > 8)
gameOver();
else
computer();
);
```
<div class="col text-center">
<table class="table text-center">
<tbody id="table">
<tr>
<td id="one">1</td>
<td id="two">2</td>
<td id="three">3</td>
</tr>
<tr>
<td id="four">4</td>
<td id="five">5</td>
<td id="six">6</td>
</tr>
<tr>
<td id="seven">7</td>
<td id="eight">8</td>
<td id="nine">9</td>
</tr>
</tbody>
</table>
<br />
<button class="btn btn-primary" type="button" id="start">Start Game</button>
</div>
【问题讨论】:
你不能,如果一个函数正在运行,所有的事件都被阻止触发。 您正在尝试混合异步和同步。在user()
中,您只是添加了一个事件侦听器,该函数会立即返回。这就是为什么您会立即看到 GameOver。您需要另一种方法......也许可以尝试使用window.setTimeout
并检查那里的当前游戏状态
@seasick 我不明白你的意思,如果可以的话,请为我编辑代码。
见developer.mozilla.org/en-US/docs/Web/API/…
下面有一个有效的异步等待示例,但选择的答案却模糊地解释了为什么绑定多个点击处理程序不是一个好主意。我出去过夜了。
【参考方案1】:
主要问题是您创建了一个while循环来运行游戏,并且您有一个“玩家点击”事件是异步的并且不会暂停游戏。
首选方法是让游戏完全异步,不使用 while 循环,并在计算机或玩家每次移动时检查移动计数器。
1) 开始时将移动计数设置为 0
2) 在玩家点击时增加移动计数并最终运行计算机移动或“游戏结束”功能,如果 move_count >8
注意1:电脑移动时记得增加移动次数并检查结束。
注意2:使用此解决方案,玩家将始终先移动。
【讨论】:
【参考方案2】:感谢@Keith 的一些指导,我正在编辑这个答案。
浏览器的用户输入本质上是异步的。也许有一天我们会有一个用于等待事件的 API,但在那之前,我们只能用 Promises 进行修补。
在多个项目中都有用的模式是解决超出其范围的 Promise。在此模式中,解析器类似于延迟。这种模式允许将解析器传送到除了 Promise 构造函数之外的工作单元中。以下是异步等待 DOM 事件的两种方式的比较:
(async () =>
const button1 = document.createElement("button")
button1.innerText = "Resolve it out of scope"
document.body.appendChild(button1)
const button2 = document.createElement("button")
button2.innerText = "Resolve it in scope"
document.body.appendChild(button2)
const remove = button => button.parentNode.removeChild(button);
const asyncResolveOutScope = async () =>
let resolver
button1.onclick = () =>
remove(button1)
resolver()
return new Promise(resolve => resolver = resolve)
const asyncResolveInScope = async () =>
return new Promise(resolve =>
button2.onclick = () =>
remove(button2)
resolve()
)
console.log("Click the buttons, I'm waiting")
await Promise.all([asyncResolveOutScope(), asyncResolveInScope()])
console.log("You did it")
)()
我不确定它对您有多大帮助,但这里有一个使用已交付(解决超出范围)Promise 模式的井字游戏示例:
class game
constructor(name, order=["Computer", "User"])
this.userTurnResolver
this.name = name
this.players = [
name: "Computer", turn: async() =>
const cells = this.unusedCells
return cells[Math.floor(Math.random() * cells.length)]
,
name: "User", turn: async() => new Promise(resolve => this.userTurnResolver = resolve)
].sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name))
this.players[0].symbol = "X"
this.players[1].symbol = "O"
log(...args)
console.log(`$this.name: `, ...args)
get cells()
return Array.from(this.table.querySelectorAll("td")).map(td => td.textContent)
get unusedCells()
return Array.from(this.table.querySelectorAll("td")).filter(td => !isNaN(td.textContent)).map(td => td.textContent)
userClick(e)
const cell = isNaN(e.target.textContent) ? false : parseInt(e.target.textContent)
if (cell && this.userTurnResolver) this.userTurnResolver(cell)
render()
//This would usually be done with HyperHTML. No need for manual DOM manipulation or event bindings.
const container = document.createElement("div")
container.textContent = this.name
document.body.appendChild(container)
this.table = document.createElement("table")
this.table.innerHTML = "<tbody><tr><td>1</td><td>2</td><td>3</td></tr><tr><td>4</td><td>5</td><td>6</td></tr><tr><td>7</td><td>8</td><td>9</td></tr></tbody>"
this.table.onclick = e => this.userClick(e)
container.appendChild(this.table)
async start()
this.render()
this.log("Game has started")
const wins = [
desc: "First Row", cells: [0, 1, 2], desc: "Second Row", cells: [3, 4, 5],
desc: "Third Row", cells: [6, 7, 8], desc: "Diagonal", cells: [0, 4, 8],
desc: "Diagonal", cells: [2, 4, 6], desc: "First Column", cells: [0, 3, 6],
desc: "Second Column", cells: [1, 4, 7], desc: "Third Column", cells: [2, 5, 8]
]
const checkWin = symbol => wins.find(win => win.cells.every(i => this.cells[i] === symbol))
const takeTurn = async (name, turn, symbol, round, i) =>
this.log(`It is $name's turn (round $round, turn $i)`)
const choice = await turn()
this.log(`$name had their turn and chose $choice`)
this.table.querySelectorAll("td")[choice-1].textContent = symbol
return checkWin(symbol)
let win, round = 0, i = 0
while (!win && i < 9)
round++
for (let player of this.players)
i++
win = await takeTurn(player, round, i)
if (win)
this.log(`$player.name is the winner with $win.desc`)
break
if (i===9)
this.log(`We have a stalemate`)
break
this.log("Game over")
(new game("Game 1", ["User", "Computer"])).start();
(new game("Game 2")).start();
(new game("Game 3")).start();
【讨论】:
使用async / await
,非常棒。但这里有一些建议。尽量避免使用延迟,这是一种承诺反模式,尤其是像这样使用时。通过这样做,您还锁定了玩家的顺序,例如。如果您现在希望用户先走,然后是计算机,您现在需要更改更多代码。顺便提一句。我得到的错误是没有先点击start game
。
这里没有比你的回答 Keith 更多的延迟反模式。
***.com/questions/26150232/…
我的代码中没有延迟。您的 userPromise
基本上是在创建延迟。 Deferred 被淘汰是有充分理由的。 developer.mozilla.org/en-US/docs/Mozilla/…
大声笑,你看过提供的答案了吗?有考虑为 ES6 Promise 正式添加此模式。你的“指导”是不必要的。【参考方案3】:
让我们分析一下你的 user() 函数:
const table = document.getElementById('table');
...
function user()
table.addEventListener('click', function(event)
let pos = event.target;
if (/[1-9]/.test(pos.innerHTML))
pos.innerHTML = "X";
player = true;
);
这里的问题是 javascript 是一种高度异步的语言。 addEventListener 函数在执行时添加了事件监听器然后返回,这意味着 user() 函数已经完成。那么这个监听器会在每次点击时触发相应的函数,它不会停止代码并等待点击输入。然后,由于您的代码在一段时间内并且用户函数已完全执行(请记住,它只有一条语句 addEventListener),因此代码完成得相当快。
要解决这个问题,在start函数的开头调用addEventListener,然后把对应的逻辑放在对应的click函数里面。这样,您的代码将仅在用户点击时执行,您可以从那里调用计算机 move 或 gameOver 函数。
【讨论】:
【参考方案4】:Javascript 是一个单线程运行时,因为您所做的很多事情都被称为asynchronous
,比如获得鼠标点击、执行 ajax / fetch 请求。所有这些都不会阻塞Javascript线程,而是使用回调等。
幸运的是,现代 JS 引擎有一个名为 async / await
的功能,这基本上是为了让异步代码看起来是同步的。
下面是你的代码稍作修改,主要的改变是让你的user
函数变成一个promise,这样它就可以是awaited
。
顺便说一句。还有其他错误,例如单击已使用的单元格,但我已将其留在这里,因此人们很容易看到我为使您的脚本运行所做的更改。
const start = document.getElementById('start');
const table = document.getElementById('table');
places = ["one", "two", "three", "four", "five", "six", "seven", 'eight', "nine"];
let i = 1;
let player = true;
start.addEventListener('click', function()
startGame();
);
async function startGame()
while (i <= 9)
if (player === true)
computer();
else
await user();
i++;
gameOver();
function gameOver()
console.log("Game Over");
function computer()
let index = places[Math.floor(Math.random() * places.length)];
let pos = document.getElementById(index);
if (/[1-9]/.test(pos.innerHTML))
pos.innerHTML = "O";
player = false;
function user()
return new Promise((resolve) =>
function evClick(event)
table.removeEventListener('click', evClick);
let pos = event.target;
if (/[1-9]/.test(pos.innerHTML))
pos.innerHTML = "X";
player = true;
resolve();
table.addEventListener('click', evClick)
);
td
border: 1px solid black;
margin: 15px;
padding: 15px;
<div class="col text-center">
<table class="table text-center">
<tbody id="table">
<tr>
<td id="one">1</td>
<td id="two">2</td>
<td id="three">3</td>
</tr>
<tr>
<td id="four">4</td>
<td id="five">5</td>
<td id="six">6</td>
</tr>
<tr>
<td id="seven">7</td>
<td id="eight">8</td>
<td id="nine">9</td>
</tr>
</tbody>
</table>
<br />
<button class="btn btn-primary" type="button" id="start">Start Game</button>
</div>
【讨论】:
嗨 Keith,您对 ES6 Promise 的使用很棒,但只是一些建议。您可以通过将事件侦听器移出到主函数体中来避免添加和删除该事件侦听器。添加它并在每次单击时再次删除它似乎有点愚蠢。您可以通过在其范围之外解决您的 Promise 来实现这一点。 @BlueWater86 不,最好保持原样。这称为关注点分离。从长远来看,它使代码更具可扩展性/可重用性。 IOW:function user()
是自包含的,不需要外部依赖。添加/删除事件侦听器几乎没有/如果有任何性能影响。这是一场胜利/胜利。 :)
@BlueWater86 仅供参考,是我对您的评论投了赞成票There's a working async await sample
。这是因为我相信async / await
路线是正确的。不幸的是,我当时给出的建议似乎侮辱了你,正如你的复制/粘贴模仿似乎暗示的那样。我宁愿不争论什么是最好的。如果您对使用承诺和为您工作的方式感到满意,那就太好了。我会坚持我的方式,你坚持你的。每个人都很高兴。 :)
我的代码比你的缺陷少,所以我很高兴。例如。在单击开始之前单击 sn-p 中的板,然后观察错误出现。接下来让您的代码让玩家先轮到。附言。我的代码中的缺陷让我几乎可以做 -> await user(); computer()
就像我说的,如果你喜欢使用 defer,我很好。不太清楚你为什么这么生气,一开始我就接受了你的答案,但从那以后你似乎变得非常防御。
完全没有防御性。总的来说,上面的代码质量不在这里也不在那里。您对反模式的建议不正确。【参考方案5】:
另一种方法是将游戏创建为可以暂停并等待用户输入的迭代器。
var game; //variable to reference the currently running game
function* newGame() //note the '*' which creates an iterator
while (i <= 9)
if (player === true)
computer();
else
user();
yield i; //pause the method (must return a value)
i++;
gameOver();
function startGame()
game = newGame(); //start the game by creating new iterator
game.next(); //perform first step
function user()
table.addEventListener('click', function(event)
let pos = event.target;
if (/[1-9]/.test(pos.innerHTML))
pos.innerHTML = "X";
player = true;
game.next(); //continue with the game...
);
请注意,按照现在的编写方式,您将分配 4 个不同的点击处理程序。
您还应该调用removeEventListener()
,因为监听器在调用后不会自动清除!
但是一旦游戏开始运行,您就会发现;)。
【讨论】:
以上是关于如何让函数等待事件发生?的主要内容,如果未能解决你的问题,请参考以下文章