浅谈redux基本概念

源代码
• 阅读 4153

网页从远古时代的『webpage』尤其是一种静态页面的存在方式,发展到当下拥有着复杂的功能与交互逻辑的面向「客户端」更愿意被称之为『webapp』的形态的整个过程中,网页的开发不再是简单的界面拼凑来显示静态的内容,而是要通过维护和管理页面上的各种状态,例如服务端返回的数据、本地临时存储的数据、视图界面该被隐藏或者显示、路由状态等等,来决定用户在不同的交互下,网页该怎样正确的显示预期的结果。而整个『webapp』可以看做一个大型的状态机,当管理这些庞大且又复杂的 states 时,很容易出现不可预测甚至会对一些状态的改变发生『失控』的情景:当一个界面改动而更新了某个 model,而这个model又更新另一个 model,最终产生的结果是与该另一个model相关的界面产生了不可预知的变更...这在拥有着双向数据绑定的前端框架的项目里尤其的面临着难以维护的局面。而redux通过基于单向数据流的模式,背靠其遵循的三大原则,确保每一次它在改变各种状态之后,其结果是可预测的。

redux遵循的三个原则

所有和webapp相关的states都可以存放在一个对象内

该对象被称作一个状态树。例如,通过一个对象来描述一个 todo list 的状态可以如下:

{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

即,当前状态下所包含的todo项列表以及过滤列表的显示方式(显示所有列表项包括完成与未完成等)

在状态树里的states只能是可读

改变states的方法只能通过 dispatch an action, action 是一个纯object,用来描述发起了何种action以及它附带的额外数据:

以下是一个完成 todo list 中某一项的action

{
    type: 'COMPLETE_TODO',
    index: 1
}

即,该动作完成了索引为1的todo项。

状态的变更是通过纯函数来完成的

所谓纯函数,就是单纯地将参数传入并加工输出成一个可以预测的返回值,在这个过程中没有产生任何副作用。这里的副作用包括但不限于对原传参的改动、发起对数据库的操作以及随之产生的对DOM结构的变更。纯函数返回的值总是可预测的,而非纯函数则更多的机会产生前面提到的状态的不可控性

//pure function
function square(x){
  return x * x;
}

function squareAll(items){
  return items.map(square);
}
// Impure functions
function square(x){
  updateXInDatabase(x);
  return x * x;
}

function squareAll(items){
  for (let i = 0; i < items.length; i++) {
    items[i] = square(items[i]);
  }
}

在redux里面,我们需要通过一个纯函数来描述状态是如何被改变的,这个纯函数接受一个初始的状态,以及改变这个状态的actions,并且返回一个新的状态。这种方法称之为reducer。

来看一个简单的reducer,一个纯函数,没什么特别的:

const counter = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

reducer通过返回一个新的state来确保上一个阶段的state没有被改写,这就保证了它的输出结果是可预测的:

expect(
  counter(2, { type: 'DECREMENT' })
).toEqual(1);

expect(
  counter(0, { type: 'INCREMENT' })
).toEqual(1);

需要留意的是,对于state为数组以及对象的这种情况,我们更要避免直接改变state本身而引起的副作用:

我们可以通过一个deep-freeze的库来确保数组或者对象类型的state不能被更改,以便来检测我们写的reducer是否会产生副作用,所以当reducer被定义成如下,

const initialState = [];
const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
        state.push({
            index: action.index,
           text: action.text,
             completed: false
        });
        return state;
    default:
      return state;
  }
};

//冻住initialState,使其无法被更改
deepFreeze(initialState);

expect(
    todos(initialState, {
        type: 'ADD_TODO',
        index: 0,
        text: 'redux',
    })
).toEqual([{
    index: 0,
    text: 'redux',
    completed: false
}]);

我们发现reducer函数中的数组push方法未能生效,因为一个被冻住的变量无法被更改。此时如果将产生副作用的push方法改为concat,

const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
        return state.concat({
            index: action.index,
           text: action.text,
             completed: false
        });    
    default:
      return state;
  }
};

