Redux进阶

1 UI组件与容器组件的拆分

UI组件叫做傻瓜组件 (负责页面的渲染)(特殊情况也可以做一些简单的逻辑),容器组件叫做聪明组件 (负责页面逻辑处理),之前我们把渲染(render)和逻辑都在一个组件(todolist)里写着

现在我们首先在src下创建一个TodoListUI.js,并创建TodoListUI组件和导出

将原Todolist.js渲染的部分(render)部分剪切出来到TodoListUI.js中。在TodoList.js组件中导入TodoListUI组件,在其render函数中返回该UI组件。

在TodoListUI.js中

1.将todolist中的UI组件拷贝过来

2.然后将其它的‘antd’组件引入

3.父组件传值给子组件

例如:在render里面组件上传值<TodoListUI inputValue={this.state.inputValue}/>,然后TodoListUI.js组件就可以接收使用了,写成this.props.inputValue即可

4.父组件传函数方法给子组件和传值一样

例如:在render里面组件上传值<TodoListUI inputValue={this.state.inputValue} handleInputChange={this.handleInputChange}/>,然后TodoListUI.js组件就可以接收使用了,写成this.props.handleInputChange即可

5.需要注意的是函数中有传值的问题

必须用箭头函数进行传值onClick={(index) => {this.props.handleItemDelete(index)}}

如果写成如下:onClick={this.props.handleItemDelete(index)}就会发生错误,因为传值有要求,必须是纯函数

TodoListUI.js
import React, { Component } from 'react';
import { Input } from 'antd';
import { Button } from 'antd';
import { List } from 'antd';

class TodoListUI extends Component {
    render() {
        return (
            <div style={{marginTop: '10px',marginLeft: '20px'}}>
                <div >
                    <Input  
                        value={this.props.inputValue}   // 修改的地方,接收使用父组件传的值
                        placeholder="todo info"  
                        style={{width: '300px'}}
                        onChange={this.props.handleInputChange}  // 修改的地方,接收使用父组件传的方法
                    />
                    <Button 
                        type='primary'
                        style={{marginLeft:'20px'}}
                        onClick={this.props.handleBtnClick}  // 修改的地方,接收使用父组件传的方法
                        >            
                            提交
                    </Button>
                </div>
                <List
                    style={{marginTop: '20px',width: '300px'}}
                    bordered
                    dataSource={this.props.list}
                    renderItem={(item,index) => (
                        // 下面是修改的地方,接收使用父组件传的方法(并解决函数传值问题)
                        <List.Item onClick={() => {this.props.handleItemDelete(index)}}>  
                        //上面这里前面已经传index值了,所以箭头函数前面不需要再传。
                            {item}
                        </List.Item>
                    )}
                />
            </div>
        )
    }
}

export default TodoListUI;
todolist.js
import React, {Component} from 'react';
import 'antd/dist/antd.css'; 
import store from './store/index';
import { getInputChangeAction,getAddItemAction,getDeleteItemAction } from './store/actionCreators'
import TodoListUI from './todolistUI';

class TodoList extends Component {

