React数据

482 阅读29分钟

前言

闲余时间对 React、Redux、React-Redux 重新翻了一遍官方 API,有了一些新的感悟与思考,将其整理与分享给大家。

Porps

Props(Properties属性的简写),是一种将数据从父组件传递给子组件的手段。它的工作方式类似于函数的入参,且是只读的,这意味着 props 是不能修改的。

大家都知道 React 数据流向是单向的,那何谓单、双向呢,赞同的是 只有 UI控件 才存在双向,非 UI控件 只有单向 这个结论(除了一些特殊场景),具体可以看这 单向数据绑定和双向数据绑定的优缺点,适合什么场景?

在 React 中单向数据流是指:数据的流向只能由父组件通过 props 将数据传递给子组件,不能由子组件向父组件传递数据,要想实现数据的双向绑定,只能由子组件接收父组件 props 传过来的方法去改变父组件的数据,而不是直接将子组件的数据传递给父组件。

Props 与 State的一些区别

描述 state props
在组件内设置默认值
在组件内可改变
可在父组件中改变其值
可作为子组件的初始值
可从父组件接收
// 最简单的Demo
function Welcome (props) {
  return <h1> Hello, { props.name }</h1>;
}
 
function App() {
  const props = { name: 'Tom' };
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome {...props} />
    </div>
  );
}
 
ReactDOM.render(
  <App />,
  document.getElementById('root')
);

注意:所有 React 组件都必须保护它们的 props 不被更改!

如图,在 Props 有值的情况下去修改对应的值,不会重新渲染,且如果修改会有报错提示。

componentDidMount() {
    this.props.common = {}
}

context

基本介绍

context 是React16新出的 API, Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

Props的传递逻辑

context的传递逻辑

API

API文档地址

  • React.createContext
    • 用法:const MyContext = React.createContext(defaultValue); MyContext中会有两个属性:Provider 与 Consumer
    • 注意事项:
      • 当 React 渲染一个订阅了 Context 对象的组件,该组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
      • 只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。
  • Context.Provider
    • 用法:<MyContext.Provider value={ 某个值 }>
    • 注意事项:
      • Provider 接收一个 value 属性,传递给消费组件。
      • 一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
      • 当传递对象给 value 时,检测变化的方式会导致一些问题 zh-hans.reactjs.org/docs/contex…
  • Class.contextType
    • 用法:zh-hans.reactjs.org/docs/contex…
    • 注意事项:
      • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。
      • 可以用 this.context 来访问到最近 Context 上的那个值。可以在任何生命周期中访问到它,包括 render 函数。
      • 该 API 只允许订阅单一的 context。多个的订阅方式:zh-hans.reactjs.org/docs/contex…
  • Context.Consumer
    • 用法:<MyContext.Consumer> {value => 基于 context 值进行渲染 } </MyContext.Consumer>
    • 注意事项:
      • 需要使用函数作为子元素。这个函数接收当前的 context 值,返回一个 React 节点。
      • 如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue
  • Context.displayName
    • 用法:const MyContext = React.createContext( some value ); MyContext.displayName = 'MyDisplayName';

Demo

// 官方Demo
class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}
 
function Toolbar(props) {
  // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}
 
class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
 
class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}
 
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
 
class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  // 需要配合babel https://babeljs.io/docs/en/babel-plugin-proposal-class-properties
  // static contextType = ThemeContext;  // 可以代替最后一句代码
  render() {
    return <Button theme={this.context} />;
  }
}
ThemedButton.contextType = ThemeContext;

其他

如果用官方的DEMO,结合ESlint 在下述情况下会有报错:

父组件 引用 子组件 子组件引用孙组件 ,即 父组件引用了孙组件 ,孙组件又直接引用了父组件的部分内容( Comsumer 而对象)。此时 Eslint 会报错误 Dependency cycle(循环依赖)

因此建议是将 Provider 与 Consumer 的创建在单独一个文件中。

Pubsub

基本介绍

一个订阅、发布模式的库,github,设计思想自然是订阅、发布模式。

在“发布者-订阅者”模式中,称为发布者的消息发送者不会将消息编程为直接发送给称为订阅者的特定接收者。这意味着发布者和订阅者不知道彼此的存在。存在第三个组件,称为代理或消息代理或事件总线,它由发布者和订阅者都知道,它过滤所有传入的消息并相应地分发它们。换句话说,pub-sub 是用于在不同系统组件之间传递消息的模式,而这些组件不知道关于彼此身份的任何信息。

API

  • 发送消息:PubSub.publish(名称,数据) PubSub.publishSync(名称,数据)
    • 名称是唯一的
    • 不太建议使用同步模式,同步模式某些场景下速度很稍快,但是同一主题更新时容易出现混乱
  • 订阅消息:PubSub.subscrib(名称,回调函数)
  • 取消订阅:PubSub.unsubscrib(名称)
    • 名称可以传publish 后返回的对象,表示取消该项的回调订阅
    • 名称可以传 publish 中的第一个参数,表示取消该名称下的所有订阅
  • 取消所有订阅:PubSub.clearAllSubscriptions()

