React系列-从零构建状态管理及Redux源码解析七

9次阅读

共计 8368 个字符,预计需要花费 21 分钟才能阅读完成。

React 系列

React 系列 — 简单模拟语法 (一)
React 系列 — Jsx, 合成事件与 Refs(二)
React 系列 — virtualdom diff 算法实现分析 (三)
React 系列 — 从 Mixin 到 HOC 再到 HOOKS(四)
React 系列 — createElement, ReactElement 与 Component 部分源码解析 (五)
React 系列 — 从使用 React 了解 Css 的各种使用方案 (六)
React 系列 — 从零构建状态管理及 Redux 源码解析 (七)
React 系列 — 扩展状态管理功能及 Redux 源码解析 (八)

前言

虽然摆在 React 系列里, 但是我没有把这当做是实现 Redux 的文章, 而是分析状态管理实现原理的科普文, 所以我会从 Redux 的实现思想和部分源码做参考, 用最原始的 Js 实现一个基本库, 所以这里不会出现任何框架库.

而且我默认大家都懂得基本概念, 所以我不会特意展开过多篇幅在细节上, 而且因为时间关系, 我会将相关的类型判断省略掉.

文章的完整代码可以直接查看

Redux 诞生的契机

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

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

Redux 将这些复杂度很大程度归因于: 变化和异步 . 它们采取的方案是通过限制更新发生的时间和方式,Redux 试图让 state 的变化变得可预测

Redux 三大原则

我们先从 Redux 的三大原则扩展开来一个基本雏形

单一数据源

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

我们用一个对象作唯一数据源, 里面可以自定义各种数据

// 唯一数据源
let state = {};

State 是只读的

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

确保修改的来源是唯一的, 而 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来

{
  type: 'DOSOMETHING',
  data: {}}

使用纯函数来执行修改

接收先前的 state 和 action,并返回新的 state

因为 reducer 只是函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务

function channgeState(state, action) {switch (action.type) {
    case 'DOSOMETHING':
      return action.data
    default:
      return state
  }
}

示例一

简单的数字计算器为例

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>js-redux</title>
</head>

<body>
    <div class="container">
        <button id="add">+</button>
        <span id="num">0</span>
        <button id="reduce">-</button>
    </div>
    <script>
        const $add = document.getElementById('add');
        const $num = document.getElementById('num');
        const $reduce = document.getElementById('reduce');

        let val = 0;
        $add.onclick = () => $num.innerText = ++val;
        $reduce.onclick = () => $num.innerText = --val;
    </script>
</body>

</html>

我们实现了基本加减功能

文章的完整代码可以直接查看 demo1

示例二 (三大原则)

把原生写法转成上面说的三大原则思想实现

index.html

------------- 省略部分代码 ----------------
// 初始数据
let initStore = {count: 0}
// 纯函数修改
function reducer(state, action) {switch (action.type) {
        case 'ADD':
            return {
                ...state,
                count: state.count + 1
            };
        case 'REDUCE':
            return {
                ...state,
                count: state.count - 1
            }
    }
}
// 实例化 store
let store = createStore(initStore, reducer);
$add.onclick = () => {
    store.dispatch({type: 'ADD'})
    $num.innerText = store.getState().count}
$reduce.onclick = () => {
    store.dispatch({type: 'REDUCE'})
    $num.innerText = store.getState().count}

index.js

function createStore (initStore = {}, reducer) {
  // 唯一数据源
  let state = initStore

  // 唯一获取数据函数
  const getState = () => state

  // 纯函数来执行修改, 只返回最新数据
  const dispatch = (action) => {state = reducer(state, action)
  }

  return {
    getState,
    dispatch
  }
}

现在看各自功能划分基本明确, 但是比较麻烦的是每次修改之后都得手动获取最新的数据展示, 这种体验相当繁琐, 而 Redux 的 store 提供了一个监听事件, 所以我们也来实现一个

文章的完整代码可以直接查看 demo2

实例三 (监听事件)

我们看看介绍