    constructor(props) {
        super (props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        this.handleBtnClick = this.handleBtnClick.bind(this);
        this.handleItemDelete=this.handleItemDelete.bind(this);
        //store的内容只要发生改变,函数就会执行
        store.subscribe(this.handleStoreChange);
    }

    render() {
        return (
        <TodoListUI                                      // 这里都是修改的地方
            inputValue={this.state.inputValue}           // 传值给子组件TodoListUI
            list={this.state.list}                       // 传值给子组件TodoListUI
            handleInputChange={this.handleInputChange}   // 传方法给子组件TodoListUI
            handleBtnClick={this.handleBtnClick}         // 传方法给子组件TodoListUI
            handleItemDelete={this.handleItemDelete}     // 传方法给子组件TodoListUI
         />  
        )
    }

    handleInputChange(e) {
        const action = getInputChangeAction(e.target.value);
        store.dispatch(action);
    }

    handleStoreChange() {
        console.log('store change');
        this.setState(store.getState());
    }

    handleBtnClick() {
        const action = getAddItemAction();
        store.dispatch(action);
    }

    handleItemDelete(index) {
        const action = getDeleteItemAction(index);
        store.dispatch(action)
    }
}

export default TodoList;

2 无状态组件

无状态组件就类似于一个函数

什么情况下使用无状态组件呢?当一个普通的组件只有render函数的时候,我们可以通过无状态组件,替换普通组件

无状态组件的优势是什么?之前普通组件是执行一个class类的组件,也会执行生命周期函数。现在改成函数,就只是执行了一个函数。所以无状态组件性能明显比较高

所以如果我们将刚刚的UI组件改为无状态组件,就需要首先定义一个函数,函数接收props参数(父组件参数)返回之前render中渲染返回的内容(所以函数里也就可以直接使用props参数,不需要写成this.props啦)。

我们将原来的UI组件TodoListUI.js改为无状态组件

TodoListUI.js
import React, { Component } from 'react';
import { Input } from 'antd';
import { Button } from 'antd';
import { List } from 'antd';

//当一个普通的组件只有render函数的时候
//我们可以通过无状态组件,替换普通组件
//无状态组件性能比较高
function TodoListUI(props) {
    return(
        <div style={{marginTop: '10px',marginLeft: '20px'}}>
            <div >
                <Input  
                    value={props.inputValue}
                    placeholder="todo info"  
                    style={{width: '300px'}}
                    onChange={props.handleInputChange}
                />
                <Button 
                    type='primary'
                    style={{marginLeft:'20px'}}
                    onClick={props.handleBtnClick}
                    >            
                        提交
                </Button>
            </div>
            <List
                style={{marginTop: '20px',width: '300px'}}
                bordered
                dataSource={props.list}
                renderItem={(item, index) => (
                    <List.Item onClick={() => {props.handleItemDelete(index)}}>
                        {item}
                    </List.Item>
                )}
            />
        </div>
    )
}

export default TodoListUI;

3 Redux中发送异步请求获取数据

todolist.js
componentDidMount() {
    axios.get('/todolist.json')
      .then((res) => {
      const data = res.data;
      const action = initListAction(data);    // 将请求到数据放入store中,这里initListAction为actionCreators.js中封装的action
      store.dispatch(action);                 // 然后将action传给store
    })    
}
actionCreators.js
import {CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM, INIT_LIST_ACTION} from './actionType'  // 新增导入type

export const getInputChangeAction = (value) => ({
    type: CHANGE_INPUT_VALUE,
    value
})

export const getAddItemAction = () => ({
    type: ADD_TODO_ITEM,
})

export const getDeleteItemAction = (index) => ({
    type: DELETE_TODO_ITEM,
    index
})

export const initListAction = (data) => ({           // 新增
    type: INIT_LIST_ACTION,
    data
})
actionType.js
export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_TODO_ITEM = 'add_todo_item';
export const DELETE_TODO_ITEM = 'delete_todo_item';
export const INIT_LIST_ACTION = 'init_list_action';       // 新增
reducer.js
import {CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM, INIT_LIST_ACTION} from './actionType'

const deafultState = {
    inputValue: '',
    list: []
}

//记录本
//state 图书馆书籍的信息-> 存储的数据
//state -> 上次存储的数据 action -> 用户传过来的那句话
//reducer 可以接受state,但是不可以修改state
export default (state = deafultState, action) => {
    if(action.type === CHANGE_INPUT_VALUE){
        const  newState = JSON.parse(JSON.stringify(state));  //深拷贝
        newState.inputValue = action.value;
        return newState
    }
    if(action.type === ADD_TODO_ITEM){
        const  newState = JSON.parse(JSON.stringify(state));  //深拷贝
        newState.list.push(newState.inputValue);
        newState.inputValue = '';
        return newState
    }
    if(action.type === DELETE_TODO_ITEM){
        const  newState = JSON.parse(JSON.stringify(state));  //深拷贝
        newState.list.splice(action.index, 1)
        return newState
    }
    if(action.type === INIT_LIST_ACTION){
        const newState = JSON.parse(JSON.stringify(state));
        newState.list = action.data;
        return newState
    }
    return state;
}

4 使用Redux-thunk中间件进行ajax请求

4.1 中间件的安装

网站:https://github.com/reduxjs/redux-thunk

安装命令:npm install --save redux-thunk

查看上面网站信息:

img

然后修改我们src/store/index中的引入

4.2 thunk作用

可以将异步请求或者复杂的逻辑放到actionCreators中的action里面去处理和管理

4.3 导入

我们按文档要求修改

src/store/index.js
import { createStore,applyMiddleware } from 'redux';
import reducer from'./reducer';
import thunk from 'redux-thunk';

const store = createStore(
    reducer,
    applyMiddleware(thunk),
    );

export default store;

在引入之后,发现Redux插件不能使用,查看文档来解决:

https://github.com/zalmoxisus/redux-devtools-extension

img

继续修改src/store/index.js
import { createStore,applyMiddleware,compose } from 'redux';       // 引入applyMiddleware和compose
import reducer from'./reducer';
import thunk from 'redux-thunk';

const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?   
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
    }) : compose;

