React: RFC澄清:为什么`setState`是异步的?

创建于 2017-11-11  ·  31评论  ·  资料来源: facebook/react

很长一段时间以来,我一直试图理解为什么setState是异步的。 过去未能找到答案,我得出的结论是,这是由于历史原因,现在可能很难改变。 但是@gaearon表示有一个明确的原因,所以我很想知道:)

无论如何,以下是我经常听到的原因,但我认为它们不能说明一切,因为它们太容易被反击

异步渲染需要 Async setState

许多人最初认为这是因为渲染效率。 但我不认为这是这种行为背后的原因,因为保持 setState 与异步渲染同步对我来说听起来微不足道,大致如下:

Component.prototype.setState = (nextState) => {
  this.state = nextState
  if (!this.renderScheduled)
     setImmediate(this.forceUpdate)
}

事实上,例如mobx-react允许对 observable 进行同步赋值,并且仍然尊重渲染的异步性质

需要异步 setState 来知道哪个状态是 _rendered_

我有时听到的另一个论点是,您想对 _rendered_ 的状态进行推理,而不是 _requested_ 的状态。 但我不确定这个原则是否有什么好处。 从概念上讲,我觉得很奇怪。 渲染是副作用,状态是关于事实的。 今天,我 32 岁了,明年我将满 33 岁,无论拥有的组件今年是否能够重新渲染:)。

画一个(可能不太好)平行:如果你在打印之前无法_阅读_你自己写的word文档的最后一个版本,那将是非常尴尬的。 例如,我认为游戏引擎不会向您提供有关游戏的确切渲染状态以及丢弃哪些帧的反馈。

一个有趣的观察:在 2 年里mobx-react没有人问过我这个问题:我怎么知道我的 observables 被渲染了? 这个问题似乎并不经常相关。

我确实遇到过一些与了解呈现哪些数据相关的情况。 我记得的情况是我需要知道某些数据的像素尺寸以进行布局。 但这通过使用didComponentUpdate优雅地解决了,并且也没有真正依赖于setState是异步的。 这些情况似乎非常罕见,以至于几乎没有理由主要围绕它们设计 api。 如果可以以某种方式完成,我认为就足够了


我毫不怀疑 React 团队意识到setState的异步性质经常带来的混乱,所以我怀疑当前语义还有另一个很好的理由。 告诉我更多:)

Discussion

最有用的评论

所以这里有一些想法。 这无论如何都不是一个完整的回应,但也许这仍然比什么都不说更有帮助。

首先,我认为我们同意延迟协调以批量更新是有益的。 也就是说,我们同意setState()同步重新渲染在许多情况下效率低下,如果我们知道可能会得到多个更新,最好批量更新。

例如,如果我们在浏览器click处理程序中,并且ChildParent调用setState ,我们不想重新渲染Child两次,而是更喜欢将它们标记为脏,并在退出浏览器事件之前将它们重新渲染在一起。

您在问:为什么我们不能做完全相同的事情(批处理),而是将setState更新立即写入this.state而不等待对帐结束。 我不认为有一个明显的答案(任一解决方案都有权衡),但这里有一些我能想到的原因。

保证内部一致性

即使state同步更新, props也不是。 (在重新渲染父组件之前,您无法知道props ,如果您同步执行此操作,批处理就会超出窗口。)

现在 React 提供的对象( statepropsrefs )在内部是一致的。 这意味着如果您只使用这些对象,它们肯定会引用一个完全协调的树(即使它是该树的旧版本)。 为什么这很重要?

当您仅使用状态时,如果它同步刷新(如您所建议的那样),则此模式将起作用:

console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2

但是,假设需要解除此状态以在几个组件之间共享,因此您将其移动到父级:

-this.setState({ value: this.state.value + 1 });
+this.props.onIncrement(); // Does the same thing in a parent

我想强调的是,在依赖setState()典型 React 应用程序中,这是您每天都会进行的最常见的一种特定于 React 的重构

然而,这破坏了我们的代码!

console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0

这是因为,在您提出的模型中, this.state会立即刷新,但this.props不会。 并且我们不能在不重新渲染父级的情况下立即刷新this.props ,这意味着我们将不得不放弃批处理(这取决于具体情况,会显着降低性能)。

