react&redux&react-redux原理(react小书)

生命周期

组件的生命周期

定制shouldComponentUpdate

    shouldComponentUpdate(){
        console.log(this.state.num) // 1 2 3 4 5....每次都正常打印
        if(this.state.num%5== 0){
            return true
        }
        return false
    }
--------------------------------
    shouldComponentUpdate(nextProps,nextState){
        console.log(nextProps,nextState) 
        console.log(this.props,this.state)
        if(nextState.num%5== 0){ // 如果下一个状态%5=0
            return true
        }
        return false
    }


SetState

setState有队列的概念,对状态的更新是异步的,比如在一个事件函数中更新多次state,render只执行一次。不要不通过setState直接修改state,每次调用setState都会塞到队列中。render中不要执行setState,因为setState会执行render,这样操作会导致循环调用。
调用setState之后,它会把当前要修改的状态存储在队列中,最终会调用队列更新的方法。


高阶组件

  • 高阶组件就是一个函数,传给它一个组件,它返回一个新的组件。
  • 高阶组件的作用:就是为了组件之间的代码复用。组件可能有着某些相同的逻辑,把这些逻辑抽离出来,放到高阶组件中进行复用。
  • 高阶组件包装的新组件和原来组件之间通过 props 传递信息,减少代码的重复程度。
function heighCom(Com,name){
    class NewComponent extends React.Component(){
        constructor(props){
            super(props)
            this.state = {
                data:null
            }
        }
        componentDidMount(){
            this.setState({
                data : name
            })
        }
        render(){
            <Com data={this.state.data}></Com>
        }
    }
    return NewComponent
}


context

  • 一个组件的 context 只有它的子组件能够访问,它的父组件是不能访问到的;
  • 要给父组件设置 context,childContextTypes 是必写的;
  • 子组件要获取context内容,必须写contextTypes声明和验证你需要获取的状态的类型。
class Index extends Component {
    //要给组件设置 context,childContextTypes 是必写的
    static childContextTypes = {
      themeColor: PropTypes.string
    }
    // 初始化themeColor状态
    constructor () {
        super()
        this.state = { themeColor: 'red' }
    }
    // 设置 context 的过程
    getChildContext () {
        return { themeColor: this.state.themeColor }
    }

}

----------------------------------------------------------

//子组件要获取context内容,必须写contextTypes声明和验证你需要获取的状态的类型
class Title extends Component {
    static contextTypes = {
      themeColor: PropTypes.string
    }
  
    render () {
      return (
        <h1 style={{ color: this.context.themeColor }}>React.js 小书标题</h1>
      )
    }
  }

context的好处和坏处 : 如果一个组件设置了 context,那么它的子组件都可以直接访问到里面的内容,它就像这个组件为根的子树的全局变量。任意深度的子组件都可以通过 contextTypes 来声明你想要的 context 里面的哪些状态,然后可以通过 this.context 访问到那些状态。
context 打破了组件和组件之间通过 props 传递数据的规范,极大地增强了组件之间的耦合性。而且,就如全局变量一样,context 里面的数据能被随意接触就能被随意修改,每个组件都能够改 context 里面的内容会导致程序的运行不可预料。


纯函数(Pure Function)简介&&为什么reducer必须是纯函数

一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。

  • 函数的返回结果只依赖于它的参数。
  • 函数执行过程里面没有副作用:一个函数执行过程对产生了外部可观察的变化那么就说这个函数是有副作用的。

纯函数很严格,也就是说你几乎除了计算数据以外什么都不能干,计算的时候还不能依赖除了函数参数以外的数据。
总结
一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。

为什么要煞费苦心地构建纯函数?因为纯函数非常“靠谱”,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。不管何时何地,你给它什么它就会乖乖地吐出什么。如果你的应用程序大多数函数都是由纯函数组成,那么你的程序测试、调试起来会非常方便。

为什么reducer必须是纯函数

