【React.js 19】React进阶-02

我又膨胀了,现在打算写一写Redux的原理了...

Redux原理

这里我们先讲redux最简单实现。
空讲原理,不如自己写一个redux,我们来一步步写一个my-redux.js,上代码:

//根据使用redux的几个关键方法,倒推出写法
export function createStore(reducer){
  let currentState = {}
  let currentListeners = []//有可能传入多个listener

  function getState(){
    return currentState
  }

  function subscribe(listener){
    currentListeners.push(listener)
  }

  function dispatch(action){
    currentState = reducer(currentState,action)
    currentListeners.map(v=>v())//把监听器中的每个监听方法都执行一下
    return action
  }
  //因为初始化的时候,我们就能getState到值,证明已经在内部自动执行过一次dispatch
  dispatch({type:'@FOR-DEMO/MY-REDUX/INIT@'})//注意尽量把type写复杂些,不要和用户的type相同了,没有对应的type时,就会走reducer的默认return,即初始的state

  return {getState ,subscribe ,dispatch}
}

其实看过代码后,大家就基本明白了,最关键的地方,就在于dispatch方法,在这一步的时候,执行了状态的修改,执行了订阅的监听事件,一般来写即变更了状态,也触发了渲染的更新。

到这里基本的redux的功能都实现了,原理也能理解了,那么,我们进一步完善下这个redux

我们要真正使用redux其实还是要靠react-redux来优雅的实现,要理解react-redux,首先我们要先理解Context的概念。

Context

当我们有多个组件嵌套的时候,如果需要传递一个变量,那就要层层用属性的方式传递,非常繁琐,对性能也不友好,而Context的出现就很好的解决了这个问题。
Context是全局的,组件里声明,所有的子元素都可以直接获取。
值得强调的一点是,因为FB认为全局使用并不好,所以严格规范了其使用,要求如果使用了Context的话,必须实现类型校验
所以需要先npm install prop-types --save一下,在要声明和引用Context的组件中要校验类型:

  //声明处
  static childContextTypes = {
    user:PropTypes.string
  }
  //引用处
  static contextTypes = {
    user:PropTypes.string
  }

有点乱,还是上完整的代码吧:

import React , { Component } from 'react'
import PropTypes from 'prop-types'

class SideBar extends Component {
  render(){
    return(
      <div>
        <p>侧边栏</p>
        <NavBar></NavBar>
      </div>
    )
  }
}
class NavBar extends Component {
  //NavBar要使用Context就要校验类型
  static contextTypes = {
    user:PropTypes.string
  }
  render(){
    return <div>{this.context.user}的导航栏</div>
  }
}
class Page extends Component {
  //声明的地方也要进行类型校验
  static childContextTypes = {
    user:PropTypes.string
  }
  constructor(props){
    super(props)
    this.state={ user:'JW' }
  }
  //返回子组件要获取的Context
  getChildContext(){
    return this.state
  }
  render(){
    return(
      <div>
        <p>我是{this.state.user}</p>
        <SideBar></SideBar>
      </div>
    )
  }
}
export default Page

看了代码就应该知道Context的作用和怎么用了吧。

那么,究竟Context和我们要说的react-redux有什么关系呢?

当然有关系了,记得react-redux里的Provider吗,它就是为全局提供Context的!

这么说吧:
connect负责连接组件,给到Redux里的数据放到组件的属性里面。
Provider负责把store放到context里,所有的子元素都可以直接取到store
先不管connect,初步实现了Providerreact-redux代码如下:

import React , { Component } from 'react'
import PropTypes from 'prop-types'

export class Provider extends Component{
  static childContextTypes = {
    store : PropTypes.object
  }
  constructor(props,context){
    super(props,context)
    this.store = props.store
  }
  getChildContext(){
    return {store : this.store}
  }
  render(){ return this.props.children }
}

Providerstore放到context后,全局都能获取到store中的属性。这时候connect可以继续使用react-redux提供的,你会发现,功能可以正常执行了。

下一步,我们就开始着手实现connect了。
connect主要有两个任务:负责接收一个组件,并且把state里的一些数据放进去,返回一个组件再者就是当数据有变化的时候,能够通知组件。

首先,实现组件的数据接收:

