We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
随着 2019 年 2 月 React 稳定版 hooks 在 16.8 版本发布,涌现了越来越多的 “hooks 时代” 的状态管理库(如 zustand、jotai、recoil 等),“class 时代” 的状态管理库(如 redux)也全面拥抱了 hooks。无一例外,它们都聚焦于解决 组件通信 的问题,
截至目前,React 中组件间的通信方式一共有 5 种,
React 组件最基础的通信方式是使用 props 来传递信息,props 是只读的,每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它,这里的信息可以是,
className
src
alt
width
height
我们通常在 “父传子” 的通信场景下使用 props,下面是一个 props 通信的例子,
import React, { useState } from "react"; function Parent() { const [count, setCount] = useState<number>(0); return ( <> <button type="button" onClick={() => setCount(count + 1)}>Add</button> <Child count={count}>Children</Child> </> ); } function Child(props) { const { count, children } = props; return ( <> <p>Received props from parent: {count}</p> <p>Received children from parent: {children}</p> </> ); }
callback 回调函数也可以是 props,利用回调,我们也可以实现简单的 “子传父” 场景,
import React, { useState } from "react"; function Parent() { const [count, setCount] = useState<number>(0); return ( <> <div>Received from child: {count}</div> <Child updateCount={(value: number) => setCount(value)} /> </> ); } function Child(props) { const { updateCount } = props; const [count, setCount] = useState<number>(0); return ( <button type="button" onClick={() => { const newCount: number = count + 1; setCount(newCount); updateCount(newCount); }} > Add </button> ); }
此外,如果多个组件需要共享 state,且层级不是太复杂时,我们通常会考虑 状态提升,实现的思路是:将公共 state 向上移动到它们的最近共同父组件中,再使用 props 传递给子组件,你可以点击这个 官方例子 看具体的实现。
通过以上例子我们不难发现,在多级嵌套组件的场景下,使用 props 进行通信是一件成本极高的事情。
为此,React 官方提供了 Context 避免一级级的属性传递,
Context 让父组件可以为它下面的整个组件树提供数据,这在一些特定的场景下非常有用,比如,
下面是一个使用 Context 完成主题切换的例子,
import React, { useState, useContext } from "react"; enum Theme { Light, Dark, } interface ThemeContextType { theme: Theme; toggle?: () => void; } const ThemeContext = React.createContext<ThemeContextType>({ theme: Theme.Light }); function ThemeProvider(props) { const { children } = props; const [theme, setTheme] = useState<Theme>(Theme.Light); const toggle = () => { setTheme(theme === Theme.Light ? Theme.Dark : Theme.Light); }; return ( <ThemeContext.Provider value={{ theme, toggle }}> {children} </ThemeContext.Provider> ); } function App() { const context: ThemeContextType = useContext(ThemeContext); const { theme, toggle } = context; return ( <ThemeProvider> <div> <p>Current theme: {theme === Theme.Light ? "light" : "dark"}</p> <button type="button" onClick={toggle}>toggle theme</button> </div> </ThemeProvider> ); }
需要注意的是,使用 Context 我们需要考量具体的场景,因为 Context 本身存在以下问题,
React.memo
shouldComponentUpdate
此外,对于异步请求和数据间的联动,Context 也没有提供任何 API 支持,如果使用 Context,需要自己做一些封装。除了上述两个通信方案外,基于发布订阅的全局事件总线也是常见一种组件通信方案。
事件总线的本质就是发布订阅,目前有非常多的开源实现(如 miit、tiny-emitter 等),
我们也可以考虑自己实现一个 EventBus,
type Callback = (...args: any[]) => void; class EventBus { private events: Map<string, Callback[]>; constructor() { this.events = new Map(); } on(eventName: string, callback: Callback): void { if (!this.events.has(eventName)) { this.events.set(eventName, []); } this.events.get(eventName).push(callback); } off(eventName: string, callback: Callback): void { if (this.events.has(eventName)) { const callbacks = this.events.get(eventName); const index = callbacks.indexOf(callback); if (index !== -1) { callbacks.splice(index, 1); } } } emit(eventName: string, ...args: any[]): void { if (this.events.has(eventName)) { this.events.get(eventName).forEach((callback) => { callback(...args); }); } } }
EventBus 可以实现跨层级的组件通信,但由于事件的订阅和发布都是在运行时动态绑定的,这会增加代码的复杂度和调试难度。此外,我们通常还需要遵循一定的规范和约定,来更好地管理事件,避免事件名重复或滥用等问题。
使用 ref 可以访问到由 React 管理的 DOM 节点,ref 一般适用以下的场景,
ref
ref 也是组件通信的一种方案,通过 ref 可以获取子组件的实例,以 input 元素的输入值为例,
input
import React, { useRef, useState } from "react"; interface ChildProps { inputRef: React.RefObject<HTMLInputElement>; } const Child: React.FC<ChildProps> = ({ inputRef }) => <input ref={inputRef} />; const Parent: React.FC = () => { const [text, setText] = useState<string>(""); const inputRef = useRef<HTMLInputElement>(null); const handleClick = () => { if (inputRef.current) { setText(inputRef.current.value); } }; return ( <div> <Child inputRef={inputRef} /> <button type="button" onClick={handleClick}>Get Input Value</button> <p>Input Value: {text}</p> </div> ); };
上述的组件通信方案都有各自的使用场景,如果你的项目庞大,组件状态复杂,你可能需要考虑状态管理库。众所周知,React 的状态管理库一直以来都是 React 生态中非常内卷的一个领域,截至目前比较常见的状态管理库包括,
我们可以在 npmtrends 查看这几个状态管理库 近一年的下载量趋势图,
可以看到,Redux 在下载量上依然遥遥领先其他状态管理库,而往年热度仅次于 Redux 的 Mobx,有逐渐被 zustand 超越的趋势,其他的一些 “hooks 时代” 的状态管理库热度也在逐步上升。我们先从 “class 时代” 走过来的老大哥 Redux 说起。
Redux 是一个基于 Flux 架构的一种实现,遵循“单向数据流”和“不可变状态模型”的设计思想,
通过 Action-Reducer-Store 的工作流程实现状态的管理,具有以下的优点,
Action-Reducer-Store
使用 Redux 就得遵循他的设计思想,包括其中的 “三大原则”,
下面是一个使用 Redux 简单的示例,
import React from "react"; import { createStore, combineReducers } from "redux"; import { Provider, useSelector, useDispatch } from "react-redux"; // 定义 action 类型 const INCREMENT = "INCREMENT"; const DECREMENT = "DECREMENT"; // 定义 action 创建函数 const increment = () => ({ type: INCREMENT }); const decrement = () => ({ type: DECREMENT }); // 定义 reducer const counter = (state = 0, action: { type: string }) => { switch (action.type) { case INCREMENT: return state + 1; case DECREMENT: return state - 1; default: return state; } }; // 创建 store const rootReducer = combineReducers({ counter }); const store = createStore(rootReducer); // 定义 Counter 组件 const Counter: React.FC = () => { const count = useSelector((state: { counter: number }) => state.counter); const dispatch = useDispatch(); return ( <div> <h2>Counter: {count}</h2> <button type="button" onClick={() => dispatch(increment())}>add</button> <button type="button" onClick={() => dispatch(decrement())}>dec</button> </div> ); }; // 使用 Provider 包裹根组件 const App: React.FC = () => <Provider store={store}> <Counter /> </Provider>
可以看到,由于没有规定如何处理异步加上相对约定式的设计,导致 Redux 存在以下的一些问题,
虽然 Redux 一致尝试致力解决上述部分问题,比如后面推出的 redux toolkit,但即便如此,对于开发者(尤其是初学者)而言,仍然有比较高的学习成本和心智负担。
相比之下,Mobx 的心智模型更加简单,Mobx 将应用划分为 3 个概念,
其中 Derivations 又分为,
Mobx 的整个工作流程非常简单,首先创建可观察的状态,然后通过 Actions 修改状态,Mobx 会自动更新所有的派生(Derivations),包括计算值(Computed value)以及副作用(Reactions),
如果你选择将 Mobx 结合 React 来用,那同样可以考虑直接使用 Vue,因为 Mobx 的实现基于 mutable + proxy,导致了与 React 结合使用时有一些额外成本,例如,
尤大在知乎的 这个回答 里也提到,一定程度上,React + Mobx 也可以被认为是更繁琐的 Vue。
zustand 是一个轻量级的状态管理库,经过 Gzip 压缩后仅 954B 大小,zustand 凭借其函数式的理念,优雅的 API 设计,成为 2021 年 Star 数增长最快的 React 状态管理库,
与 redux 的理念类似,zustand 也是基于不可变状态模型和单向数据流,区别在于,
zustand 的心智模型非常简单,包含一个发布订阅器和渲染层,工作原理如下,
其中 Vanilla 层是发布订阅模式的实现,提供了setState、subscribe 和 getState 方法,React 层是 Zustand 的核心,实现了 reselect 缓存和注册事件的 listener 的功能,并且通过 forceUpdate 对组件进行重渲染,发布订阅相信大家都比较了解了,我们重点介绍下渲染层。
首先思考一个问题,React hooks 语法下,我们如何让当前组件刷新?
是不是只需要利用 useState 或 useReducer 这类 hook 的原生能力即可,调用第二个返回值的 dispatch 函数,就可以让组件重新渲染,这里 zustand 选择的是 useReducer,
useState
useReducer
const [, forceUpdate] = useReducer((c) => c + 1, 0) as [never, () => void]
有了 forceUpdate 函数,接下来的问题就是什么时候调用 forceUpdate,我们参考源码来看,
// create 函数实现 // api 本质就是就是 createStore 的返回值,也就是 Vanilla 层的发布订阅器 const api: CustomStoreApi = typeof createState === 'function' ? createStore(createState) : createState // 这里的 useIsomorphicLayoutEffect 是同构框架常用 API 套路,在前端环境是 useLayoutEffect,在 node 环境是 useEffect useIsomorphicLayoutEffect(() => { const listener = () => { try { // 拿到最新的 state 与上一次的 compare 函数 const nextState = api.getState() const nextStateSlice = selectorRef.current(nextState) // 判断前后 state 值是否发生了变化,如果变化调用 forceUpdate 进行一次强制刷新 if (!equalityFnRef.current(currentSliceRef.current as StateSlice, nextStateSlice)) { stateRef.current = nextState currentSliceRef.current = nextStateSlice forceUpdate() } } catch (error) { erroredRef.current = true forceUpdate() } } // 订阅 state 更新 const unsubscribe = api.subscribe(listener) if (api.getState() !== stateBeforeSubscriptionRef.current) { listener() } return unsubscribe }, [])
我们首先从第 24 行 api.subscribe(listener) 开始,这里先创建了 listener 的订阅,这就使得任何的 setState 调用都会触发 listener 的执行,接着回到 listener 函数的内部,利用 api.getState() 拿到了最新 state,以及上一次的 compare 函数 equalityFnRef,然后执行比较函数后判断值前后是否发生了改变,如果改变则调用 forceUpdate 进行一次强制刷新。
api.subscribe(listener)
api.getState()
这就是 zustand 渲染层的原理,简单而精巧,zustand 实现状态共享的方式本质是将状态保存在一个对象里,与之相对的是一些原子化的状态管理工具,比如接下来我们要介绍的 recoil。
recoil 是 React Europe 2020 Conference 上 Facebook 官方推出的一个 React 状态管理库,是对 React 内置的状态管理能力的一个补充,recoil 实现的动机如下,考虑到 React 原生状态管理的一些局限性,
在实现上,recoil 定义了一个有向图 (directed graph),正交且天然连结于 React 树上,
看着很高级,其实就是定义原子状态,然后通过原子状态在组件中进行自由地组合和订阅。
recoil 中提供了两个核心方法用于定义状态,
消费状态的方式有 useRecoilState、useRecoilValue、useSetRecoilState 等,用法和 react 的 useState 类似,所以几乎没有上手成本,下面是一个简单的示例可以直观感受下,
import React from "react"; import { RecoilRoot, atom, useRecoilState } from "recoil"; // 定义一个原子 const countState = atom({ key: "countState", default: 0, }); function Counter() { // 获取 countState 的值和修改函数 const [count, setCount] = useRecoilState(countState); const handleIncrement = () => setCount(count + 1); const handleDecrement = () => setCount(count - 1); return ( <div> <h1>Count: {count}</h1> <button onClick={handleIncrement}>+1</button> <button onClick={handleDecrement}>-1</button> </div> ); } function DisplayCount() { // 组件间共享 countState 原子状态 const [count] = useRecoilState(countState); return <h2>Count: {count}</h2>; } function App() { return ( // 使用 RecoilRoot 包裹组件 <RecoilRoot> <Counter /> <DisplayCount /> </RecoilRoot> ); }
与 recoil 类似的原子状态管理库还有 jotai,它们的设计理念基本相同。
recoil 最为人诟病的是原子状态(atom)定义时需要一个唯一的键值 key,这一反人类的设计导致键命名本身就是一个繁琐的任务,为了降低开发成本和心智负担,而 jotai 中定义 atom 不需要指定键值,同时也支持非 Provider 包裹的语法,
jotai 在实现上使用了 Context 和订阅机制相结合,核心的概念只有四个,
getDefaultStore
同样是上面的例子,使用 jotai 改写会简单很多,
import React from "react"; import { atom, useAtom } from "jotai"; // 定义原子状态不需要唯一 key const countAtom = atom(0); function Counter() { const [count, setCount] = useAtom(countAtom); const handleIncrement = () => setCount(count + 1); const handleDecrement = () => setCount(count - 1); return ( <div> <h1>Count: {count}</h1> <button onClick={handleIncrement}>+1</button> <button onClick={handleDecrement}>-1</button> </div> ); } function DisplayCount() { const [count] = useAtom(countAtom); return <h2>Count: {count}</h2>; } function App() { return ( // 不需要 Provider 包裹 <div> <Counter /> <DisplayCount /> </div> ); }
zustand 和 jotai 都来自同一个组织 pmndrs,包括我们接下来要介绍的 valtio。
valtio 是一个基于可变状态模型和 Proxy 实现的状态管理库,核心实现非常简洁,只有两个方法,
import React from "react"; import { proxy, useSnapshot } from "valtio"; // 创建一个状态对象 const state = proxy({ count: 0, }); function Counter() { // 使用 useSnapshot 获取状态快照 const snapshot = useSnapshot(state); return ( <div> <p>Count: {snapshot.count}</p> <button onClick={() => state.count++}>Increment</button> </div> ); }
valtio 和 mobx 都是基于 Proxy 的状态管理库,理念上 valtio 较于 mobx 更为简单和自由。
以上的库都聚焦于状态管理,接下来我们要介绍的是一个专注于 “组件共享状态” 的库。
hox 聚焦于一个痛点:如何在多个组件间共享状态,是一个简单、轻量的状态共享库。
hox 只有一个 API,
createStore 会返回一个数组,包含两个参数,
createStore
import { useState } from "react"; import { createStore } from "hox"; // 使用 createStore 包装一个自定义 hook export const [useTaskStore, TaskStoreProvider] = createStore(() => { const [tasks, setTasks] = useState([]); function addTask(task) { setTasks((v) => [...v, task]); } return { tasks, addTask, }; }); function TaskList() { // 消费状态 const { tasks } = useTaskStore(); return ( <> {tasks.map((task) => ( <div key={task.id}>{task}</div> ))} </> ); } function App() { return ( // Provider 包裹的组件间可以进行状态共享 <TaskStoreProvider> <TaskList /> </TaskStoreProvider> ); }
hox 底层实现是一个基于 React Context 的 singleton 单例,感兴趣的可以点击 这里 上阅读它的源码。
时至 2023 年,对于 React 组件的通信,我们有太多可选的方式,对于选型可以参考以下大致的思路,
本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!
如果有疑问或者发现错误,可以在评论区进行提问和勘误,
如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
随着 2019 年 2 月 React 稳定版 hooks 在 16.8 版本发布,涌现了越来越多的 “hooks 时代” 的状态管理库(如 zustand、jotai、recoil 等),“class 时代” 的状态管理库(如 redux)也全面拥抱了 hooks。无一例外,它们都聚焦于解决 组件通信 的问题,
截至目前,React 中组件间的通信方式一共有 5 种,
props & callback
React 组件最基础的通信方式是使用 props 来传递信息,props 是只读的,每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它,这里的信息可以是,
className
、src
、alt
、width
和height
等我们通常在 “父传子” 的通信场景下使用 props,下面是一个 props 通信的例子,
callback 回调函数也可以是 props,利用回调,我们也可以实现简单的 “子传父” 场景,
此外,如果多个组件需要共享 state,且层级不是太复杂时,我们通常会考虑 状态提升,实现的思路是:将公共 state 向上移动到它们的最近共同父组件中,再使用 props 传递给子组件,你可以点击这个 官方例子 看具体的实现。
通过以上例子我们不难发现,在多级嵌套组件的场景下,使用 props 进行通信是一件成本极高的事情。
Context
为此,React 官方提供了 Context 避免一级级的属性传递,
Context 让父组件可以为它下面的整个组件树提供数据,这在一些特定的场景下非常有用,比如,
下面是一个使用 Context 完成主题切换的例子,
需要注意的是,使用 Context 我们需要考量具体的场景,因为 Context 本身存在以下问题,
React.memo
和shouldComponentUpdate
的对比此外,对于异步请求和数据间的联动,Context 也没有提供任何 API 支持,如果使用 Context,需要自己做一些封装。除了上述两个通信方案外,基于发布订阅的全局事件总线也是常见一种组件通信方案。
Event Bus
事件总线的本质就是发布订阅,目前有非常多的开源实现(如 miit、tiny-emitter 等),
我们也可以考虑自己实现一个 EventBus,
EventBus 可以实现跨层级的组件通信,但由于事件的订阅和发布都是在运行时动态绑定的,这会增加代码的复杂度和调试难度。此外,我们通常还需要遵循一定的规范和约定,来更好地管理事件,避免事件名重复或滥用等问题。
ref
使用
ref
可以访问到由 React 管理的 DOM 节点,ref
一般适用以下的场景,ref
也是组件通信的一种方案,通过ref
可以获取子组件的实例,以input
元素的输入值为例,状态管理库
上述的组件通信方案都有各自的使用场景,如果你的项目庞大,组件状态复杂,你可能需要考虑状态管理库。众所周知,React 的状态管理库一直以来都是 React 生态中非常内卷的一个领域,截至目前比较常见的状态管理库包括,
我们可以在 npmtrends 查看这几个状态管理库 近一年的下载量趋势图,
可以看到,Redux 在下载量上依然遥遥领先其他状态管理库,而往年热度仅次于 Redux 的 Mobx,有逐渐被 zustand 超越的趋势,其他的一些 “hooks 时代” 的状态管理库热度也在逐步上升。我们先从 “class 时代” 走过来的老大哥 Redux 说起。
redux
Redux 是一个基于 Flux 架构的一种实现,遵循“单向数据流”和“不可变状态模型”的设计思想,
通过
Action-Reducer-Store
的工作流程实现状态的管理,具有以下的优点,使用 Redux 就得遵循他的设计思想,包括其中的 “三大原则”,
下面是一个使用 Redux 简单的示例,
可以看到,由于没有规定如何处理异步加上相对约定式的设计,导致 Redux 存在以下的一些问题,
虽然 Redux 一致尝试致力解决上述部分问题,比如后面推出的 redux toolkit,但即便如此,对于开发者(尤其是初学者)而言,仍然有比较高的学习成本和心智负担。
mobx
相比之下,Mobx 的心智模型更加简单,Mobx 将应用划分为 3 个概念,
其中 Derivations 又分为,
Mobx 的整个工作流程非常简单,首先创建可观察的状态,然后通过 Actions 修改状态,Mobx 会自动更新所有的派生(Derivations),包括计算值(Computed value)以及副作用(Reactions),
如果你选择将 Mobx 结合 React 来用,那同样可以考虑直接使用 Vue,因为 Mobx 的实现基于 mutable + proxy,导致了与 React 结合使用时有一些额外成本,例如,
尤大在知乎的 这个回答 里也提到,一定程度上,React + Mobx 也可以被认为是更繁琐的 Vue。
zustand
zustand 是一个轻量级的状态管理库,经过 Gzip 压缩后仅 954B 大小,zustand 凭借其函数式的理念,优雅的 API 设计,成为 2021 年 Star 数增长最快的 React 状态管理库,
与 redux 的理念类似,zustand 也是基于不可变状态模型和单向数据流,区别在于,
zustand 的心智模型非常简单,包含一个发布订阅器和渲染层,工作原理如下,
其中 Vanilla 层是发布订阅模式的实现,提供了setState、subscribe 和 getState 方法,React 层是 Zustand 的核心,实现了 reselect 缓存和注册事件的 listener 的功能,并且通过 forceUpdate 对组件进行重渲染,发布订阅相信大家都比较了解了,我们重点介绍下渲染层。
首先思考一个问题,React hooks 语法下,我们如何让当前组件刷新?
是不是只需要利用
useState
或useReducer
这类 hook 的原生能力即可,调用第二个返回值的 dispatch 函数,就可以让组件重新渲染,这里 zustand 选择的是useReducer
,有了 forceUpdate 函数,接下来的问题就是什么时候调用 forceUpdate,我们参考源码来看,
我们首先从第 24 行
api.subscribe(listener)
开始,这里先创建了 listener 的订阅,这就使得任何的 setState 调用都会触发 listener 的执行,接着回到 listener 函数的内部,利用api.getState()
拿到了最新 state,以及上一次的 compare 函数 equalityFnRef,然后执行比较函数后判断值前后是否发生了改变,如果改变则调用 forceUpdate 进行一次强制刷新。这就是 zustand 渲染层的原理,简单而精巧,zustand 实现状态共享的方式本质是将状态保存在一个对象里,与之相对的是一些原子化的状态管理工具,比如接下来我们要介绍的 recoil。
recoil
recoil 是 React Europe 2020 Conference 上 Facebook 官方推出的一个 React 状态管理库,是对 React 内置的状态管理能力的一个补充,recoil 实现的动机如下,考虑到 React 原生状态管理的一些局限性,
在实现上,recoil 定义了一个有向图 (directed graph),正交且天然连结于 React 树上,
看着很高级,其实就是定义原子状态,然后通过原子状态在组件中进行自由地组合和订阅。
recoil 中提供了两个核心方法用于定义状态,
消费状态的方式有 useRecoilState、useRecoilValue、useSetRecoilState 等,用法和 react 的 useState 类似,所以几乎没有上手成本,下面是一个简单的示例可以直观感受下,
与 recoil 类似的原子状态管理库还有 jotai,它们的设计理念基本相同。
jotai
recoil 最为人诟病的是原子状态(atom)定义时需要一个唯一的键值 key,这一反人类的设计导致键命名本身就是一个繁琐的任务,为了降低开发成本和心智负担,而 jotai 中定义 atom 不需要指定键值,同时也支持非 Provider 包裹的语法,
jotai 在实现上使用了 Context 和订阅机制相结合,核心的概念只有四个,
getDefaultStore
创建默认 Store同样是上面的例子,使用 jotai 改写会简单很多,
zustand 和 jotai 都来自同一个组织 pmndrs,包括我们接下来要介绍的 valtio。
valtio
valtio 是一个基于可变状态模型和 Proxy 实现的状态管理库,核心实现非常简洁,只有两个方法,
valtio 和 mobx 都是基于 Proxy 的状态管理库,理念上 valtio 较于 mobx 更为简单和自由。
以上的库都聚焦于状态管理,接下来我们要介绍的是一个专注于 “组件共享状态” 的库。
hox
hox 聚焦于一个痛点:如何在多个组件间共享状态,是一个简单、轻量的状态共享库。
hox 只有一个 API,
createStore
会返回一个数组,包含两个参数,hox 底层实现是一个基于 React Context 的 singleton 单例,感兴趣的可以点击 这里 上阅读它的源码。
小结
时至 2023 年,对于 React 组件的通信,我们有太多可选的方式,对于选型可以参考以下大致的思路,
写在最后
本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!
如果有疑问或者发现错误,可以在评论区进行提问和勘误,
如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: