🔥🔥🔥给npy的Redux秘籍

517 阅读24分钟

前言

大家好我是给npy的前端秘籍,给大家宣布一件事情,以后小弟就要以尹框花名混迹前端江湖了,最近业务需求太多,没时间写文。还请见谅。

多年来,前端工程师忍辱负重,操着卖白粉的心,赚着买白菜的钱,一直处于程序员鄙视链的底层 于是有大牛就把后端 MVC 的开发思维搬到前端,将应用中所有的动作与状态都统一管理,让一切有据可循

redux简介

redux是一个用来管理数据状态和UI状态的JavaScript应用工具。随着JavaScript单页应用(SPA)开发日趋复杂,JavaScript需要管理比任何时候都要多的state(状态),Redux就是降低管理难度的。(Redux支持React,Angular、jQuery甚至纯JavaScript)它体小精悍(只有2kb,包括依赖),却有着很强大的插件扩展生态。 Redux简化图 从图中可以看出,如果不用Redux,我们要传递state是非常麻烦的。Redux中,可以把数据先放在数据仓库(store-公用状态存储空间)中,这里可以统一管理状态,然后哪个组件用到了,就去stroe中查找状态。如果途中的紫色组件想改变状态时,只需要改变store中的状态,然后其他组件就会跟着中的自动进行改变。

首先我们先来看一下redux的工作流程:

redux工作流程

Redux简化图

React Components就相当于借书者,然后我们去图书馆借书,我们先见到的是Action Creators“图书管理员”,我们说我要找一本《自顶向下学习React》。"图书管理员"就回到了Store,然后让Reducer看看"《自顶向下学习React》“还在不在(现在的状态),如果在就让把它取出来借给借书者。

Redux工作流程中有四个部分,最重要的就是store这个部分,因为它把所有的数据都放到了store中进行管理。

Store

  • 首先要区分store 和 state

store是应用的状态,一般本质上是一个普通的对象

举一个栗子:

比如我们有一个程序,包含 计数器 和 待办事项 两大功能

那么我们可以为该应用设计出对应的存储数据结构(应用初始状态):

{
  counter: 0,
  todos: []
}

store 是应用状态 state 的管理者,包含下列四个函数:

  • getState() # 获取整个 state
  • dispatch(action) # ※ 触发 state 改变的【唯一途径】※
  • subscribe(listener) # 您可以理解成是 DOM 中的 addEventListener
  • replaceReducer(nextReducer) # 一般在 Webpack Code-Splitting 按需加载的时候用

二者的关系是:state === store.getState()

Redux 规定,一个应用只应有一个单一的 store,其管理着唯一的应用状态 state Redux 还规定,不能直接修改应用的状态 state,也就是说,下面的行为是不允许的:

var state = store.getState()
state.counter = state.counter + 1 // 禁止在业务逻辑中直接修改 state

若要改变 state,必须 dispatch 一个 action,这是修改应用状态的不二法门

现在您只需要记住 action 只是一个包含 type 属性的普通对象即可 例如 { type: 'INCREMENT' }

上面提到,state 是通过 store.getState() 获取,那么 store 又是怎么来的呢? 想生成一个 store,我们需要调用 Redux 的 createStore

import { createStore } from 'redux'
...
const store = createStore(reducer, initialState) // store 是靠传入 reducer 生成的哦!
//现在您只需要记住 reducer 是一个 函数,负责更新并返回一个新的 state

Action

上面提到,action(动作)实质上是包含 type 属性的普通对象,这个 type 是我们实现用户行为追踪的关键 例如,增加一个待办事项 的 action 可能是像下面一样:

{
  type: 'ADD_TODO',
  payload: {
    id: 1,
    content: '待办事项1',
    completed: false
  }
}

如果需要新增一个代办事项,实际上就是将上面代码中的 payload “写入”state.todos 数组中(如何“写入”?在此留个悬念)

{
  counter: 0,
  todos: [{
    id: 1,
    content: '待办事项1',
    completed: false
  }]
}
Action Creator

