【小程序 setData 的“幽灵更新”:setData 明明调用了,渲染却还是旧数据?JavaScript容易被忽略的细节】

小程序 setData 的“幽灵更新”:setData 明明调用了,渲染却还是旧数据?JavaScript容易被忽略的细节

先给大家看一道题,你觉得这个输出的结果是什么?请打在评论区。
在这里插入图片描述

最近在开发一款包含即时通讯功能的小程序时,我的小程序聊天界面有这样一个需求:用户发起聊天会先发送一张“卡片”(比如商品链接),如果此时输入框里还有文字,那么在卡片发送成功后,这些文字会自动作为一条普通文本消息发送出去。

听起来很简单,对吧?我的思路也很直接:

  1. 聊天记录保存在一个 messages 数组里。
  2. 有一个 sendMessage() 方法负责发消息。 发送后把这条消息状态设置为sending=true ,表示正在发送转圈。
  3. WebSocket 收到发送成功的回调后,再把sending设置为false表示发送成功,如果还有输入内容,就再调用一次 sendMessage() 把文本消息发出去。

一切逻辑看起来没有问题,但实际运行中却发现:

卡片发送出去了,文本消息也成功自动发送了,但是界面上却只显示卡片消息,文本信息“消失”了!但刷新页面就出现了。
我一开始以为是消息覆盖了、websocket 注册了两次、或者 setData 不生效……经过调试发现在sendMessage 里的的时候变量值已经是两条消息了,但是在msgCallBack里的时候又变回一条了,文本消息却消失了!这个问题让我找了两个小时。

经过2个小时的排查终于找到原因

解决这个问题其实非常的简单,其实就是一个异步的问题,但是如果js学的不透彻,根本就没有思路。首先我们先要知道什么是js的事件循环
JavaScript 是一门单线程语言,意味着它一次只能做一件事,但现代应用要同时处理很多任务,比如用户点击、网络请求、定时器等等。这就需要一种机制来“排队”这些任务,这就是事件循环的职责。

事件循环的核心结构如下:

  • 同步任务:最先执行的代码,放在主线程中。
  • 异步任务:比如 setTimeout、Promise、WebSocket消息,这些任务不会立即执行,而是“排队等待”。

再来看上面的模拟题目:

this.list = [];

//发送
const send = (msg) => {
    setTimeout(() => {
        callback(msg)
    }, 0);
    this.list = [...this.list, msg]
    console.log("=========send:", this.list)
}

//回调
callback = (msg) => {
    let data = [...this.list];
    console.log("=========back:", data)
    if (msg == 1) {
        send(2);
    }
    this.list = data;
}

send(1);

正确的输出结果为:
=========send: [ 1 ]
=========back: [ 1 ]
=========send: [ 1, 2 ]
=========back: [ 1 ]




我们看到最后结果第二次回调的时候 数据[1,2] 又变回 [1]了
那么问题出在哪里?

在我的代码中,发送消息是同步执行的,callback回调却是异步执行的(通过 setTimeout 模拟),让我们一步步追踪代码的执行和 this.list 的变化:

  1. this.list 初始化: this.list = []
  2. 调用 send(1): (S1) setTimeout(() => { callback(1); }, 0);callback(1) 被注册为一个宏任务,放入事件队列中。它不会立即执行。
  3. this.list =[…this.list, 1]; 读取当前的 this.list (是 [])。 创建新数组 [1], this.list 被更新为[1]。
  4. (发送 1 后)"):输出: =========send: [ 1 ] 。send(1) 函数执行完毕。
  5. 当前脚本主线程任务执行完毕,事件循环开始工作:调用栈为空。
    事件循环从任务队列中取出之前由 setTimeout 注册的 callback(1) 并推入调用栈执行。
  6. 进入callback(1),因为send(1)的时候更新了this.list。所以同样输出2
  7. (第1次回调):输出=========back: [ 1 ]。
  8. 执行条件判断 if (msg == 1) { send(2)},重新调用send(2)
  9. 调用send(2): (S2) setTimeout(() => { callback(2); }, 0);callback(2) 同样放入事件队列中不会立即执行。
  10. this.list = […this.list, 2],读取当前的 this.list (是 [1])。合并数组后 this.list 被更新为[1,2]。
  11. (发送 2 后)"):输出: =========send: [ 1,2 ] 。send(2) 函数执行完毕。
  12. 这里要特别注意,此时callback(1)并没有执行结束。
  13. 继续执行callback(1):this.list = data; 因为还没有执行结束,当前上下文还是属于callback(1),data还是等于[1],这里this.list=[1]覆盖了之前的值。
  14. 执行callback(2):this.list已经是[1]了
  15. 输出:=========back: [ 1 ]

以上就是整个执行顺序,现在再来看就很简单,只要将回调send(2) 后面加上return就可以了,阻止后面的赋值代码执行就不会导致旧数据覆盖了。

这个问题说难不难,说简单也不简单。它不是那种明显报错、页面崩溃的 bug,而是那种看起来没问题、其实逻辑错位”的陷阱。你看到界面没显示,调试发现数据发了,回调也进了,就是最后数据没了,很容易让人怀疑人生。一个简单的异步问题,往往这种问题最考验开发人员的水平,是真有东西还是假有东西一下就看出来了。

其实这类问题,本质是对 JavaScript 的异步模型不熟悉,尤其是事件循环机制、任务队列执行顺序等理解不到位,就会很难定位。

它虽然只是一个很常见的异步覆盖问题,但它背后涉及的知识面却很宽,比如:

JS 的执行栈和事件循环(Event Loop)

同步和异步的执行时机

闭包与数据快照

状态管理的时序问题

这些知识点,不光在小程序里会踩坑,在 React/Vue/WebSocket、甚至 Node.js 的服务端开发中也都随处可见。

所以别小看这种“小 bug”。越是这种不起眼、没有报错提示的问题,越考验程序员对底层机制的理解和编码的严谨性。

如果你也曾遇到类似“写进去又没了”的数据问题,不妨静下心来,从事件循环的角度重新梳理一遍流程,也许问题并不复杂,只是思路被打乱了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值