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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。