Action Creator 可以是同步的,也可以是异步的

顾名思义,Action Creator 是 action 的创造者,本质上就是一个函数,返回值是一个 action对象) 例如下面就是一个 “新增一个待办事项” 的 Action Creator:

var id = 1
function addTodo(content) {
  return {
    type: 'ADD_TODO',
    payload: {
      id: id++,
      content: content, // 待办事项内容
      completed: false  // 是否完成的标识
    }
  }
}

通俗点讲,Action Creator 用于绑定到用户的操作(点击按钮等),其返回值 action 用于之后的 dispatch(action)

刚刚提到过,action 明明就没有强制的规范,为什么 store.dispatch(action) 之后, Redux 会明确知道是提取 action.payload,并且是对应写入到 state.todos 数组中? 又是谁负责“写入”的呢?悬念即将揭晓...

Reducer

Reducer 必须是同步的纯函数

用户每次 dispatch(action) 后,都会触发 reducer 的执行 reducer 的实质是一个函数,根据 action.type更新 state 并返回 nextState 最后会用 reducer 的返回值 nextState 完全替换掉原来的 state

注意:上面的这个 “更新” 并不是指 reducer 可以直接对 state 进行修改 Redux 规定,须先复制一份 state,在副本 nextState 上进行修改操作 例如,可以使用 lodash 的 cloneDeep,也可以使用 Object.assign / map / filter/ ... 等返回副本的函数

在上面 Action Creator 中提到的 待办事项的 reducer 大概是长这个样子 (为了容易理解,在此不使用 ES6 / Immutable.js):

var initState = {
  counter: 0,
  todos: []
}

function reducer(state, action) {
  // ※ 应用的初始状态是在第一次执行 reducer 时设置的 ※
  if (!state) state = initState
  
  switch (action.type) {
    case 'ADD_TODO':
      var nextState = _.cloneDeep(state) // 用到了 lodash 的深克隆
      nextState.todos.push(action.payload) 
      return nextState

    default:
    // 由于 nextState 会把原 state 整个替换掉
    // 若无修改,必须返回原 state(否则就是 undefined)
      return state
  }
}

§ 总结

  • store 由 Redux 的 createStore(reducer) 生成
  • state 通过 store.getState() 获取,本质上一般是一个存储着整个应用状态的对象
  • action 本质上是一个包含 type 属性的普通对象,由 Action Creator (函数) 产生
  • 改变 state 必须 dispatch 一个 action
  • reducer 本质上是根据 action.type 来更新 state 并返回 nextState函数
  • reducer 必须返回值,否则 nextState 即为 undefined
  • 实际上,state 就是所有 reducer 返回值的汇总(本教程只有一个 reducer,主要是应用场景比较简单)

Action Creator => action => store.dispatch(action) => reducer(state, action) => 原 state state = nextState

Redux 与传统后端 MVC 的对照

Redux传统后端 MVC
store数据库实例
state数据库中存储的数据
dispatch(action)用户发起请求
action: { type, payload }type 表示请求的 URL,payload 表示请求的数据
reducer路由 + 控制器(handler)
reducer 中的 switch-case 分支路由,根据 action.type 路由到对应的控制器
reducer 内部对 state 的处理控制器对数据库进行增删改操作
reducer 返回 nextState将修改后的记录写回数据库

redux原理

开始

在开始之前我想先讲一种常用的设计模式:观察者模式。先来说一下我对观察者模式的个人理解:观察者模式(Publish/Subscribe)模式。对于这种模式很清楚的同学下面这段代码可以跳过。如果你还不清楚,你可以试着手敲一遍下面的代码!!

观察者模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。 —— Graphic Design Patterns

观察者模式,是所有 JavaScript 设计模式中使用频率最高,面试频率也最高的设计模式,所以说它十分重要——如果我是面试官,考虑到面试时间有限、设计模式这块不能多问,我可能在考查你设计模式的时候只会问观察者模式这一个模式。该模式的权重极高。

