React 组件通信

193 阅读11分钟

简介:
1、本文是个人课上学习内容的总结和梳理,主要知识点来自于网课以及React官方文档、掘金社区。
2.目前尚处于 React 乃至前端入门阶段,因此很多内容理解的不是很透彻,文章更多是用来学习记录而非干货分享。

React 组件间通信

父子组件通信

若不借鉴第三方库,原生React是只支持单向数据流的:父级可以向子级通过props的方式传递state和callback函数,而子级不能向父级发送事件或者修改父级的props。因此,若子级想要修改父级组件的状态state,只能通过父级传递下来的callback函数进行回调触发。

父组件向子组件通信

父组件:父组件为两个按钮,用来选中切换Item的信息,其中引用子组件。
子组件:使用props属性接收传递来的数据,进行render

类组件
import React from 'react' 
class Parent extends React.Component{
	constructor(props){
  	super(props);
  	this.state = {item:{content:'jsx'}}
  };
  changeItem(e){
     this.setState({item:{content:e}});
  };
  
  render(){
  	let {item} = this.state;
      return (
  		<div>    
  			<button onClick={this.changeItem.bind(this,'jsx')}>item1</button> 
  			<button onClick={this.changeItem.bind(this,'li')}>item2</button> 
              <Child item={item} />
          </div>
       )
  }
}

class Child extends Component {
render() {
  return <div> content: {this.props.item.content} </div>;
}
}
Function组件
import React,{useState} from 'react'
function Parent(){
    const [item,setItem] = useState({content:'jsx'})
    return (
		<div>    
        	<button onClick={() => setItem('jsx')}>item1</button> 
			<button onClick={() => setItem('li')}>item2</button> 
            <Child item={item} />
        </div>
    )
}

function Child(){
   render() {
   	return <div> content: {this.props.item.content} </div>;
  } 
}

子组件向父组件通信

父组件:接收子组件的callback 函数传递会数据
子组件:两个按钮,用来选中callbackh回调

类组件
import React from 'react' 
class Parent extends React.Component{
   constructor(props){
   	super(props);
   	this.state = {item:{content:'jsx'}}
   };
   changeItem(e){
      this.setState({item:{content:e}});
   };
   render(){
   	let {item} = this.state;
       return (
       	<div> 
           	<div> content: {item.content} </div>
               <Child callback={value =>this.changeItem({content:value})} />
           </div>
       )
     }
}

class Child extends Component {
 	render() {
   	return (
   		<div> 
   			<button onClick={this.props.callback('jsx')}>item1</button> 
   			<button onClick={this.props.callback('li')}>item2</button> 
           </div>
       )
 }
}
Fuction组件
import React,{useState} from 'react'
function Parent(){
    const [item,setItem] = useState({content:'jsx'})
    return (
		<div>    
    		<div> content: {item.content} </div>
            <Child callback={value =>setItem({content:value})} />
        </div>
    )
}

function Child(){
   render() {
    return  (
				<div> 
    				<button onClick={this.props.callback('jsx')}>item1</button> 
					<button onClick={this.props.callback('li')}>item2</button> 
               </div>
             )
  } 
}

兄弟组件通信

若不借鉴第三方库,原生React是只支持单向数据流的:通过父组件作为桥梁,来让两个组件之间通信(主模块模式)。

