React Native——当 JS 太忙时
让我们一起来看看几行代码是如何导致你的应用无响应的。你始终要记住,即使你编写的是 JavaScript 代码,你的代码仍然是在资源有限的设备上执行的。
我尽量用最简单的方式解释。
目前 React Native 使用 3 个主要线程。
-
JavaScript 线程。这是放置和编译所有 JavaScript 代码的地方。
-
本地线程。这是执行本地代码的地方。
-
影子线程。它是计算应用程序布局的地方。
因此,很明显,原生端和 JavaScript 端需要通信。这是通过桥接器实现的:JavaScript 调用原生方法,并通过桥接器接收方法返回的结果以及来自用户交互的事件。
我们举个例子:用户按下一个按钮。
- 本地代码处理该
onPress事件- 将有效载荷打包,以便通过桥梁发送。
- 发送有效载荷
- JS 代码
- 解包接收到的有效载荷
- 执行绑定代码
🕵️ 要监视桥梁,请将此代码添加到某个地方。
import MessageQueue from "react-native/Libraries/BatchedBridge/MessageQueue";
const spyFunction = (spyData: SpyData) => {
console.log(spyData);
};
MessageQueue.spy(spyFunction);
桥接数据是什么样的?
这是一个从原生端发送到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
]
]
}
我们的应用将无法响应🥶
到达桥接器的 JavaScript 调用数量并不确定,会随时间变化,具体取决于您在应用程序中执行的交互次数。此外,每次调用都需要时间,因为 JavaScript 参数需要被转换为 JSON 格式,这是这两个领域都能理解的既定格式。
例如,当您的桥接器忙于处理数据时,另一个调用将不得不阻塞并等待。如果该交互与手势和动画相关,则很可能出现丢帧——某些操作未执行,导致 UI 出现抖动。
💥 [...] 当你的桥接器忙于处理数据时👈,
这正是我们关注的重点。JS 运行时必须处理来自桥接器的所有传入消息,同时还要运行 JS 应用代码。
那么,如果我们编写的代码非常糟糕,导致 JS 运行时长时间处于忙碌状态,会发生什么呢?
它还能处理来自原生端的事件吗?
不能!它不能。
JavaScript 单线程模型
JavaScript 是一种单线程编程语言。换句话说,JavaScript 在同一时间点只能执行一件事。

它力求做到最好,以最佳性能执行所有代码:事件循环检查是否有要执行的代码(在调用堆栈中)并执行它!
当你编写一段可能在未来才会完成的代码,例如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>
);
};
现在我们引入一些代码来阻塞 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>
);
};
如您所见,我们按下了按钮,但界面在3-4秒后才更新😱
这是因为 JS 运行时正忙于执行我们那段有问题的代码。当它忙于执行这段代码时,用户多次按下按钮,原生端会通过桥接发送这些事件,但这些事件会被放入队列,JS 只有在阻塞任务执行完毕后才能对其进行评估。
这就是为什么你会看到计数器在几秒钟后更新成一行的原因。
能否Promise解决或缓解这个问题?不能。
在 Promise 内部执行阻塞代码与在回调函数末尾执行该代码的效果相同。
const heavyCodeAsync = (): Promise<void> =>
new Promise(resolve => {
heavyCode();
resolve();
});
太长不看
如果在 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();
}
};
哪些情况会导致这种陷阱?
- 进行繁重的计算(例如:对大型数组进行排序、加密、强大的数学运算、图像处理等)
- 生成大量任务
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