Demo

const PubSub = require('pubsub-js');
 
class App extends React.Component {
 
  handleClick = () => {
    PubSub.publish('testPubSub', {test: '123'});
  }
 
  render() {
    return (
      <button onClick={this.handleClick}>按钮</button>
    );
  }
}
 
 
class OtherAPP extends React.Component {
     
  pubsubItem = null
 
 
  componentDidMount() {
    PubSub.subscribe('testPubSub', this.testPub);
    // this.pubsubItem = PubSub.subscribe('testPubSub', this.testPub);
  }
 
  testPub = (msg:any, data:any)=>{
    console.log(msg, data);
  }
 
  componentWillUnmount() {
    PubSub.unsubscrib(this.testPub);
    // PubSub.unsubscrib(this.pubsubItem);
    // PubSub.unsubscrib('testPubSub');
  }
 
  render() {
    return <Button theme={this.context} />;
  }
}
ThemedButton.contextType = ThemeContext;

Redux

基本介绍

可能会认为 Redux 是为了解决 React 父子组件数据交流问题而实现的一个第三方库,因为从实际应用中,它的价值也常常体现在这。

但实际上 Redux 是 JavaScript 状态容器,用于提供可预测化的状态管理的一个第三方库,与 React 无关。

Redux 的设计动机是为了让 state 的变化变的可预测,这里的 state 不仅仅只是指 React 中的 state,它可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。

可预测的意思就是字面意思,是指开发者知道什么时候会让 state 产生变更,以及期待的变更后的值是什么样。

先从它的逻辑处理说起,Redux的设计思想可以用下图表示,官网介绍的设计思想

这里有三个新名词:Action、Store、Reducer,这就是Redux的三个核心模块,下面会具体分析。先用一个通俗坐公交的例子,帮助大家理解Redux(此处假设公交车司机只专注驾驶、开关门)。

React Components: 要坐车的人

Action Creators: 要坐车的人

Store: 公交车上的人数

Reducers:公交车辅助管理人员(不知道详细职位名称)

在一个公交站,车来了,要坐车的人( React Components )想要坐车,于是做出了上车招手的动作( dispatch action ),车上的管理人员看到了( Reducers 收到了 action ,以及收到了车上变更的人数),他告诉司机有人要上车,待司机开门,乘客成功上车( Reducers 返回了新的state,React Components 重新渲染)。从例子中,我们可以清晰的预测到,车上人数将要更新,且人数要+1,这就是可预测。

回到Redux,它的可预测是通过它的三大原则实现的:

  • 单一数据源:整个项目的应用只有一个 state ,这个集合了整个项目状态的状态树就是 store。
    • 整个项目中有且仅有一个 store 。
  • state 只读:状态树上的 state,开发者仅可读取,不可以修改。想要修改,只能通过 action。
    • action 是一个用于描述已发生事件的普通对象。这样确保了视图和网络请求都不能直接修改 state,它们只能表达想要修改的意图。
    • 目的是为了让所有的修改都被集中化处理,且严格按照一个接一个的顺序执行。
  • 使用纯函数执行修改:开发者需要描述 action 如何改变 state tree ,这个描述的内容我们称为 reducer,reducer 应该为一个纯函数。
    • 纯函数的目的是保证不会产生副作用,即 y = f(x),在 x 相同时,得到的 y 一定相同。

总结:全局只有一个状态,正常情况下,我们只能读数据,不能修改,当想要修改的时候,只能发起一个修改状态的请求,处理这个请求的功能函数为一个纯函数,保证传出新的状态没有副作用。

API

createStore(reducer, [preloadedState], enhancer)

用途:创建一个 store 来以存放应用中所有的 state。

参数解释:

  • reducer (Function): 这是个纯函数,它接收两个参数,分别是当前的 state 树和要处理的action,这个函数的返回结果是新的state。

  • [preloadedState] (any): 初始时的 state。可以是个普通对象,也可以是immutable,也可以是其他值,只要我们编写的reducer能够理解。

    • 简单介绍一下,immutable是一种一旦创建,就不能更改的数据。每当对Immutable对象进行修改的时候,就会返回一个新的Immutable对象,以此来保证数据的不可变。
  • enhancer (Function): Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 相似,它允许我们通过复合函数改变 store 接口。

// 简单的例子
import { createStore } from 'redux'
 
function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}
 
let store = createStore(todos, ['Use Redux'])
 
store.dispatch({
  type: 'ADD_TODO',
  text: 'Read the docs'
})
// enhancer例子
import { createStore } from 'redux'
 
