阅读 138

Redux 在原生 JavaScript 中的使用,感受最原汁原味的 Redux

前言

作为前端开发者,相信即使没有使用过 redux,一定也听过 redux 的大名。我们常常和 react 搭配一起使用 redux,这样更能发挥 redux 的作用,因为这类库允许你通过改变 state 来更新试图。但 redux 同样可以与 Angular、jQuery 等库搭配使用,同样也可以在原生的 js 中使用。了解清楚了在原生 js 中的使用后更容易帮助开发者理解 redux。

Redux 是什么

简单说,Redux 是 JavaScript 的一个状态容器,提供可预测化的状态管理

随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。

管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。

Redux 要做的就是试图让 state 的变化变得可预测

Redux 的三大原则

redux 可以用这三个基本原则来描述:

1. 单一数据源

整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

2. State 是只读的

唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

3. 使用纯函数来执行修改

为了描述 action 如何改变 state tree,你需要编写 reducers

Redux 的基本使用

为了能够在原生 js 中使用 Redux,我们需要先来了解一个 Redux 的基本使用和常用的概念。

初识 store

store 是 Redux 中最核心的概念,是 Redux 架构的根本。前文提到过 Redux 是一个可预测状态的 “容器”,这里所说的容器,其实就是指 store。store 是惟一的, 保存着整个页面的状态数据数,并且为开发者提供了重要的 API。

1. store 提供的方法

事实上,store 就是一个 JavaScript 对象,里面包含了以下方法:

  1. getState():获取当前页面状态数据数,即 store 中的 state
  2. dispatch(action): 派发 action,更新 state
  3. subscribe(listener):注册监听器,订阅页面数据状态,即 store 中 state 的变化

2. 如何创建一个 store

import { createStore } from 'redux';
import reducer from './reducers';
// 利用 createStore 来创建
let store = createStore(reducer);
复制代码

reducer 参数为 createStore 的必传参数,也就是说,当开发者创建一个 store 的时候,必须同时定义一个 reducer,用来告知 store 数据状态应该如何根据 action 进行变更。

构造 action

action 描述了状态变更的信息,也就是需要页面做出的变化。这是由开发者定义并借助于 store.dispatch 派发的,action 本质上也是一个 JavaScript 对象。我们约定,action 对象内必须要有一个 type 属性,作为描述这个 action 的名称来唯一确定这个 action。

const action = {
    type: 'ADD_TODO',
    text: 'Build my first Redux app'
}
复制代码

定义好了 action 之后,可以利用 dispatch 派发这个 action

store.dispatch(action)
复制代码

编写 reducer 函数更新数据

action 描述了一种变化,并携带了这种变化的数据信息,但是 action 只是描述了有事情发生这一事实,并没有描述应用如何更新state。真正执行这种变化并生成正确数据状态的是 reducer 函数。

为了保证数据的可预测性,即相同的输入有相同的输出,所以 reducer 必须是一个纯函数。下面是一个最简单的 reducer 函数:

// initialState 为初始化的 state
function updateState(state = initialState, action) {
  // 这里暂不处理任何 action,
  // 仅返回传入的 state。
  return state
}
复制代码

当然,一个完整的 reducer 函数可能需要对多个 action 进行处理,如下:

function updateState(state = initialState, action) {
    switch (action.type) {
        case 'case1':
            return newState1;
        case 'case2':
            return newState2;
        case 'case3':
            return newState3;
        default:
            return state
    }
}
复制代码

总结一下 Redux 的数据流

严格的单向数据流 是 Redux 架构的设计核心。这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。

Redux 应用中数据的生命周期遵循下面 4 个步骤:

  1. 通过 Redux 的 createStore 方法创建了一个全局唯一的 store
  2. 调用 store.dispatch(action) 派发一个用来描述变化的 action
  3. Redux store 调用传入的 reducer 函数
  4. 如果有多个 reducer 函数,根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树
  5. Redux store 保存了根 reducer 返回的完整 state 树,这棵树就是最新的 state。
  6. 当 store 发生变化后,所有订阅 store.subscribe 的监听器都将被调用,监听器里可以调用 store.getState() 获取当前 state,可以应用最新的state 来更新 UI。