还有更微妙的情况说明它是如何破坏的,例如,如果您将props (尚未刷新)和state (建议立即刷新)中的数据混合以创建新状态: https://github.com/facebook/react/issues/122#issuecomment -81856416。 参考文献提出了同样的问题: https :

这些例子根本不是理论上的。 事实上,React Redux 绑定曾经有过这种问题,因为它们将 React 道具与非 React 状态混合在一起: https : //github.com/ reactjs /反应-终极版/拉/ 99https://github.com/reactjs/react-redux/issues/292https://github.com/reactjs/redux/issues/1415https://开头github上。 com/reactjs/react-redux/issues/525。

我不知道为什么 MobX 用户没有遇到这种情况,但我的直觉是他们可能会遇到这种情况,但认为他们是他们自己的错。 或者他们可能不会从props读取那么多,而是直接从 MobX 可变对象中读取。

那么今天 React 是如何解决这个问题的呢? 在 React 中, this.statethis.props都只在协调和刷新之后更新,所以你会看到0在重构之前和之后都被打印出来。 这使得提升状态安全。

是的,这在某些情况下会很不方便。 特别是对于来自更多 OO 背景的人来说,他们只想多次改变状态而不是考虑如何在一个地方表示完整的状态更新。 我可以理解这一点,尽管我确实认为从调试的角度来看保持状态更新集中更清晰: https :

尽管如此,您仍然可以选择将立即读取的状态移动到某些横向可变对象中,尤其是当您不将其用作渲染的真实来源时。 这几乎就是 MobX 让你做的事情🙂。

如果您知道自己在做什么,您还可以选择刷新整棵树。 该 API 称为ReactDOM.flushSync(fn) 。 我认为我们还没有记录它,但我们肯定会在 16.x 发布周期的某个时候这样做。 请注意,它实际上强制对调用内部发生的更新进行完全重新渲染,因此您应该非常谨慎地使用它。 这样它就不会破坏propsstaterefs之间内部一致性的保证。

总而言之, React 模型并不总是导致最简洁的代码,但它是内部一致的,并确保提升状态是安全的

启用并发更新

从概念上讲,React 的行为就像每个组件都有一个更新队列。 这就是讨论有意义的原因:我们讨论是否立即对this.state应用更新,因为我们毫不怀疑更新将按照确切的顺序应用。 然而,事实并非如此(哈哈)。

最近,我们一直在谈论“异步渲染”。 我承认我们在传达这意味着什么方面做得不是很好,但这就是研发的本质:你追求一个在概念上看起来很有希望的想法,但只有在花了足够的时间之后才能真正理解它的含义。

我们一直在解释“异步渲染”的一种方式是React 可以根据调用的来源为setState()分配不同的优先级:事件处理程序、网络响应、动画等

例如,如果您正在输入消息,则需要立即刷新TextBox组件中的setState()调用。 但是,如果您在打字时收到一条新消息,最好将新MessageBubble渲染延迟到某个阈值(例如一秒),而不是让打字因阻塞而断断续续线。

如果我们让某些更新具有“较低的优先级”,我们可以将它们的渲染分成几毫秒的小块,这样用户就不会注意到它们。

我知道像这样的性能优化听起来可能不太令人兴奋或令人信服。 你可以说:“MobX 不需要这个,我们的更新跟踪足够快,可以避免重新渲染”。 我不认为在所有情况下都是如此(例如,无论 MobX 有多快,您仍然必须创建 DOM 节点并为新安装的视图进行渲染)。 尽管如此,如果这是真的,并且如果您有意识地决定始终将对象包装到跟踪读取和写入的特定 JavaScript 库中是可以的,那么您可能不会从这些优化中受益。

但异步渲染不仅仅是性能优化。

例如,考虑从一个屏幕导航到另一个屏幕的情况。 通常,您会在渲染新屏幕时显示一个微调器。