类组件
import React from 'react' 
class Parent extends Component {
  constructor(props) {
      super(props);
      this.state = {
          title:'jsx'
      }
  }
  onChange(title){
      this.setState({title})
  }
  render() {
      const {title} = this.state;
      return (
          <div>
              <Child1 title={title} onChange={value =>this.onChange(value)}>
              <Child2 title={title} onChange={value =>this.onChange(value}>
          </div>
      );
  }
}
                  
class Child extends Component {
render() {
  return (
  			<div> 
          		{this.props.title} 
             </div>
           )
}
}
Function组件
import React,{useState} from 'react'
function Parent(){
  const [title,setTitle] = useState('jsx')
  return (
              <div>
                  <Child1 title={title} onChange={value =>setTitle(value)}>
                  <Child2 title={title} onChange={value =>setTitle(value}>
              </div>
           )
}

function Child(){
 render() {
  return  (
  			<div> 
  				{this.props.title} 
             </div>
           )
} 
}

爷孙组件通信

1、利用props和callback函数,由父组件传递给子组件,一层层传递下去。
2、利用发布者与订阅者模式(context), 进行共享一些数据。

//context.js
import React from 'react'
export default const LabelContext = React.createContext({content:'jsx li love li'});
export default const LabelProvider = LabelContext.Provider;
export default const LabelConsumer = LabelContext.Consumer;

//App.jsx
import React from 'react';
import { LabelProvider } from './context';
import LabelParent from './pages/label';
export default class App extends Component {
  construltor(props){
      super(props);
      this.state = {context:{content:'jsx li love li'}}
  }
  render() {
      const {context} = this.state;
      return (
          <LabelProvider value={context}>
      		<LabelParent />
    		</LabelProvider>
      )
  }
}
类组件
//parent.jsx ---consumer
import { LabelConsumer } from '../../context'
export default class LabelParent extends Component {
   render() {
       return (
           <div>
               <LabelConsumer>
                 {labelContext => <div>{labelContext.content}</div>}
               </LabelConsumer>
               LabelParent
           </div>
       )
   }
}

//parent.jsx ---contextType
import { LabelContext } from '../../context'
export default class LabelParent extends Component {
   static contextType = LabelContext
   render() {
       return (
           <div>
               {this.context.content}
               LabelParent
           </div>
       )
   }
}
Fuction组件
import React ,{useContext} from 'react';
import { LabelContext } from '../../context';
export default function LabelParent (){
   const {content} = useContext(LabelContext)
   return (
       <div>
           {content}
         Scene  
       </div>
   )
}

任意组件通信

总共有三种办法:共同祖先法、消息中间件、状态管理

共同祖先法

只要找到两个组件的共同祖先,从而转化为通过共同祖先之间通信。

消息中间件

利用观察者模式,将两个组件之间的耦合解耦成了组件与消息中心+消息名称的耦合,但为了解耦却引入全局消息中心和消息名称,消息中心对组件的入侵性很强和第三方组件通信不能使用这种方式。

class EventEimtter{
    constructor(){
        this.eventMap = {};
    }
    sub(name,cb){
        const eventList = this.eventMap[name] = this.eventMap[name] || [];
        eventList.push(cb)
    }
    pub(name,...data){
        (this.eventMap[name] || []).forEach(cb =>cd(...data))
    }
}
//全局消息工具
const ec = new EventEimtter();

class ElemComp1 extends Component{
    constructor(){
        //订阅消息
        ec.sub('Label_update',()=>{console.log('jsx')})
    }
}

class ElemComp2 extends Component{
     constructor(){
        //发布消息
        ec.pub('Label_update'})
    }
}

状态管理(Redux)

状态管理目前有Redux,React-Redux

基本Redux架构

Redux在单向数据流的基础上强调三个基础原理:首先,唯一数据源,即应用的状态数据应该只存在唯一一个Store上。整个应用只保持一个Store,所有组件的数据源就是Store上的状态,每个组件往往只是用树形对象上的一部分数据。然后,保持状态只读。最后,数据改变只能通过纯函数完成。

├── actionCreator
│   └── index.js
├── actionTypes
│   └── index.js
├── index.js
├── reducers
│   └── index.js
├── store
│   └── index.js
└── views
  ├── ControlPanel.js
  ├── Counter.js
  └── Summary.js

actionTypes 用于定义Action类型,通常暴露一下常量给组件使用,比如Action类型:增加、减少

export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'

reducers 即处理action的纯函数,通过传入action对象以及旧的state,返回新的state

import * as actionTypes from '../actionTypes'
export default (state,action) =>{
  const {cunterCaption} = action
  switch(action.type){
      case actionTypes.INCREMENT:
          return {...state,[cunterCaption]:state[cunterCaption]+1}
      case actionTypes.DECREMENT:
          return {...state,[cunterCaption]:state[cunterCaption]-1}
      default:
          return {...state}
  }
}

actionCreator 简单返回一个action.js对象

import * as actionTypes from '../actionTypes'
export default {
  increment:(caption)=>{
      return {
          type:actionTypes.INCREMENT,
          cunterCaption:caption
      }
  },
   decrement:(caption)=>{
      return {
          type:actionTypes.DECREMENT,
          cunterCaption:caption
      }
  }
}

store 只是传入初始化的数据和处理Action的纯函数

import { createStore } from 'redux'
import reducer from '../reducers'
const initValues = {
   'num':0,
   'amout':10,
   'profit':2
}
export default createStore(reducer,initValues)

多个reducer的情况
1、reducerA 影响的state只是store上的一个局部状态(index),无法触及到tag,scene
2、dispatch传递action对象过来后,store无法智能选择具体相关的reducer去执行,只会将所有reducer函数全部执行一遍,然后组装成一个完整的store,所以可能触发一些无用功的reducer
3、actionTypes 设置字符串,过于简单话,容易导致原本不相干的reducer处理了action ,因此,最好加前缀来区分
4、store.subscribe 无法智能区分哪些数据发生变化,只会将所有回调统统执行一遍返回新的值\

import { createStore, combineReducers } from 'redux'
import reducerA from './reducers/reducerA'
import reducerB from './reducers/reducerB'
import reducerC from './reducers/reducerC'
const initValues = {
 'num':0,
 'amout':10,
 'profit':2
}
const reducers = combineReducers({
 'index':reducerA,
 'tag':reducerB,
 'scene':reducerC
})

export default createStore(reducers,initValues)

view层中: 若要用到数据,通过调用store.getState函数 ,获取当前数据
若要修改数据,通过调用store.dispatch 分发action
在组件加载完成后,通过store.subscribe去订阅数据变化的回调函数,一旦数据发生变化,就触发回调同步刷新局部变量

import store from '../store'
import actionCreator from '../actionCreator'

export default class Counter extends Component{
  getOwnState(){
      return {
          value:store.getState()[this.props.caption]
      }
  }
  clickChange = (isIncrement) =>{
      const {caption} = this.props
      if(isIncrement){
          store.dispatch(actionCreator.increment(caption))
      }else{
          store.dispatch(actionCreator.decrement(caption))
      }
  }
  updateCounter = ()=>{
      this.setState({
          ...this.getOwnState()
      })
  }
  componentDidMount(){
      store.subScribe(this.updateCounter)
  }
}

整个流程:点击了增加按钮,通过 actionCreator.increment 返回一个 JS 对象,将它传递给 store.dispatch 分发出去,这时候交给 store 的 reducers 纯函数处理,通过 store.getState() 获取当前状态,以及 action 对象,返回一个新的 state,之后再调用 subscribe 的回调函数,将 store 上的变量映射同步更新到局部变量,局部变量通过 setState即可更新视图

改进Redux架构

1,store在每个需要数据的页面都需要引入,显得不够华丽优雅:通过context发布者/订阅者模式 ,将store 挂载到this.contenxt.store上
只是将store 通过从传入的props中转化为context的属性;设置Provider.childContextTypes,否则可能无法生效

//包装Provider组件,用于将store转化到context上
import {Component} from 'react'
import PropTypes from 'prop-types'

class Provider extends Component{
   getChildContext(){
       return {
           store:this.props.store
       }
   }
   render(){
       return this.props.children
   }
}

Provider.propTypes = {
   store:propTypes.object.isRequired
}
Provider.childContextTypes = {
   store:PropTypes.object
}

export default Provider

将store从根引入,传递给Provider对象,转化为this.context属性,以后需要用到store数据,只要引用this.context 即可

import React from 'react'
import { render } from 'react-dom'
import Provider from './Provider'
import ControlPanel from './views/ControlPanel'
import store from './store'

render(){
    <Provider store={ store } >
       <ControlPanel />
   </Provider>,
   document.getElementById('root')
}

2,由于将全局变量到局部变量的映射的逻辑跟视图组件耦合在一起,不利于维护:需要拆分成纯函数组件和容器组件

纯函数组件:没有react生命周期的事件,通过props传递的值进行渲染 jsx const Counter = (props) => { const { caption, clickChange, value } = props return ( <div> <input type='button' value='-' onClick={ () => clickChange(false) } /> <input type='button' value='+' onClick={ () => clickChange(true) } /> <span> { caption } Count: { value } </span> </div> ) } 容器组件:将所有包含全局变量到局部变量的映射的逻辑,若本身有局部state,将本身的state转化为props传递给 纯函数组件进行渲染

class CounterContainner extends Component{
    updateCounter(){
        this.setState({this.getOwnState()})
    }
    componentDidMount(){
        this.context.store.subscribe(this.updateCounter)
    }
    render(){
        return (
           <Counter
               caption={this.props.caption}
               clickChange={this.updateCounter}
               value={this.state.value}
           />
        )
    }
}
CounterContainer.contextTypes = {
    store: PropTypes.object
}

最终Redux架构

减少重复写类似的容器组件,增加一个connect 高阶组件,用于构建一个通用的容器组件

const map = new WeakMap()
export const connect = (mapStateToProps,mapDispatchToProps) =>{
  return (WrappedComponent) =>{
      const HOOCCompnent = class extends Componet{
          constructor(...args){
              super(...args)
              this.state = {}
          }
          onchange = () =>{
              this.setState({})
          }
          componentDidMount(){
              this.context.store.subscribe(this.onchange)
          }
          //shouldComponentUpdate 通过判断当前state跟之前的是否发生变化,决定是否进行vdon diff 进行重新渲染
          shouldComponentUpdate(nextProps, nextState) {
              return map.get(this).value !== mapStateToProps(this.context.store.getState(), nextProps).value
          }
          render(){
              const store = this.context.store
              const stateToProps = mapStateToProps(store.getState(),this.props)
              const newProps = {
                  ...this.props,
                  ...stateToProps,
                  ...mapDispatchToProps(store.dispatch,this.props)
              }
              //shouldComponentUpdate 通过判断当前state跟之前的是否发生变化,决定是否进行vdon diff 进行重新渲染
              map.set(this, stateToProps)
              return <WrappedComponent {...newProps} />
          }
      }
      HOOCCompnent.contextTypes = {
          store:PropTypes.object
      }
      return HOOCCompnent
  }
}

整个流程:WrappedComponent 即传递的进来的纯函数组件,connect 接收两个参数:状态映射以及函数映射。代码的核心在于 store.subscribe 的onChange 函数,通过 setState({}) 从而触发组件重新执行 render 函数进行 vdom 的diff, mapStateToProps 函数执行后返回的便是最新全局变量返回的局部映射,然后将新的值通过 props 的方式传递给WrappedComponent 纯函数组件,从而引起重新渲染。

const Counter = ({caption,value,increment,decrement}) =>{
   return (
       <div>
           <input type='button' value='-' onClick={ increment  } />
           <input type='button' value='+' onClick={ decrement} />
           <span> { caption } Count: { value } </span>
       </div>
   )
}

const mapStateToProps = (state,ownProps) =>{
   return {
       caption:ownProps.caption,
       value:state[ownProps.caption]
   }
}

const mapDispatchToProps = (dispatch, ownProps) => {
   return {
       increment: () => {
           dispatch(actionCreator.increment(ownProps.caption))
       },
       decrement: () => {
           dispatch(actionCreator.decrement(ownProps.caption))
       }
   }
}

export default connect(mapStateToProps,mapDispatchToProps)(Counter)

React-Redux

已经将Provider ,connect 实现好了

import {Provider} from 'react-redux'
……
render(){
  <Provider store={ store } >
     <ControlPanel />
 </Provider>,
 document.getElementById('root')
}

import React from 'react'
import { connect } from 'react-redux'
export default connect(mapStateToProps,mapDispatchToProps)(Counter)

状态管理(Mobx)

Mobx 将变量进行双向绑定,不要setState

import {observer} from 'mobx-react'
import {observable} from 'mobx'

@observer
class Timer extends Component{
   @observable secondsPassed = 0
   