Redux接收一个给定的state(对象),然后通过循环将state的每一部分传递给每个对应的reducer。如果有发生任何改变,reducer将返回一个新的对象。如果不发生任何变化,reducer将返回旧的state。
Redux只通过比较新旧两个对象的存储位置来比较新旧两个对象是否相同(译者注:也就是Javascript对象浅比较)。如果你在reducer内部直接修改旧的state对象的属性值,那么新的state和旧的state将都指向同一个对象。因此Redux认为没有任何改变,返回的state将为旧的state。
因为比较两个Javascript对象所有的属性是否相同的的唯一方法是对它们进行深比较。但是深比较在真实的应用当中代价昂贵,因为通常js的对象都很大,同时需要比较的次数很多。
因此一个有效的解决方法是作出一个规定:无论何时发生变化时,开发者都要创建一个新的对象,然后将新对象传递出去。同时,当没有任何变化发生时,开发者发送回旧的对象。也就是说,新的对象代表新的state。
必须注意到你只能使用slice(译者注:此处slice类似数组的slice方法,具体可以使用本文例子中解构赋值等方法进行slice)或者类似的机制去复制旧的值到新的对象里。
现在使用了新的策略之后,你能够比较两个对象通过使用!==比较两个对象的存储位置而不是比较两个对象的所有属性。同时当两个对象不同的时候,你就能知道新的对象已经改变了旧的state(也就是说,JavaScript对象当中的某些属性的值发生了变化)。这正是Redux所采取的策略。

这就是为什么Redux需要reducers是纯函数的原因!
为什么Redux需要reducers是纯函数


redux

  • 专注于状态管理的库
  • 单一状态,单向数据流
    Redux 是一种架构模式(Flux 架构的一种变种),它不关注你到底用什么库,你可以把它应用到 React 和 Vue,甚至跟 jQuery 结合都没有问题。而 React-redux 就是把 Redux 这种架构模式和 React.js 结合起来的一个库,就是 Redux 架构在 React.js 中的体现。
1、抽离出 store
function createStore(state,reducer){
    const getState = ()=> state
    const dispatch = (action) => reducer(state,action)
    return {getState,dispatch}
}
//es5便于理解
function dispatch(action) {
  return reducer(state, action);
};
  • createStore: 会返回一个对象,这个对象包含两个方法 getStatedispatchgetState 用于获取 state 数据,其实就是简单地把 state 参数返回。
  • dispatch: 用于修改数据,和以前一样会接受action ,然后它会把 stateaction 一并传给 reducer ,那么reducer 就可以根据 action 来修改 state 了。
监控数据变化

上面的代码有一个问题,我们每次通过 dispatch 修改数据的时候,其实只是数据发生了变化,如果我们不手动调用 renderApp,页面上的内容是不会发生变化的。但是我们总不能每次 dispatch 的时候都手动调用一下 renderApp,我们肯定希望数据变化的时候程序能够智能一点地自动重新渲染数据,而不是手动调用。

你说这好办,往 dispatch里面加 renderApp 就好了,但是这样 createStore 就不够通用了。我们希望用一种通用的方式“监听”数据变化,然后重新渲染页面,这里要用到观察者模式。

function createStore(state,reducer){
    let listeners = []
    let subscribe = (listener)=>{listeners.push(listener)}
    let getState = ()=>state
    let dispatch = (action)=>{
        reducer(action,state)
        listeners.forEach( listener => {listener()} )
    }
    return {getState ,subscribe ,dispatch}
}

我们修改了 dispatch,每次当它被调用的时候,除了会调用 reducer 进行数据的修改,还会遍历 listeners 数组里面的函数,然后一个个地去调用。相当于我们可以通过 subscribe 传入数据变化的监听函数,每当 dispatch 的时候,监听函数就会被调用,这样我们就可以在每当数据变化时候进行重新渲染:

const store = createStore(appState, reducer)
store.subscribe(() => renderApp(store.getState()))

renderApp(store.getState()) // 首次渲染页面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小书》' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改标题颜色
// ...后面不管如何 store.dispatch,都不需要重新调用 renderApp