但是,如果导航速度足够快(在一秒钟左右),闪烁并立即隐藏微调器会导致用户体验下降。 更糟糕的是,如果您有多个具有不同异步依赖项(数据、代码、图像)的组件,您最终会得到一连串的微调器,它们会一个一个地短暂闪烁。 由于所有 DOM 回流,这在视觉上既不愉快,又会使您的应用程序在实践中变慢。 它也是许多样板代码的来源。

如果当你做一个简单的setState()渲染不同的视图时,我们可以“开始”在“后台”渲染更新的视图,这不是很好吗? 想象一下,无需自己编写任何协调代码,如果更新时间超过某个阈值(例如一秒),您可以选择显示一个微调器,否则当整个新子树的异步依赖项为满意。 此外,当我们在“等待”时,“旧屏幕”保持交互(例如,您可以选择一个不同的项目来过渡到),并且 React 强制要求如果时间太长,您必须显示一个微调器。

事实证明,通过当前的 React 模型和对生命周期的一些调整,我们实际上可以实现这一点! @acdlite过去几周一直在研究此功能,并将很快为其发布RFC

请注意,这是唯一可能的,因为this.state不会立即刷新。 如果它被立即刷新,我们将无法在“旧版本”仍然可见且可交互时开始在后台渲染视图的“新版本”。 它们的独立状态更新会发生冲突。

关于宣布所有这些,我不想从@acdlite 抢风头,但我希望这听起来至少有点令人兴奋。 我知道这听起来仍然像雾器,或者我们真的不知道我们在做什么。 我希望我们能在接下来的几个月里说服你,你会喜欢 React 模型的灵活性。 据我所知,至少在某种程度上,这种灵活性是可能的,这要归功于立即刷新状态更新。

所有31条评论

我们都在等@gaearon

@Kaybarax嘿,周末了 ;-)

@mweststrate哦! 我的错。 凉爽的。

我要在这里说一下,这是因为在同一个滴答声中批处理了多个setState

我下周要去度假,但我可能会在周二去,所以我会尽量在周一回复。

函数入队更新(组件){
确保注入();

// 我们代码的各个部分(例如 ReactCompositeComponent 的
// _renderValidatedComponent) 假设对渲染的调用不是嵌套的;
// 验证是否是这种情况。 (这被每个顶级更新调用
// 函数,如 setState、forceUpdate 等; 创造和
// 顶级组件的销毁在 ReactMount 中受到保护。)

如果(!batchingStrategy.isBatchingUpdates){
batchingStrategy.batchedUpdates(enqueueUpdate, component);
返回;
}

dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}

@mweststrate只需 2 美分:这是一个非常有效的问题。
我相信我们都同意如果 setState 是同步的,那么推理状态会容易得多。
无论使 setState 异步的原因是什么,我不确定 React 团队将其与会引入的缺点进行比较,例如难以推理现在的状态以及给开发人员带来的混乱。

@mweststrate有趣的是我在这里问了同样的问题: https : //discuss.reactjs.org/t/historic-reasons-behind-setstate-not-being-immediately-visible/8487

我个人曾在其他开发人员中看到并在此主题上感到困惑。 @gaearon如果您有时间的

抱歉,今年年底了,我们在 GitHub 等方面有点落后,试图在假期前结束我们一直在做的一切。

我确实打算回到这个线程并讨论它。 但这也是一个不断变化的目标,因为我们目前正在研究异步 React 功能,这些功能与this.state的更新方式和时间直接相关。 我不想花很多时间写一些东西,然后因为基本假设已经改变而不得不重写它。 所以我想保持这个开放,但我还不知道什么时候我能给出一个明确的答案。

所以这里有一些想法。 这无论如何都不是一个完整的回应,但也许这仍然比什么都不说更有帮助。

首先,我认为我们同意延迟协调以批量更新是有益的。 也就是说,我们同意setState()同步重新渲染在许多情况下效率低下,如果我们知道可能会得到多个更新,最好批量更新。

例如,如果我们在浏览器click处理程序中,并且ChildParent调用setState ,我们不想重新渲染Child两次,而是更喜欢将它们标记为脏,并在退出浏览器事件之前将它们重新渲染在一起。

您在问:为什么我们不能做完全相同的事情(批处理),而是将setState更新立即写入this.state而不等待对帐结束。 我不认为有一个明显的答案(任一解决方案都有权衡),但这里有一些我能想到的原因。