重点不一定是难点。观察者模式十分重要,但它并不抽象,理解难度不大。这种模式不仅在业务开发中遍地开花,在日常生活中也是非常常见的。为了帮助大家形成初步的理解,在进入代码世界之前,我们来看一段日常:

生活中的观察者模式

周一刚上班,前端开发阿噗就被产品经理小红拉进了一个钉钉群——“员工管理系统需求第99次变更群”。这个群里不仅有阿噗,还有后端开发 A,测试同学 B。三位技术同学看到这简单直白的群名便立刻做好了接受变更的准备、打算撸起袖子开始干了。此时小红却说:“别急,这个需求有问题,我需要和业务方再确认一下,大家先各忙各的吧”。这种情况下三位技术同学不必立刻投入工作,但他们都已经做好了本周需要做一个新需求的准备,时刻等待着产品经理的号召。

一天过去了,两天过去了。周三下午,小红终于和业务方确认了所有的需求细节,于是在“员工管理系统需求第99次变更群”里大吼一声:“需求文档来了!”,随后甩出了"需求文档.zip"文件,同时@所有人。三位技术同学听到熟悉的“有人@我”提示音,立刻点开群进行群消息和群文件查收,随后根据群消息和群文件提供的需求信息,投入到了各自的开发里。上述这个过程,就是一个典型的观察者模式

重点角色对号入座

观察者模式有一个“别名”,叫发布 - 订阅模式(之所以别名加了引号,是因为两者之间存在着细微的差异,下面我会讲到这点)。这个别名非常形象地诠释了观察者模式里两个核心的角色要素—— “发布者” “订阅者” 。 在上述的过程中,需求文档(目标对象)的发布者只有一个——产品经理小红。而需求信息的接受者却有多个——前端、后端、测试同学,这些同学的共性就是他们需要根据需求信息开展自己后续的工作、因此都非常关心这个需求信息,于是不得不时刻关注着这个群的群消息提醒,他们是实打实的订阅者,即观察者对象。

现在我们再回过头来看一遍开头我们提到的略显抽象的定义:

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。 在我们上文这个钉钉群里,一个需求信息对象对应了多个观察者(技术同学),当需求信息对象的状态发生变化(从无到有)时,产品经理通知了群里的所有同学,以便这些同学接收信息进而开展工作:角色划分 --> 状态变化 --> 发布者通知到订阅者,这就是观察者模式的“套路”。

在实践中理解定义

结合我们上面的分析,现在大家知道,在观察者模式里,至少应该有两个关键角色是一定要出现的——发布者和订阅者。用面向对象的方式表达的话,那就是要有两个类

首先我们来看这个代表发布者的类,我们给它起名叫Publisher。这个类应该具备哪些“基本技能”呢?大家回忆一下上文中的韩梅梅,韩梅梅的基本操作是什么?首先是拉群(增加订阅者),然后是@所有人(通知订阅者),这俩是最明显的了。此外作为群主&产品经理,韩梅梅还具有踢走项目组成员(移除订阅者)的能力。OK,产品经理发布者类的三个基本能力齐了,下面我们开始写代码:

// 定义发布者类
class Publisher {
  constructor() {
    this.observers = []
    console.log('Publisher created')
  }
  // 增加订阅者
  add(observer) {
    console.log('Publisher.add invoked')
    this.observers.push(observer)
  }
  // 移除订阅者
  remove(observer) {
    console.log('Publisher.remove invoked')
    this.observers.forEach((item, i) => {
      if (item === observer) {
        this.observers.splice(i, 1)
      }
    })
  }
  // 通知所有订阅者
  notify() {
    console.log('Publisher.notify invoked')
    this.observers.forEach((observer) => {
      observer.update(this)
    })
  }
}

ok,搞定了发布者,我们一起来想想订阅者能干啥——其实订阅者的能力非常简单,作为被动的一方,它的行为只有两个——被通知、去执行(本质上是接受发布者的调用,这步我们在Publisher中已经做掉了)。既然我们在Publisher中做的是方法调用,那么我们在订阅者类里要做的就是方法的定义

