[译]从零开始Redux(四)提炼组件

前言

接上一篇,在明白了如何组合状态转移之后,我们回过头来再来看看整个实现,我们提炼了一些简单的展示性的组件,例如FilterLink:

class FilterLink extends Component {
  constructor(props) {
    super(props);
  }

  render(){
    let {curFilter, value, onClick} = this.props
      if(curFilter === value){
          return {this.props.children} 
      }else{
          return 
                    {e.preventDefault();onClick(value)}}>
                        {this.props.children}
                    
                     
                
      }
  }
}

export default FilterLink;

它只负责组件的展示而不需要关注页面逻辑问题。例如点击之后的处理是通过属性传递进来的。但是这就导致了一个比较大的问题,大家还记得我们的点击onClick函数是从哪儿传递进来的么?是从最根部的容器组件传递到Todo组件再传递到FilterLink的,这个传递的链条过长并且这个联调上的组件其实并不需要知道每个link具体的逻辑。因此,我们需要根据在这个展示组件的基础上提炼出他们对应的用来管理行为的容器组件。另外,有的组件也太过庞大,包含了太多的展示和逻辑的成分(比如本身的Todo组件,也需要提炼)

提炼容器

我们提炼的步骤一般来说分为两步

  • 提炼出展示组件,这种组件只负责显示样式,逻辑由外部传入
  • 提炼出容器组件,这种组件是在展示组件的基础上形成的,它负责搞定展示组件的逻辑与状态

展示组件

根据这样的精神,我们先提炼出三个展示组件:

  • 添加待办事项组件
  • 待办事项列表组件
  • 显示过滤栏组件
import React, { Component } from 'react';
import './App.css';
    
class AddToDo extends Component {
    constructor(props) {
        super(props);
        this.state = {
            text:'',
        }
    }
    handleChange(event) {
        this.setState({text: event.target.value})
    }

  render() {
    return (
     
this.handleChange(e)}>
); } } export default AddToDo;
import React, { Component } from 'react';
import './App.css';

class ToDoList extends Component {
    constructor(props) {
        super(props);

    }

  render() {
    let {todos, doToggle} = this.props
    return (
        
    {todos .map(todo =>
  • doToggle(todo.id)}> {todo.text}
  • )}
); } } export default ToDoList;
import React, { Component } from 'react';
import './App.css';
    
class FilterLink extends Component {
  constructor(props) {
    super(props);
  }

  render(){
    let {active, onClick} = this.props
      if(active){
          return {this.props.children} 
      }else{
          return 
                    {e.preventDefault();onClick()}}>
                        {this.props.children}
                    
                     
                
      }
  }
}
export default FilterLink;

在完成了展示组件的提炼之后,我们就要考虑容器组件的提炼。

容器组件

从我们刚才的分析,容器组件需要完成的事情有:展示组件的状态以及操作逻辑维护,状态更新之后的刷新,因此我们可以相应的提炼出三个容器

  • 添加事项容器
  • 事项列表容器
  • 展示过滤容器
import React, { Component } from 'react';
import './App.css';
import AddToDo from './AddToDo'


class AddTodoContainer extends Component {
  
    componentDidMount(){
      this.unsubscribe = this.props.store.subscribe(()=>{
          this.forceUpdate()
      })
    }
  
    componentWillUnmount(){
      this.unsubscribe()
    }
      constructor(props) {
        super(props);
        this.id = 0;
      }
    
      render() {
        let {store} = this.props;
        return (
          
{store.dispatch({type: 'ADD_TODO', id: this.id++, text:input})}}>
); } } AddTodoContainer.contextType = MyContext export default AddTodoContainer;
import React, { Component } from 'react';
import './App.css';
import ToDoList from './ToDoList'

class TodoContainer extends Component {
  
  componentDidMount(){
    this.unsubscribe = this.props.store.subscribe(()=>{
        this.forceUpdate()
    })
  }

  componentWillUnmount(){
    this.unsubscribe()
  }
    constructor(props) {
      super(props);
    }
  
    render() {
      let {store} = this.props;
      let data = store.getState();
      let todos = data.todos.filter(todo => {
        switch (data.visibilityFilter) {
          case 'SHOW_ALL':
            return true
          case 'SHOW_COMPLETED':
            return todo.completed
        
          case 'SHOW_UNCOMPLETED':
            return !todo.completed  
        }
      })
      return (
        
{store.dispatch({type: 'TOGGLE_TODO', id:input})}} todos={todos}>
); } } TodoContainer.contextType = MyContext export default TodoContainer;
import React, { Component } from 'react';
import './App.css';
import FilterLink from './FilterLink'


class LinkContainer extends Component {
  componentDidMount(){
      this.unsubscribe = this.props.store.subscribe(()=>{
          this.forceUpdate()
      })
  }

  componentWillUnmount(){
      this.unsubscribe()
  }
  constructor(props) {
    super(props);
  }

