React & React-Redux & Redux

 文章总结自:

2022-web前端-春招面试-❤️面试题的讲解💕_哔哩哔哩_bilibili转载https://www.bilibili.com/video/BV1s541177W5?p=52但这个视频总结的其实不是太好,大多数时间都只是在照着PPT念,根本讲不出个所以然。借助视频提供的关键考点,又参考了一些文章,最后总结出了本文。如果文章有什么问题,欢迎大家指正,谢谢!

一、Redux是如何将state注入到React组件中去的?

参考文章:

Redux 入门教程(三):React-Redux 的用法 - 阮一峰的网络日志https://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html在开始接下来的分析之前,必须要先明白一点,就是React组件分为哪几类?

组件的分类方式有很多,这里我们着重去分析React-Redux对组件的分类方式。

React-Redux将所有的组件分为两大类:UI组件容器组件

  • UI组件:顾名思义,主要负责的就是界面的渲染,不包含任何的业务逻辑处理,渲染所需的所有数据都来自于参数(props)
  • 容器组件:专门处理数据的部分,将业务逻辑的执行结果,通过组件参数(props)的形式,交给UI组件,再由UI组件进行渲染

React、Redux、React-Redux三者之间的关系

  • React:是一个负责构建用户界面的JS库
  • Redux:是一个管理数据的工具
  • React-Redux:将 “数据仓库” 与 “构建用户界面的UI组件” 相互绑定的绑定库

   Redux

Redux是一个独立于任何语言的数据管理工具。不仅可以在JS中使用,在Java等多门语言中也可以使用。

即使是同一门语言,就比如JS,在使用同一个数据仓库Redux的前提下,不同的 “构建用户界面的JS的库” ,也会对应着不同的绑定库。就像React的绑定库是React-Redux,Vue、Angular的的绑定库是VueJS-Redux、Angular-Redux。

Redux主要负责保存数据,在数据发生变化时,发布变化的数据。React-Redux则是负责订阅发布,将变化的数据交给UI组件,由UI组件将数据呈现到页面上。

这二者各司其职,也正是由于React-Redux的存在,才能将数据仓库ReduxUI组件进行连接。

  • Redux原理

Redux的工作原理为:在window上创建一个对象,将数据保存在这个对象上,那么任意组件就可以跨层级传递数据。

Redux数据交互的方式,就是一个经典的发布订阅器

Button.addEventListener('click', ()=>{ ... });  // 订阅 -- 当触发A的时候就执行B
Button.removeEventListener('click', ()=>{ ... });  // 取消订阅 -- 当触发A的时候,不再执行B
Button.onClick;  // 发布 -- 触发 A 

事件被触发,也就是发布

一个博主发布了一篇文章,所有关注订阅了该博主的用户,而React就相当于是这个博主。

  • Redux 核心源码分析(可能会要求手写!)
/** createStore 参数:
 * reducer:像下面这样,这就是一个reducer   
function todoListReducer(preTodoList, action){
  const {type, data} = action;
  switch(type){
    case 'ADD_TODO':
      return  {
        todoList: [...preTodoList.todoList, data ]
      }
    case 'REMOVE_TODO':
      return {
        todoList: preTodoList.todoList.filter(todo => todo.id !== data)
      }
    default:
      return preTodoList;
  }
}
export {
  todoListReducer
}

 * preloadedState 初始化的 state
 * enhancer       中间件(数据类型:函数,作用:处理异步Redux)
“中间件” 是一个选填的参数,如果想要填,就必须放在参数的末尾。
   如果第二个参数我们想要放中间件,那么就不能有第三个参数;
   如果第二个参数放的是初始化的state,第三个参数可以放中间件,也可以为空
 */

export default function createStore(reducer, preloadedState, enhancer) {
  // 因为我们要根据形参对数据做相应的处理,很明显第二个参数是用来保存初始化的状态的,第三个参数才是用来保存中间件。
  // 所以当第二个参数为中间件时,需要把中间件从第二个参数移到第三个参数上,形参preloadedState置为空即可
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState;
    preloadedState = undefined;
  }
  
  let currentReducer = reducer;
  let currentState = preloadedState; // 整个应用所有的 State 都存储在这个变量里
  let currentListeners = []; // 订阅传进来的的回调函数 <=>  Button.addEventListener('click', () => { ... })

  let nextListeners = currentListeners;
 
  function getState() {
    return currentState;
  }

  // 订阅方法提供给React-Redux的connect,子组件使用该方法订阅store中的数据,以获取变化的数据
  function subscribe(listener) {
  
    if (nextListeners === currentListeners) {
      nextListeners = [...currentListeners];
    }
    nextListeners.push(listener);
		

    /*
      订阅
        const a = Store.subscribe(()=>{});
      取消订阅
        a.unsubscribe();

      因为我们返回的是一个有名字的函数,所以可以通过.funName()的方式调用函数,如果是一个匿名函数,就只能是a()的方式去调用返回的函数了
    */
    return function unsubscribe() {
       if (nextListeners === currentListeners) {
        // 浅复制
        nextListeners = [...currentListeners];
      }

      const index = nextListeners.indexOf(listener);
      nextListeners.splice(index, 1);
    }
  }