添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。你可以在回调函数里调用 getState() 来拿到当前 state。

index.js

function createStore (initStore = {}, reducer) {
  // 唯一数据源
  let state = initStore
  // 监听队列
  const listenList = []

  // 唯一获取数据函数
  const getState = () => state

  // 纯函数来执行修改, 只返回最新数据
  const dispatch = (action) => {state = reducer(state, action)
    listenList.forEach((listener) => {listener(state)
    })
  }

  // 添加监听器, 同时返回解绑该事件的函数
  const subscribe = (fn) => {listenList.push(fn)
    return function unsubscribe () {listenList = listenList.filter((listener) => fn !== listener)
    }
  }

  return {
    getState,
    dispatch,
    subscribe
  }
}

index.html

------------- 省略部分代码 ----------------
// 实例化 store
let store = createStore(initStore, reducer);
// 自动监听渲染数据
store.subscribe(() => {$num.innerText = store.getState().count
})
$add.onclick = () => {
    store.dispatch({type: 'ADD'})
}
$reduce.onclick = () => {
    store.dispatch({type: 'REDUCE'})
}

文章的完整代码可以直接查看 demo3

实例四 (模块划分)

因为我们已经达到功能使用的阶段, 接下来就该将每个功能区划分开来, 按照 Redux 的使用模式重写代码

createStore.js

function createStore (initStore = {}, reducer) {
  // 唯一数据源
  let state = initStore
  // 监听队列
  const listenList = []

  // 唯一获取数据函数
  const getState = () => state

  // 纯函数来执行修改, 只返回最新数据
  const dispatch = (action) => {state = reducer(state, action)
    listenList.forEach((listener) => {listener(state)
    })
  }

  // 添加监听器, 同时返回解绑该事件的函数
  const subscribe = (fn) => {listenList.push(fn)
    return function unsubscribe () {listenList = listenList.filter((listener) => fn !== listener)
    }
  }

  return {
    getState,
    dispatch,
    subscribe
  }
}

actions.js

将每个 action 都定义成一个函数

function add () {
  return {type: 'ADD'}
}
function reduce () {
  return {type: 'REDUCE'}
}

reducers.js

注意, 即使没有符合条件, 也必须返回原值

这里可以看出, 随着分发器越多显得就越臃肿, 不适于业务代码的编写, 下面会讲怎么解决

// 纯函数修改
function reducers (state, action) {switch (action.type) {
    case 'ADD':
      return {
        ...state,
        count: state.count + 1
      }
    case 'REDUCE':
      return {
        ...state,
        count: state.count - 1
      }
    // 默认返回原值
    default:
      return state
  }
}

store.js

// 初始数据
const initStore = {count: 0}
// 实例化 store
let store = createStore(initStore, reducers)

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>js-redux</title>
</head>

<body>
    <div class="container">
        <button id="add">+</button>
        <span id="num">0</span>
        <button id="reduce">-</button>
    </div>
    <script src="./createStore.js"></script>
    <script src="./actions.js"></script>
    <script src="./reducers.js"></script>
    <script src="./store.js"></script>
    <script>
        // 选择器
        const $add = document.getElementById('add');
        const $num = document.getElementById('num');
        const $reduce = document.getElementById('reduce');
        // 自动监听渲染数据
        store.subscribe(() => {$num.innerText = store.getState().count
        })
        $add.onclick = () => {store.dispatch(add())
        }
        $reduce.onclick = () => {store.dispatch(reduce())
        }
    </script>
</body>

</html>

文章的完整代码可以直接查看 demo4

合并分发器

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

合并后的 reducer 可以调用各个子 reducer,并把它们返回的结果合并成一个 state 对象。由 combineReducers() 返回的 state 对象,会将传入的每个 reducer 返回的 state 按其传递给 combineReducers() 时对应的 key 进行命名。

从介绍可以知道大概需要实现的功能

  • 接收多个不同的 reducer 函数对象
  • 将传入的每个 reducer 返回的 state 按其传递给 combineReducers() 时对应的 key 进行命名
  • 每个 reducer 单独处理子 state
  • 返回最终的 reducer 函数