export const connect = (mapStateToProps=state=>state,mapDispatchToProps={})=>(WrapComponent)=>{
  return class connectCompnent extends Component {
    static contextTypes={
      store:PropTypes.object
    }
    constructor(props,context){
      super(props,context)
      this.state={
        props:{}
      }
    }
    componentDidMount(){
      this.update()
    }
    //获取mapStateToProps和mapDispatchToProps放入this.props里
    update(){
      //重点就在这,Context的作用发挥出来了
      const {store} = this.context
      //通过这个方法给出对应的store中的数据
      const stateProps = mapStateToProps(store.getState())
      //新获取数据后,执行刷新wrapComponent的动作
      this.setState({
        props : {...stateProps , ...this.state.props}
      })
      //这样组件就有了获取props的功能
    }
    render(){
      return <WrapComponent {...this.state.props}></WrapComponent>
    }
  }
}

第二步,实现组件的事件的传递:
为了实现事件的传递和绑定,需要在我们自己的redux中新增一个方法:

//相当于每个creator包一层dispatch
export function bindActionCreators(creators ,dispatch){
  let bound = {}
  Object.keys(creators).forEach(v=>{
    let creator = creators[v]
    bound[v] = bindActionCreator(creator ,dispatch)
  })
  return bound
}

function bindActionCreator(creator ,dispatch){
  return (...args) => dispatch(creator(...args))
}

然后就是把connect传入的creatorsdispatch包裹起来,并且再每次调用creator的时候,把新返回的状态赋值给connectprops,达到更新数据时刷新渲染,上面尚未实现事件传递的代码更新成这样:

    componentDidMount(){
      const {store} = this.context
      //组件加载时重新调用监听和更新组件视图
      store.subscribe(()=>this.update())
      this.update()
    }
    //获取mapStateToProps和mapDispatchToProps放入this.props里
    update(){
      const {store} = this.context
      //通过这个方法给出对应的数据
      const stateProps = mapStateToProps(store.getState())
      //获取组件对应的事件,不能直接赋值或者执行,没有意义,因为需要dispatch()来传入事件去执行才有用
      const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch)
      //新获取数据后,执行刷新wrapComponent的动作
      this.setState({
        props : {
          ...this.state.props ,
          ...stateProps ,
          ...dispatchProps
        }
      })
      //这样组件就有了获取props的功能
    }

至此,redux的基本功能就实现的差不多了,因为redux默认只处理同步事件,所以还需要中间件的形式来处理异步事件。


我们在这里先理一遍整体流程:

redux:

1、两个变量:
currentState(包含了所有的状态)
currentListeners(所有监听事件)
2、三个函数:
getState(直接返回store的所有状态)
subscribe(把订阅监听事件pushcurrentListeners中)
dispatch(发起修改状态事件,把传入的reducer的事件执行并且接收事件执行返回的状态,把所有的监听事件都执行一遍,并返回一个dispatch)
3、默认执行一次dispatch,用一个生僻的action type完全不匹配到type直接返回默认的state已达到初始化的目的。
4、最后返回包含三个函数的一个对象当做store,就足够实现redux的逻辑了。
PS : React-Redux所需方法bindActionCreators提供给dispatch进行一次包裹,并返回包裹后的事件。

React-Redux:

1、Provider把传递进来的store属性放到context中以便子元素可以全局获取到,而render()事件什么都不用渲染,只需要渲染其子元素即可。
2、connect是一个高阶组件,接收4个参数,但是在这里我们只实现基础的功能,所以只需要2个参数:
mapStateToProps告诉组件我们需要state的哪个属性。
mapDispatchToProps告诉组件需要redux的哪些事件,并且自动为其包裹上一层dispatch
3、connect首先返回一个WrapComponent高阶组件,再将其传递给内层函数,内层函数则渲染WrapComponent,并且返回一个connectCompnent组件。
4、connectCompnent里面有一个最核心的方法update(),首先执行mapStateToProps方法,获得store中所有的state并添加到connect组件的state中,再将它展开对应所需要的state以属性的方式一一传递给WrapComponent
5、dispatchProps则是将mapDispatchToProps对象中的事件进行一次dispatch包裹,执行的方法就是上文中所讲到的redux中的bindActionCreators事件。
6、再在update()方法中,setState将对应属性和dispatch赋值给connectstate,并且自动刷新WrapComponent组件。
7、之后在组件加载后获取到context里的store;再订阅一下update()方法,保证每次dispatch都能进行刷新;最终达到redux数据修改,组件显示也同步更新的效果。