/*
  订阅
    Button.addEventListener('click', () => { ... })
  取消订阅
    Button.removeEventListener('click', () => { ... })
*/

  // 提供给React-Redux的connect,在connect中封装进setState,供子组件调用,来传递子组件修改数据的action
  function dispatch(action) {
    currentState = currentReducer(currentState, action); // 调用 reducer 来更新数据
   
    const listeners = (currentListeners = nextListeners); // 保证当前的 listeners 是最新的
    
    for (let i = 0; i < listeners.length; i++) {
      listeners[i](); // 依次执行回调函数
    }

    return action;
  }

  // 手动触发一次 dispatch,初始化
  dispatch({type: 'INIT'});

  return {
    getState,
    dispatch,
    subscribe
  }
}

   React-Redux

在React-Redux中,和Store通讯的API只有两个,分别是connectProvider

1 connect

connect函数的返回值也是一个函数,这个函数可以接收一个UI 组件作为参数,可以生成一个对应的容器组件,并将二者进行连接。

  • counter的使用
// UI组件Counter 

class Counter extends Component {
  render() {
    const { value, onIncreaseClick } = this.props
    return (
      <div>
        <span>{value}</span>
        <button onClick={onIncreaseClick}>Increase</button>
      </div>
    )
  }
}
import { connect } from 'react-redux'

const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

 用法就像上面这样,Counter是一个纯粹的UI组件,而App就是由connect生成的容器组件,同时,connect会将这二者进行连接

之后,Counter就只需要负责页面渲染的部分,所有业务逻辑的处理,只需要交给App,App对数据做相应的处理后,通过各种形式,再交给Counter进行渲染。

connect接收两个参数:mapDispatchToProps、mapStateToProps,负责UI组件与容器组件之间的双向交互,也就是

① 输出逻辑:UI组件想要对数据做处理,如何将这一想法转化为Action并交给容器组件

② 输入逻辑:容器组件根据Action对数据做了处理,如何将处理后最新的数据交还给UI组件

  •  mapDispatchToProps:

主要负责输出逻辑:将UI组件想要对数据进行的操作(例如下面的点击增加3的操作),生成相应的Action。这个Action会由Redux封装进state中,自动发送给Store,让Store对数据去做处理

class Counter extends Component {
  render() {
                // onIncreaseClick方法就是通过mapDispatchToProps提供给组件的修改state的方法
    const { value, onIncreaseClick } = this.props
    return (
      <div>
        <span>{value}</span>
        <button onClick={onIncreaseClick}>Increase</button>
      </div>
    )
  }
}
function mapDispatchToProps(dispatch) {
  return {
    onIncreaseClick: () => dispatch({ 
      // Action
      type: 'INCREMENT',
      data: 3
    })
  }
}
  • mapStateToProps:

主要负责输入逻辑mapStateToProps接受一个state作为参数,通过state内Action中的具体行为指令,对数据做相应的逻辑处理,最后将处理完的数据映射到UI组件的props属性中(映射的过程会在下文的connect源码分析中去讲解,我们只需要暂时先记住,mapStateToProps会将修改完的数据映射到props中即可)。这时,UI组件就可以通过props拿到最新的数据。

我们来看个例子:

class Counter extends Component {
  render() {
         // value的变化就是通过mapStateToProps方法映射来的
    const { value, onIncreaseClick } = this.props
    return (
      <div>
        <span>{value}</span>
        <button onClick={onIncreaseClick}>Increase</button>
      </div>
    )
  }
}
function mapStateToProps(state) {
  return {
    todos: todoListReducer(state.todos, state.type);
  }
}

const todoListReducer = (todos, type) => {
  switch (type) {
    case 'SHOW_ALL':
      return todos;
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed);
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed);
    default:
      return todos;
  }
}