我的小结

  • subscribe : 订阅,将参数push到处理数组
  • getState : 返回state
  • dispatch : 发布,遍历事件处理数组,并执行;调用reducer,传入state和action

共享结构的对象提高性能

const obj = { a: 1, b: 2}
const obj2 = { ...obj } // => { a: 1, b: 2 }

const obj2 = { ...obj } 其实就是新建一个对象 obj2,然后把 obj 所有的属性都复制到 obj2 里面,相当于对象的浅复制。上面的 obj 里面的内容和 obj2 是完全一样的,但是却是两个不同的对象。

const obj = { a: 1, b: 2}
//  { a: 1, b: 3, c: 4 },覆盖了 b,新增了 c
const obj2 = { ...obj, b: 3, c: 4} 

我们可以把这种特性应用在 state 的更新上,我们禁止直接修改原来的对象,一旦你要修改某些东西,你就得把修改路径上的所有对象复制一遍,例如,我们不写下面的修改代码:

appState.title.text = '《React.js 小书》'

取而代之的是,我们新建一个 appState,新建 appState.title,新建 appState.title.text:

let newAppState = { // 新建一个 newAppState
  ...appState, // 复制 appState 里面的内容
  title: { // 用一个新的对象覆盖原来的 title 属性
    ...appState.title, // 复制原来 title 对象里面的内容
    text: '《React.js 小书》' // 覆盖 text 属性
  }
}

修改数据的时候就把修改路径都复制一遍,但是保持其他内容不变,最后的所有对象具有某些不变共享的结构(例如上面三个对象都共享 content 对象)。大多数情况下我们可以保持 50% 以上的内容具有共享结构,这种操作具有非常优良的特性,我们可以用它来优化上面的渲染性能。

优化性能

我们修改 reducer,让它修改数据的时候,并不会直接修改原来的数据 state,而是产生上述的共享结构的对象

function reducer (state, action) {
  switch (action.type) {
    case 'UPDATE_TITLE_TEXT':
      return { // 构建新的对象并且返回
        ...state,
        title: {
          ...state.title,
          text: action.text
        }
      }
    case 'UPDATE_TITLE_COLOR':
      return { // 构建新的对象并且返回
        ...state,
        title: {
          ...state.title,
          color: action.color
        }
      }
    default:
      return state // 没有修改,返回原来的对象
  }
}

代码稍微比原来长了一点,但是是值得的。每次需要修改的时候都会产生新的对象,并且返回。而如果没有修改(在 default 语句中)则返回原来的 state 对象。

因为 reducer 不会修改原来对象了,而是返回对象,所以我们需要修改一下 createStore。让它用每次 reducer(state, action) 的调用结果覆盖原来的 state:

function createStore (reducer) {
  let state = null
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
//reducer只是返回对象,所以需要用每次 reducer(state, action) 的调用结果覆盖原来的 state
    state = reducer(state, action)
    listeners.forEach((listener) => listener())
  }
  dispatch({}) // 初始化 state
  return { getState, dispatch, subscribe }
}

我们成功地把不必要的页面渲染优化掉了,问题解决。另外,并不需要担心每次修改都新建共享结构对象会有性能、内存问题,因为构建对象的成本非常低,而且我们最多保存两个对象引用(oldState 和 newState),其余旧的对象都会被垃圾回收掉。

我的小结

修改数据时,禁止直接修改原来的对象,一旦你要修改某些东西,你就得把修改路径上的所有对象复制一遍,但是保持其他内容不变,最后的所有对象具有某些不变共享的结构,多数情况下我们可以保持 50% 以上的内容具有共享结构,这种操作具有非常优良的特性。
Redux只通过比较新旧两个对象的存储位置来比较新旧两个对象是否相同(译者注:也就是Javascript对象浅比较)。如果你在reducer内部直接修改旧的state对象的属性值,那么新的state和旧的state将都指向同一个对象。因此Redux认为没有任何改变,返回的state将为旧的state。

