发布于 2026-01-06 2 阅读
0

React Native——当 JS 太忙时

React Native——当 JS 太忙时

让我们一起来看看几行代码是如何导致你的应用无响应的。你始终要记住,即使你编写的是 JavaScript 代码,你的代码仍然是在资源有限的设备上执行的。

5myqp4.jpg

我尽量用最简单的方式解释。

目前 React Native 使用 3 个主要线程。

image.png
来源

  1. JavaScript 线程。这是放置和编译所有 JavaScript 代码的地方。

  2. 本地线程。这是执行本地代码的地方。

  3. 影子线程。它是计算应用程序布局的地方。

因此,很明显,原生端和 JavaScript 端需要通信。这是通过桥接实现的:JavaScript 调用原生方法,并通过桥接器接收方法返回的结果以及来自用户交互的事件。

桥.png

我们举个例子:用户按下一个按钮。

  • 本地代码处理该onPress事件
    • 将有效载荷打包,以便通过桥梁发送。
    • 发送有效载荷
  • JS 代码
    • 解包接收到的有效载荷
    • 执行绑定代码

event.png

🕵️ 要监视桥梁,请将此代码添加到某个地方。



import MessageQueue from "react-native/Libraries/BatchedBridge/MessageQueue";
const spyFunction = (spyData: SpyData) => {
    console.log(spyData);
};
MessageQueue.spy(spyFunction);


Enter fullscreen mode Exit fullscreen mode

桥接数据是什么样的?
这是一个从原生端发送到JS的单个事件。



{
  "type": 0,
  "module": "RCTEventEmitter",
  "method": "receiveTouches",
  "args": [
    "topTouchStart",
    [
      {
        "target": 363,
        "pageY": 552.3333282470703,
        "locationX": 11.666666030883789,
        "locationY": 16.666662216186523,
        "identifier": 1,
        "pageX": 198,
        "timestamp": 12629700.739797002
      }
    ],
    [
      0
    ]
  ]
}


Enter fullscreen mode Exit fullscreen mode

我们的应用将无法响应🥶

摘自《React Native 优化终极指南》

到达桥接器的 JavaScript 调用数量并不确定,会随时间变化,具体取决于您在应用程序中执行的交互次数。此外,每次调用都需要时间,因为 JavaScript 参数需要被转换为 JSON 格式,这是这两个领域都能理解的既定格式。
例如,当您的桥接器忙于处理数据时,另一个调用将不得不阻塞并等待。如果该交互与手势和动画相关,则很可能出现丢帧——某些操作未执行,导致 UI 出现抖动。

💥 [...] 当你的桥接器忙于处理数据时👈,
这正是我们关注的重点。JS 运行时必须处理来自桥接器的所有传入消息,同时还要运行 JS 应用代码。

那么,如果我们编写的代码非常糟糕,导致 JS 运行时长时间处于忙碌状态,会发生什么呢?
它还能处理来自原生端的事件吗?
不能!它不能。

JavaScript 单线程模型

JavaScript 是一种单线程编程语言。换句话说,JavaScript 在同一时间点只能执行一件事。

nodejs-event-loop.png
它力求做到最好,以最佳性能执行所有代码:事件循环检查是否有要执行的代码(在调用堆栈中)并执行它!

当你编写一段可能在未来才会完成的代码,例如setTimeout()函数或发起获取请求(在原生端执行)时,事件循环——由于它必须等待——会将这些未来任务放入队列,然后从堆栈中选择下一个要执行的代码继续执行!这就是为什么你会感觉像是在并行执行代码。

当一段代码执行完毕后,事件循环会检查队列中是否有待处理的未来结果回调函数,并将它们添加到执行栈中。

☠️ 导致应用无响应

你只需要注意如何编写代码,避免任何可能阻塞线程的操作,例如同步网络调用或无限循环。

在前面的段落中,我们了解到 JavaScript 运行时一次只能执行一个任务。当前任务的执行会阻塞其他等待它完成的任务。我们还了解到,那些可能在未来完成的代码(例如网络调用或原生端执行的方法)会被放入一个“等待”队列,并在运行时空闲时进行检查:如果结果已准备就绪,则会执行该代码。

我们还了解到,JS 运行时必须处理所有来自原生端的消息。因此,从本文开头开始。

一段代码会导致我们的应用程序崩溃吗?

是的!原因如下。

如果我们编写一段非常耗时的代码,事件循环就会长时间处于忙碌状态(具体时间取决于任务耗时)。在这段时间内,JavaScript 无法处理其他事件,例如来自原生端的事件。

以下是一个用户与用户界面交互且交互正常流畅的示例。