function autoLogger() {
  return createStore => (reducer, initialState, enhancer) => {
    const store = createStore(reducer, initialState, enhancer)
    function dispatch(action) {
      console.log(`dispatch an action: ${JSON.stringify(action)}`);
      const res = store.dispatch(action);
      const newState = store.getState();
      console.log(`current state: ${JSON.stringify(newState)}`);
      return res;
    }
    return {...store, dispatch}
  }
}
function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}
 
let store = createStore(todos, ['Use Redux'], autoLogger())
 
store.dispatch({
  type: 'ADD_TODO',
  text: 'Read the docs'
})

这个例子中autoLogger()就是一个store enhancer, 它改变了store dispatch的默认行为,在每次发送action前后,都会输出日志信息。

其他注意项:

  • 可以使用 combineReducers 来把多个 reducer 创建成一个根 reducer。这种情况下[preloadedState] 只能传递普通对象。
  • 不要在 reducer 函数中修改值,比如不要使用 Object.assign(state, newData),应该使用 Object.assign({}, state, newData)。这样才不会覆盖旧的 state。
  • 如果 reducer 的第一个参数接受到了 undefined,reducer 应该返回一个初始的 state。 需要使用多个 store 增强器的时候,可以使用 compose API。

Store

Store 就是用来维持应用所有的 state 树的一个对象,注意:它不是类,它只是有几个方法的对象。

  • getState() 返回应用当前的 state 树。它与 store 的最后一个 reducer 返回值相同。
  • dispatch(action)
    • 会使用当前 getState() 的结果和传入的 action 以同步方式的调用 store 的 reduce 函数。
    • 返回值会被作为下一个 state。从现在开始,这就成为了 getState() 的返回值。
    • 会同时触发变化监听器(change listener)。
    • 传入的action是同步的,但是创建action的action 的creator可以异步的,这种我们称为异步action,它虽然不会立即把数据传递给 reducer,但是一旦操作完成就会触发 action 的分发事件。
    • 会返回值,返回的要 dispatch 的 action
  • subscribe(listenerFunction)
    • 这是一个底层 API。多数情况下,我们不会直接使用它,而会使用一些 React(或其它库)的绑定
    • 每当 state 变化的时候就会执行,可以在回调函数里调用 getState() 来拿到当前 state
    • 会返回一个解绑函数,需要解绑的时候,执行下这个函数即可
  • replaceReducer()
    • 替换 store 当前用来计算 state 的 reducer
    • 一般在实现 Redux 按需加载的时候可能会用到,比如在A页面用A页面的reducer,在B页面只用B页面的Reducer
    • 网上的例子非常少,估计比较少使用
// 监听器例子
function select(state) {
  return state.some.deep.property || '一些数据'
}
 
let currentValue = ''
function handleChange() {
  let previousValue = currentValue
  currentValue = select(store.getState())
 
  if (previousValue !== currentValue) {
    console.log('监测到某个值发生了改变', previousValue, 'to', currentValue)
  }
}
 
let unsubscribe = store.subscribe(handleChange)
unsubscribe() // 解除监听

combineReducers

随着应用变得越来越复杂,可能需要将 reducer 函数 拆分成多个单独的函数,拆分后的每个函数负责独立管理 state 的一部分。

combineReducers 辅助函数的作用是,把一个或者多个不同 reducer 函数合并成一个最终的 reducer 函数,然后就可以对这个合并后的 reducer 调用 createStore 方法。

合并后的 reducer 会调用各个子 reducer,并把它们返回的结果合并成一个 state 对象。

由 combineReducers(object) 返回的 state 对象,他的命名规则是在调用combineReducers() 时,根据reducers与对应的 key 而进行命名。不太好描述,具体看例子。

rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer})
// rootReducer 将返回如下的 state 对象
{
  potato: {
    // ... potatoes, 和一些其他由 potatoReducer 管理的 state 对象 ...
  },
  tomato: {
    // ... tomatoes, 和一些其他由 tomatoReducer 管理的 state 对象 ...
  }
}

每个传入 combineReducers 的 reducer 都需满足以下规则:

  • 所有未匹配到的 action,必须把它接收到的第一个参数也就是那个 state 原封不动返回。
  • 永远不能返回 undefined。当过早 return 时非常容易犯这个错误,为了避免错误扩散,遇到这种情况时 combineReducers 会抛异常。
  • 如果传入的 state 就是 undefined,一定要返回对应 reducer 的初始 state。根据上一条规则,初始 state 禁止使用 undefined。使用 ES6 的默认参数值语法来设置初始 state 很容易,也可以手动检查第一个参数是否为 undefined。
// counter.js
export default function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}
 
// reducer.js
import { combineReducers } from 'redux'
import todos from './todos'
import counter from './counter'
 
export default combineReducers({
  todos,
  counter
})
 
 
// index.js
import { createStore } from 'redux'
import reducer from './reducers/index'
 