state = reducer(state, action) // 覆盖原对象

reducer总结:

createStore 接受一个叫 reducer 的函数作为参数,这个函数规定是一个纯函数,它接受两个参数,一个是 state,一个是 action。
如果没有传入 state 或者 state 是 null,那么它就会返回一个初始化的数据。如果有传入 state 的话,就会根据 action 来“修改“数据,但其实它没有、也规定不能修改 state,而是要通过上节所说的把修改路径的对象都复制一遍,然后产生一个新的对象返回。如果它不能识别你的 action,它就不会产生新的数据,而是(在 default 内部)把 state 原封不动地返回。
!!!reducer 是不允许有副作用的。你不能在里面操作 DOM,也不能发 Ajax 请求,更不能直接修改 state,它要做的仅仅是 —— 初始化和计算新的 state。
还要注意:为什么reducer必须是纯函数?在纯函数章节已经做了总结。

redux总结
不知不觉地,到这里大家不仅仅已经掌握了 Redux,而且还自己动手写了一个 Redux。我们从一个非常原始的代码开始,不停地在发现问题、解决问题、优化代码的过程中进行推演,最后把 Redux 模式自己总结出来了。这就是所谓的 Redux 模式,我们再来回顾一下这几节我们到底干了什么事情。

我们从一个简单的例子的代码中发现了共享的状态如果可以被任意修改的话,那么程序的行为将非常不可预料,所以我们提高了修改数据的门槛:你必须通过 dispatch 执行某些允许的修改操作,而且必须大张旗鼓的在 action 里面声明。

这种模式挺好用的,我们就把它抽象出来一个 createStore,它可以产生 store,里面包含 getState 和 dispatch 函数,方便我们使用。

后来发现每次修改数据都需要手动重新渲染非常麻烦,我们希望自动重新渲染视图。所以后来加入了订阅者模式,可以通过 store.subscribe 订阅数据修改事件,每次数据更新的时候自动重新渲染视图。

接下来我们发现了原来的“重新渲染视图”有比较严重的性能问题,我们引入了“共享结构的对象”来帮我们解决问题,这样就可以在每个渲染函数的开头进行简单的判断避免没有被修改过的数据重新渲染。

我们优化了 stateChanger 为 reducer,定义了 reducer 只能是纯函数,功能就是负责初始 state,和根据 state 和 action 计算具有共享结构的新的 state。

createStore 现在可以直接拿来用了,套路就是:

// 定一个 reducer
function reducer (state, action) {
  /* 初始化 state 和 switch case */
}

// 生成 store
const store = createStore(reducer)

// 监听数据变化重新渲染页面
store.subscribe(() => renderApp(store.getState()))

// 首次渲染页面
renderApp(store.getState()) 

// 后面可以随意 dispatch 了,页面自动更新
store.dispatch(...)

Redux和 React.js 一点关系都没有!!!接下来我们要把 React.js 和 Redux 结合起来,用 Redux 模式帮助管理 React.js 的应用状态。


react-redux

前端中应用的状态存在的问题:一个状态可能被多个组件依赖或者影响,而 React.js 并没有提供好的解决方案,我们只能把状态提升到依赖或者影响这个状态的所有组件的公共父组件上,我们把这种行为叫做状态提升。但是需求不停变化,共享状态没完没了地提升也不是办法。
后来我们在 React.js 的 context 中提出,我们可用把共享状态放到父组件的 context 上,这个父组件下所有的组件都可以从 context 中直接获取到状态而不需要一层层地进行传递了。但是直接从 context 里面存放、获取数据增强了组件的耦合性;并且所有组件都可以修改 context 里面的状态就像谁都可以修改共享状态一样,导致程序运行的不可预料。

解决思路把 context 和 store 结合起来。毕竟 store 的数据不是谁都能修改,而是约定只能通过 dispatch 来进行修改,这样的话每个组件既可以去 context 里面获取 store 从而获取状态,又不用担心它们乱改数据了。