至此,一个简易的react-redux就实现了。


现在就来实现一下我们欠下的中间件,做一个真正支持异步操作的react-redux
放一张中间件机制图,大家体会一下。
中间件机制

要实现中间件,我们先要扩展一下前面自己写的reduxcreateStore方法,使其能够接收applyMiddleware()参数,并且实现applyMiddleware()方法。

export function createStore(reducer,enhancer){//enhancer增强器,扩展createStore方法
  //如果传入了中间件,则套一层增强器再返回store
  if (enhancer) {
    return enhancer(createStore)(reducer)
  }
  .
  .
  .
  return {getState ,subscribe ,dispatch}
}

除了套了一层enhancer返回一个被扩展了的store,其他并没有什么不同。
再来就实现enhancer方法了,也就是我们对外暴露的applyMiddleware了。

export function applyMiddleware(middleware){
  return createStore=>(...args)=>{
    //生成原生的store
    const store = createStore(...args)
    //获取原生的dispatch
    let dispatch = store.dispatch
    //定义中间件的接口
    const midApi = {
      getState : store.getState,
      dispatch : (...args)=>dispatch(...args)
    }
    //dispatch用middleware做一次扩展
    dispatch = middleware(midApi)(store.dispatch)
    //返回被新的dispatch覆盖掉的store
    return {
      ...store,
      dispatch
    }
  }
}

至此,thunk先应用redux-thunk提供的,那么,redux的异步操作也就实现了。


现在来实现以下thunk吧。

const thunk = ({dispatch,getState})=>next=>action=>{
  //如果action是一个方法,执行一下,参数是dispatch和getState
  if (typeof(action)==='function') {
    return action(dispatch,getState)
  }
  //如果action是除方法之外的,例如对象{type : LOGIN}这样的,就直接返回,什么都不操作,按同步事件处理
  return next(action)
}
export default thunk

是不是有点懵,打印一下action你就大概清楚了:

action: ƒ (dispatch) {
    setTimeout(function () {
      // 异步结束后,手动执行dispatch
      dispatch(doSomething());
    }, 2000);
  } 

其实就是等异步操作结束了,再执行对应的方法即可。


但是中间件可能不止有一个,我们经常要传入多个中间件,所以,我们要来做一个合并中间件的操作。
首先要支持传入多个中间件参数:

export function applyMiddleware(...middlewares){
  return createStore=>(...args)=>{
    .
    .//重复代码我就不贴了,只贴有变动的
    .
    //dispatch用middleware做一次扩展(注释掉,大家可以自己对比看看)
    // dispatch = middleware(midApi)(store.dispatch)
    //多个中间件,就把他遍历一遍,接入中间件,放入middlewareChain数组中
    const middlewareChain = middlewares.map(middleware=>middleware(midApi))
    dispatch = compose(...middlewareChain)(store.dispatch)
    //返回被新的dispatch覆盖掉的store
    return {
      ...store,
      dispatch
    }
  }
}

这里对组合中间件的dispatch我们用到了compose()方法
补上compose()方法:

export function compose(...funcs){
  //如果传入的funcs长度为0
  if (funcs.length === 0) {
    //直接返回给入的参数,什么都不执行
    return arg=>arg
  }
  //如果传入的funcs长度为1,代表当前传入了一个参数
  if (funcs.length === 1) {
    //直接返回第0个
    return funcs[0]
  }
  //用reduce()方法组合所有的中间件
  return funcs.reduce((ret,item)=>(...args)=>ret(item(...args)))
}

写一个数组类型的中间件处理

const arrayThunk = ({dispatch,getState})=>next=>action=>{
  if (Array.isArray(action)) {
    return action.forEach(v=>dispatch(v))
  }
  return next(action)
}
export default arrayThunk


以上就是一个基础的react-redux的实现。
上代码:

