吾辈的博客原文在:https://blog.rxliuli.com/p/a9…,欢送来玩!
场景
吾辈同时是 vue/react 的使用者。
在应用古代前端框架时,咱们常常要要面对的问题之一就是组件之间的通信,目前咱们有着很多抉择,包含但不限于以下这些。
Super Component Props
: 将状态或操作放在父组件,而后传递到子组件。该个性在 vue/react 都存在Context/Provider Pattern
: 将状态放在父组件,而后所有的子组件都能够获取到。例如 react 中的 context 或 vue 中的provide/inject
Global State
: 全局状态管理器。蕴含 redux/vuex/mobx/xstate 等一系列状态管理器。EventEmitter
: 全局事件触发器。蕴含 nodejs 和一些第三方实现。
然而,有了如此多的解决方案,到底该在那些场景下应用那些解决方案呢?
分类
首先,咱们将以上组件通信的解决方案分为两类
解决方案 | 是否在 react 生命周期中 |
---|---|
Super Component Props |
√ |
Context/Provider Pattern |
√ |
Global Store |
× |
EventEmitter |
× |
抉择树
Super Component Props
适宜简略的父子组件通信。参考: 组件 & Props
为什么 props
适宜简略的父子通信呢?因为 props
是框架中根底的父子组件通信形式,模板代码也是起码的。
上面是一个简略的示例:将一个组件内输入框的值渲染到另一个组件中。
const Hello: React.FC<{name: string}> = (props) => {
return (
<section>
<h3>hello {props.name}</h3>
</section>
)
}
const InputName: React.FC<Pick<
InputHTMLAttributes<HTMLInputElement>,
'value' | 'onChange'
>> = (props) => {return <input {...props} />
}
const App = () => {const [name, setName] = useState('')
return (
<div>
<InputName value={name} onChange={(e) => setName(e.target.value)} />
<Hello name={name} />
</div>
)
}
如题,对于这种简略的父子组件传值,应用 props 是最简略适合的。
上面演示应用其余几种形式的实现
应用 context
const HelloContext = React.createContext<{
name: string
setName: (name: string) => void
}>({
name: '',
setName: () => {},
})
const Hello: React.FC = () => {
// 模板代码
const context = useContext(HelloContext)
return (
<section>
<h3>hello {context.name}</h3>
</section>
)
}
const InputName: React.FC = () => {
// 模板代码
const context = useContext(HelloContext)
return (
<input
name={context.name}
onChange={(e) => context.setName(e.target.value)}
/>
)
}
const App = () => {const [name, setName] = useState('')
return (<HelloContext.Provider value={{ name, setName}}>
<InputName />
<Hello />
</HelloContext.Provider>
)
}
能够看到,加的 context 能被深层子组件读取的劣势并未体现进去,反而多了一些模板代码。
应用 global state,此处应用 mobx 进行演示
// 模板代码
class HelloStore {
@observable
name = ''
@action
setName(name: string) {this.name = name}
}
const helloStore = new HelloStore()
const Hello: React.FC = observer(() => {
return (
<section>
<h3>hello {helloStore.name}</h3>
</section>
)
})
const InputName: React.FC = observer(() => {
return (
<input
name={helloStore.name}
onChange={(e) => helloStore.setName(e.target.value)}
/>
)
})
const App = () => {
// 模板代码
useMount(() => {helloStore.setName('')
})
return (
<div>
<InputName />
<Hello />
</div>
)
}
能够看到,store 是全局可用的,但也须要在相应组件内做初始化动作,而非像 props/context 那样受组件生命周期管制,主动的初始化和销毁状态。
const em = new EventEmitter<{update: [string]
}>()
const Hello: React.FC = () => {
// 模板代码
const [name, setName] = useState('')
// 模板代码
useEffectOnce(() => {em.add('update', setName)
return () => {em.remove('update', setName)
}
})
return (
<section>
<h3>hello {name}</h3>
</section>
)
}
const InputName: React.FC = () => {
// 模板代码
const [name, setName] = useState('')
function handleChange(e: ChangeEvent<HTMLInputElement>) {
const value = e.target.value
setName(value)
// 模板代码
em.emit('update', value)
}
return <input name={name} onChange={handleChange} />
}
const App = () => {
return (
<div>
<InputName />
<Hello />
</div>
)
}
能够看到,emitter 能够在不扭转原有组件 (InputName
) 的状况下在新的组件增加相干的逻辑,但的确会多一些模板代码,且须要保护两次状态。
Context/Provider Pattern
适宜深层的父组件共享状态给多个子组件,有时候会联合
EventEmitter
一起应用。
为什么咱们有了 props,甚至有了更弱小的 render props
(vue 中被称为 slot
),却还是须要 context 呢?
思考以下场景,咱们想要为一颗组件树的所有组件增加一些全局个性,例如 theme
、local
、全局配色
,而你应用这些状态的组件又扩散在各个中央时,便能够思考应用 context。相比于全局状态,context 仅与框架而非状态治理库绑定,这对于第三方库(尤其是 UI 组件库)是大有益处的,例如 rc-field-form 和 react-router 均有如此实现。实践上,当咱们须要状态共享但 props 又不须要在组件外操作状态时,就应该首先抉择 context。
上面是一个简略的示例来阐明应用 context 实现全局的 theme 管制。
type ThemeContextType = {color: 'light' | 'black'}
const ThemeContext = React.createContext<ThemeContextType>({color: 'light',})
const Theme = ThemeContext.Provider
const Button: React.FC = (props) => {const context = useContext(ThemeContext)
return (<button className={context.color !== 'black' ? 'light' : 'black'}>
{props.children}
</button>
)
}
const Tag: React.FC = (props) => {const context = useContext(ThemeContext)
return (<span className={context.color !== 'black' ? 'light' : 'black'}>
{props.children}
</span>
)
}
const App = () => {
return (<Theme value={{ color: 'black'}}>
<Button> 按钮 </Button>
<Tag> 标签 </Tag>
</Theme>
)
}
问题
- 仅限于同一个父组件树下的两个子组件共享状态,当然你也能够说所有的组件都只有 单根。
- 无奈在组件内部应用,这点是致命的,例如路由
history
对象弹窗无奈在逻辑层应用是不可承受的(须要在申请接口报 404 时跳转登录页面)。 - 应用时的模板代码要略微更多一点,相比与 mobx 的话。
Global Store
适宜在组件树上相隔较远的组件 / 组件外共享状态和逻辑应用。
那么,持续来看以下场景,当咱们须要在多个组件 / 组件外共享状态时,例如以后登录的用户信息,便应该优先思考应用状态管理器。
interface UserInfo {
id: string
username: string
nickname: string
}
class UserStore {
@observable
userInfo?: UserInfo
refresh(userInfo: UserInfo) {this.userInfo = userInfo}
}
const userStore = new UserStore()
async function post<T>(url: string, data: object) {
const response = await fetch(url, {
method: 'post',
headers: {'Content-Type': 'application/json',},
body: JSON.stringify({
client: {
// 在组件外应用用户信息
uid: userStore.userInfo?.id,
},
data: data,
}),
})
return (await response.json()) as T
}
type UserInfoForm = {username: string; password: string}
const Login: React.FC = () => {const [form, dispatchForm] = useReducer<
Reducer<UserInfoForm, {name: keyof UserInfoForm; value: string}>
>((state, action) => {
return {
...state,
[action.name]: action.value,
}
},
{username: '', password:''},
)
async function handleSubmit(e: FormEvent<HTMLFormElement>) {e.preventDefault()
console.log('handleSubmit:', form)
// 登录时刷新用户信息
const userInfo = await post<UserInfo>('/login', form)
userStore.refresh(userInfo)
}
return (<form onSubmit={handleSubmit}>
<div>
<label htmlFor={'username'}> 用户名:</label>
<input
name={'username'}
value={form.username}
onChange={(e) =>
dispatchForm({
name: 'username',
value: e.target.value,
})
}
/>
</div>
<div>
<label htmlFor={'password'}> 明码:</label>
<input
name={'password'}
type={'password'}
value={form.password}
onChange={(e) =>
dispatchForm({
name: 'password',
value: e.target.value,
})
}
/>
</div>
<div>
<button type={'submit'}> 提交 </button>
</div>
</form>
)
}
const App = observer(() => {
return (
<div>
{/* 在组件中应用 store 的值 */}
<header>{userStore.userInfo?.nickname}</header>
</div>
)
})
问题
- 须要本人治理状态的初始化与清理,不追随组件的生命周期进行变动。
- 全局状态是凌乱的本源,适度应用害人害己
- 无论何时都存在,占用额定的内存资源
EventEmitter
适宜用于逻辑层的状态通信或是组件之间的监听 / 告诉操作,不批改组件状态存储的构造,无奈复用状态。
在不想扭转组件状态 / 操作的代码构造而仅仅只是想要简略的通信时,EventEmitter 是一种适合的形式。构想以下场景,当你曾经写完了一个简单的组件,而忽然 UI/UX 又在另一个相隔很远的中央增加了另一个相干的组件并且须要通信时,在你不想对原组件大刀阔斧的改变时,那么 EventEmitter 是一个适合的抉择。
例如上面这段代码,假如你曾经写完了简单的 MainContent 组件(当然上面代码中的 MainContent 并不算简单),但起初需要变动,想在 Header 组件中增加一个刷新按钮,而不心愿变更代码状态构造的时候,便能够尝试应用 EventEmitter 了。
/**
* 随机数生成器(从 0 开始,不蕴含最大值)* 线性同余生成器
* @link 网上常能见到的一段 JS 随机数生成算法如下,为什么用 9301, 49297, 233280 这三个数字做基数?- 猫杀的答复 - 知乎
https://www.zhihu.com/question/22818104/answer/22744803
*/
export const rand = (function () {let seed = Date.now()
function rnd() {seed = (seed * 9301 + 49297) % 233280.0
return seed / 233280.0
}
return function rand(num: number) {// return Math.ceil(rnd(seed) * number);
return Math.floor(rnd() * num)
}
})()
const Header: React.FC = () => {
return (
<header>
<h2> 题目 </h2>
</header>
)
}
const MainContent: React.FC = () => {const [list, setList] = useState<number[]>([])
function load() {
setList(Array(10)
.fill(0)
.map(() => rand(100)),
)
}
useMount(() => {load()
})
return (
<section>
<ul>
{list.map((i) => (<li key={i}>{i}</li>
))}
</ul>
</section>
)
}
const App = () => {
return (
<div>
<Header />
<MainContent />
</div>
)
}
应用 EventEmitter 进行告诉,其中的 useEventEmitter hooks 来源于 应用 React Hooks 联合 EventEmitter。
type RefreshEmitterType = {refresh: [] }
const Header: React.FC = () => {const { emit} = useEventEmitter<RefreshEmitterType>()
return (
<header>
<h2> 题目 </h2>
{/* 不同,增加触发操作 */}
<button onClick={() => emit('refresh')}> 刷新 </button>
</header>
)
}
const MainContent: React.FC = () => {const [list, setList] = useState<number[]>([])
const load = useCallback(() => {
setList(Array(10)
.fill(0)
.map(() => rand(100)),
)
}, [])
useMount(load)
// 不同,增加监听
const {useListener} = useEventEmitter<RefreshEmitterType>()
useListener('refresh', load)
return (
<section>
<ul>
{list.map((v, i) => (<li key={i}>{v}</li>
))}
</ul>
</section>
)
}
const App = () => {
return (
// 不同
<EventEmitterRC>
<Header />
<MainContent />
</EventEmitterRC>
)
}
问题
- 须要本人治理事件的注册和清理,不追随特定组件的生命周期变动。
- 无论何时都存在,占用额定的内存资源(但相比于全局状态占用的依然是非常低的)
- 使用不当可能导致多个组件由反复的状态
论断
一种解决方案的劣势可能是另一种计划的劣势,总是要抉择适合的计划才是最好的。