let store = createStore(reducer)
console.log(store.getState())

applyMiddleWare

middleware 可以让我们包装 store 的 dispatch 方法来达到想要的目的,Middleware是被作为扩展 store 的 dispatch 的唯一标准的方式。

middleware 拥有“可组合”这一关键特性,多个 middleware 可以被组合到一起使用,形成 middleware 链。这时候可以使用该API处理,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。

参数

middlewares:(解释较长,通过例子去理解)每个 middleware 接受 Store 的 dispatch 和 getState 函数作为命名参数,并返回一个函数。该函数会被传入 被称为 next 的下一个 middleware 的 dispatch 方法[1],并返回一个接收 action 的新函数[2],这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store 的 dispatch 方法作为 next 参数[3],并结束调用链。

返回值:一个应用了 middleware 后的 store enhancer。

关系

middleWare与store enhancer 的关系:store enhancer 和 middleware 可以实现相同的功能。

middleWare 与 applyMiddleWare的关系: applyMiddleware 是middleWare的应用者,它来调用这些middleware

applyMiddleWare与store enhancer 的关系:applyMiddleWare 是 store enhancer的一个例子。但是applyMiddleWare将middleware限制为只可以增强store dispatch的功能。其他的store enhancer,可以增强store中包含的任意方法的功能,如dispatch、subscribe、getState、replaceReducer等。

// applyMiddleWare例子 在上述的store enhancer 例子上做修改
import { createStore,applyMiddleware } from 'redux'
 
function logger({ getState }) { // 这里的参数实际是store,
  return (next) // 这是参数[1]的解释 next 实际上是下一个 中间件的dispatch方法
    => (action) // 这是参数【2】的解释 接受action的参数
        => {
            console.log('will dispatch', action)
 
            // 调用 middleware 链中下一个 middleware 的 dispatch。
            let returnValue = next(action) // 由于我们这边只有一个middleware 所以此处相当于 store.dispatch(action) 即正常的更新值
     
            console.log('state after dispatch', getState())
 
            return returnValue
        }
}
 
function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}
 
let store = createStore(todos, ['Use Redux'], applyMiddleware(logger))
 
store.dispatch({
  type: 'ADD_TODO',
  text: 'Read the docs'
})

注意

middleware 只是包装了 store 的 dispatch 方法。技术上讲,任何 middleware 能做的事情,都可能通过手动包装 dispatch 调用来实现,但是放在同一个地方统一管理会使整个项目的扩展变的容易得多。

如果除了 applyMiddleware,还用了其它 store enhancer,一定要把 applyMiddleware 放到组合链的前面,因为 middleware 可能会包含异步操作。

想要使用多个 store enhancer,可以使用 compose() 方法。

bindActionCreators

作用是将单个或多个ActionCreator转化为dispatch(action)的函数集合形式,开发者不用再手动dispatch(action),而是可以直接调用方法

惟一会使用到 bindActionCreators 的场景是当需要把 action creator 往下传到一个组件上,却不想让这个组件觉察到 Redux 的存在,而且不希望把 dispatch 或 Redux store 传给它。

参数

actionCreators (Function || Object): function是一个 action creator,或者一个 value 是 action creator 的对象。

dispatch (Function): 一个由 Store 实例提供的 dispatch 函数。

返回结果 一个与原对象类似的对象,只不过这个对象的 value 会直接 dispatch 原 action creator 返回的结果的函数。如果传入一个单独的函数作为 actionCreators,那么返回的结果也是一个单独的函数。

// 例子
export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}
 
export function removeTodo(id) {
  return {
    type: 'REMOVE_TODO',
    id
  }
}
// 经过 bindActionCreators 后,得到结果为
 
{
   addTodo : text => dispatch(addTodo('text'));
   removeTodo : id => dispatch(removeTodo('id'));
}
 
const oneParma = bindActionCreators(addTodo,dispatch)
// 输出 text => dispatch(addTodo('text'))
// 例子 src/containers/xiaomi/XiaoMi.tsx
import * as React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as action from '../../actions';
import { withRouter } from 'react-router-dom';
import Son from './components/son';
 
// import * as styles from './xiaomi.styl';
 
class Xiaomi extends React.Component<any, any> {
 
    componentDidMount () {
        // this.props.initPhone({ name: '小米MIX2s', money: 2800 });
        this.props.actions.initPhone({ name: '小米MIX2s', money: 2800 });
    }
    handleChange = () => {
        // this.props.actions.setPhoneMoney({ money: this.props.money - 20 });
        this.props.actions.setPhoneMoney({ money: this.props.money - 20 });
    }
    render () {
        return <>
            <button
                onClick={this.handleChange}
                // className={styles.blueFont}
            >
                点我降价
            </button>
            <Son />
                现在仅售价
            {this.props.money}
        </>;
    }
}
 