// 定义订阅者类
class Observer {
    constructor() {
        console.log('Observer created')
    }
    update() {
        console.log('Observer.update invoked')
    }
}

以上,我们就完成了最基本的发布者和订阅者类的设计和编写。在实际的业务开发中,我们所有的定制化的发布者/订阅者逻辑都可以基于这两个基本类来改写。比如我们可以通过拓展发布者类,来使所有的订阅者来监听某个特定状态的变化。仍然以开篇的例子为例,我们让开发者们来监听需求文档(prd)的变化:

// 定义一个具体的需求文档(prd)发布类
class PrdPublisher extends Publisher {
    constructor() {
        super()
        // 初始化需求文档
        this.prdState = null
        // 小红还没有拉群,开发群目前为空
        this.observers = []
        console.log('PrdPublisher created')
    }
    
    // 该方法用于获取当前的prdState
    getState() {
        console.log('PrdPublisher.getState invoked')
        return this.prdState
    }
    
    // 该方法用于改变prdState的值
    setState(state) {
        console.log('PrdPublisher.setState invoked')
        // prd的值发生改变
        this.prdState = state
        // 需求文档变更,立刻通知所有开发者
        this.notify()
    }
}

作为订阅方,开发者的任务也变得具体起来:接收需求文档、并开始干活:

class DeveloperObserver extends Observer {
    constructor() {
        super()
        // 需求文档一开始还不存在,prd初始为空对象
        this.prdState = {}
        console.log('DeveloperObserver created')
    }
    
    // 重写一个具体的update方法
    update(publisher) {
        console.log('DeveloperObserver.update invoked')
        // 更新需求文档
        this.prdState = publisher.getState()
        // 调用工作函数
        this.work()
    }
    
    // work方法,一个专门搬砖的方法
    work() {
        // 获取需求文档
        const prd = this.prdState
        // 开始基于需求文档提供的信息搬砖。。。
        ...
        console.log('996 begins...')
    }
}

下面,我们可以 new 一个 PrdPublisher 对象(产品经理),她可以通过调用 setState 方法来更新需求文档。需求文档每次更新,都会紧接着调用 notify 方法来通知所有开发者,这就实现了定义里所谓的:

目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

OK,下面我们来看看小红和她的小伙伴们是如何搞事情的吧:

// 创建订阅者:前端开发阿噗
const liLei = new DeveloperObserver()
// 创建订阅者:服务端开发小A(sorry。。。起名字真的太难了)
const A = new DeveloperObserver()
// 创建订阅者:测试同学小B
const B = new DeveloperObserver()
// 小红出现了
const hanMeiMei = new PrdPublisher()
// 需求文档出现了
const prd = {
    // 具体的需求内容
    ...
}
// 小红开始拉群
hanMeiMei.add(liLei)
hanMeiMei.add(A)
hanMeiMei.add(B)
// 小红发送了需求文档,并@了所有人
hanMeiMei.setState(prd)

以上,就是观察者模式在代码世界里的完整实现流程了。

相信走到这一步,大家对观察者模式的核心思想、基本实现模式都有了不错的掌握。

观察者模式与发布-订阅模式的区别是什么?

在面试过程中,一些对细节比较在意的面试官可能会追问观察者模式与发布-订阅模式的区别。这个问题可能会引发一些同学的不适,因为在大量参考资料以及已出版的纸质书籍中,都会告诉大家“发布-订阅模式和观察者模式是同一个东西的两个名字”。本书在前文的叙述中,也没有突出强调两者的区别。其实这两个模式,要较起真来,确实不能给它们划严格的等号。

为什么大家都喜欢给它们强行划等号呢?这是因为就算划了等号,也不影响我们正常使用,毕竟两者在核心思想、运作机制上没有本质的差别。但考虑到这个问题确实可以成为面试题的一个方向,此处我们还是单独拿出来讲一下。