可将以上concat的写法用ES6的...spread方法代替为,

const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
        return [
            ...state,
            {
                index: action.index,
               text: action.text,
                 completed: false
            }  
        ];  
    default:
      return state;
  }
};

该reducer返回了一个新的state,符合纯函数的概念。类似的,借助slice数组方法,可以实现同样纯函数式的删除todo或者是在指定位置插入todo,

const todos = (state = [], action) => {
  switch (action.type) {
    case 'REMOVE_TODO':
        return [
            ...state.slice(0, action.index),
            ...state.slice(action.index + 1) 
        ]; 
     case 'INSERT_TODO':
         return [
             ...state.slice(0, action.index),
             {
                 index: action.index,
                 text: action.text,
                 completed: false,
             },
             ...state.slice(action.index + 1)
         ]; 
     default:
      return state;
  }
};

同样当state为object类型时,我们也可以通过ES6 spread来实现纯函数式的reducer,当标记某个todo项完成时,

const todos = (state = {}, action) => {
  switch (action.type) {
    case 'TOGGLE_TODO':
        return {
            ...state,
            completed: !action.completed
        }
        default:
      return state;
  }
};

等同于,

const todos = (state = {}, action) => {
  switch (action.type) {
    case 'TOGGLE_TODO':
        return Object.assign({}, state, {
            completed: !action.completed
        });
        default:
      return state;
  }
};

Redux Store --- 一个让redux三原则融汇贯通的对象

通过创建store对象,我们可以调用其getState()dispatch(action)subscribe(listener)来依次获取当前state执行一次action注册当state被改变时的回调,以创建一个简单的计数器为例,

import { createStore } from Redux

//创建一个store,reducer作为参数传入
const store = createStore(counter);

//执行一个action
store.dispatch({ type: 'INCREMENT' });

//当state被改变时,在回调内重新渲染DOM,执行render()
let unsubscribe = store.subscribe(render);

//取消回调函数的注册
unsubscribe();

关于redux store一个很重要的点在于,整个运用了redux的应用里,有且只有一个store,当我们处理不同业务逻辑下的数据时,我们需要通过不同的reducers来处理而不是对应到多个store。所以这么一看来reducer的比重会比较大,我们可以利用redux 提供的 combineReducers() 合并多个reducers到一个根reducer。这样组织reducers的方式有点类似react里一个根组件下有多个子组件。

createStore的源码非常简洁,我们可以用不到20行的代码来简单重现其背后的逻辑,帮助我们更好的理解store,

const createStore = (reducer) => {

  let state;
  let listeners = [];

  const getState = () => state;

  const dispatch = (action) => {
        state = reducer(state, action);
         listeners.forEach(listener => listener());
  };
  
  const subscribe = (listener) => {
        listeners.push(listener);
          return () => {
            listeners = listeners.filter(l => l !== listener);
          };
  };
  
  dispatch({});
  
  return { getState, dispatch, subscribe };
};

为了能够在任何时刻返回对应的state,我们需要一个state变量来记录,getState()只需要负责返回它。
dispatch方法则负责把需要执行的action传给reducer,返回新的state,并同时执行注册过的回调函数。注册的回调可能会有多个,我们通过一个数组来保存即可。subscribe通过返回一个thunk函数,来实现unsubscribe。最后为了能够让store.getState()可以获得初始的state,直接dispatch一个空的action即可让reducer返回initialState。

redux可以和react很好的结合一起使用,我们只需要把react对应的ReactDOM.render()方法写在subscribe回调里,而为了更优雅的在react内书写redux,redux官方提供了react-redux

redux的源码非常简单,它只有2kb大小,更多有关redux的介绍可以参考如下,

参考
redux
redux-cookbook
Getting Started with Redux by the author of Redux
react-redux

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
4年前
SpringBoot自定义序列化的使用方式
场景及需求:项目接入了SpringBoot开发,现在需求是服务端接口返回的字段如果为空,那么自动转为空字符串。例如:\    {        "id":1,        "name":null    },    {        "id":2,        "name":"x
Easter79 Easter79
4年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
4年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这