function mapDataToProps (data: any) {
    return {
        money: data.Phone.money
    };
}
 
function mapDispatch (dispatch: any) {
    return {
        actions: bindActionCreators(action, dispatch)
        // initPhone: (data: object) => {
        //  bindActionCreators(TodoActionCreators, dispatch)
        //  // dispatch(action.initPhone(data));
        // },
        // setPhoneMoney: (data: object) => {
        //  dispatch(action.setPhoneMoney(data));
        // },
    };
}
 
export default connect(mapDataToProps, mapDispatch)(withRouter(Xiaomi));

compose

从右到左来组合多个函数。当需要把多个 store enhancer 依次执行的时候,需要用到它。

参数

需要合成的多个函数。预计每个函数都接收一个参数。它的返回值将作为一个参数提供给它左边的函数,以此类推。例外是最右边的参数可以接受多个参数,因为它将为由此产生的函数提供签名。

compose(funcA, funcB, funcC) 等同于 compose( funcA( funcB( funcC() ) ) )

返回值

从右到左把接收到的函数合成后的最终函数

// 例子
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import DevTools from './containers/DevTools'
import reducer from '../reducers'
 
const store = createStore(
  reducer,
  compose(
    applyMiddleware(thunk),
    DevTools.instrument()
  )
)

经过compose组合后,所有的store enhancer会形成一个洋葱模型,compose中的第一个enhancer处于洋葱模型的最外层,最后一个enhancer处于洋葱模型的最内层,每当发送一个action,都会经过洋葱模型从外到内的每一层enhancer的处理,如下图所示。因为一般我们通过middleware处理异步action,为保证其他enhancer接收到的都是普通action,所以需要将applyMiddleware作为第一个store enhancer 传给 compose,让所有的action先经过applyMiddleware的处理。

React-Redux

基本介绍

从上面的基础介绍中我们可以知道,Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。因此想要在React中更方便的使用在 Redux,出现了 React-Redux 这个库。

React-Redux 是 Redux 的官方React绑定库。它能够使 React 组件从 Redux store 中读取数据,并且向 store 分发 actions 以更新数据。

API(最常用的为前两个)

Provider

React-Redux 提供组件,能够使整个app访问到Redux store中的数据 正常情况下,应该仅在根组件使用 。

如果不想把根组件嵌套在 中,可以把 store 作为 props 传递到每一个被 connect() 包装的组件,但是只推荐在单元测试中要使用伪造的 store 或者在非完全基于 React 的代码中才这样做。

参数介绍:store:应用程序中唯一的 Redux store 对象,children:组件层级的根组件

// 例子
ReactDOM.render(
  <Provider store={store} >
    <Router history={history} >
        <Route path="/" component={App} >
        <Route path="foo" component={Foo} />
        <Route path="bar" component={Bar} />
      </Route>
    </Router>
  </Provider>,
  document.getElementById('root')
)

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

连接 React 组件与 Redux store。

连接操作不会改变原来的组件类,且返回的是一个新的已与 Redux store 连接的组件类。类似 Object.assign({},component, {store})

参数介绍

mapStateToProps(state, [ownProps])

  • 如果定义该参数,组件将会监听 Redux store 的变化。
  • 该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。
  • 如果省略了state参数,组件将不会监听 Redux store。
  • 如果仅有state,任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。
  • state的值是来源于store, ownProps的值来源于组件本身自带的属性prop
  • 当定义了props时,只要组件接收到新的 props,mapStateToProps 也会被调用。
  • 新的props与旧的props的区别仅做了浅层比较
// 例子
const mapStateToProps = (state, ownProps) => ({
  todo: state.todos[ownProps.id]
})
export default connect(mapDataToProps, mapDispatch)(withRouter(Xiaomi))

mapDispatchToProps(dispatch, [ownProps])

  • 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,对象所定义的方法名将作为属性名。
  • 每个方法将返回一个新的函数,函数中dispatch方法会将 action creator 的返回值作为参数执行。
  • 如果传递的是一个函数,该函数将接收一个 dispatch 函数,然后由我们来决定如何返回一个对象,这个对象通过 dispatch 函数与 action creator 以某种方式绑定在一起
  • 第二个参数ownProps 同上
  • 这些属性会被合并到组件的 props 中
  • 如果在connect上省略这个 mapDispatchToProps 参数,默认情况下,dispatch 会注入到组件 props 中。
// 传入对象的例子
// const mapDispatch = {
    // initPhone: (data: object) => action.initPhone(data),
    // setPhoneMoney: (data: object) => action.setPhoneMoney(data),\
// };
// 传入函数的例子
function mapDispatch (dispatch: any) {
    return {
        initPhone: (data: object) => {
            dispatch(action.initPhone(data));
        },
        setPhoneMoney: (data: object) => {
            dispatch(action.setPhoneMoney(data));
        },
    };
}
export default connect(mapDataToProps, mapDispatch)(withRouter(Xiaomi))