   componetWillMount(){
       setInterVal(() =>{
           this.secondsPassed++
       },1000)
   }
   render(){
       return (
           <span>
           Seconds:{this.secondsPassed}
           </span>
       )
   }
}
React.render(<Timer />, document.body)

Mobx 架构目录

.
├── index.js
├── store
│   ├── CounterStore
│   │   └── index.js
│   └── index.js
└── views
   ├── ControlPanel.js
   ├── Counter.js
   └── Summary.js

CounterStore 减少一大堆回调函数来更新视图

import {observable,computed,action} from 'mobx'
class CounterStore {
   @observable counters = {
       'num':0,
       'amout':10,
       'profit':2
   }
   @computed get totalValue(){
       let total = 0
        for(let key in this.counters) {
           if(this.counters.hasOwnProperty(key)) {
               total += this.counters[key]                
           }
       }
       return total
   }
   @computed get dataKeys(){
       return Object.keys(this.counters)
   }
   @action changeCounter = (caption,type) =>{
       if(type === 'increment') {
           this.counters[caption]++            
       }else {
           this.counters[caption]--
       }
   }
}
const counterStore = new CounterStore()
export default counterStore
export { counterStore }

store 一样从根注入

import React from 'react'
import { render } from 'react-dom'
import ControlPanel from './views/ControlPanel'
import * as stores from './store'
import { Provider } from 'mobx-react'

render(
    <Provider { ...stores }> 
        <ControlPanel />
    </Provider>,
    document.getElementById('root')
)

当组件需要用到某个store 需要通过inject注入

import React, { Component } from 'react'
import { observer, inject } from "mobx-react";

@inject('counterStore')
@observer
class Counter extends Component {
   render() {
       const store = this.props.counterStore
       const { caption } = this.props
       return (
           <div>
               <input type='button' value='-' onClick={ store.changeCounter.bind(this, caption, 'decrement') } />
               <input type='button' value='+' onClick={ store.changeCounter.bind(this, caption, 'increment') } />
               <span> { caption } Count: { store.counters[caption] } </span>
           </div>
       )
   }
}
export default Counter

异步状态管理(saga)

redux-thunk

首先介绍一下redux-thunk ,支持function形式的action,将dispatch句柄交由function去处理,在action进行异步调用,等到结果返回再进行dispatch,从而达到异步更新的效果。

export default function thunk({dispatch,getSate}){
 return (next) => (action) =>{
     if(typeof action === 'function'){
         action(dispatch,getState())
     }
     next(action)
 }
}
const addCountAction = (text) =>{
   return {
       type:ADD,
       text
   }
 }
const getData = (next) =>(dispatch) =>{
   new Promise((resolve) =>{
       setTimeout(()=>{
           resolve()
       },1000)
   }).then(()=>{
       dispatch(addCountAction(text))
   })
 }

缺点:容易掉入回调地狱;异步存在在各个action,无法统一管理

redux-saga

saga 是一个Redux中间件,可以通过正常的redux action从主应用程序启动,暂停和取消,它能访问完整的redux state ,也能dispatch redux action;其大概流程是 action1 => redux-saga 监听 =>执行相应的Effect方法 =>返回描述对象=>恢复执行副作用 =>action2;其主要特点:使用generator的方式实现,更加符合同步代码的风格,并且统一监听action,当命中action时,执行对应saga任务,且支持各个saga之间的相互调用。

sage核心API

takeEvery : 发起异步任务