combineReducers .js

function combineReducers (reducers) {
  // 获取索引值
  const reducerKeys = Object.keys(reducers)
  // 最终返回的 reducer 对象
  const finalReducers = {}
  // 筛选索引值对应的函数类型才赋值到最终 reducer 对象
  reducerKeys.forEach((key) => {if (typeof reducers[key] === 'function') finalReducers[key] = reducers[key]
  })
  // 获取最终 reducer 对象索引值
  const finalReducerKeys = Object.keys(finalReducers)

  // 返回给 store 初始化使用的分发函数
  return function (state = {}, action) {
    // 是否改变和新的 state
    let isChange = false
    const nextState = {}
    // 遍历触发对应分发器
    finalReducerKeys.forEach((key) => {
      // 当阶段数据
      const oldState = state[key]
      // 分发器处理后最新数据
      const newState = finalReducers[key](oldState, action)
      nextState[key] = newState
      // 对比前后数据是否一致
      isChange = isChange || oldState !== newState
    })
    // 检测分发器处理后阶段的数据值有没发生变化
    isChange = isChange || finalReducerKeys.length !== Object.keys(state).length
    return isChange ? nextState : state
  }
}

实际源码大体一致, 只是里面使用 ts 实现并且我省略了很多参数判断和错误提示, 大家可以直接查看源码, 两百行左右并不复杂 combineReducers

示例五 (合并分发器)

我们投入实战使用

actions.js

新增 action 描述

function add () {
  return {type: 'ADD'}
}
function reduce () {
  return {type: 'REDUCE'}
}
function multiply () {
  return {type: 'MULTIPLY'}
}
function divide () {
  return {type: 'DIVIDE'}
}

reducer.js

实现重点:

  • 数据处理映射到每个单独的函数操作
  • 每个函数只负责该映射数据的处理
// 纯函数修改
function arNum (state, action) {switch (action.type) {
    case 'ADD':
      return state + 1
    case 'REDUCE':
      return state - 1
    // 默认返回原值
    default:
      return state
  }
}

// 纯函数修改
function mdNum (state, action) {switch (action.type) {
    case 'MULTIPLY':
      return state * 2
    case 'DIVIDE':
      return state / 2
    // 默认返回原值
    default:
      return state
  }
}

const reducers = combineReducers({
  arNum,
  mdNum
})

store.js

数据源的初始数据修改

// 初始数据
const initStore = {
  arNum: 0,
  mdNum: 1
}
// 实例化 store
let store = createStore(initStore, reducers)

index.html

新增结构实现加减乘除功能

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>js-redux</title>
</head>

<body>
    <div class="container">
        <button id="add">+</button>
        <span id="num1">0</span>
        <button id="reduce">-</button>
        <button id="multiply">×</button>
        <span id="num2">1</span>
        <button id="divide">÷</button>
    </div>
    <script src="./createStore.js"></script>
    <script src="./combineReducers .js"></script>
    <script src="./actions.js"></script>
    <script src="./reducers.js"></script>
    <script src="./store.js"></script>
    <script>
        // 选择器
        const $add = document.getElementById('add');
        const $reduce = document.getElementById('reduce');
        const $multiply = document.getElementById('multiply');
        const $divide = document.getElementById('divide');
        const $num1 = document.getElementById('num1');
        const $num2 = document.getElementById('num2');
        // 自动监听渲染数据
        store.subscribe(() => {$num1.innerText = store.getState().arNum
            $num2.innerText = store.getState().mdNum})
        $add.onclick = () => store.dispatch(add())
        $reduce.onclick = () => store.dispatch(reduce())
        $multiply.onclick = () => store.dispatch(multiply())
        $divide.onclick = () => store.dispatch(divide())
    </script>
</body>

</html>

至此 redux 的基本功能我们都已经一步步实现完成了

文章的完整代码可以直接查看 demo5

正文完
 0