JavaScript 异步调用结果赋值问题:聊天APP中帖子未完成头像赋值就被存储
核心问题根因
当前代码的执行逻辑没有适配JavaScript的异步执行机制,具体触发点如下:
getProfileImage是典型的异步回调方法,内部没有同步返回值,直接将它的执行结果赋值给post.profileImage时,拿到的永远是undefined- 异步回调里你修改的是接口返回的原始
element对象的profileImage,不是你手动构造、最终要存入模型的post对象字段,就算异步回调执行完成,模型存储的帖子也不会带上对应头像值 forEach循环不会等待内部的异步逻辑执行完成,会立刻走完所有循环、执行this.onPostListChanged触发视图更新,此时所有头像请求都还在等待数据库/API的响应,传给视图的帖子列表自然是没有头像的- 额外的隐藏bug:
savePosts调用getProfileImage时传入的sid,在当前方法的作用域内没有定义,属于跨作用域引用的错误写法
适配现有回调结构的修复方案
不需要大改现有代码逻辑,只需要通过计数器统计所有头像加载的完成状态,等所有请求都走完之后再执行模型存储、列表更新的操作即可,修改后的代码如下:
class ModelChannel { constructor() { this._posts = [] } savePosts = (response, sid) => { // 显式传入sid,解决之前的变量未定义问题 var json = JSON.parse(response); var post_list = json.posts; const totalPostCount = post_list.length let loadedCount = 0 // 统计已完成头像加载的帖子数 const finalPosts = [] post_list.forEach(element => { // 先构造基础帖子对象,头像字段初始为空 const post = { uid: element.uid, name: element.name, pversion: element.pversion, pid: element.pid, type: element.type, content: element.content, lat: element.lat, lon: element.lon, profileImage: '' // 留空待异步填充 } finalPosts.push(post) this.getProfileImage(sid, element.uid, element, (imageUrl) => { // 把拿到的头像值绑定到对应的post对象上 post.profileImage = imageUrl console.log("Callback getProfileImage"); console.log("element.profile: " + imageUrl); loadedCount++ // 所有帖子的头像都加载完成后,再统一存入模型、触发视图更新 if (loadedCount === totalPostCount) { this._posts = finalPosts this.onPostListChanged(this._posts) console.log("All posts save into model"); console.log(this._posts); } }) }); } getPosts = (channelName, sid) => { // 同步透传sid参数 comunicationController.getChannel(sid, channelName, (response) => { console.log("Call \"getChannel\" succeded"); this.savePosts(response, sid) }) } getProfileImage = (sid, uid, post, callback) => { databaseHandler.getProfile(uid, (response) => { let profileDB try { profileDB = JSON.parse(response); } catch (e) { profileDB = null } // 本地头像版本满足要求,直接读本地缓存 if (profileDB != null && profileDB.pversion >= post.pversion) { console.log("Profile of " + post.name + " loaded from database"); callback(profileDB.picture) return } // 本地缓存失效,从API拉取新头像 comunicationController.getUserPicture(sid, uid, (response) => { var json = JSON.parse(response); var picture = json.picture; // 拉取成功后存入本地数据库 databaseHandler.saveProfileImage(response) callback(picture) }) }) } bindOnPostListChanged(callback) { this.onPostListChanged = callback } }
更易维护的进阶写法(可选)
如果后续还要加更多异步加载逻辑,可以把现有回调风格的异步方法统一包装成Promise对象,通过Promise.all统一等待所有异步任务完成,不需要手动维护计数器,代码可读性会高很多。示例片段:
// 把getProfileImage包装为Promise版本 const getProfileImagePromise = (sid, uid, post) => { return new Promise((resolve, reject) => { this.getProfileImage(sid, uid, post, (imageUrl) => resolve(imageUrl)) }) } // 批量等待所有头像加载完成 const loadAllPosts = async (post_list, sid) => { const postPromises = post_list.map(async (element) => { const imageUrl = await getProfileImagePromise(sid, element.uid, element) return { ...element, profileImage: imageUrl } }) const allPosts = await Promise.all(postPromises) // 所有帖子加载完成后再更新模型 this._posts = allPosts this.onPostListChanged(this._posts) }
注意:不管采用哪种实现方式,核心逻辑必须确保所有异步资源加载完成后,再把完整的帖子列表存入模型并通知视图更新,避免出现头像空值先渲染、后续异步补全的体验问题,也不会触发视图层的重复渲染。
内容的提问来源于stack exchange,提问作者angeloandrea.isola
相关产品推荐
相关产品推荐