const initialCounter = 1;
export const Counter = () => {
  const [counter, setCounter] = useState(initialCounter);
  return (
    <Content>
      <Button onPress={() => setCounter(c => c + 1)}>
        <Text>Increase counter</Text>
      </Button>
      <Text>{`counter: ${counter}`}</Text>
      <Button
        primary={true}
        bordered={true}
        onPress={heavyCode}
      >
        <Text>🔥🔥 run heavy code</Text>
      </Button>

      <Button
        primary={true}
        bordered={true}
        onPress={() => setCounter(initialCounter)}
      >
        <Text>reset</Text>
      </Button>
      <Slider minimumValue={0} maximumValue={10} style={{ width: 200 }} />
    </Content>
  );
};


Enter fullscreen mode Exit fullscreen mode

现在我们引入一些代码来阻塞 JS 运行时,这样其他任何任务都无法执行。



const heavyCode = () => {
  let n = 100000000;
  while (n > 0) {
    n--;
  }
};

const initialCounter = 1;
export const Counter = () => {
  const [counter, setCounter] = useState(initialCounter);
  return (
    <Content contentContainerStyle={styles.contentContainerStyle}>
      <Button onPress={() => setCounter(c => c + 1)}>
        <Text>Increase counter</Text>
      </Button>
      <Text>{`counter: ${counter}`}</Text>

      <Button
        primary={true}
        bordered={true}
        onPress={heavyCode}
      >
        <Text>🔥🔥 run heavy code</Text>
      </Button>
      <Button
        primary={true}
        bordered={true}
        onPress={() => setCounter(initialCounter)}
      >
        <Text>reset</Text>
      </Button>
      <Slider minimumValue={0} maximumValue={10} style={{ width: 200 }} />
    </Content>
  );
};


Enter fullscreen mode Exit fullscreen mode

如您所见,我们按下了按钮,但界面在3-4秒后才更新😱

这是因为 JS 运行时正忙于执行我们那段有问题的代码。当它忙于执行这段代码时,用户多次按下按钮,原生端会通过桥接发送这些事件,但这些事件会被放入队列,JS 只有在阻塞任务执行完毕后才能对其进行评估。
这就是为什么你会看到计数器在几秒钟后更新成一行的原因。

能否Promise解决或缓解这个问题?不能。
在 Promise 内部执行阻塞代码与在回调函数末尾执行该代码的效果相同。



const heavyCodeAsync = (): Promise<void> =>
  new Promise(resolve => {
    heavyCode();
    resolve();
  });


Enter fullscreen mode Exit fullscreen mode

太长不看

如果在 JS/TS 中运行阻塞代码,应用程序将会卡顿或延迟:交互输入将无法在预期的时间得到处理。

我该如何避免这种情况?

  • 👍 不要编写阻塞代码
  • 👍 如果无法避免这段代码,请在原生端处理它,然后只返回结果。
  • ☘️充分利用即将推出的React Native架构:JSI + Fabric渲染 + Turbo模块
  • 🔨 如果可能,请在合适的时机运行您的代码InteractionManager.runAfterInteractions
  • 🤮 给你的代码一些喘息时间。这样运行时就可以安排其他任务,例如处理来自原生应用的输入。


const breath = (): Promise<void> =>
  new Promise(resolve => {
    setTimeout(resolve, 20);
  });

const heavyCode = async () => {
  let n = 100000000;
  while (n > 0) {
    n--;
    await breath();
  }
};


Enter fullscreen mode Exit fullscreen mode

哪些情况会导致这种陷阱?

  • 进行繁重的计算(例如:对大型数组进行排序、加密、强大的数学运算、图像处理等)
  • 生成大量任务Promise(大量“异步”任务可能会导致 JS 运行时资源耗尽)
  • 不必要的重新渲染
  • 未优化/糟糕的代码

希望您喜欢这篇文章💪

这是我运行示例的环境。iOS上的JS 运行时是Hermes 。



System:
    OS: macOS 11.4
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 219.67 MB / 32.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 12.13.0 - ~/.nodenv/versions/12.13.0/bin/node
    Yarn: 1.22.11 - /usr/local/bin/yarn
    npm: 6.12.0 - ~/.nodenv/versions/12.13.0/bin/npm
    Watchman: 2021.09.06.00 - /usr/local/bin/watchman
  Managers:
    CocoaPods: 1.10.1 - /usr/local/bin/pod
  SDKs:
    iOS SDK:
      Platforms: iOS 14.5, DriverKit 20.4, macOS 11.3, tvOS 14.5, watchOS 7.4
    Android SDK: Not Found
  IDEs:
    Android Studio: 4.2 AI-202.7660.26.42.7486908
    Xcode: 12.5.1/12E507 - /usr/bin/xcodebuild
  Languages:
    Python: 2.7.16 - /usr/bin/python
  npmPackages:
    @react-native-community/cli: Not Found
    react: 17.0.1 => 17.0.1 
    react-native: 0.64.2 => 0.64.2 
  npmGlobalPackages:
    *react-native*: Not Found


Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/matteoboschi/react-native-when-js-is-too-busy-5fhn