小程序里不要用 requestAnimationFrame

小程序里不要用 requestAnimationFrame

输入框一点,键盘弹起来,画面停住。终端里字已经发出去了,要等收起键盘才一下子补上。跟在同一次帧回调里的事一起停:画布合并渲染、列表滚到底、打字机推下一个字。

逻辑层并没有停。setTimeout 照样触发,网络回调也在进。停的是「下一次重绘」------requestAnimationFrame 排的就是这次重绘。

文档里的帧 API 是 Canvas.requestAnimationFrame,基础库 2.7.0 起,说明是「在下次进行重绘时执行」。它挂在某一块 Canvas 上,不是逻辑层的全局时钟。下面键盘把重绘停住、回调一直不来,是真机上终端输入的现象:字符已经进了 buffer,画面要等键盘收起才画出来。换成 setTimeout 之后,键盘弹着也能画。

全局函数在,不代表每帧都会跑

小程序运行时里 typeof requestAnimationFrame !== 'undefined' 经常是 true。写成「有就用 rAF,没有再退回 setTimeout」会稳定地选中 rAF。

这个全局函数不是文档承诺的逻辑层 API。文档里的用法是画布实例上的方法,回调绑在那块画布的下一次重绘上,2D 和 WebGL 还不能混用:

javascript 复制代码
const canvas = res[0].node
canvas.requestAnimationFrame(() => {
  // 只适合这块画布自己的逐帧绘制
})

拿它去调度逻辑层的活------合并多次 setData、把列表滚到底、逐字推进打字机------等于把这些活绑到「画布什么时候重绘」。键盘弹起期间真机上的画布动画回调会停,排进去的刷新就不执行。收起键盘后积压的那一帧才跑,所以看起来像冻住,然后突然补上一截。

开发者工具里这个全局函数也在,模拟键盘不一定会停掉重绘。要在真机上把键盘弹起来看:数据已经到了,画面不动,收起键盘才动。

setTimeout(0) 能跑,但合并不了批次

setTimeout 在键盘弹起时仍会触发,因为页面还在前台,逻辑层线程没有挂起。这和切到后台不一样:切后台之后 JS 线程会停,定时器一起停。键盘只停重绘。

把 rAF 换成 setTimeout(fn, 0) 可以让这一帧不再干等。它合并不了跨消息的批次。

javascript 复制代码
let timer = null

function scheduleFlush() {
  if (timer !== null) return
  timer = setTimeout(() => {
    timer = null
    flush()
  }, 0)
}

timer !== null 只能挡住同一次调用栈里 的重复安排。当前函数返回之后,这条 setTimeout(0) 作为下一个宏任务马上执行,timer 清零。下一条 WebSocket 消息是一次新的任务,又会重新排一次 flush。

所以高频输出几乎是每个分片一次完整渲染。top、tail -f、安装日志、进度条都会打满。rAF 原本的价值是大约 16 毫秒的窗口:窗口里来多少次写入,只画一次。setTimeout(0) 的窗口只覆盖当前这次同步调用,网络分片彼此错过。

窗口按一次刷新有多重来选

定时器只保留一个。窗口内后来的数据写进同一份缓冲,到点读最新的那份,不要每个分片都刷 UI。

一次刷新在做什么 间隔 原因
画布脏行、计算不重 16ms 对齐 60Hz 的一帧。窗口内的写入合成一次重绘
每次都要扫整块缓冲,或顺带一次 setData 33ms 16ms 一次仍会把逻辑层打满。30Hz 足够看清输出
一段不断变长的文本,每次 setData 都过逻辑层到渲染层 200ms 用户不需要 60 帧的日志。只保留最后一份快照
只是「等这次同步代码走完」 0 用来错开时序。不要用它合并网络分片

命令输出适合 200ms 这一档:缓冲里可以堆多次进度,到点只把最后一份 交给 setData。中间快照丢掉,界面不会少字,因为最后一份已经包含前面的内容。

javascript 复制代码
const FLUSH_INTERVAL_MS = 16