可以看到,state中就保存着数据源Action的属性

todoListReducer其实就是一个reducer,在reducer中,会根据不同的行为指令,对数据源做不同的处理,然后将处理完的数据源返回出去,最终会交给UI组件,来重新渲染。

  • connect核心源码(可能会要求手写!): 
import React from 'react';
import PropTypes from 'prop-types';

const connect = (
  mapStateToProps = state => todoList, 
  mapDispatchToProps = {}
//   |---------------  这里是connect方法的返回值,这个返回值也是一个函数  ---------------|
) => (WrapComponent) => {
  return class ConnectComponent extends React.Component {
    constructor(props, context){
      super(props, context);
      this.state = {
        props: {}  // 为组件添一个state,其中包含一个空的props属性
      }
    }

    // 此处为React-Redux的强制要求,子组件只有设置了该属性,才能够获取到context
    state contextTypes = {
      store: PropTypes.object
    }

    componentDidMount(){
      const {store} = this.context;  // 来源于我们在Provider中创建的context对象,其内部就包含一个store属性
      store.subscribe(() => this.update());  // 订阅Redux数据的更新,一旦Redux中有数据更新,就会执行update方法
      this.update();
    }

    update(){
      const {store} = this.context;
      const todoList = mapStateToProps(store.getState());
      const dispatchProps = mapDispatchToProps(store.dispatch);

      // 将最新的数据、调用dispatch的方法,都放在props中,供子组件使用
      this.setState({
        props: {
          ...this.state.props,  // 将原有的props属性放进去
          ...todoList, // 将获取到的最新数据放在props中
          ...dispatchProps  // 再将调用dispatch的方法放在props中
        }
      })
    }

    render(){
      return <WrapComponent {...this.state.props}></WrapComponent>
    }
  }
}

connect作用总结:connect可以为子组件添加订阅,在订阅中,会将最新的数据连带调用dispatch的方法一同放在props中。供子组件获取最新的数据,以及调用dispatch传递Action。

2. Provider

作用:将Store挂载到context上,让每个组件都可以访问其中的state。

Redux会自动地将UI组件操作数据的Action发送给Store,但数据处理完后,如何将更新的数据交给UI组件呢?

如果将state作为参数一级一级地传递给各个UI组件,目的肯定是可以达到的,但是效果一定会非常差。

于是Provider就出现了,它的核心原理是借助context,将Store放在context上,使用context的发布订阅机制,让每个子组件就在订阅过后,都可以直接访问到Store中的数据。就省去大量的数据传递。

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

// 创建一个store,并将todoList存储在这个store中
let store = createStore(todoTist);

render(
  // 只要将store作为Provider的参数,就可以让store挂到context上
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

这么一来,在数据发生变化时,Redux就会重新在context中发布更新过后的store。UI组件均订阅了context中的store,所在就可以直接拿到更新的数据。

  • Provider核心源码
import React from 'react'
import PropTypes from 'prop-types'

export default class Provider extends React.Component{

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }

  // 此处为React-Redux的强制要求,作用在下方
  getChildContext() {
    return { store: this.store }
  }

  render() {
    return React.children.only(this.props.children)
  }
}
// 依旧是设定context的强制要求
Provider.childContextTypes = {
  store: PropTypes.object
}

定义是双向的,React-Redux强制要求:

  • 根组件Provider 提供实例方法childContextTypes,以及类属性getChildContext
  • 还要求子组件(UI组件)提供contextTypes属性。

在满足了这两个要求之后,子组件才能够获取到context。缺少任意一个,子组件都无法获取到context

最后,生成store对象,并使用Provider在根组件外面包一层。

React中数据的类型

React中数据有三种:

1. context:往所有子组件、孙组件内传递数据

2. props:父组件向子组件传递数据

3. state:组件自身的数据

   答题思路

  1. 首先。明确React与Redux产生关联的是React-Redux库
  2. Redux原理,其实就是一个发布订阅器,它用一个变量帮我们存储所有的state,并且提供了发布功能,来帮我们修改数据
  3. React-Redux的作用就是订阅store里数据的更新,他包含两个重要元素,Provider、connect
  4. Provider的作用就是通过ContextApi将store对象注入到React组件上去
  5. connect方法本质上就是一个高阶组件,在高阶组件中通过订阅Store数据的更新通过调用setState方法触发组件更新

二、Redux在实际项目中的使用,以及它所产生的问题

   为什么要使用Redux?