  render(){
    let { store, filter} = this.props;
    let state = store.getState();
    return  store.dispatch({type:'SET_VISIBILITY_FILTER', filter: filter})} 
            active={state.visibilityFilter === filter}>
            {this.props.children}
        
  }
}

export default LinkContainer

上面这两个组件通过传递进来的store对象维护各自展示组件的状态和操作逻辑,并且在自己componentDidMountcomponentWillUnmount的时候向store注册和注销状态变更回调。

整体重构

有了容器组件,我们整个项目的结构就可以简化成这样了:

import React, { Component } from 'react';
import './App.css';
import LinkContainer from './LinkContainer'
    

class Footer extends Component {
  constructor(props) {
    super(props);
  }

  render(){
    let {store} = this.props
    return 
ALL COMPLETED UNCOMPLETED
} } export default Footer;
import React, { Component } from 'react';
import './App.css';
import LinkContainer from './LinkContainer'
import TodoContainer from './TodoContainer'
import PropTypes from 'prop-types';
import Footer from './Footer'
import AddTodoContainer from './AddTodoContainer'

class Todo extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    let { store } = this.props
    return (
      
); } } export default Todo;

在index.js文件中

const todoApp = combineReducers({
    todos: reducerTodo,
    visibilityFilter: reducerFilter
})

let store = createStore(todoApp);
ReactDOM.render(, document.getElementById('root'));
/*
const render = () => 
    ReactDOM.render( {store.dispatch({type: 'ADD_TODO', id: idnum++, text:input})}}
        doToggle={(input) => {store.dispatch({type: 'TOGGLE_TODO', id:input})}}
        doFilter={(input) => {store.dispatch({type:'SET_VISIBILITY_FILTER', filter: input})}}
    />, document.getElementById('root'));
store.subscribe(()=> render());
render();
*/

可以看到在index.js文件内,我们不需要显式的声明并执行一个render方法,而只需要调用一次ReactDOM.render就行了,因为刷新的工作我们都委托给了下游的容器组件了。另外我们也不用传递一堆回调函数了,这些也都由容器组件来管理了。
可以看到,我们通过提炼组件可以慢慢的把不必要的回调和状态传递链条给消除,但是由引入了一个新的问题,我们实际上是将我们的store状态对象一层一层的传递了下去,对于少量的组件可能这样显式的传递倒还好,一旦组件数量增多,这样显式传递会非常痛苦,因此,我们需要一种能够隐式传递的机制。Redux使用了React里的context来解决这个问题。

React上下文

React提供了一个上下文机制,只要在通过api创建一个上下文并且包裹原有的组件,那这个组件及其子节点以及其后代都可以通过上下文获取到变量,Redux就是通过这样的机制将store变量隐式传递的(原视频中所使用的react版本应该比较老,这里根据v16.8的官方demo重新做了一版)。
首先我们根据React的api创建一个上下文:

import React, { Component} from 'react';

export const MyContext = React.createContext({})
export const Provider = MyContext.Provider;

然后从上到下修改组件,首先是index.js:

import {MyContext, Provider} from './MyContext'

const todoApp = combineReducers({
    todos: reducerTodo,
    visibilityFilter: reducerFilter
})

let store = createStore(todoApp);
ReactDOM.render(
        
, document.getElementById('root'));

然后是todo:

class Todo extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      
); } }

然后以TodoContainer为例:

import {MyContext} from './MyContext'

class TodoContainer extends Component {
  
  componentDidMount(){
    console.log('TodoContainer:' + this.context)
    this.unsubscribe = this.context.store.subscribe(()=>{
        this.forceUpdate()
    })
  }

  componentWillUnmount(){
    this.unsubscribe()
  }
    constructor(props) {
      super(props);
      this.id = 0;
    }
  
    render() {
      let {store} = this.context;
      let data = store.getState();
      let todos = data.todos.filter(todo => {
        switch (data.visibilityFilter) {
          case 'SHOW_ALL':
            return true
          case 'SHOW_COMPLETED':
            return todo.completed
        
          case 'SHOW_UNCOMPLETED':
            return !todo.completed  
        }
      })
      return (
        
{store.dispatch({type: 'ADD_TODO', id: this.id++, text:input})}}> {store.dispatch({type: 'TOGGLE_TODO', id:input})}} todos={todos}>
); } } TodoContainer.contextType = MyContext export default TodoContainer;

从上面代码我们可以看到,以上下文隐式传递实际上就是在外层包裹一个Provider组件,将store放进上下文进行传递,Provider包含的后代节点都可以通过context获取到store,避免了store显式的传递。

还没结束

我们这时候在回过头来看看,实际上在我们的容器组件内有非常多的冗余代码:

  • componentDidMountcomponentWillUnmount里的回调注册注销代码
  • 声明上下文类型