保证内部一致性

即使state同步更新, props也不是。 (在重新渲染父组件之前,您无法知道props ,如果您同步执行此操作,批处理就会超出窗口。)

现在 React 提供的对象( statepropsrefs )在内部是一致的。 这意味着如果您只使用这些对象,它们肯定会引用一个完全协调的树(即使它是该树的旧版本)。 为什么这很重要?

当您仅使用状态时,如果它同步刷新(如您所建议的那样),则此模式将起作用:

console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2

但是,假设需要解除此状态以在几个组件之间共享,因此您将其移动到父级:

-this.setState({ value: this.state.value + 1 });
+this.props.onIncrement(); // Does the same thing in a parent

我想强调的是,在依赖setState()典型 React 应用程序中,这是您每天都会进行的最常见的一种特定于 React 的重构

然而,这破坏了我们的代码!

console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0

这是因为,在您提出的模型中, this.state会立即刷新,但this.props不会。 并且我们不能在不重新渲染父级的情况下立即刷新this.props ,这意味着我们将不得不放弃批处理(这取决于具体情况,会显着降低性能)。

还有更微妙的情况说明它是如何破坏的,例如,如果您将props (尚未刷新)和state (建议立即刷新)中的数据混合以创建新状态: https://github.com/facebook/react/issues/122#issuecomment -81856416。 参考文献提出了同样的问题: https :

这些例子根本不是理论上的。 事实上,React Redux 绑定曾经有过这种问题,因为它们将 React 道具与非 React 状态混合在一起: https : //github.com/ reactjs /反应-终极版/拉/ 99https://github.com/reactjs/react-redux/issues/292https://github.com/reactjs/redux/issues/1415https://开头github上。 com/reactjs/react-redux/issues/525。

我不知道为什么 MobX 用户没有遇到这种情况,但我的直觉是他们可能会遇到这种情况,但认为他们是他们自己的错。 或者他们可能不会从props读取那么多,而是直接从 MobX 可变对象中读取。

那么今天 React 是如何解决这个问题的呢? 在 React 中, this.statethis.props都只在协调和刷新之后更新,所以你会看到0在重构之前和之后都被打印出来。 这使得提升状态安全。

是的,这在某些情况下会很不方便。 特别是对于来自更多 OO 背景的人来说,他们只想多次改变状态而不是考虑如何在一个地方表示完整的状态更新。 我可以理解这一点,尽管我确实认为从调试的角度来看保持状态更新集中更清晰: https :

尽管如此,您仍然可以选择将立即读取的状态移动到某些横向可变对象中,尤其是当您不将其用作渲染的真实来源时。 这几乎就是 MobX 让你做的事情🙂。

如果您知道自己在做什么,您还可以选择刷新整棵树。 该 API 称为ReactDOM.flushSync(fn) 。 我认为我们还没有记录它,但我们肯定会在 16.x 发布周期的某个时候这样做。 请注意,它实际上强制对调用内部发生的更新进行完全重新渲染,因此您应该非常谨慎地使用它。 这样它就不会破坏propsstaterefs之间内部一致性的保证。

总而言之, React 模型并不总是导致最简洁的代码,但它是内部一致的,并确保提升状态是安全的

启用并发更新

从概念上讲,React 的行为就像每个组件都有一个更新队列。 这就是讨论有意义的原因:我们讨论是否立即对this.state应用更新,因为我们毫不怀疑更新将按照确切的顺序应用。 然而,事实并非如此(哈哈)。

最近,我们一直在谈论“异步渲染”。 我承认我们在传达这意味着什么方面做得不是很好,但这就是研发的本质:你追求一个在概念上看起来很有希望的想法,但只有在花了足够的时间之后才能真正理解它的含义。

我们一直在解释“异步渲染”的一种方式是React 可以根据调用的来源为setState()分配不同的优先级:事件处理程序、网络响应、动画等

例如,如果您正在输入消息,则需要立即刷新TextBox组件中的setState()调用。 但是,如果您在打字时收到一条新消息,最好将新MessageBubble渲染延迟到某个阈值(例如一秒),而不是让打字因阻塞而断断续续线。

