Redux专题:实用

栏目: 服务器 · 发布时间: 6年前

内容简介:本文是『horseshoe·Redux专题』系列文章之一,后续会有更多专题推出来我的来我的个人博客 获得无与伦比的阅读体验

本文是『horseshoe·Redux专题』系列文章之一,后续会有更多专题推出

来我的 GitHub repo 阅读完整的专题文章

来我的个人博客 获得无与伦比的阅读体验

Redux是一套精巧而实用的工具,这也是它在开发者中如此流行的原因。

所以对待Redux,最重要的就是熟练使用它的主要API,一旦将它了然于胸,就会对Redux的设计思想有一个全局的认识,也就能清楚的判断自己的应用需不需要劳驾Redux出手。

需要注意:咱们默认将Redux和React搭配使用,不过Redux不是非得和它在一起的。

Action

要达成某个目的,开发者首先要描述自己的意图。Action就是用来描述开发者意图的。它不是一个函数,而是一个普通的对象,通过声明的类型来触发相应的动作。

我们来看一个例子:

{
    type: 'ADD_TODO_ITEM',
    payload: {
        content: '每周看一本书',
        done: false,
    },
}
复制代码

Redux官方定义了字段的一些规范:一个Action必须包含 type 字段,同时一个Action包含的字段不应该超过 typepayloaderrormeta 这四种。

有效载荷
元

因为意图是通过类型来定义的,所以type字段必不可少,称某个对象为一个Action的标志就是它有一个type字段。

除此之外,一个动作可能包含更为丰富的信息。开发者可以随意添加字段,毕竟它就是个普通对象。不过遵守一定的规范便于其他开发者阅读你的代码,可以提升协作效率。

Constants

前面说了type字段一般用全大写的字符串表示,多个字母用下划线分隔。不仅如此,大家还有一个约定俗成:用一个结构相同的变量保存该字符串,因为它会在多处用到。

const ADD_TODO_ITEM = 'ADD_TODO_ITEM';
复制代码

集中保存这些变量的文件就叫 Constants.js

在此,我提出一点异议。如果你觉得不麻烦,那遵循规范再好不过。但开发者向来觉得Redux过于繁琐,如果你也这么觉得,大可不必维护所谓的Constants。维护Constants的好处不过是一处更改处处生效,然而字符串和变量是结构相同的,如果字符串作了修改,语意上必然大打折扣,况且type字段一旦定义极少更改,所以视你的协作规模和个人喜好而定,为Redux的繁琐减负不是么?

Action Creators

我们知道Action是一个对象,但是如果多次用到这个对象,我们可以写一个生成Action的函数。

function addTodoItem(content) {
    return {
        type: ADD_TODO_ITEM,
        payload: { content, done: false },
    };
}
复制代码

同理,如果你觉得繁琐,这一步是可以免去的。

异步场景下Action Creators会大有用处,后面会讲到。

需要注意的是:所谓的Action更确切的说是一个执行动作的指令,而不是一个动作。或者我们换一种说法,这里的动作指的是动作描述,而不是动作派发。

Store

Redux的本质不复杂,就是用一个全局的外部的对象来存储状态,然后通过观察者模式来构建一套状态更新触发通知的机制。

这里的Store就是存储状态的容器。

但是呢?它需要开发者动手写一套逻辑来指导它怎么处理状态的更新,这就是后面要讲的Reducer,暂且按下不表。

问题是Store怎么接收这套逻辑呢?

import { createStore } from 'redux';

import reducer from './reducer';

const store = createStore(reducer);
复制代码

看到没有,Redux专门有一个API用来创建Store,它接受三个参数: reducerpreloadedStateenhancer

reducer就是处理状态更新的逻辑。

preloadedState是初始状态,如果你需要让Store一开始不是空对象,那么可以从这里传进去。

enhancer翻译成中文是 增强器 ,是用来装载第三方插件以增强Redux的功能的。

怎么存

我们已经了解了Action的作用,但是Action只是对动作的描述,怎么说它得有个发射器吧。这个发射器就隐藏在Store里。

执行 createStore 返回的对象包含了一个函数 dispatch ,传入Action执行就会发射一个动作。

import React, { Component } from 'react';
import store from './store';
import action from './action';

class App extends Component {
    render() {
        return (
            <button onClick={() => store.dispatch(action)}>dispatch</button>
        );
    }
}

export default App;
复制代码

怎么取

好了我们已经发射了一个动作,假设现在Store中已经有状态了,我们怎么把它取出来呢?

直接 store.xxx 么?

我们先来打印Store这个对象看看:

{
    dispatch: ƒ dispatch(action),
    getState: ƒ getState(),
    replaceReducer: ƒ replaceReducer(nextReducer),
    subscribe: ƒ subscribe(listener),
    Symbol(observable): ƒ observable(),
}
复制代码

打印出来一堆API,这可咋整?

别着急,茫茫人海中看到一个叫 getState 的东西,它就是我们要找的高人吧。插一句,大家注意区分Store和State的区别,Store是存储State的容器。

Redux隐藏了Store的内部细节,所以开发者只能用getState来获取状态。