如果我们的UI层十分简单,并且没有非常多的交互,那么就没有必要使用Redux,借助Redux会需要非常多的样板代码,例如reducer、action等等。并且修改数据也会十分地麻烦,我们需要借助dispatch调用对应的reducer,然后去触发相应的回调,最终才能更新数据,整个过程会十分地麻烦。

在这种情况下,借助state与setState、props会非常地简便。

但是如果我们的代码层级较为复杂,且需要关系较远的几个组件之间相互通信,借助state与props就会十分地复杂,这个时候,选择使用Redux就会很方便。它可以很好地解决跨组件间数据传递的问题,相比较一层一层传递数据的方式,修改数据的过程也会显得非常清晰。

   如果一定是使用,有哪些最佳实践?

较好的最佳实践,其实就是指如何解决上面提到的痛点 - 过多的样板代码。

1. redux-actions

Redux-actions框架之createAction()与handleActions() 用法讲解 - 简书redux-actions redux-actions框架提供了两个常用的API函数 createAction() handleActions() createAction(...https://www.jianshu.com/p/c6096d61ae1c在初始化reducer和action构造器时,减少样板代码

  • Action 一般长这样:
const CounterAction = {
  increase() {
    return {
      type: Constants.INCREASE
    }
  },

  decrease() {
    return {
      type: Constants.DECREASE
    }
  }
}
  • 借助了redux-action的createAction
import {createAction} from 'redux-actions'
import type from '../../constants/actionType'
import actions from '../../actionCreators/movie'

const getMovieList = createAction(type.MOVIE_LIST, actions.movieList)
const getMovieListForDemo = createAction(type.MOVIE_LIST, actions.movieListForDemo)
const getMovieDetail = createAction(type.MOVIE_DETAIL, actions.movieDetail)
const getMovieStory = createAction(type.MOVIE_STORY, actions.movieStory)
const getMovieShowTimeList = createAction(type.MOVIE_SHOWTIME_LIST, actions.movieShowTimeList)
const getMovieComeingNewList = createAction(type.MOVIE_COMEING_NEW_LIST, actions.movieComeingNewList)
const getMovieComment = createAction(type.MOVIE_COMMENT_LIST, actions.movieCommentList)
const getMiniComment = createAction(type.MOVIE_MINI_COMMENT, actions.movieMiniCommentList)
const getPlusComment = createAction(type.MOVIE_PLUS_COMMENT, actions.moviePlusCommentList)
const getTrailerList = createAction(type.MOVIE_TRAILER_LIST, actions.movieTrailerList)
const getActorList = createAction(type.MOVIE_ACTOR_LIST, actions.movieActorList)
const getPictureList = createAction(type.MOVIE_PICTURE_LIST, actions.moviePictureList)

const actionCreators = {
  getMovieList: params => getMovieList(params),
  getMovieDetail,
  getMovieStory,
  getMovieListForDemo,
  getMovieShowTimeList,
  getMovieComeingNewList,
  getMovieComment,
  getMiniComment,
  getPlusComment,
  getTrailerList,
  getActorList,
  getPictureList,
}

export default {actionCreators}

可以发现,整个action的创建逻辑就变得十分清晰明了,一眼就能看出来在做什么,而且,省去很多的模板语句。 

  • reducer:
// react-redux
const defaultState = 10

const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case Constants.INCREASE:
      return state + 1
    case Constants.DECREASE:
      return state - 1
    default:
      return state
  }
}
  • 借助了redux-action的handleActions
import type from '../../constants/actionType'
import {handleActions} from 'redux-actions'

const initialState = {
  movieDetail: {},
  commentData: {}
}

const actions = {}

actions[type.MOVIE_DETAIL + type.FETCH_SUCCESS_SUFFIX] = (state, action) => {
  return {
    ...state,
    movieDetail: action.payload.data
  }
}

actions[type.MOVIE_COMMENT_LIST + type.FETCH_SUCCESS_SUFFIX] = (state, action) => {
  return {
    ...state,
    commentData: action.payload.data
  }
}

const reducer = handleActions(actions, initialState)

export default reducer

2. yeoman

yeoman是一个cli工具,可以用来一键生成模板代码

yo project-name: component my/namespaced/components/name

   答题思路

Redux最大的弊端就是样板代码太多,修改数据的链路太长。

解决方案:

  1. 借助Redux-actions来减少对固定代码的书写,同时可以让action、reducer的解构更清晰
  2. 借助yeoman等cli工具,帮助我们用命令一键创建模板代码

   Redux的异步问题怎么解决?