const enhancer = composeEnhancers(
    applyMiddleware(thunk),
    );

const store = createStore(reducer, enhancer);

export default store;

这样配置后reducer和thunk都可以使用了。在actionCreators.js中,如果不使用thunk中间件的话,我们就不能return一个函数(只能return对象,例如:type对象)给action,就会报错。

现在我们可以返回一个函数,所以就可以将Ajax请求数据封装在actionCreatoes.js中,然后在组件(TodoList.js)componentDidMount中创建action时调用所封装的函数。因为需要使用axios,所以把组件中axios的导入剪切到actionCreators.js中。

src/TodoList.js 添加代码
import { getInputChangeAction, getAddItemAction, getDeleteItemAction,getTodoList} from './store/actionCreators'


componentDidMount() {
    const action = getTodoList();             //这里调用getTodoList返回的是一个函数,getTodoList在actionCreators中封装
    //当把函数发送给store,该函数会自动执行
    store.dispatch(action);  
}
src/store/actionCreators.js
import axios from 'axios';

//函数方法可以接收到dispatch方法
//可以使用dispatch给store发送action
export const getTodoList = () => {
    return (dispatch) => {
        axios.get('/todolist.json')
        .then((res) => {
            const data = res.data;
            const action = initListAction(data);
            dispatch(action);
        })
    } 
}

由上述代码,我们的getTodoList返回的是一个函数,然后todolist通过 store.dispatch来执行该函数。函数执行的时候,我们通过axios获取到了json数据,然后将其赋值给data

之后我们创建action来更改数据,所以再执行initListAction的方法,此时的action就是一个对象了,然后我们通过dispatch来发送action给store。这里需要将display方法传递过来使用。

最后会在Reducer中执行,返回给store新的state

if(action.type === INIT_LIST_ACTION){
    const newState = JSON.parse(JSON.stringify(state));
    newState.list = action.data;
    return newState
}

最终由组件中 store.subscribe(this.handleStoreChange)监听store数据变化来执行handleStoreChange

使用setState来更新数据。

handleStoreChange() {
  this.setState(store.getState());
}

4.4 到底什么是Redux中间件?

img

中间:指的是Action和Store之间。中间件都是对store的Dispatch方法的升级,使其具有其他能力。

比如:thunk中间件使Dispatch可以派发action定义的函数(之前只能是对象),又比如logger中间件使Dispatch在派发action时打印日志。

这里thunk中间件的使用,使得Dispatch派发action时,如果传递过来的是对象,直接传递给store;如果传递过来的是函数,不会直接传递给store(先调用函数);让函数先执行,需要调用store才调用store

5 Redux-saga中间件的使用

5.1 Redux-saga的安装

https://github.com/redux-saga/redux-saga

参考文档:https://redux-saga-in-chinese.js.org/docs/basics/index.html

npm install redux-saga --save

yarn add redux-saga

5.2 Redux-saga的使用

首先在store文件夹下创建sagas.js文件。然后修改store中的代码(index.js)。

src/store/index.js
import { createStore,applyMiddleware,compose } from 'redux';
import reducer from'./reducer';
import createSagaMiddleware from 'redux-saga'
import TodoSagas from './sagas'

const sagaMiddleware = createSagaMiddleware()

const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?   
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
    }) : compose;

const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));

const store = createStore(reducer, enhancer);

sagaMiddleware.run(TodoSagas);

export default store;

这里首先导入 Redux的模块:import { createStore,applyMiddleware,compose } from 'redux';然后导入 saga模块:import createSagaMiddleware from 'redux-saga'之后导入我们自己创建的模块:import TodoSagas from './sagas'为了使得Redux tool 和 saga都能使用,因此使用了类似redux-thunk里面的方法,最后:sagaMiddleware.run(TodoSagas);