订阅

Redux是基于观察者模式的,所以它开放了一个订阅的API给开发者,每次发射一个动作,传入订阅器的回调都会执行。通过它开发者就能监听动作的派发以执行相应的逻辑。

import store from './store';

store.subscribe(() => console.log('有一个动作被发射了'));
复制代码

replaceReducer

顾名思义,替换Reducer,这主要是方便开发者调试Redux用的。

Reducer

Reducer是Redux的核心概念,因为Redux的作者Dan Abramov这样解释 Redux 这个名字的由来:Reducer+Flux。

其实Reducer是一个计算机术语,包括JavaScript中也有用于迭代的 reduce 函数。所以我们先来聊聊应该怎样理解Reducer这个概念。

reduce翻译成中文是 减少 ,Reducer在计算机中的含义是归并,也是化多为少的意思。

我们来看JavaScript中reduce的写法:

const array = [1, 2, 3, 4, 5];
const sum = array.reduce((total, num) => total + num);
复制代码

再来看Redux中Reducer的写法:

function todoReducer(state = [], action) {
    switch (action.type) {
        case 'ADD_TODO_ITEM':
            const { content, done } = action.payload;
            return [...state, { content, done }];
        case 'REMOVE_TODO_ITEM':
            const todos = state.filter(todo => todo.content !== action.content);
            return todos;
        default:
            return state;
    }
}
复制代码

state参数是一个旧数据集合,action中包含的payload是一个新的数据项,Reducer要做的就是将新的数据项和旧数据集合归并到一起,返回给Store。这样看起来Reducer这个名字起的也没那么晦涩了是不是?

一个Reducer接受两个参数,第一个参数是旧的state,我们返回的数据就是用来替换它的,然后风水轮流转,这次返回的数据下次就变成旧的state了,如此往复;第二个参数是我们派发的action。

因为Reducer的结构类似,都是根据Action的类型返回相应的数据,所以一般采用 switch case 语句,如果没有变动则返回旧的state,总之它必须有返回值。

纯函数

Reducer的作用是归并,也只能是归并,所以Redux规定它必须是一个纯函数。相同的输入必须返回相同的输出,而且不能对外产生副作用。

所以开发者在返回数据的时候不能直接修改原有的state,而是应该在拷贝的副本之上再做修改。

多个Reducer

一个Reducer只应该处理一个动作,可是我们的应用不可能只有一个动作,所以一个典型的Redux应用会有很多Reducer函数。那么怎么管理这些Reducer呢?

首先来看只有一个Reducer的情况:

import { createStore } from 'redux';

import reducer from './reducer';

const store = createStore(reducer);

export default store;
复制代码

如果只有一个Reducer,那我们只需要将它传入 createStore 这个函数中,就这么简单。这时候Reducer返回的状态就是Store中的全部状态。

而如果有多个Reducer,我们就要动用Redux的另一个API了: combineReducers

const reducers = combineReducers({
    userStore: userReducer,
    todoStore: todoReducer,
});
复制代码

当我们有多个Reducer,就意味着有多个状态需要交给Store管理,我们就需要子容器来存储它们,其实就是对象嵌套对象的意思。combineReducers就是用来干这个的,它把每一个Reducer分门别类的与不同的子容器对应起来,某个Reducer只处理对应的状态。

{
    userStore: {},
    todoStore: {},
};
复制代码

当我们用getState获取整个Store的状态,返回的对象就是上面这样的。

你猜对了,传入combineReducers的对象的key就是子容器的名字。

默认值

当开发者调用createStore创建Store时,传入的所有Reducer都会执行一遍。注意,这时开发者还没有发射任何动作呢,那为什么会执行一遍?

const randomString = () => Math.random().toString(36).substring(7).split('').join('.');

const ActionTypes = {
    INIT: `@@redux/INIT${randomString()}`,
    REPLACE: `@@redux/REPLACE${randomString()}`,
    PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
};

dispatch({ type: ActionTypes.INIT });
复制代码

因为Redux源码中,在createStore函数里面放了这样一段逻辑,这初始化时的dispatch是Redux自己发射的。

为什么?

还记得Reducer接受两个参数吗?第一个是state,而我们可以给state设置默认值。

聪明的你一定想到了,初始化Store时Redux自己发射一个动作的目的是为了收集这些默认值。Reducer会将这些默认值返回给Store,这样默认值就保存到Store中了。

聪明的你大概还想到一个问题:createStore也有默认值,Reducer也有默认值,不会打架么?

Redux的规矩:createStore的默认值优先级更高,所以不会打架。

执行

在一个有若干Reducer的应用中,一个动作是怎么找到对应的Reducer的?

这是一个好问题,答案是挨个找。

假如应用有1000个Reducer,与某个动作对应的Reducer又恰好在最后一个,那要把1000个Reducer都执行一遍,Redux不会这么傻吧?

Redux还真就这么傻。

因为当一个动作被派发时,Redux并不知道应该由哪个Reducer来处理,所以只能让每个Reducer都处理一遍,看看到底是谁的菜。可不可以在设计上将动作与Reducer对应起来呢?当然是可以的,但是Redux为了保证API的简洁和优美,决定牺牲这一部分性能。

