各位版友大家好,想要請教個新手問題,
這幾天在練習串接API,選擇使用pokemon api作出寶可夢圖鑑,
想要加入按照地區分類的功能,按下該地區的button後,
在畫面上呈現出該地區的寶可夢,目前的作法是用id區分,
在button放入該地區最後一隻的id,執行for loop render畫面。
例如關都地區的寶可夢id為1~151
<button type="button" class="kanto btn" pokenum="151">關都</button>
問題:
第一次點擊按鈕,可以按照id順序排列,但多按幾次後卻會順序錯亂,
似乎為非同步的問題? 不知道該如何解決,
想請問有什麼關鍵字或是怎麼改寫比較好呢?
希望有人可以解惑,感謝
const poke_container = document.getElementById("poke_container");
const kanto = document.querySelector(".kanto");
kanto.addEventListener("click", (e) => {
navbarbuttons.classList.remove("active");
poke_container.innerHTML = "";
let pokemons_number = e.target.attributes[2].value;
const fetchPokemons = async () => {
for (let i = 1; i <= pokemons_number; i++) {
await getPokemon(i);
}
};
const getPokemon = async (id) => {
const url = `https://pokeapi.co/api/v2/pokemon/${id}`;
const res = await fetch(url);
const pokemon = await res.json();
createPokemonCard(pokemon);
};
const createPokemonCard = (pokemon) => {
const pokemonEl = document.createElement("div");
pokemonEl.classList.add("pokemon");
const { id, name, sprites, types } = pokemon;
const type = types[0].type.name;
const pokeInnerHTML = `
<div class="img-container">
<img src="${sprites.front_default}" alt="${name}" />
</div>
<div class="info>
<span class="number">${id}</span>
<h3 class="name">${name}</h3>
<small class="type">Type: <span>${type}</span></small>
</div>
`;
pokemonEl.innerHTML = pokeInnerHTML;
poke_container.appendChild(pokemonEl);
};
fetchPokemons();
});
多按幾次順序會亂掉(X
第一次還沒跑完,第二次又加上去了
所以開頭你會看到插隊如下圖
然後看後面又是正常(因為上一次的都跑完了
可以click時的appendChild加到一個新的div
舊的div hide之後再處理
或者看看有什麼別的解法
雖然結案了,但還是提供參考
html 部分如果要自訂屬性的話,建議加上 data-
,之後 javascript 就可以用 dataset 來存取。
以下為範例:
html 部分
<ul class="topnav">
<li class="btn-home"><a href="index.html">Home</a></li>
<li>
<button type="button" class="btn" data-pokenum-start="1" data-pokenum-end="3">關都</button>
</li>
<li>
<button type="button" class="btn" data-pokenum-start="4" data-pokenum-end="5">城都</button>
</li>
</ul>
javascript 部分
const navbarbuttons = document.querySelector(".topnav");
//可以考慮把事件掛載在外層容器統一處理
navbarbuttons.addEventListener("click", (e) => {
if(!e.target.classList.contains('btn')) {
return;
}
navbarbuttons.classList.remove("active");
poke_container.innerHTML = "";
//用 dataset 取得開始與結束的編號,這樣就不用每個按鈕都寫個別的程式碼
let pokemons_number_start = parseInt(e.target.dataset['pokenumStart'], 10);
let pokemons_number_end = parseInt(e.target.dataset['pokenumEnd'], 10);
const fetchPokemons = async () => {
const pokemons = [];
for (let i = pokemons_number_start; i <= pokemons_number_end; i++) {
const pokemon = await getPokemon(i);
pokemons[i] = pokemon;
}
for (let i = pokemons_number_start; i <= pokemons_number_end; i++) {
createPokemonCard(pokemons[i]);
}
};
//以下與原程式碼同
});
我看完先整理一下你的程式碼
首先先抓出共用邏輯,因為你的資料是固定的所以就不用多從html抓參數了,如果是動態的那你每個地區的起始id也是要從html傳
你的問題是for迴圈已經派fetch去抓資料了,但資料還沒抓完又有新迴圈開始跑了
,所以要用Promise.all,把所有資料抓完在一起append、或者是讓等資料回傳在跑下一個迭代。
主流做法是用 Promise.all 把資料都抓完再去走訪,而我使用 for await 來實作,for await 就是達到你想要的抓一次資料 append一次卡片,整體順暢感好很多,可以參考看看
// 取得地區的資料集
const fetchPokemons = async (startId, endId) => {
const range = [];
for (let i = startId; i <= endId; i++) {
range.push(getPokemon(i));
}
// promise all 寫法
// (await Promise.all(range)).map((pokemon) => {
// createPokemonCard(pokemon);
// });
// for await 寫法
for await (let pokemon of range) {
createPokemonCard(pokemon);
}
};