src/store/sagas.js
function* mySaga() {

}

export default mySaga;

先默认saga.js内容写成上面这样,防止报错。配置sagas后,dispatch派发的action不止store(默认给reducer)能接收到action,sagas.js也就能接收到dispatch派发的action了。

5.3 Todolist代码修改(使用Redux-saga完成功能.1)

修改如下的代码

src/TodoList.js
import { getInitList } from './actionCreators';  // 增加getInitList引入

componentDidMount() {
  const action = getInitList();
  store.dispatch(action);  
}
src/store/actionCreators.js
import { GET_INIT_LIST } from './actionType';  // 增加这个action的type常量的导入

export const initListAction = (data) => ({
    type: INIT_LIST_ACTION,
    data
})

export const getInitList = () => ({    // 创建这个action
    type: GET_INIT_LIST
})
src/store/actionTypes.js
export const GET_INIT_LIST = 'get_init_list'; // 增加这个type常量的定义

5.4 使用Redux-saga完成功能.2

可以参考官方文档:https://github.com/redux-saga/redux-saga

首先需要在saga.js中导入takeEvery,put方法:import { takeEvery,put } from 'redux-saga/effects';其中takeEvery意思是捕捉每一个类型

src/store/sagas.js
import { takeEvery,put } from 'redux-saga/effects';   // 导入takeEvery,put方法
import { GET_INIT_LIST } from './actionType';
import axios from 'axios';
import { initListAction } from './actionCreators';

function* mySaga() {
  //当捕捉到type为GET_INIT_LIST的action,就会执行后面的getInitList方法。以前是在reducer中判断type来执行方法
  yield takeEvery(GET_INIT_LIST, getInitList);
}

function* getInitList() {
  //ajax 获取失败
  try {
    const res = yield axios.get('/todolist.json'); // yield指等后面执行完成之后再往下进行
    const action = initListAction(res.data);
    yield put(action);    // 之前我们使用store.dispatch(action)来派发,不过这里没有store这个仓库,不过saga中间件新增了put方法。
  }catch(e){
    console.log('list.json 网络请求失败');
  }          
}

export default mySaga;

这里saga.js接收到store.js派发的action后,来判断action的type类型,从而执行对应的函数方法。按generator函数规则来写,并把store.dispatch换成put方法。

注:saga与thunk的区别

thunk只是拓展了action的功能,使得action能够返回一个函数,同时也是和action混合在一起使用的。而saga则将这些异步请求的代码,单独放到了一个文件中进行处理,防止对action污染。此外saga还拓展了许多API,可以实现不同的功能。可以参考如下的文档。

https://redux-saga-in-chinese.js.org/docs/basics/index.html

6 React-Redux的使用

之前文件全部清除,只留下一个src/index.js文件,重新来。创建Todolist组件并引入到index.js中。新建store文件夹内容(store/index.js,store/reducer.js)

6.1 React-Redux的安装

yarn add react-redux

npm install --save react-redux

6.2 使用React-Redux

它可以让我们更方便的在React 中使用Redux

(1)创建Provider(react-redux的第一个核心API)

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './todolist';
import { Provider } from 'react-redux';    //导入Provider
import store from './store';

const App = (
  //提供器连接了store,那么Provider里面的所有组件,都有能力获得store中的内容
  <Provider store={store}>
      <TodoList />
  </Provider>
);

ReactDOM.render(
    App,
  document.getElementById('root')
);

我们创建了一个App,里面有一个Provider 提供器,然后里面是我们的组件TodoList,只要写在Provider 中的组件,都有能力获取store中的内容。

(2)TodoList(使用Provider后store数据的获取)

src/TodoList.js(connect为react-redux的第二个核心API)
import React,{ Component }from 'react';
import { connect } from 'react-redux';

class TodoList extends Component {

    render() {
        return (
            <div>
                <div>
                    <input 
                        value={this.props.inputValue}
                        onChange={this.props.changeInputValue}
                    />
                    <button>提交</button>
                </div>
                <ul>
                    <li>hello</li>
                </ul>
            </div>
        )
    }
}