回到我们上文的例子里。小红把所有的开发者拉了一个群,直接把需求文档丢给每一位群成员,这种发布者直接触及到订阅者的操作,叫观察者模式。但如果小红没有拉群,而是把需求文档上传到了公司统一的需求平台上,需求平台感知到文件的变化、自动通知了每一位订阅了该文件的开发者,这种发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式

相信大家也已经看出来了,观察者模式和发布-订阅模式之间的区别,在于是否存在第三方、发布者能否直接感知订阅者(如图所示)。

观察者模式

在我们见过的这些例子里,小红拉钉钉群的操作,就是典型的观察者模式;而通过EventBus去实现事件监听/发布,则属于发布-订阅模式。

既生瑜,何生亮?既然有了观察者模式,为什么还需要发布-订阅模式呢?

大家思考一下:为什么要有观察者模式?观察者模式,解决的其实是模块间的耦合问题,有它在,即便是两个分离的、毫不相关的模块,也可以实现数据通信。但观察者模式仅仅是减少了耦合,并没有完全地解决耦合问题——被观察者必须去维护一套观察者的集合,这些观察者必须实现统一的方法供被观察者调用,两者之间还是有着说不清、道不明的关系。

而发布-订阅模式,则是快刀斩乱麻了——发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上。发布-订阅模式下,实现了完全地解耦。

但这并不意味着,发布-订阅模式就比观察者模式“高级”。在实际开发中,我们的模块解耦诉求并非总是需要它们完全解耦。如果两个模块之间本身存在关联,且这种关联是稳定的、必要的,那么我们使用观察者模式就足够了。而在模块与模块之间独立性较强、且没有必要单纯为了数据通信而强行为两者制造依赖的情况下,我们往往会倾向于使用发布-订阅模式。

观察者模式,基于一个主题/事件通道,希望接收通知的对象(称为subscriber)通过自定义事件订 阅主题,通过deliver发布主题事件的方式被通知。就和用户订阅微信公众号道理一样,只要发布,用户就能接收到最新的内容。

/**
 * describe: 实现一个观察者模式
 */
let data = {
    hero: '凤凰',
};
//用来储存 订阅者 的数组
let subscribers = [];
//订阅 添加订阅者 方法
const addSubscriber = function(fn) {
    subscribers.push(fn)
}
//发布
const deliver = function(name) {
    data.hero = name;
    //当数据发生改变,调用(通知)所有方法(订阅者)
    for(let i = 0; i<subscribers.length; i++){
        const fn = subscribers[i]
        fn()
    }
}
//通过 addSubscriber 发起订阅
addSubscriber(() => {
    console.log(data.hero)
})
//改变data,就会自动打印名称
deliver('发条') 
deliver('狐狸')
deliver('卡牌')

这个发布订阅通过 addSubscriber 来储存订阅者(方法fn),当通过调用 deliver 来改变数据的时候,就会自动遍历 addSubscriber 来执行里面的 fn 方法 。

重新学习一下Redux

首先我们把上面那个发布订阅代码优化一下,顺便改一下命名,为什么要改命名?主要是紧跟 Redux 的步伐。让同学们更加眼熟。
let state = {hero: '凤凰'};
let subscribers = [];
//订阅 定义一个 subscribe 
const subscribe = (fn) => {
    subscribers.push(fn)
}
//发布
const dispatch = (name) => {
    state.hero = name;
    //当数据发生改变,调用(通知)所有方法(订阅者)
    subscribers.forEach(fn=>fn())
}
//通过 subscribe 发起订阅
subscribe(() => {
    console.log(state.hero)
})
//改变state状态,就会自动打印名称
//这里要注意的是,state状态不能直接去修改
dispatch('发条') 
dispatch('狐狸')
dispatch('卡牌')

现在这样一改是不是很眼熟了,没错这就是一个类似redux改变状态的思路。但是光一个发布订阅还是不够的,不可能改变一个状态需要去定义这么多方法。所以我们把他封装起来。