  import {delay,put} from 'redux-saga'

   function* incrementAsync(){
       yield delay(1000)

       yield put({
           type:'increment'
       })
   }

   import takeEvery from 'redux-saga'
   function* watchIncrementAsync(){
       yield takeEvery('incremntAsync',incrementAsync)
   }

tabkEvery是每次发起‘incrementAsync' action的时候都会执行,若想得到最新那个请求的响应,则可以使用takeLatest

import {takeLatest} from 'redux-saga'
function* watchIncrementAsync(){
    yield takeLatest('incrementAsync',incrementAsync)
}

redux-saga 中间件提供很多创建effect(副作用)的函数:

take(pattern):监听未来的action,它创建了一个命令对象,告诉middleware 等待一个特定的action,直到一个与pattern匹配的action被发起,才会继续执行下面的js\

put(action):用来发送action的effect,可以简单的认为是redux中的dispatch函数,当put一个action后,reducer中就会计算新的state并返回\

fork(fn,...args):用来调用其他函数的,但fork函数是非阻塞函数,即js执行完 yield fork(fn,...args) 之后,会立即接着执行下一行的代码js

完整Demo
//saga.js
import {delay,put,takeEvery,all} from 'redux-saga/effects'


function* hisaga(){
    console.log('hi,jsx')
}

function* addAsync(){
    var url = 'https://xxx/api/getAddParams';
    var result = yield fetch(url);
    yield put({
        type: 'addAsync',
        params:result
    })
}

function* watchAddAsync(){
    yield takeEvery('addAsync',addAsync)
}

export default function* rootSaga(){
    yield all([
        hisaga(),
        watchAddAsync()
    ])
}
//Main.js 入口
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import createSagaMiddleware from 'redux-saga'
import rootSaga from './saga'

import App from './App'
import reducer from './reducer'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(reducer,applyMiddleware(sagaMiddleware))

sagaMiddleware.run(rootsaga)

ReactDOM.render(
 <Provider store={store}>
    <App />
  </Provider>,
  document.body.appendChild(document.createElement('div'))
)
//reducer.js
export default function reducer(state = {count: 10}, action) {
      switch (action.type) {
        case 'add': {
          return {
           count: state.count + 1
          }
        }
        default:
          return state
      }
}
//myComponent
import React from 'react'
import ReactDOM from 'react-dom'

class MyComponent extends React.Component {
    const {add,addAsync} = this.props
    render() {
        return (
            <div className="index">
                <p>{this.props.count}</p>
                <button onClick={add}>加1</button>
                <button onClick={addAsync}>1秒后加1</button>
            </div>
        )
    }
}

export default MyComponent
//app.js
import { connect } from 'react-redux'
import MyComponent from './myComponent'

// Map Redux state to component props
function mapStateToProps(state) {
  console.log('state', state)
  return {
    count: state.count
  }
}

// Map Redux actions to component props
function mapDispatchToProps(dispatch) {
  return {
    increment: () => dispatch({
      type: 'add'
    }),
    incrementAsync: () => dispatch({
      type: 'addAsync'
    })
  }
}

// Connected Component
const App = connect(mapStateToProps,mapDispatchToProps)(MyComponent)

export default App