//把Store中的内容映射到props中,这里state就是指store里面的数据,props指公共组件的里面的数据(reducer.js)
const mapStateToProps = (state) => {
    return {
      	// 这里inputValue实际上在props中
        inputValue: state.inputValue   // 使得store里面的inputValue映射到props里面的inputValue
    }
}

//把store的dispatch方法映射到props中
const mapDispatchToProps = (dispatch) => {
    return {
      	// 这里changeInputValue实际上在props中,所以可以直接使用store的dispatch方法
        changeInputValue(e) {
            const action = {
                type: 'change_input_value',
                value: e.target.value
            }
            dispatch(action);
        }
    }
}


//connect将TodoList这个UI组件和mapStateToProps,mapDispatchToProps连接形成容器组件。
// 所以实际上返回的是连接后的容器组件。
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

connect这个API使得这个组件和store做连接(这个组件在Provider里面)

mapStateToProps, mapDispatchToProps是衍生出的连接方式

之后changeInputValue发送action到reducer中进行一个处理:

src/store/reducer.js
const defaultState = {
    inputValue: 'Hello world',
    list: []
}
export default(state = defaultState, action) => {
    if(action.type === 'change_input_value'){
        const newState = JSON.parse(JSON.stringify(state));
        newState.inputValue = action.value;
        return newState;
    }
    return state;
}

6.3 代码补全及其优化

connect连接了数据,页面跟着数据改变。所以也不需要我们像以前那样还需要用store的subscribe方法来监测数据的改变,从而进一步重新渲染页面。

以下代码:Todolist是一个UI组件,connect 将UI组件和逻辑相结合,只负责渲染,组件里只有一个render方法中,所以我们可以将其写成一个无状态组件。

src/TodoList.js
import React from 'react';
import { connect } from 'react-redux';

function TodoList(props) {
    const { inputValue,
        changeInputValue,
        handleClick,
        list,
        handleDelete } = props;

    return (
        <div>
            <div>
                <input 
                    value={inputValue}
                    onChange={changeInputValue}
                />
                <button onClick={handleClick}>提交</button>
            </div>
            <ul>
                {
                    list.map((item, index)=>{
                        return (
                            <li onClick={() => handleDelete(index)} key={index}>
                                {item}
                            </li>
                        )
                    })
                }
            </ul>
        </div>
    )
}

//把Store中的内容映射到props中,这里state就是指store里面的数据,props指公共组件的里面的数据(reducer.js)
const mapStateToProps = (state) => {
    return {
      	// 这里inputValue实际上在props中
        inputValue: state.inputValue,  // 使得store里面的inputValue映射到props里面的inputValue
        list: state.list
    }
}

//把store的dispatch方法映射到props中
const mapDispatchToProps = (dispatch) => {
    return {
        // 这里changeInputValue实际上在props中,所以可以直接使用store的dispatch方法
        changeInputValue(e) {
            const action = {
                type: 'change_input_value',
                value: e.target.value
            };
            dispatch(action);
        },

        handleClick() {
            const action = {
                type: 'add_item'
            };
            dispatch(action);
        },

        handleDelete(index) {
            const action = {
                type: 'delete_item',
                index
            }
            dispatch(action);
        }
    }
}

//connect将TodoList这个无状态组件(UI组件改的)组件和mapStateToProps,mapDispatchToProps连接形成容器组件。
// 所以实际上返回的是连接后的容器组件。
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

容器组件:包含组件渲染和业务逻辑。

UI组件:只有渲染,没有逻辑。

无状态组件:是一个函数,UI组件只有render函数时,可改为无状态组件。

src/store/index.js
import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(
    reducer,
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
    );

export default store;
src/store/reducer.js
const defaultState = {
    inputValue: '',
    list: []
}

export default(state = defaultState, action) => {
    if(action.type === 'change_input_value'){
        const newState = JSON.parse(JSON.stringify(state));
        newState.inputValue = action.value;
        return newState
    }
    if(action.type === 'add_item'){
        const newState = JSON.parse(JSON.stringify(state));
        newState.list.push(newState.inputValue);
        newState.inputValue = "";
        return newState
    }

    if(action.type === 'delete_item'){
        const newState = JSON.parse(JSON.stringify(state));
        newState.list.splice(action.index, 1);
        return newState
    }
    return state;
}

其实,上述代码还没有完全的精简

还需将代码中的方法 actionCreators 还有actionType 将代码进行一个分离。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值