creatStore 方法
const creatStore = (initState) => {
    let state = initState;
    let subscribers = [];
    //订阅 定义一个 subscribe 
    const subscribe = (fn) => {
        subscribers.push(fn)
    }
    //发布
    const dispatch = (currentState) => {
        state = currentState;
        //当数据发生改变,调用(通知)所有方法(订阅者)
        subscribers.forEach(fn=>fn())
    }
    // 这里需要添加这个获取 state 的方法
    const getState = () => {
        return state;
    }
    return {
        subscribe,
        dispatch,
        getState,
    }
}

这样就创建好了一个 createStore 方法。没有什么新东西,就传进去一个初始状态,然后在返回 subscribe, dispatch, getState 三大方法。这里新增了个 getState 方法,代码很简单就是一个 return state 为了获取 state.

creatStore 使用

实现了 createStore 下面我们来试试如何使用他,那就拿那个非常经典的案例--计数器来试试

let initState = {
    num: 0,
}
const store = creatStore(initState);
//订阅
store.subscribe(() => {
    let state = store.getState()
    console.log(state.num)
})
// +1
store.dispatch({
   num: store.getState().num + 1
})
//-1
store.dispatch({
   num: store.getState().num - 1
})
复制代码

这个样子又接近了一点 Redux 的模样。 不过这样有个问题。如果你使用 store.dispatch 方法时,中间万一写错了或者传了个其他东西那就比较麻烦了。就比如下面这样: img

其实我是想 +1,+1,-1 最后应该是 1 (初始 num 为0)!但是由于写错了一个导致后面的都会错。而且他还有个问题就是可以随便的给一个新的状态。那么就显得不那么单纯了。比如下面这样: img

因为恶意修改 num 为 String 类型,导致后面在使用 dispatch 由于 num 不再是 Number 类型,导致打印出 NaN,这就不是我们想要的啦。所以我们要在改造一下,让 dispatch 变得单纯一些。那要怎么做呢?我们请一个管理者来帮我们管理,暂且给他命名 reducer

为什么叫 reducer

找一找中文 Redux 官网,他是这样说的:

之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue) 里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作。

诶,这个翻译似乎就清楚了很多。正如下面评论者说的一样 灵感来自于数组中reduce方法,是一种运算合成。那么说到这里我就来介绍一下 reduce。

什么是 reduce

话不多说直接上代码

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15

/*该减速作用有四个参数:
*累加器(acc)
*当前价值(cur)
*当前指数(idx)
*源数组(src)
*您的reducer函数的返回值被分配给累加器,其值在整个阵列的每次迭代中被记住,并最终成为最终的单个结果值。
*/

具体参数介绍

callback
  函数在数组中的每个元素上执行,有四个参数:
accumulator
  累加器累加回调的返回值; 它是先前在回调调用中返回的累计值,或者initialValue,如果提供(参见下文)。
currentValue
  当前元素在数组中处理。
currentIndex可选的
  数组中正在处理的当前元素的索引。如果initialValue提供了an,则从索引0开始,否则从索引1开始
array可选的
  该阵列reduce()被召唤。
initialValue可选的
  用作第一次调用的第一个参数的值callback。如果未提供初始值,则将使用数组中的第一个元素。调用reduce()没有初始值的空数组是一个错误。
复制代码

这个方法相对比 forEach, map, filter 这个理解起来还是算比较困难的。也可以看 MDN 的 Array.prototype.reduce() 详细介绍

注:首先感谢下面评论者 panda080 的指导,受他的建议,我重新去 Rudex 官网寻找。通过学习自己也更加的理解了 reducer 和 reduce reducer官网

ps:理解完之后,其实个人觉得 reducer 这个命名从翻译过来的角度总觉得很怪异。可能英语有限,或许他有更加贴切的意思我还不知道。
什么是 reducer

reducer 在我学习的过程中我把他认为是个管理者(可能这个认为是不正确的),然后我们每次想做什么就去通知管理者,让他在来根据我们说的去做。如果我们不小心说错了,那么他就不会去做。直接按默认的事情来。噔噔蹬蹬 reducer 登场!!