function createBatch(flush, intervalMs) {
  let timer = null

  return {
    schedule() {
      if (timer !== null) return
      timer = setTimeout(() => {
        timer = null
        flush()
      }, intervalMs)
    },
    cancel() {
      if (timer === null) return
      clearTimeout(timer)
      timer = null
    },
  }
}

const renderBatch = createBatch(() => {
  paintLatest()
}, FLUSH_INTERVAL_MS)

flush 里读当前缓冲,不要读安排定时器时闭包住的那一截。取消用 clearTimeout。从 rAF 迁过来如果还调用 cancelAnimationFrame,这个 id 清不掉,收起页面之后仍会刷一次。

页面卸载、组件卸掉时要 cancel()。画布用 wx:if 直接卸掉、帧回调还在跑,是另一类问题:回调打到已经销毁的画布上。隐藏画布用样式挪走或 display: none,先停掉定时器再卸。

打字机不要逐字 setData

Web 上用 rAF 每 12 毫秒推进一个字,改的是 DOM,成本低。搬到小程序有两关都过不了。

键盘弹着时,rAF 那一关直接不走。就算改成 setTimeout,逐字 setState 最快大约 22 毫秒一次,每次都是一次 setData。真机上会掉帧,字是一顿一顿出来的。

揭晓效果把整段文字先渲染好,用 clip-path 和 steps() 在合成层上逐格露出,一次 setData 都没有:

css 复制代码
@keyframes type-clip {
  from { clip-path: inset(0 100% 0 0); }
  to { clip-path: inset(0 0 0 0); }
}

.type-line {
  display: inline-block;
  animation-name: type-clip;
  animation-duration: 1.08s;
  animation-timing-function: steps(12, end);
  animation-fill-mode: both;
}

steps(12, end) 适合十几个字的标题。更长的副标题把步数加到和字数接近,例如 30 步、0.9 秒,再加一点 animation-delay,两行就会错开。老基础库不支持 clip-path 时不要留空白,文字直接全显。

流式正文不是这一种动画。内容在变,CSS 揭晓盖不住新增的字。那种要走上面的批量 setData:固定窗口,只提交最新全文,不要每个 token 一次。

判断自己是不是踩中了

真机上点开输入框,看这三件事:

  • 数据或日志已经在更新,画面不动;收起键盘后补上一截。调度挂在 rAF 上。
  • 键盘弹着时画面能跟,但 top、长日志一来就发烫、一卡一卡。多半已经换成了 setTimeout(0),窗口太短。
  • 打字机每个字都顿一下,和键盘无关。逐字 setData。改合成层动画,或把多次更新收成一份再提交。

切到别的 App 再回来才停,是后台挂起 JS 线程,不是这篇文章里的键盘问题。那种定时器本身也不会跑。

相关推荐
9624561 小时前
一次 Kubernetes 临时存储压力引发的小程序授权雪崩:从 Pod 驱逐到定时任务漏执行
容器·小程序·kubernetes
imperialeast18 小时前
WinAXP 2.4.0.830正式版
小程序·个人开发
CRMEB系统商城19 小时前
鲜花零售的预售与积分营销
开发语言·人工智能·科技·小程序·零售
镭封21 小时前
AI文字转语音能制作多人对白吗?角色分配操作指南
人工智能·小程序·音视频·语音识别·媒体
zhongben_keji21 小时前
批发商城APP怎么做:价格引擎、授信台账与资金合规的设计要点
ios·小程序·web app
2501_915921431 天前
iOS 实时日志怎么看?KeyMob 的日志过滤、崩溃报告与符号化
android·ios·小程序·https·uni-app·iphone·webview
小小王app小程序开发1 天前
教育 AI 小程序玩法开发分析
人工智能·小程序
普通人62 天前
uniapp模版JS
开发语言·javascript·小程序·uni-app·1024程序员节
andrsted2 天前
企业培训时间难统一?练题簿让在线免费刷题与培训考核更好安排
学习·微信小程序·小程序·学习方法·刷题