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
文章的完整代码能够直接查看算法
随着 JavaScript 单页应用开发日趋复杂,JavaScript 须要管理比任什么时候候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成还没有持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。管理不断变化的 state 很是困难。若是一个 model 的变化会引发另外一个 model 变化,那么当 view 变化时,就可能引发对应 model 以及另外一个 model 的变化,依次地,可能会引发另外一个 view 的变化。直至你搞不清楚到底发生了什么。state 在何时,因为什么缘由,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。redux
Redux将这些复杂度很大程度归因于: 变化和异步.它们采起的方案是经过限制更新发生的时间和方式,Redux 试图让 state 的变化变得可预测segmentfault
咱们先从Redux的三大原则扩展开来一个基本雏形缓存
整个应用的 state 被储存在一棵 object tree 中,而且这个 object tree 只存在于惟一一个 store 中。
咱们用一个对象做惟一数据源,里面能够自定义各类数据服务器
// 惟一数据源 let 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
把原生写法转成上面说的三大原则思想实现
-------------省略部分代码---------------- // 初始数据 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 }
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。
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 } }
-------------省略部分代码---------------- // 实例化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的使用模式重写代码
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 } }
将每一个action都定义成一个函数
function add () { return { type: 'ADD' } } function reduce () { return { type: 'REDUCE' } }
注意,即便没有符合条件,也必须返回原值
这里能够看出,随着分发器越多显得就越臃肿,不适于业务代码的编写,下面会讲怎么解决
// 纯函数修改 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 } }
// 初始数据 const initStore = { count: 0 } // 实例化store let store = createStore(initStore, reducers)
<!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 进行命名。
从介绍能够知道大概须要实现的功能
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
咱们投入实战使用
新增action描述
function add () { return { type: 'ADD' } } function reduce () { return { type: 'REDUCE' } } function multiply () { return { type: 'MULTIPLY' } } function divide () { return { type: 'DIVIDE' } }
实现重点:
// 纯函数修改 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 })
数据源的初始数据修改
// 初始数据 const initStore = { arNum: 0, mdNum: 1 } // 实例化store let store = createStore(initStore, reducers)
新增结构实现加减乘除功能
<!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