如果我们让某些更新具有“较低的优先级”,我们可以将它们的渲染分成几毫秒的小块,这样用户就不会注意到它们。

我知道像这样的性能优化听起来可能不太令人兴奋或令人信服。 你可以说:“MobX 不需要这个,我们的更新跟踪足够快,可以避免重新渲染”。 我不认为在所有情况下都是如此(例如,无论 MobX 有多快,您仍然必须创建 DOM 节点并为新安装的视图进行渲染)。 尽管如此,如果这是真的,并且如果您有意识地决定始终将对象包装到跟踪读取和写入的特定 JavaScript 库中是可以的,那么您可能不会从这些优化中受益。

但异步渲染不仅仅是性能优化。

例如,考虑从一个屏幕导航到另一个屏幕的情况。 通常,您会在渲染新屏幕时显示一个微调器。

但是,如果导航速度足够快(在一秒钟左右),闪烁并立即隐藏微调器会导致用户体验下降。 更糟糕的是,如果您有多个具有不同异步依赖项(数据、代码、图像)的组件,您最终会得到一连串的微调器,它们会一个一个地短暂闪烁。 由于所有 DOM 回流,这在视觉上既不愉快,又会使您的应用程序在实践中变慢。 它也是许多样板代码的来源。

如果当你做一个简单的setState()渲染不同的视图时,我们可以“开始”在“后台”渲染更新的视图,这不是很好吗? 想象一下,无需自己编写任何协调代码,如果更新时间超过某个阈值(例如一秒),您可以选择显示一个微调器,否则当整个新子树的异步依赖项为满意。 此外,当我们在“等待”时,“旧屏幕”保持交互(例如,您可以选择一个不同的项目来过渡到),并且 React 强制要求如果时间太长,您必须显示一个微调器。

事实证明,通过当前的 React 模型和对生命周期的一些调整,我们实际上可以实现这一点! @acdlite过去几周一直在研究此功能,并将很快为其发布RFC

请注意,这是唯一可能的,因为this.state不会立即刷新。 如果它被立即刷新,我们将无法在“旧版本”仍然可见且可交互时开始在后台渲染视图的“新版本”。 它们的独立状态更新会发生冲突。

关于宣布所有这些,我不想从@acdlite 抢风头,但我希望这听起来至少有点令人兴奋。 我知道这听起来仍然像雾器,或者我们真的不知道我们在做什么。 我希望我们能在接下来的几个月里说服你,你会喜欢 React 模型的灵活性。 据我所知,至少在某种程度上,这种灵活性是可能的,这要归功于立即刷新状态更新。

对 React 架构背后的决策的精彩深入解释。 谢谢。

标记

谢谢你,丹。

我❤️这个问题。 很棒的问题和很棒的答案。 我一直认为这是一个糟糕的设计决定,现在我必须重新思考😄

谢谢你,丹。

我称之为 asyncAwesome setState :smile:

我倾向于认为一切都应该首先实现异步,如果你发现需要同步操作,那么,用等待完成来包装异步操作。 用异步代码制作同步代码(您只需要一个包装器)比反向(基本上需要完全重写,除非您使用线程,这根本不是轻量级)要容易得多。

@gaearon感谢您的

或者他们可能不会从 props 中读取那么多,而是直接从 MobX 可变对象中读取。

我认为这确实是真的,在 MobX 中 props 通常仅用作组件配置,域数据通常不会在 props 中捕获,而是在组件之间传递的域实体中。

再次,非常感谢!

@gaearon感谢您详细而精彩的解释。
虽然这里仍然缺少一些我认为我理解但想确定的东西。

例如,当事件注册为“外部反应”时,这意味着可能通过addEventListener上的引用。 然后不进行批处理。
考虑这个代码:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    this.refBtn.addEventListener("click", this.onClick);
  }

  componentWillUnmount() {
    this.refBtn.removeEventListener("click", this.onClick);
  }

  onClick = () => {
    console.log("before setState", this.state.count);
    this.setState(state => ({ count: state.count + 1 }));
    console.log("after setState", this.state.count);
  };

  render() {
    return (
      <div>
        <button onClick={this.onClick}>React Event</button>
        <button ref={ref => (this.refBtn = ref)}>Direct DOM event</button>
      </div>
    );
  }
}