到底什么样的组件才叫复用性强的组件。如果一个组件对外界的依赖过于强,那么这个组件的移植性会很差,就像这些严重依赖 context 的组件一样。

如果一个组件的渲染只依赖于外界传进去的 props 和自己的 state,而并不依赖于其他的外界的任何数据,也就是说像纯函数一样,给它什么,它就吐出(渲染)什么出来。这种组件的复用性是最强的,别人使用的时候根本不用担心任何事情,只要看看 PropTypes 它能接受什么参数,然后把参数传进去控制它就行了。

我们需要高阶组件帮助我们从 context 取数据,我们也需要写 Dumb 组件帮助我们提高组件的复用性。所以我们尽量多地写 Dumb 组件,然后用高阶组件把它们包装一层,高阶组件和 context 打交道,把里面数据取出来通过 props 传给 Dumb 组件。

实现过程:
1、connect 现在是接受一个参数 mapStateToProps,然后返回一个函数,这个返回的函数才是高阶组件。它会接受一个组件作为参数,然后用 Connect 把组件包装以后再返回。

//connect高阶组件,从context取出store,将state传给mapStateToProps
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor () {
      super()
      this.state = {
        allProps: {}
      }
    }

    componentWillMount () {
      const { store } = this.context
      this._updateProps()
      store.subscribe(() => this._updateProps())
    }

    _updateProps () {
      const { store } = this.context
      let stateProps = mapStateToProps
        ? mapStateToProps(store.getState(), this.props)
        : {} // 防止 mapStateToProps 没有传入
      let dispatchProps = mapDispatchToProps
        ? mapDispatchToProps(store.dispatch, this.props)
        : {} // 防止 mapDispatchToProps 没有传入
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }

    render () {
      return <WrappedComponent {...this.state.allProps} />
    }
  }
  return Connect
}

2、高阶组件用法

const mapStateToProps = (state) => {
    return {
      themeColor: state.themeColor,
      themeName: state.themeName,
      fullName: `${state.firstName} ${state.lastName}`
    }
}
const mapDispatchToProps = (dispatch) => {
  return {
    onSwitchColor: (color) => {
      dispatch({ type: 'CHANGE_COLOR', themeColor: color })
    }
  }
}
Header = connect(mapStateToProps)(Header)

我们在 Connect 组件的 constructor 里面初始化了 state.allProps,它是一个对象,用来保存需要传给被包装组件的所有的参数。生命周期 componentWillMount 会调用调用 _updateProps 进行初始化,然后通过 store.subscribe 监听数据变化重新调用 _updateProps。

为了让 connect 返回新组件和被包装的组件使用参数保持一致,我们会把所有传给 Connect 的 props 原封不动地传给 WrappedComponent。所以在 _updateProps 里面会把 stateProps 和 this.props 合并到 this.state.allProps 里面,再通过 render 方法把所有参数都传给 WrappedComponent。

mapStateToProps 也发生点变化,它现在可以接受两个参数了,我们会把传给 Connect 组件的 props 参数也传给它,那么它生成的对象配置性就更强了,我们可以根据 store 里面的 state 和外界传入的 props 生成我们想传给被包装组件的参数。

现在已经很不错了,Header.js 和 Content.js 的代码都大大减少了,并且这两个组件 connect 之前都是 Dumb 组件。接下来会继续重构 ThemeSwitch。

Provider
我们要把 context 相关的代码从所有业务组件中清除出去,额外构建一个组件来做这种脏活,然后让这个组件成为组件树的根节点,那么它的子组件都可以获取到 context 了
Provider 做的事情也很简单,它就是一个容器组件,会把嵌套的内容原封不动作为自己的子组件渲染出来。它还会把外界传给它的 props.store 放到 context,这样子组件 connect 的时候都可以获取到。

export class Provider extends Component {
  static propTypes = {
    store: PropTypes.object,
    children: PropTypes.any
  }

  static childContextTypes = {
    store: PropTypes.object
  }

