前言
在React中数据的流通有多种方式,本文介绍了props
、发布订阅模式、Context API
和redux
。
props
props
可以将父组件中的数据值传递给子组件,这个是单向传递的,即不允许子组件传值给父组件。基于props
单向传递的特性,我们可以实现集中数据交流的模式:
父子组件之间的通信
使用props
父组件将text
值传递给子组件,子组件通过调用props.changeText
的方法,改变父组件建中text
的值,从而改变自身text
的值。
// 父组件
import React from 'react'
import Child from './components/Children'
class App extends React.Component {
state = {
text: '初始化父组件的文本'
}
changeText = (text: string) => {
this.setState({
text
})
}
render() {
return (
<div>
<p>我是父组件</p>
<Child text={ this.state.text }
changeText={ this.changeText } />
</div>
)
}
}
export default App
// 子组件
import React from 'react'
class Child extends React.Component<any, any> {
handleChange = () => {
this.props.changeText('我是子组件')
}
render() {
return (
<>
<div>Child:{ this.props.text }</div>
<button onClick={ this.handleChange }>点击改变props</button>
</>
)
}
}
export default Child
改变前:
改变后:
兄弟组件通信
兄弟组件之间的通信可以化解为父子之间的通信,将值提升到父组件。
// 父组件
import React from 'react'
import Child from './components/Children'
import NewChild from './components/NewChildren'
class App extends React.Component {
state = {
text: '初始化父组件的文本'
}
changeText = (text: string) => {
this.setState({
text
})
}
render() {
return (
<div>
<p>我是父组件</p>
<Child text={ this.state.text } />
<NewChild changeText={ this.changeText } />
</div>
)
}
}
export default App
// 子组件1
import React from 'react'
class Child extends React.Component<any, any> {
render() {
return (
<div>Child:{ this.props.text }</div>
)
}
}
export default Child
// 子组件2
import React from 'react'
class Child extends React.Component<any, any> {
handleChange = () => {
this.props.changeText('NewChildren改变了Children的值')
}
render() {
return (
<button onClick={ this.handleChange }>点击改变children的值</button>
)
}
}
export default Child
这样点击子组件2的按钮,就可以改变子组件1的值了。
除此上述两种传递方式之外, props
并不适合处理其它复杂场景,例如任意层级组件间通信,层层传递 props 不可行,太难维护了。
发布订阅模式
数据通信这件事我们可以联想到,其实就在某人发起了一个动作-我想要修改数据,某人收到了这个动作信号,更改了数据。有点类似于JS中的addEventListener()
。
addEventListener('click', function(){});
当用户点击的时候,就会触发某个回调函数。接下来我们来写一个发布订阅的函数。
发布订阅有两个关键动作:事件监听(订阅)和事件触发(发布):
on
:负责事件的监听,指定事件触发时的回调函数;emit
:负责触发事件,可以传参;off
:为了性能考虑,要移除一下监听器。
class myEventEmitter {
constructor(){
this.eventMap = {}
}
// 监听,每一种动作对应一个回调函数数组
on(type, handler){
if(typeof handler !== 'function'){
throw new Error('handler must be function');
}
if(!this.eventMap[type]){
this.eventMap[type] = [];
}
this.eventMap[type].push(handler);
}
// 触发,触发每一个动作对应的数组中的回调函数
emit(type, params){
if(this.eventMap[type]){
this.eventMap[type].forEach(handler => {
handler(params);
})
}
}
// 移除,移除对应的动作中对应的回调函数
off(type, handler){
if(this.eventMap[type]){
this.eventMap[type].splice(this.eventMap[type].indexOf(handler) >>> 0, 1)
}
}
}
Context API
React 15的Context API由于有多种问题(比如如果中间某一个组件的shouldComponentUpdate
返回了false
,则该组件无法获得context
的值),所以这里只学习React 16的Context API。
由于如果使用props
一层一层传递属性的话,会相当麻烦,所以可以使用Context API
创建一个全局的数据,给子组件共享,避免了通过中间元素传递props
。
-
创建Context:
const MyContext = React.createContext(defalutValue)
; -
将
Context
的值传递下去:<MyContext.Provider value={xxx}
; -
子组件使用
Context
中的数据: `<MyContext.Consumer> {value => /* 基于 context 值进行渲染*/} </MyContext.Consumer>
栗子:
// context.ts
import { createContext } from 'react';
export const MyContext = createContext({} as any);
// 父组件
import React from 'react'
import Child from './components/Children'
import { MyContext } from './utils/Context'
class App extends React.Component {
state = {
text: '初始化父组件的文本'
}
changeText = (text: string) => {
this.setState({
text
})
}
render() {
return (
// @ts-ignore
<MyContext.Provider value={this.state} >
<Child />
</MyContext.Provider>
)
}
}
export default App
// 第一个子组件
import React from 'react'
import SubChildren from './SubChildren'
class Child extends React.Component {
render() {
return (
<>
<SubChildren />
</>
)
}
}
export default Child
// 第二个子组件(内嵌组件)
import React from 'react'
import { MyContext } from '../utils/Context'
class SubChildren extends React.Component<any, any> {
render() {
return (
<div>
<MyContext.Consumer>
{(value: any) => `Hello:${value.text}`}
</MyContext.Consumer>
</div>
)
}
}
export default SubChildren
redux
用过vuex
的应该也能理解redux
,这里给一下redux
的流程图:
用户通过界面组件 触发ActionCreator,携带Store中的旧State与Action 流向Reducer,Reducer返回新的state,并更新界面。
接下来说一下使用:
npm install --save redux
- 创建
actionType.ts
: 存放action
中type
的常量; - 创建
action.ts
: 返回不同的action
; - 创建
reducer.ts
: 根据action
执行不同的操作; - 创建
store.ts
:初始化store
,并绑定reducer.ts
。
// actionType.ts
export const ADDTEXT: string = 'ADDTEXT'
export const CHANGETEXT: string = 'CHANGETEXT'
// action.ts
import { ADDTEXT, CHANGETEXT } from './actionType'
export const addText = (text: string) => ({
type: ADDTEXT,
value: text
})
export const changeText = (text: string) => ({
type: CHANGETEXT,
value: text
})
// reducer.ts
import { ADDTEXT, CHANGETEXT } from './actionType'
const defaultState = {
text: ''
}
function handleText(state = defaultState, action: {[key: string]: any}){
switch(action.type){
case ADDTEXT:
return { ...state, text: action.value };
case CHANGETEXT:
return { ...state, text: action.value };
default:
return state;
}
}
export const finalReducer = handleText
// store.ts
import { createStore } from 'redux'
import { finalReducer } from './reducer'
const store = createStore(finalReducer)
export default store
- 我们在
App.ts
中给text
新增一个值; - 在
SubChildren
中修改text
的值:
// App
import React from 'react'
import store from './store/store'
import { ADDTEXT } from './store/actionType'
import Child from './components/Children'
class App extends React.Component {
componentDidMount(){
// 初始化store的值
store.dispatch({
type: ADDTEXT,
value: 'value from App'
})
}
render() {
return (
<Child />
)
}
}
export default App
// Children(我只是工具人)
import React from 'react'
import SubChildren from './SubChildren'
class Child extends React.Component {
render() {
return (
<>
<SubChildren />
</>
)
}
}
export default Child
// SubChildren
import React from 'react'
import store from '../store/store'
import { CHANGETEXT } from '../store/actionType'
class SubChildren extends React.Component<any, any> {
constructor(props: any){
super(props)
this.state = {
text: ''
}
}
componentDidMount(){
this.setState({
text: store.getState().text
})
// 监听store的值并修改state.text
store.subscribe(() => {
this.setState({
text: store.getState().text
})
})
}
handleChange(){
// 修改store的值
store.dispatch({
type: CHANGETEXT,
value: 'value from SubChildren'
})
}
render() {
return (
<div>
<p>{this.state.text}</p>
<button onClick={this.handleChange}>修改</button>
</div>
)
}
}
export default SubChildren
p.s 我们在react中使用redux
的同时,有时候为了便利,还会搭配使用react-redux
和redux-thunk
:
react-redux
:上面我们发现每个组件使用store
都得引入一遍,但是我们可以使用react-redux
从而直接在App
中使用Provider
注入就会方便很多,还可以使用connect
将UI组件和容器组件结合起来,详情可以查看 React Redux;redux-thunk
: 使用dispatch
的话,只能传入一个action
对象,使用redux-thunk
的话,可以传入一个函数。
总结
- 了解React中的数据通信;
props
是单向数据流,可以实现父子通信、子父通信和兄弟之间的通信,如果要实现其他通信反而会很鸡肋;- 可以使用发布订阅的模式进行多级、平级通信
Context API
: 便于多级嵌套的组件进行通信;redux
:会在全局创建一个store
,当用户在视图层面修改数据的时候,会dispatch
一个action
,store
收到之后传递给reducer
去处理,reducer
将处理好的值传递给store
,store
就会通知视图。
参考
如有错误,欢迎指出,感谢~