当我们点击“React Event”按钮时,我们将在控制台中看到:
"before setState" 0
"after setState" 0
当另一个按钮“Direct DOM event”被点击时,我们将在控制台中看到:
"before setState" 0
"after setState" 1

经过一些小的研究和浏览源代码,我想我知道为什么会发生这种情况。
react 不能完全控制事件的流程,也不能确定下一个事件何时以及如何触发,因此作为“恐慌模式”,它只会立即触发状态更改。

你对此有何看法? :思维:

@sag1v虽然有点相关,但为新问题打开一个新问题可能更清楚。 只需在描述中的某处使用 #11527 将其链接到这个。

@sag1v @gaearon在这里给了我一个非常简洁的回复https://twitter.com/dan_abramov/status/949992957180104704 。 我认为他对此的看法也会更具体地回答我。

@mweststrate我想开一个新问题,但后来我意识到这与您的问题“为什么setState异步?”直接相关。
由于此讨论是关于使setState “异步”的决定,所以我想添加何时以及为什么要使其“同步”。
如果我没有说服你我的帖子与这个问题有关,我不介意开一个新问题:wink:

@Kaybarax那是因为你的问题是“_什么时候同步_”而不是“_为什么是同步_”?。
正如我在我的帖子中提到的,我我知道原因,但我想确定并得到官方答复,呵呵。 :微笑:

react 不能完全控制事件的流向,也不能确定下一个事件何时以及如何触发,因此作为“恐慌模式”,它只会立即触发状态更改

有点。 虽然这与更新this.state的问题并不完全相关。

您要问的是 React 在哪些情况下启用批处理。 React目前由 React 管理的事件处理程序内批量更新

如果事件处理程序不是由 React 设置的,当前它会使更新同步。 因为它不知道等待是否安全,以及是否会很快发生其他更新。

在 React 的未来版本中,这种行为将会改变。 该计划是在默认情况下将更新视为低优先级,以便它们最终合并并批处理(例如在一秒钟内),并选择立即刷新它们。 您可以在此处阅读更多信息: https :

惊人的!

这个问题和答案应该记录在更容易到达的地方。 谢谢各位大侠赐教。

学到了很多 。 谢谢

试图将我的观点添加到线程中。 我在基于 MobX 的应用程序上工作了几个月,多年来我一直在探索 ClojureScript 并制作了我自己的 React 替代品(称为 Respo),虽然时间很短,但我在早期尝试过 Redux,我押注于 ReasonML。

结合 React 和函数式编程 (FP) 的核心思想是,您可以获得一段数据,您可以使用任何符合 FP 定律的技能将其渲染为视图。 如果您只使用纯函数,则不会有任何副作用。

React 不是纯函数式的。 通过在组件内部包含本地状态,React 具有与 DOM 和其他浏览器 API(也对 MobX 友好)相关的各种库交互的能力,同时也使得 React 变得不纯。 然而,我在 ClojureScript 中尝试过,如果 React 是纯的,那可能会是一场灾难,因为它真的很难与这么多有副作用的现有库进行交互。

因此,在 Respo(我自己的解决方案)中,我有两个似乎相互矛盾的目标,1) view = f(store)因此不需要本地状态; 2)我不喜欢在全局化简器中编写所有组件 UI 状态,因为这可能很难维护。 最后,我发现我需要一个语法糖,它可以帮助我使用paths维护全局存储中的组件状态,同时我使用 Clojure 宏在组件内部编写状态更新。

所以我学到了什么:本地状态是一个开发人员体验功能,在我们想要的全局状态下,它使我们的引擎能够执行更深层次的优化。 那么,MobX 的人更喜欢 OOP,是为了开发者体验,还是为了引擎?

顺便说一下,我谈到了 React 的未来及其异步特性,以防你错过它:
https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html

丹,你是我的偶像……非常感谢。

此页面是否有帮助?
0 / 5 - 0 等级