mergeProps(stateProps, dispatchProps, ownProps)

  • 会将mapStateToProps() 与 mapDispatchToProps() 的执行结果和组件自身的 props 将传入到这个回调函数中。
  • 该回调函数返回的对象作为 props 传递到被包装的组件中。
  • 默认情况下返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的结果
  • 可以用这个回调函数,处理那些组件里由 props 控制的 state 数据,或者对 mapDispatchToProps中的那些props做一些额外的处理。
// 根据组件的 props 注入特定用户的 todos 并把 props.userId 传入到 action 中
import * as actionCreators from './actionCreators'
 
function mapStateToProps(state) {
  return { todos: state.todos }
}
 
function mergeProps(stateProps, dispatchProps, ownProps) {
  return Object.assign({}, ownProps, {
    todos: stateProps.todos[ownProps.userId],
    addTodo: (text) => dispatchProps.addTodo(ownProps.userId, text)
  })
}
 
export default connect(mapStateToProps, actionCreators, mergeProps)(TodoApp)

options

用于定制 connector 的行为,有以下选项,都是非必填类型

// options
{
  context?: Object,
  pure?: boolean,
  areStatesEqual?: Function,
  areOwnPropsEqual?: Function,
  areStatePropsEqual?: Function,
  areMergedPropsEqual?: Function,
  forwardRef?: boolean,
}

context

React-Redux v6版本以上才支持使用,允许提供一个供React-Redux使用的自定义上下文实例。可以通过将context作为选项字段传递到此处,来将上下文传递给所连接组件。

注意:官方Demo不完整,必须也在Provider以及要渲染的组件上都加入context

// 官方demo
const MyContext = React.createContext();
connect(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  { context: MyContext }
)(MyComponent)
// 完善的demo
// store 是经过createStore处理后的store
 
const Context = React.createContext({ // TS 版本 必须要 {store, storeState} 类型,是由react-redux的connect的TS规则规定的
// JS版本可以任意值
    store,
    storeState: store.getState
})
 
<Provider store={store} context={Context} > // 这里需要加入context
                <BrowserRouter>
                    <Route
                        path="/phone/huawei"
                        children={(props: any) =>
                            <Wrap>
                                <Huawei
                                    {...props}
                                    context={Context} // 这里需要加入context
                                />
                            </Wrap>
                        }
                    />
                </BrowserRouter >
</Provider >
 
 
// 在huawei文件夹的option加入context
connect(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  { context: Context }
)(Huawei)
// 这样处理 可以通过 this.props.context 拿到context

TS的规定connect类型

  • pure

    • 默认值为true,若为true,在相关的state/props分别进行了一些比较发现没有改变的情况下,connect()会避免重新渲染以及对mapStateToProps、mapDispatchToProps和mergeProps的调用。
    • 生效的前提是当前组件是一个pureComponent,它不依赖于任何的输入或 state ,而只依赖于 props 和 Redux store 的 state。
    • 进行的比较为areStatesEqual,areOwnPropsEqual,areStatePropsEqual和areMergedPropsEqual。
    • 尽管默认值可能在99%的情况下都适用,但也可以使用自定义实现覆盖默认值
    • 以下函数都是当返回true时,表示值没有变更,不需要再改变
  • areStatesEqual?: (next: Object, prev: Object) => boolean

    • 比较的是 store 中的 state
    • 一般情况下如果不是 store 中的值变化,不会被调用,但是只要 areOwnPropsEqualy 执行过就一定会被调用
    • 用于控制下次的 mapStateToProps 会不会调用
    • 如果返回的是true,则下一次的 mapStateToProps 不会再被调用
  • areOwnPropsEqual?:(next: Object, prev: Object) => boolean

    • 用于比较组件自身的 props 与其之前的 props (注意: 不包括 react-redux 传入的props)(如果不是组件自身的props变化,不会被调用)
    • 先于 areStatesEqual 调用
    • 如果返回 false, areStatePropsEqual 不会被调用
    • 无论 true,false areStatesEqual 都会被调用
  • areStatePropsEqual?: (next: Object, prev: Object) => boolean

    • 用以比较 mapStateToProps 函数的调用后的结果与之前的结果值
    • 因此是在 areStatesEqual 后执行,如果 areStatesEqual 返回了true,那么 areStatePropsEqual 就不会调用 next,prev的值都是 mapStateToProps 里编写的值
    • 如果返回true,新变化的 mapStateToProps 结果不会生效,即不会更新
  • areMergedPropsEqual? (next: Object, prev: Object) => boolean

    • 用于比较 mergeProps 函数的结果与之前的值,即经过 react-redux 生成的 props,组件自身的 props 最终合并的值
    • 在 areStatePropsEqual 后执行
    • 如果返回 true ,新变化的值都不会生效
  • forwardRef

    • 仅 >= v6.0支持此参数
    • 如果{ forwardRef : true },组件可以接收到一个叫做 forwardRef 的 props,这个值是包装组件的实例