//my-redux.js
export function createStore(reducer,enhancer){//enhancer增强器,扩展createStore方法
  //如果传入了中间件,则套一层增强器再返回store
  if (enhancer) {
    return enhancer(createStore)(reducer)
  }
  let currentState = {}
  let currentListeners = []

  function getState(){
    return currentState
  }
  function subscribe(listener){
    currentListeners.push(listener)
  }
  function dispatch(action){
    currentState = reducer(currentState,action)
    currentListeners.map(v=>v())//把监听器中的每个监听方法都执行一下
    return action
  }
  //因为初始化的时候,我们就能getState到值,证明已经在内部自动执行过一次dispatch
  dispatch({type:'@FOR-DEMO/MY-REDUX/INIT@'})//注意尽量把type写复杂些,不要和用户的type相同了,没有对应的type时,就会走reducer的默认return,即初始的state

  return {getState ,subscribe ,dispatch}
}
export function applyMiddleware(...middlewares){
  return createStore=>(...args)=>{
    //生成原生的store
    const store = createStore(...args)
    //获取原生的dispatch
    let dispatch = store.dispatch
    //定义中间件的接口
    const midApi = {
      getState : store.getState,
      dispatch : (...args)=>dispatch(...args)
    }
    //dispatch用middleware做一次扩展
    // dispatch = middleware(midApi)(store.dispatch)
    //多个中间件,就把他遍历一遍,接入中间件,放入middlewareChain数组中
    const middlewareChain = middlewares.map(middleware=>middleware(midApi))
    dispatch = compose(...middlewareChain)(store.dispatch)
    //返回被新的dispatch覆盖掉的store
    return {
      ...store,
      dispatch
    }
  }
}
export function compose(...funcs){
  //如果传入的funcs长度为0
  if (funcs.length === 0) {
    //直接返回给入的参数,什么都不执行
    return arg=>arg
  }
  //如果传入的funcs长度为1,代表当前传入了一个参数
  if (funcs.length === 1) {
    //直接返回第0个
    return funcs[0]
  }
  return funcs.reduce((ret,item)=>(...args)=>ret(item(...args)))
}
export function bindActionCreators(creators ,dispatch){
  let bound = {}
  Object.keys(creators).forEach(v=>{
    let creator = creators[v]
    bound[v] = bindActionCreator(creator ,dispatch)
  })
  return bound
}
function bindActionCreator(creator ,dispatch){
  return (...args) => dispatch(creator(...args))
}


//my-react-redux.js
import React , { Component } from 'react'
import PropTypes from 'prop-types'
import { bindActionCreators } from './my-redux'

export const connect = (mapStateToProps=state=>state,mapDispatchToProps={})=>(WrapComponent)=>{
  return class connectCompnent extends Component {
    static contextTypes={
      store:PropTypes.object
    }
    constructor(props,context){
      super(props,context)
      this.state={
        props:{}
      }
    }
    componentDidMount(){
      const {store} = this.context
      //组件加载时重新调用监听和更新组件视图
      store.subscribe(()=>this.update())
      this.update()
    }
    //获取mapStateToProps和mapDispatchToProps放入this.props里
    update(){
      const {store} = this.context
      //通过这个方法给出对应的数据
      const stateProps = mapStateToProps(store.getState())
      //获取组件对应的事件,不能直接赋值或者执行,没有意义,因为需要dispatch()来传入事件去执行才有用
      const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch)
      //新获取数据后,执行刷新wrapComponent的动作
      this.setState({
        props : {
          ...this.state.props ,
          ...stateProps ,
          ...dispatchProps
        }
      })
      //这样组件就有了获取props的功能
    }
    render(){
      return <WrapComponent {...this.state.props}></WrapComponent>
    }
  }
}
class Provider extends Component{
  static childContextTypes = {
        store: PropTypes.object
    }
    getChildContext(){
        return {store:this.store}
    }
    constructor(props, context){
        super(props, context)
        this.store = props.store
    }
    render(){
        return this.props.children
    }
}
export {Provider}


//my-redux-thunk.js
const thunk = ({dispatch,getState})=>next=>action=>{
  console.log('action:',action,'\n','typeof action:',typeof action);
  //如果action是一个方法,执行一下,参数是dispatch和getState
  if (typeof(action)==='function') {
    return action(dispatch,getState)
  }
  //如果action是除方法之外的,例如对象{type : LOGIN}这样的,就直接返回,什么都不操作,按同步事件处理
  return next(action)
}
export default thunk


//my-redux-array.js
const arrayThunk = ({dispatch,getState})=>next=>action=>{
  if (Array.isArray(action)) {
    return action.forEach(v=>dispatch(v))
  }
  return next(action)
}
export default arrayThunk

写在最后:这里的redux只做原理分析所用,实际项目还请使用正式的redux,因为毕竟还有很多细节处处理的问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容