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

React系列

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

前言

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

并且我默认你们都懂得基本概念,因此我不会特地展开过多篇幅在细节上,并且由于时间关系,我会将相关的类型判断省略掉.github

文章的完整代码能够直接查看算法

Redux诞生的契机

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

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

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

Redux 三大原则

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

单一数据源

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

咱们用一个对象做惟一数据源,里面能够自定义各类数据服务器

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

State 是只读的

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

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

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

使用纯函数来执行修改

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

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

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

相关文章
相关标签/搜索