总结下调用顺序(如果全部都触发的情况下) :

areOwnPropsEqual → areStatesEqual → areStatePropsEqual → areMergedPropsEqual

// options demo
const Test = React.createContext({
    store: store,
    storeState: store.getState()
});
 
const options = {  
    areStatesEqual: (next: any, prev: any) => {
        console.log(next, prev, 123);
        return false;
    },
    areStatePropsEqual: (next: any, prev: any) => {
        console.log(next, prev, 'areStatePropsEqual');
        return false;
    },
    areMergedPropsEqual: (next: any, prev: any) => {
        console.log(next, prev, 'areMergedPropsEqual');
        return false;
    },
    areOwnPropsEqual: (next: any, prev: any) => {
        console.log(next, prev, 'props');
        return true;
    },
    context: Test,
     
};
const mergeProps = (stateProps: any, dispatchProps: any, ownProps: any) => {
    return { ...ownProps, ...stateProps, ...dispatchProps };
};
 
export default connect(mapDataToProps, mapDispatch, mergeProps, options)(withRouter(Xiaomi));

connectAdvanced

基于connect上开发的API,用于自身控制props、store、dispatch如何合并到组件的props上

connectAdvanced并没有对产生的props做缓存来优化性能,都留给了调用者去实现。

也是返回一个被包装的react.class

大多数应用程序都不需要使用此功能,因为默认行为connect适用于大多数用例。(个人吐槽:官方文档对这个API也不完善,估计也是一个少人使用的原因)

selectorFactory: (dispatch, factoryOptions) => { return (state, ownProps) => props }

  • store状态更改或接收props,连接的组件需要计算新props时,都会调用该函数
  • 一般称 (state, ownProps) => props 为selector
  • (state, ownProps) => props 预期的结果将是一个普通对象,该对象将作为props传递给包装的组件
  • 如果连续的调用selector 与其先前调用得到的对象是相同的,则不会重新渲染该组件
  • factoryOptions这个参数,官方没有给出示例,在源码分析文章上,看到以下内容,猜测是上述 connect 的options 以及下述connectOption的集合

connectOptions?

  • 用来进一步定制connector
  • getDisplayName(function): 用来计算连接组件的displayName,默认值:name => 'ConnectAdvanced('+name+')' methodName(string):用来在错误信息中显示,默认值为connectAdvanced
  • renderCountProp(string): 如果定义了这个属性,以该属性命名的值会被以props传递给包裹组件。该值是组件渲染的次数,可以追踪不必要的渲染
  • shouldHandleStateChanges(boolean): 控制连接器组件是否订阅redux存储状态更改。如果设置为false,则仅在重新渲染父组件时才会重新渲染,默认为true
  • forwardRef: 如果为true,则向连接的包装器组件添加ref实际上将返回被包装的组件的实例。
  • 在connectOptions中额外的属性会被传递给selectorFactory函数的factoryOptions属性
// 官方 demo
import * as actionCreators from './actionCreators'
import { bindActionCreators } from 'redux'
 
function selectorFactory(dispatch) {
  let ownProps = {}
  let result = {}
  const actions = bindActionCreators(actionCreators, dispatch)
  const addTodo = (text) => actions.addTodo(ownProps.userId, text)
  return (nextState, nextOwnProps) => {
    const todos = nextState.todos[nextProps.userId]
    const nextResult = { ...nextOwnProps, todos, addTodo }
    ownProps = nextOwnProps
    if (!shallowEqual(result, nextResult)) result = nextResult // shallowEqual 是redux 内部的一个函数 是浅比较
    // 浅比较并不是指 == , == 对应的是疏松比较,=== 对应的是严格比较
    // 当对比的类型为Object的时候并且key的长度相等的时候,浅比较是用Object.is()对Object的value做了一个基本数据类型的比较,如果key里面是对象的话,有可能出现比较不符合预期的情况
    // Object.is(),其行为与===基本一致,不过有两处不同:+0不等于-0 、 NaN等于自身
    // 详细介绍可以看这 https://www.imweb.io/topic/598973c2c72aa8db35d2e291
    return result
  }
}
export default connectAdvanced(selectorFactory)(TodoApp)

batch

在React中有一个unstable_batchedUpdate()API,这个API 允许将事件循环中的所有React更新都批处理到一个渲染通道中。不过多迁移,有兴趣的可以看这 React的批量更新

比如:可以把子组件的forceUpdate干掉,防止组件在一个批量更新中重新渲染两次。

React-Redux 参考这个做法,暴露出一个batch的API,这个API的作用也类似上面,可以将多个Redux 的dispatch 合并到一起,再通知React进行渲染。

// 官方 demo
import { batch } from 'react-redux'
 