备注: 当我们执行 store.dispatch 派发一个 action 之后,Redux 会 “自动” 帮我们执行处理变化并更新数据的 reducer 函数。从 store.dispatch 到 reducer 这个过程可以认为是由 Redux 内部处理的。

Redux 在原生 JavaScript 中的开发基础实例

前面介绍了 Redux 的一些基础知识,但是可能还是感到无从下手,有点 “镜花水月” 的感觉,这是因为还需要打通 “任督二脉”,将所有的知识点进行连接,下面通过一个简单的在原生 JavaScript 中的实例来进一步加深理解。

以下是一个非常常见的场景,页面截图来自豆瓣 App,我可以对影评 “点赞” 或者 “踩”,并且记录 “点赞” 和 “踩” 的数量。如下图所示:

组件逻辑比较简单,点击 “有用” 按钮,页面展示点赞数 + 1,点击 “没用” 按钮,页面展示没用的数目 + 1。接下来我们来看下在原生 JavaScript 中利用 Redux 架构应该如何实现:

1. 引入 redux 资源

<!DOCTYPE html>
<html lang="en">
<head>
    <title>redux 在原生 JavaScript 中的使用</title>
    <script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
</head>
<body>
    <div class="wrap">
        <div class="like">点赞 <span>0</span></div>
        <div class="hate">踩 <span>0</span></div>
    </div>
</body>
</html>
复制代码

直接在浏览器打开上面的 html 文件, 然后在控制台通过全局变量 window.Redux 即可访问 Redux 对象

2. 定义初始状态

需要有两个状态来存储点击 “有用” 和 “无用” 的人数

// 定义初始状态
var initialState = {
    likeNum: 0,  // 点赞的总数
    hateNum: 0   // 踩的总数
}
复制代码

3. 创建 action

分析后得知,这里只需要两个 action,一个为点击 “有用” 按钮,一个为点击 “没用” 按钮。

// 定义有用 action
var likeAction = {
    type: 'like'
}

// 定义没用 action 
var hateAction = {
    type: 'hate'
}
复制代码

4. 编写 reducer 函数

function reducer(state = initialState, action) {
    switch(action.type){
        case 'like':
            return {
                likeNum: state.likeNum + 1,
                hateNum: state.hateNum
            };
        case 'hate':
            return {
                likeNum: state.likeNum,
                hateNum: state.hateNum + 1
            };
        default:
            return state;
    }
}
复制代码

创建唯一的 store

// 引入 createStore
var createStore = window.Redux.createStore;
// 创建 store
var store = createStore(reducer);
复制代码

点击按钮的时候派发 action

// 点击有用按钮,派发 likeAction
var likeButton = document.querySelector('.like');
likeButton.addEventListener('click', function(){
    store.dispatch(likeAction);
})

// 点击没用按钮,派发 hateAction
var hateButton = document.querySelector('.hate');
hateButton.addEventListener('click', function(){
    store.dispatch(hateAction);
})
复制代码

更新视图

// 定义更新视图的方法
var render = function() {
    document.querySelector('.like span').innerText = store.getState().likeNum;
    document.querySelector('.hate span').innerText = store.getState().hateNum;
}

// 监听 store 的变化,store 发生变化后调用 render 方法,利用原生 JavaScript 的方法更新视图
store.subscribe(render);
复制代码

总结

以上就是 Redux 的一个最简单的实例了,比较清晰地阐述了 Redux 的思想,Redux 完全可以独立于 React 而存在。

它的架构也很简单:需要开发者预先定义 action 及 reducer 函数,同时在恰当的时候派发 action,即 dispatch(action)。如果所编写的 reducer 函数没有问题,那么在正常更新状态之后,就可以通过 store.subscribe 注册每一次数据更新后的回调逻辑,这个回调逻辑往往就是对页面的渲染。在回调逻辑中,使用 store.subscribe() 获取最新数据,完成正确的页面响应,貌似一个发布订阅系统。

使用过 React 搭配 Redux 开发的读者可能对 store.subscribe() 有些陌生,因为它已经由 react-redux 库进行了封装,这也是 store 数据更新后便可以直接触发相关组件重新渲染的原因。 后续有时间的话会讲一下 react-redux 相关的内容。

关注下面的标签,发现更多相似文章
评论