在Redux遇到如请求后台数据、延迟执行、setTimeout、setInterval等异步问题时,该如何解决?

redux之所以不能处理异步问题,是因为在dispatch中,默认只能接收一个Object类型的action,后面需要根据对象里面的type属性去做相应的处理。但是如果数据类型变为了其他类型,就比如在异步时,通常需要传入一个函数,这时就无法获取到type属性。

我们可以通过Redux-thunkRedux-saga等其他中间件来使Redux支持异步

redux异步问题可以通过中间件来解决。

1. Redux-thunk

function increment() {
  return {
    type: 'INCREMENT_COUNTER'
  };
}

// 引入了redux-thunk中间件后,就可以异步使用了
function incrementAsync(){
  return (dispatch) => {
    setTimeout(() => {
      dispatch(increment());
    }, 1000)
  };
}

2. Redux-saga

让异步行为成为架构中独立的一层(成为saga)

三、React中的Hooks

React的Hooks解决了函数式组件一下三个问题:

区别函数式组件类组件
是否有this
是否有声明周期
是否有state

同时,也让一些公共方法可以被更简便、更实用地定义出来。 

原生的Hooks我们就不讲解了,使用起来可以说是非常地简便,网上的文档很多,主要就是讲解一下如何自定义一个Hooks。

   需求:提取公共的发送请求的方法

需求:在页面A与页面B上请求一部电影的详细数据

A -> 加载时 -> 发送请求:http://swapi.io/api/films/1

B -> 加载时 -> 发送请求:http://swapi.io/api/films/2

拿到请求后,调用this.setState更新数据。

class App extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      loading: false,
      data: {}
    };
  }

  componentDidMount() {
    this.setState({
      loading: true
    })

    fetch(`http://swapi.io/api/films/${this.props.fileId}`)
      .then(data => {
        this.setState({
          data: data,
          loading: false
        })
      });
  }

  componentDidUpdate() {
    this.setState({
      loading: true
    })

    fetch(`http://swapi.io/api/films/${this.props.fileId}`)
      .then(data => {
        this.setState({
          data: data,
          loading: false
        })
      });
  }

  // 抽取发送请求以及更改状态的方法:(错误,此处是无法抽取的)
  fetchData = () => {
    this.setState({
      loading: true
    })
    fetch(`http://swapi.io/api/films/${this.props.fileId}`)
      .then(data => {
        this.setState({
          data: data,
          loading: false
        })
      });
  }

  render() {
    const {loading, data} = this.state;
    if(loading === true){
      return <p>Loading...</p>
    }
    return(
      <div>
        <p>电影名称:{data.title}</p>
        <p>导演:{data.producer}</p>
        <p>发布日期:{data.release_data}</p>
      </div>
    );
  }
}

可以很明显地看到在DidMount和DidUpdate时都有发送请求的过程,但是却无法被抽离出来,因为这其中有一个this.setState方法,将该方法抽成一个公共方法时,this始终都指向当前组件,无法指向调用该方法的组件,解决方案:抽取成一个自定义的Hooks。

自定义具有特定功能的Hooks

// 注意 hooks 约定必须以 use 开头(useA, useB)
const useFetchData = (filmId) => {
  const [loading, setLoading] = useState(false); // 只有在第一次加载的时候才会 被 false 复制
  const [data, setData] = useState({});
  
  useEffect(() => {
    setLoading(true); // 1. 设置 loading 为 true
    fetch(`https://swapi.co/api/films/${filmId}`) // 2. 发送请求
      .then(data => {
        setData(data); // 3. 收到请求后,设置 data 为请求到的数据
        setLoading(false); // 4. 设置 loading 为 false
      });
  }, [filmId]); // filmId 变化的时候,才触发 useEffect

  return loading, data];
};

使用自定义的Hooks

import useFetchData from './useFetchData';

function App({filmId}){
  const [loading, data] = useFetchData(fileId);

  if(loading === true){
    return <p>Loading...</p>
  }

  return(
    <div>
      <p>电影名称:{data.title}</p>
      <p>导演:{data.producer}</p>
      <p>发布日期:{data.release_data}</p>
    </div>
  );
}

   答题思路

  1. React Hooks是一个全新的API,可以用函数来写所有的组件
  2. 可以让函数式组件也拥有自己的状态(包括state和声明周期函数)
  3. 可以通过创建自定义的Hooks来抽离可复用的业务组件
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

麦田里的POLO桔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值