只是一些纯函数而已,莫慌。

react-redux

当我们使用Redux时,我们希望每发射一个动作,应用的状态自动发生改变,从而触发页面的重新渲染。

import React, { Component } from 'react';
import store from './store';

class App extends Component {
    state = { name: 'Redux' };

    render() {
        const { name } = this.state;
        return (
            <div>{name}</div>
        );
    }

    componentDidMount() {
        this.unsubscribe = store.subscribe(() => {
            const { name } = store.getState();
            this.setState({ name });
        });
    }

    componentWillUnmount() {
        this.unsubscribe();
    }
}
复制代码

怎么办呢?开发者得手动维护一个订阅器,才能监听到状态变化,从而触发页面重新渲染。

但是React最佳实践告诉我们,一个负责渲染UI的组件不应该有太多的逻辑,那么有没有更好的办法使得开发者可以少写一点逻辑,同时让组件更加优雅呢?

别担心,Redux早就帮开发者做好了,不过它是一个独立的模块: react-redux 。顾名思义,这个模块的作用是连接React和Redux。

Provider

连接React和Redux的第一步是什么呢?当然是将Store集成到React组件中,这样我们就不用每次在组件代码中 import store 了。多亏了React context的存在,Redux只需要将Store传入根组件,所有子组件就能通过某种方式获取传入的Store。

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import store from './store';

import App from './App';

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>
    ,
    document.getElementById('root')
);
复制代码

connect

老式的context写法,在子组件中定义 contextTypes 就可以接收到传入的参数。当然,你肯定也想到,Redux把这些细节都封装好了,这就是 connect

connect接口的意义主要有三点:

  • 封装用context从根组件获取数据的细节。
  • 封装Redux订阅器的细节。
  • 作为一个容器组件真正连接React和Redux。
import React from 'react';
import Todo from './Todo';

const App = ({ todos, addTodoItem }) => {
    return (
        <div>
            <button onClick={() => addTodoItem()}>add</button>
            {todos.map(todo => <Todo key={todo.id} {...todo} />)}
        </div>
    );
}

const mapStateToProps = (state, ownProps) => {
    return {
        todos: state.todoStore,
    };
};

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        addTodoItem: (todoItem) => dispatch({ type: 'ADD_TODO_ITEM', payload: todoItem }),
    };
};

export default connect(mapStateToProps, mapDispatchToProps)(App);
复制代码

我们看上面例子,connect接受的两个参数: mapStateToPropsmapDispatchToProps ,所谓的map就是映射,意思就是将所有state和dispatch依次映射到props上。如此真正的组件需要的数据和功能都在props上,它就可以安安心心的做一个傻瓜组件。

connect接受四个参数:

<App value={value} />
Object.assign()

我们注意到,connect要先执行一次,返回的结果再次执行才传入开发者定义的组件。它返回一个新的组件,这个新的组件不会修改原组件(除非你操纵了ownProps的返回),而是为组件增加一些新的props。

我们也可以用装饰器写法来重写connect:

import React from 'react';
import Todo from './Todo';

const mapStateToProps = (state, ownProps) => {
    return {
        todos: state.todoStore,
    };
};

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        addTodoItem: (todoItem) => dispatch({ type: 'ADD_TODO_ITEM', payload: todoItem }),
    };
};

@connect(mapStateToProps, mapDispatchToProps)
const App = ({ todos, addTodoItem }) => {
    return (
        <div>
            <button onClick={() => addTodoItem()}>add</button>
            {todos.map(todo => <Todo key={todo.id} {...todo} />)}
        </div>
    );
}

export default App;
复制代码

总结

Redux通过调用createStore返回Store,它是一个独立于应用的全局对象,通过观察者模式能让应用监听到Store中状态的变化。最佳实践是一个应用只有一个Store。

Redux必须通过一个明确的动作来修改Store中的状态,描述动作的是一个纯对象,必须有type字段,传递动作的是Store的属性方法dispatch。

Store本身并没有任何处理状态更新的逻辑,所有逻辑都要通过Reducer传递进来,Reducer必须是一个纯函数,没有任何副作用。如果有多个Reducer,则需要利用combineReducers定义相应的子状态容器。

基于容器组件和展示组件分离的设计原则,也为了提高开发者的编程效率,Redux通过一个额外的模块将React和Redux连接起来,使得所有的状态管理接口都映射到组件的props上。其中,Provider将Store注入应用的根组件,解决的是连接的充分条件;connect将需要用到的state和dispatch都映射到组件的props上,解决的是连接的必要条件。只有被Provider包裹的组件,才能使用connect包裹。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Fluent Python

Fluent Python

Luciano Ramalho / O'Reilly Media / 2015-8-20 / USD 39.99

Learn how to write idiomatic, effective Python code by leveraging its best features. Python's simplicity quickly lets you become productive with it, but this often means you aren’t using everything th......一起来看看 《Fluent Python》 这本书的介绍吧!

html转js在线工具
html转js在线工具

html转js在线工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试