点击按钮向数组添加影视数据异常:仅获最后一项数据求助
问题原因
- 异步循环导致DOM渲染与事件绑定不同步:原代码用
forEach(async...)遍历电影列表,异步请求电影详情时循环会继续执行,导致后续querySelectorAll('.watchlist')执行时,大部分电影卡片还未添加到DOM,无法获取全部按钮元素。 - 闭包变量污染:事件回调里的
data变量是循环中的同一变量,异步循环结束后会指向最后一次循环的电影数据,所以点击任何按钮都会获取最后一个电影的信息。 - innerHTML拼接无法关联数据:用字符串拼接HTML的方式,无法直接将按钮与对应的电影数据绑定,只能事后通过DOM查询,容易出现数据不匹配的问题。
解决方案
改用Promise.all统一处理所有异步请求,确保电影数据全部获取后再渲染DOM,同时为每个按钮单独绑定事件,直接关联当前电影的数据:
//api key const apikey = '' //grab DOM elements const searchInput = document.getElementById('input-el') const searchBtn = document.getElementById('button-el') const movieList = document.getElementById('movies') //array of objects that makes up watchlist let watchlist = [] // 页面加载时从本地存储恢复数据 if(localStorage.getItem('watchlist')) { watchlist = JSON.parse(localStorage.getItem('watchlist')) } //determine whether to display add or remove button let addRemoveWatchlist = '' //fetch api data if search input is truthy const fetchMovies = async(searchTerm) => { const URL = `http://www.omdbapi.com/?apikey=${apikey}&s=${searchTerm}` const res = await fetch(`${URL}`) const movies = await res.json() if(movies.Response == 'True') { displayMovieList(movies.Search) } else { movieList.innerHTML = ` <section class='movie-card'> We can't seem to find anything . . . try being more specific </section> ` } } const fetchMoreInfo = async(movieId) => { const URL = `http://www.omdbapi.com/?apikey=${apikey}&i=${movieId}` const res = await fetch(`${URL}`) const moreInfo = await res.json() return moreInfo } //loop through the api data array and display in html const displayMovieList = async(movies) => { movieList.innerHTML = '' // 先批量获取所有电影的详细数据,确保异步操作全部完成 const movieDetails = await Promise.all( movies.map(movie => fetchMoreInfo(movie.imdbID)) ) movieDetails.forEach(data => { // 创建卡片元素 const movieCard = document.createElement('section') movieCard.className = 'movie-card' // 填充卡片内容 movieCard.innerHTML = ` <img src='${data.Poster}' class='movie-img' /> <div class='movie-info'> <section class='section1'> <div class='movie-title'>${data.Title}</div> <div class='movie-year'>${data.Year}</div> <div class='imdb-rating'><i class="fa-solid fa-star"></i>${data.imdbRating}</div> </section> <section class='section2'> <div class='movie-rating'><b>${data.Rated}</b></div> <div class='movie-runtime'>${data.Runtime}</div> <button class='watchlist'>Add to Watchlist</button> </section> <section class='section3'> <div class='actors'><b>Actors:</b> ${data.Actors}</div> <div class='director'><b>Director:</b> ${data.Director}</div> <div class='plot'>${data.Plot}</div> </section> </div> ` // 给当前卡片的按钮绑定点击事件,直接使用当前循环的data const watchBtn = movieCard.querySelector('.watchlist') watchBtn.addEventListener('click', () => { // 将电影的关键信息存入对象,添加到watchlist const movieToAdd = { id: data.imdbID, title: data.Title, year: data.Year, poster: data.Poster, rating: data.imdbRating } // 避免重复添加 if(!watchlist.find(item => item.id === movieToAdd.id)) { watchlist.push(movieToAdd) // 同步到本地存储 localStorage.setItem('watchlist', JSON.stringify(watchlist)) console.log(watchlist) } }) // 将卡片和分隔线添加到页面 movieList.appendChild(movieCard) const hr = document.createElement('hr') movieList.appendChild(hr) }) } //grab search bar input const findMovies = () => { let searchTerm = searchInput.value fetchMovies(searchTerm) } searchBtn.addEventListener('click', findMovies) searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { searchBtn.click() } })
关键改动说明
- 批量异步处理:用
Promise.all+map替代异步forEach,确保所有电影详情都获取完成后再渲染DOM,避免事件绑定时机错误。 - 元素级事件绑定:每个电影卡片创建后,直接获取内部的按钮并绑定事件,此时
data是当前电影的具体数据,不会出现闭包污染问题。 - 本地存储同步:添加了从本地存储恢复数据、同步存储的逻辑,确保页面刷新后watchlist数据不会丢失。
- 避免重复添加:添加电影时检查是否已存在,防止重复存入相同电影。
内容的提问来源于stack exchange,提问作者p_bowles
相关产品推荐
相关产品推荐