function reducer(state, action) {
    //通过传进来的 action.type 让管理者去匹配要做的事情
    switch (action.type){
        case 'add':
            return {
                ...state,
                count: state.count + 1
            }
        case 'minus':
            return {
                ...state,
                count: state.count - 1
            }
        // 没有匹配到的方法 就返回默认的值
        default:
            return state;
    }
}

增加了这个管理者,那么我们就要重新来写一下之前的 createStroe 方法了:把 reducer 放进去

const creatStore = (reducer,initState) => {
    let state = initState;
    let subscribers = [];
    //订阅 定义一个 subscribe 
    const subscribe = (fn) => {
        subscribers.push(fn)
    }
    //发布
    const dispatch = (action) => {
        state = reducer(state,action);
        subscribers.forEach(fn=>fn())
    }
    const getState = () => {
        return state;
    }
    return {
        subscribe,
        dispatch,
        getState,
    }
}

很简单的一个修改,为了让你们方便看出修改的地方,和区别,我特意重新码了这两个前后的方法对比,如下图 img

好,接下来我们试试添加了管理者的 creatStore 效果如何。

function reducer(state, action) {
    //通过传进来的 action.type 让管理者去匹配要做的事情
    switch (action.type){
        case 'add':
            return {
                ...state,
                num: state.num + 1
            }
        case 'minus':
            return {
                ...state,
                num: state.num - 1
            }
        // 没有匹配到的方法 就返回默认的值
        default:
            return state;
    }
}

let initState = {
    num: 0,
}
const store = creatStore(reducer,initState);
//订阅
store.subscribe(() => {
    let state = store.getState()
    console.log(state.num)
})

为了看清楚结果,dispatch(订阅)我直接在控制台输出,如下图: img 效果很好,我们不会再因为写错,而出现 NaN 或者其他不可描述的问题。现在这个 dispatch 比较纯粹了一点。

我们只是给他一个 type ,然后让管理者自己去帮我们处理如何更改状态。如果不小心写错,或者随便给个 type 那么管理者匹配不到那么这个动作那么我们这次 dispatch 就是无效的,会返回我们自己的默认 state。

好叻,现在这个样子基本上就是我脑海中第一次使用 redux 看到的样子。那个时候我使用起来都非常困难。当时勉强实现了一下这个计数器 demo 我就默默的关闭了 vs code。

接下来我们再完善一下这个 reducer,给他再添加一个方法。并且这次我们再给 state 一个

function reducer(state, action) {
    //通过传进来的 action.type 让管理者去匹配要做的事情
    switch (action.type){
        case 'add':
            return {
                ...state,
                num: state.num + 1
            }
        case 'minus':
            return {
                ...state,
                num: state.num - 1
            }
        // 增加一个可以传参的方法,让他更加灵活
        case 'changeNum':
            return {
                ...state,
                num: state.num + action.val
            }
        // 没有匹配到的方法 就返回默认的值
        default:
            return state;
    }
}

let initState = {
    num: 0,
}
const store = creatStore(reducer,initState);
//订阅
store.subscribe(() => {
    let state = store.getState()
    console.log(state.num)
})

控制台再使用一次新的方法:

img

好叻,这样是不是就让 dispatch 更加灵活了。

现在我们在 reducer 中就写了 3 个方法,但是实际项目中,方法一定是很多的,那么都这样写下去,一定是不利于开发和维护的。那么这个问题就留给大家去思考一下。

总结

专栏第二篇与大家一起学习了Redux基本知识、后续还会有更精彩的哇、一起加油哇~

❤️ 感谢大家

如果你觉得这篇内容对你挺有有帮助的话: 点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)关注公众号给npy的前端秘籍,我们一起学习一起进步。 觉得不错的话,也可以阅读其他文章(感谢朋友的鼓励与支持🌹🌹🌹)

开启LeetCode之旅

LeetCode之双指针

Leet27、移除元素

前端工程师必学的经典排序算法

LeetCode20、括号匹配

LeetCode7、整数反转

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。