function myThunk() {
  return (dispatch, getState) => {
    batch(() => {
      dispatch(increment())
      dispatch(increment())
    })
  }
}

hook

在React 开发hook功能后,React Redux 提供了一组钩子API,以替代现有的connect()高阶组件,这些API允许我们订阅Redux存储和调度操作,而不必将组件包装在中connect()。但是在Redux 7.1.0 以上才能使用,即目前最新版本,和使用 connect() 一样,首先应该将整个应用包裹在 中,使得 store 暴露在整个组件树中。

// 官方 demo
const store = createStore(rootReducer)
 
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

useSelector( selector : Function, equalityFn? : Function ) 核心API

selector 应该是一个纯函数,参数是store, 应该返回的是store中某个数据。

  • 该函数的作用是从 Redux 的 store 中获取 状态(state) 数据
  • selector 函数被调用时,将会被传入Redux store的整个state,作为唯一的参数。
  • 每次函数组件渲染时, selector 函数都会被调用。
  • useSelector()同样会订阅 Redux 的 store,并且在每分发(dispatch) 一个 action 时,都会被执行一次。
  • selector 函数的返回值会被用作调用 useSelector() hook 时的返回值
  • 当分发(dispatch) 了一个 action 时,useSelector() 会将上一次调用 selector 函数结果与当前调用的结果进行引用(===)比较,如果不一样,组件会被强制重新渲染。如果一样,就不会被重新渲染(后面会提)
  • selector 函数不会接收到 ownProps 参数。但是 props 可以通过闭包获取使用(后面有例子)
  • 可以在一个函数组件中多次调用 useSelector()。每一个 useSelector() 的调用都会对 Redux 的 store 创建的一个独立的订阅(subscription)。
  • 对于一个组件来说,如果一个分发后(dispatched) 的 action 导致组件内部的多个 useSelector() 产生了新值,那么仅仅会触发一次重渲染

equalityFn

  • 比较方式,决定了如何比较 selector 的调用结果,以此来判断是否需要重新渲染
  • 默认采用严格比较 ===
  • 因为默认采用严格比较,所以返回一个新的对象引用总是会触发重渲染,因此官方提供了equalityFn这个接口,用于替换默认的比较方式
// 官方 demo
import { shallowEqual, useSelector } from 'react-redux'
 
// 这里使用 react-redux编写的浅比较代替默认比较方式
const selectedData = useSelector(selectorReturningObject, shallowEqual)

// 基本用法
import React from 'react'
import { useSelector } from 'react-redux'
 
export const CounterComponent = () => {
  const counter = useSelector(state => state.counter)
  return <div>{counter}</div>
}

// 通过闭包使用 props 来选择取回什么状态
import React from 'react'
import { useSelector } from 'react-redux'
 
export const TodoListItem = props => {
  const todo = useSelector(state => state.todos[props.id])
  return <div>{todo.text}</div>
}

useActions()

已被移除

useDispatch()

  • 这个 hook 返回 Redux store 的 分发(dispatch) 函数的引用。可以用于分发(dispatch) 某些需要的 action
  • 在将一个使用了 dispatch 函数的回调函数传递给子组件时,建议使用 useCallback 函数将回调函数记忆化,防止因为回调函数引用的变化导致不必要的渲染
  • 原因是:避免在component render时候声明匿名方法,因为这些匿名方法会被反复重新声明而无法被多次利用,然后容易造成component反复不必要的渲染
import React from 'react'
import { useDispatch } from 'react-redux'
 
export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()
 
  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch({ type: 'increment-counter' })}>
        Increment counter
      </button>
    </div>
  )
}
// 优化代码
import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'
// useCallback返回缓存的函数 const fnA = useCallback(fnB, [a]) useCallback会将我们传递给它的函数fnB返回,并且将这个结果缓存;如果当依赖a变更时,会返回新的函数。
export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()
  const incrementCounter = useCallback(
    () => dispatch({ type: 'increment-counter' }),
    [dispatch]
  )
 
  return (
    <div>
      <span>{value}</span>
      <MyIncrementButton onIncrement={incrementCounter} />
    </div>
  )
}
 
export const MyIncrementButton = React.memo(({ onIncrement }) => (
  <button onClick={onIncrement}>Increment counter</button>
))

useStore()

  • 这个 hook 返回传递给 组件的 Redux sotore 的引用。
  • 这个 hook 不应该被经常使用,而应该将 useSelector() 作为首选。
  • 在一些不常见的场景下,可能需要访问 store,这个还是有用的,比如替换 store 的 reducers。
import React from 'react'
import { useStore } from 'react-redux'
 
export const CounterComponent = ({ value }) => {
  const store = useStore()
  // 只是例子,不要在实际开发中使用
  // 组件不会自动更新,即使store更新,因为仅是store的引用
  return <div>{store.getState()}</div>
}