  getChildContext () {
    return {
      store: this.props.store
    }
  }

  render () {
    return (
      <div>{this.props.children}</div>
    )
  }
}

总结:

  • 到这里大家已经掌握了 React-redux 的基本用法和概念,并且自己动手实现了一个 React-redux,我们回顾一下这几节都干了什么事情。
  • React.js 除了状态提升以外并没有更好的办法帮我们解决组件之间共享状态的问题,而使用 context 全局变量让程序不可预测。通过 Redux 的章节,我们知道 store 里面的内容是不可以随意修改的,而是通过 dispatch 才能变更里面的 state。所以我们尝试把 store 和 context 结合起来使用,可以兼顾组件之间共享状态问题和共享状态可能被任意修改的问题。
  • 第一个版本的 store 和 context 结合有诸多缺陷,有大量的重复逻辑和对 context 的依赖性过强。我们尝试通过构建一个高阶组件 connect 函数的方式,把所有的重复逻辑和对 context 的依赖放在里面 connect 函数里面,而其他组件保持 Pure(Dumb) 的状态,让 connect 跟 context 打交道,然后通过 props 把参数传给普通的组件。
  • 而每个组件需要的数据和需要触发的 action 都不一样,所以调整 connect,让它可以接受两个参数 mapStateToProps 和 mapDispatchToProps,分别用于告诉 connect 这个组件需要什么数据和需要触发什么 action。
  • 最后为了把所有关于 context 的代码完全从我们业务逻辑里面清除掉,我们构建了一个 Provider 组件。Provider 作为所有组件树的根节点,外界可以通过 props 给它提供 store,它会把 store 放到自己的 context 里面,好让子组件 connect 的时候都能够获取到。
  • 这几节的成果就是 react-redux.js 这个文件里面的两个内容:connect 函数和 Provider 容器组件。这就是 React-redux 的基本内容,当然它是一个残疾版本的 React-redux,很多地方需要完善。例如上几节提到的性能问题,现在不相关的数据变化的时候其实所有组件都会重新渲染的,这个性能优化留给读者做练习。
  • 通过这种方式大家不仅仅知道了 React-redux 的基础概念和用法,而且还知道这些概念到底是解决什么问题,为什么 React-redux 这么奇怪,为什么要 connect,为什么要 mapStateToProps 和 mapDispatchToProps,什么是 Provider,我们通过解决一个个问题就知道它们到底为什么要这么设计的了。

使用真正的 Redux 和 React-redux

现在 make-react-redux 工程代码中的 Redux 和 React-redux 都是我们自己写的,现在让我们来使用真正的官方版本的 Redux 和 React-redux。

在工程目录下使用 npm 安装 Redux 和 React-redux 模块:

npm install redux react-redux --save

src/ 目录下 Header.jsThemeSwitch.jsContent.js 的模块导入中的:

import { connect } from './react-redux'

改成:

import { connect } from 'react-redux'

也就是本来从本地 ./react-redux 导入的 connect 改成从第三方 react-redux 模块中导入。

修改 src/index.js,把前面部分的代码调整为:

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import Header from './Header'
import Content from './Content'
import './index.css'

const themeReducer = (state, action) => {
  if (!state) return {
    themeColor: 'red'
  }
  switch (action.type) {
    case 'CHANGE_COLOR':
      return { ...state, themeColor: action.themeColor }
    default:
      return state
  }
}

const store = createStore(themeReducer)

...

我们删除了自己写的 createStore,改成使用第三方模块 reduxcreateStoreProvider 本来从本地的 ./react-redux 引入,改成从第三方 react-redux 模块中引入。其余代码保持不变。

接着删除 src/react-redux.js,它的已经用处不大了。最后启动工程 npm start

可以看到我们原来的业务代码其实都没有太多的改动,实际上我们实现的 reduxreact-redux 和官方版本在该场景的用法上是兼容的。接下来的章节我们都会使用官方版本的 reduxreact-redux

推荐阅读更多精彩内容