还有很重要的但是不易被发现的一点,就是容器组件所包含的展示组件,无论是回调操作逻辑还是状态取值,其实都是通过store来衍生变化的,其中:

  • 属性来源于store.getState获取的值
  • 操作回调来源于store.dispatch操作

因此,我们可以通过一些操作来简化我们的代码,使我们的容器组件生成模型化。redux提供了一个叫connect的函数来完成我们想要的操作,他需要两个入参函数:

  • mapStateToProps 将store.getState和本身的props映射到一个新的对象,这个对象中的字段就是展示参数的属性
  • mapDispatchToProps 将store.dispatch和本身的props映射到一个新的对象,这个对象中的字段就是展示参数的回调操作

这样说会比较抽象,我们以过滤显示为例来说明:

class LinkContainer extends Component {
  componentDidMount(){
      this.unsubscribe = this.context.store.subscribe(()=>{
          this.forceUpdate()
      })
  }

  componentWillUnmount(){
      this.unsubscribe()
  }
  constructor(props) {
    super(props);
  }

  render(){
    let { store } = this.context;
    let state = store.getState();
    let { filter } = this.props;
    return  store.dispatch({type:'SET_VISIBILITY_FILTER', filter: filter})} 
            active={state.visibilityFilter === filter}>
            {this.props.children}
        
  }
}

我们可以看到展示组件需要一个onClick回调和一个active属性,我们可以这么来编写我们需要的两个函数:

const mapStateToProps = (state, ownProps) => {
    return {
        active: state.visibilityFilter === ownProps.filter
    }
}


const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        onClick: ()=> dispatch({type:'SET_VISIBILITY_FILTER', filter: ownProps.filter})
    }
}

接着我们就尝试编写我们自己的connect函数。按照connect函数的定义,他的入参是mapStateToPropsmapDispatchToProps(简化版,实际上还有其他函数),返回是另一个函数,入参是展示组件,返回是容器组件,因此我们可以写出大致框架:

const connect = (mapStateToProps, mapDispatchToProps) =>{
    return (WrappedComponent) => {
        class Connect extends Component {
        }
        return Connect
    }
}

然后按照我们之前的逻辑,我们可以将componentDidMountcomponentWillUnmount里的回调注册注销代码以及声明上下文类型的代码拷贝进来,然后在返回的容器组件中调用我们传入的mapStateToPropsmapDispatchToProps方法来生成新的属性并传递给展示组件返回即可,最终成型的代码如下:

import React, {Component} from 'react'
import {MyContext} from './MyContext'

const connect = (mapStateToProps, mapDispatchToProps) =>{
    return (WrappedComponent) => {
        class Connect extends Component {
            componentDidMount(){
                this.unsubscribe = this.context.store.subscribe(()=>{
                    this.forceUpdate()
                })
            }
          
            componentWillUnmount(){
                this.unsubscribe()
            }
            
            constructor(props){
                super(props)
            }

            render() {
    
                let { store } = this.context;
                let state = store.getState();
                return 
                    {this.props.children}
                
            }
        }
        Connect.contextType = MyContext
        return Connect
    }
}

export default connect;

然后我们使用这个方法来重新创建我们的容器组件:

export default connnect(mapStateToProps, mapDispatchToProps)(FilterLink);

就完成了之前那一堆代码做的事情,同样的,我们还可以针对添加事项容器和事项列表容器做同样的操作,先来看添加事项容器:

  let id = 0;
  export default connect(
    ()=>{}, 
    (dispatch, props)=> {
      return {
        doAdd: (input)=> dispatch({type: 'ADD_TODO', id: id++, text:input})
      }
    })(AddToDo);

因为添加事项没有需要传入的属性,所以connect函数的第一个参数是个空,第二个方法是传入了doAdd的逻辑。再来看事项列表容器:

  export default connect(
    (state, props) => {
      return {todos: state.todos.filter(todo => {
        switch (state.visibilityFilter) {
          case 'SHOW_ALL':
            return true
          case 'SHOW_COMPLETED':
            return todo.completed
        
          case 'SHOW_UNCOMPLETED':
            return !todo.completed  
        }
      })}
    },
    (dispatch, props) => {
      return {doToggle: (input)=> dispatch({type: 'TOGGLE_TODO', id:input})}
    }
    )(ToDoList)

经试验这两个改造都是可用的。

结语

作为从零开始Redux的最后一篇,这次的内容显得多了一点,但是我觉得也是最充实和最能体现Redux价值的一篇。其实我为什么选择Redux作为我一个前端菜鸡的分享题目也是因为,我在看作者整个教程的时候感觉没有任何障碍,完全可以用后端开发的思路来理解

  • 函数式
  • 不可变性
  • 模块化
  • 逻辑分层

可能未来真有可能天下大同吧。
最后再贴一次作者的系列视频。

你可能感兴趣的:([译]从零开始Redux(四)提炼组件)