0. 前言
猿人们都知道什么React、Redux、React-redux,其实他们根本不是同一个东西,Redux 是一种架构模式(Flux 架构的一种变种),它不关注你到底用什么库,你可以把它应用到 React 和 Vue,甚至跟 jQuery 结合都没有问题。而 React-redux 就是把 Redux 这种架构模式和 React.js 结合起来的一个库,就是 Redux 架构在 React.js 中的体现。至于我为什么会写这篇文章,是有原因的,因为项目以前没有用Redux,有很多数据更新的地方不能达到实时的、同步的更新,记得在官网上看到过这样一句话,曰“如果你不知道自己的项目中要不要用Redux,那就说明不需要用”。而现在我的项目中,就有这种需求,所以。。。不说了,让我静一静。
1. 简介
引用书中的一段话
Redux是JavaScript的状态容器,它提供可预测的状态管理。Redux可以运行在不同的环境中,不论在客户端、服务器端、还是原生应用都可以运行Redux。
2. 动机
一般我们要想学习一个东西的时候,做一件事的时候都是需要有动机的,为什么要去做?Redux的动机!!!前端开发的应用真正变得越来越复杂,随着各种框架的推出,单页面应用也层出不穷,这些应用的状态(state)也变得复杂起来。状态其实就是这个运行的时候需要的各种各样的动态数据。
管理这些不断的变化令人非常苦恼,改变一个model的时候可能会引起其他无法预料的副作用,比如说其他model的变化或者view的变化。state在何时、什么原因发生了改变都变得无法预测。
Redux正是试图解决这个问题、让state的变化可以预测的工具。
3. 三大定律
1. 单一数据源
整个应用的state存储在一个JavaScript对象中,Redux用一个称为store的对象来存储整个state。
//模拟数据存储
{
posts : {
isLoading : false,
items : [
{
id : 1, content : " hello world "
}
]
}
}
2. state是只读的
不能在state上面直接修改数据,改变state的唯一方法是触发action。action只是一个信息载体,一个普通的JavaScript对象。这样确保了其他操作都无法修改state数据,整个修改都被集中处理,而且严格按顺序执行
//使用dispatch触发store的改变
store.dispatch({
type : ' GET_INFO ',
post : {id : 2, content : " hello there "}
})
//使用getState 方法返回当前的state
store.getState()
3. 使用纯函数执行修改
为了描述action怎样改变state,需要编写reducer来规定修改的规则。
reducer是纯函数,接收先前的state和处理的action,返回新的state。reducer可以根据应用的大小拆分成多个,分别操作state的不同部分。
那么问题就来了,你可能天天都在码代码,天天都在用,却说不出来什么是纯函数,那么我来举个栗子...
简单来说,一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。
- 函数的返回结果只依赖于它的参数。
- 函数执行过程里面没有副作用。
函数的返回结果只依赖于它的参数
const a = 1
const foo = (b) => a + b
foo(2) // => 3
foo 函数不是一个纯函数,因为它返回的结果依赖于外部变量 a,我们在不知道 a 的值的情况下,并不能保证 foo(2) 的返回值是 3。虽然 foo 函数的代码实现并没有变化,传入的参数也没有变化,但它的返回值却是不可预料的,现在 foo(2) 是 3,可能过了一会就是 4 了,因为 a 可能发生了变化变成了 2。
const a = 1
const foo = (x, b) => x + b
foo(1, 2) // => 3
现在 foo 的返回结果只依赖于它的参数 x 和 b,foo(1, 2) 永远是 3。今天是 3,明天也是 3,在服务器跑是 3,在客户端跑也 3,不管你外部发生了什么变化,foo(1, 2) 永远是 3。只要 foo 代码不改变,你传入的参数是确定的,那么 foo(1, 2) 的值永远是可预料的。
这就是纯函数的第一个条件:一个函数的返回结果只依赖于它的参数。
函数执行过程没有副作用
一个函数执行过程对产生了外部可观察的变化那么就说这个函数是有副作用的。
我们修改一下 foo:
const a = 1
const foo = (obj, b) => {
return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 3
counter.x // => 1
我们把原来的 x 换成了 obj,我现在可以往里面传一个对象进行计算,计算的过程里面并不会对传入的对象进行修改,计算前后的 counter 不会发生任何变化,计算前是 1,计算后也是 1,它现在是纯的。但是我再稍微修改一下它:
const a = 1
const foo = (obj, b) => {
obj.x = 2
return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 4
counter.x // => 2
现在情况发生了变化,我在 foo 内部加了一句 obj.x = 2,计算前 counter.x 是 1,但是计算以后 counter.x 是 2。foo 函数的执行对外部的 counter 产生了影响,它产生了副作用,因为它修改了外部传进来的对象,现在它是不纯的。
但是你在函数内部构建的变量,然后进行数据的修改不是副作用:
const foo = (b) => {
const obj = { x: 1 }
obj.x = 2
return obj.x + b
}
虽然 foo 函数内部修改了 obj,但是 obj 是内部变量,外部程序根本观察不到,修改 obj 并不会产生外部可观察的变化,这个函数是没有副作用的,因此它是一个纯函数。
除了修改外部的变量,一个函数在执行过程中还有很多方式产生外部可观察的变化,比如说调用 DOM API 修改页面,或者你发送了 Ajax 请求,还有调用 window.reload 刷新浏览器,甚至是 console.log 往控制台打印数据也是副作用。
纯函数很严格,也就是说你几乎除了计算数据以外什么都不能干,计算的时候还不能依赖除了函数参数以外的数据。
那么,得知上面的结论可能你看了也不是太明白,准备两张图来看看区别:
1.原来模块(组件)修改共享数据是直接改的
2.我们很难把控每一根指向 appState 的箭头,appState 里面的东西就无法把控。但现在我们必须通过一个“中间人” —— dispatch,所有的数据修改必须通过它,并且你必须用 action 来大声告诉它要修改什么,只有它允许的才能修改:
我们再也不用担心共享数据状态的修改的问题,我们只要把控了 dispatch,所有的对 appState 的修改就无所遁形,毕竟只有一根箭头指向 appState 了。
4. 共享结构对象
其实你从字面的意思就能看出我想表达什么!共享对象,就是有一个模板,我直接引用过来,要想设置自己的属性,就设置,不想设置,就用原来的,可能就是你理解的深拷贝、浅拷贝,什么是什么拷贝呢,深拷贝就是将对象在堆区完整的copy一份,再返回栈区的引用,浅拷贝就是复制一份堆区的引用地址。
手画一张图。。。有点丑
大家都知道这种 ES6 的语法:
const obj = { a: 1, b: 2}
const obj2 = { ...obj } // => { a: 1, b: 2 }
const obj2 = { ...obj } 其实就是新建一个对象 obj2,然后把 obj 所有的属性都复制到 obj2 里面,相当于对象的浅复制。上面的 obj 里面的内容和 obj2 是完全一样的,但是却是两个不同的对象。除了浅复制对象,还可以覆盖、拓展对象属性:
const obj = { a: 1, b: 2}
const obj2 = { ...obj, b: 3, c: 4} // => { a: 1, b: 3, c: 4 },覆盖了 b,新增了 c
我们可以把这种特性应用在 state 的更新上,我们禁止直接修改原来的对象,一旦你要修改某些东西,你就得把修改路径上的所有对象复制一遍,例如,我们不写下面的修改代码:
模板代码
let appState = {
title: {
text: 'React.js 小书',
color: 'red',
},
content: {
text: 'React.js 小书内容',
color: 'blue'
}
}
我们新建一个 appState
let newAppState = { // 新建一个 newAppState
...appState, // 复制 appState 里面的内容
title: { // 用一个新的对象覆盖原来的 title 属性
...appState.title, // 复制原来 title 对象里面的内容
text: '《React.js 小书》' // 覆盖 text 属性
}
}
如果我们用一个树状的结构来表示对象结构的话:
appState 和 newAppState 其实是两个不同的对象,因为对象浅复制的缘故,其实它们里面的属性 content 指向的是同一个对象;但是因为 title 被一个新的对象覆盖了,所以它们的 title 属性指向的对象是不同的。同样地,修改 appState.title.color:
let newAppState1 = { // 新建一个 newAppState1
...newAppState, // 复制 newAppState1 里面的内容
title: { // 用一个新的对象覆盖原来的 title 属性
...newAppState.title, // 复制原来 title 对象里面的内容
color: "blue" // 覆盖 color 属性
}
}
我们每次修改某些数据的时候,都不会碰原来的数据,而是把需要修改数据路径上的对象都 copy 一个出来。这样有什么好处?看看我们的目的达到了:
appState !== newAppState // true,两个对象引用不同,数据变化了,重新渲染
appState.title !== newAppState.title // true,两个对象引用不同,数据变化了,重新渲染
appState.content !== appState.content // false,两个对象引用相同,数据没有变化,不需要重新渲染
修改数据的时候就把修改路径都复制一遍,但是保持其他内容不变,最后的所有对象具有某些不变共享的结构(例如上面三个对象都共享 content 对象)。大多数情况下我们可以保持 50% 以上的内容具有共享结构,这种操作具有非常优良的特性,我们可以用它来优化上面的渲染性能。
5. action
action是信息的载体,里面有action的名称和要传递的信息,然后可以被传递到store中去,传递的方法是利用store的dispatch方法,action是store的唯一信息来源。
6. reducer
createStore 接受一个叫 reducer 的函数作为参数,这个函数规定是一个纯函数,它接受两个参数,一个是 state,一个是 action。
如果没有传入 state 或者 state 是 null,那么它就会返回一个初始化的数据。如果有传入 state 的话,就会根据 action 来“修改“数据,但其实它没有、也规定不能修改 state,而是要通过上节所说的把修改路径的对象都复制一遍,然后产生一个新的对象返回。如果它不能识别你的 action,它就不会产生新的数据,而是(在 default 内部)把 state 原封不动地返回。
reducer 是不允许有副作用的。你不能在里面操作 DOM,也不能发 Ajax 请求,更不能直接修改 state,它要做的仅仅是 ——** 初始化和计算新的 state。**
现在我们可以用这个 createStore 来构建不同的 store 了,只要给它传入符合上述的定义的 reducer 即可:
function themeReducer (state, action) {
if (!state) return {
themeName: 'Red Theme',
themeColor: 'red'
}
switch (action.type) {
case 'UPATE_THEME_NAME':
return { ...state, themeName: action.themeName }
case 'UPATE_THEME_COLOR':
return { ...state, themeColor: action.themeColor }
default:
return state
}
}
const store = createStore(themeReducer)
...
7. store
store就是两者的粘合剂,它能完成以下这些任务:
- 保存整个程序的state
- 可以通过getState()方法访问state的值
- 可以通过dispatch()方法执行一个action
- 还可以通过subscribe(listenter)注册回调,监听state的变化
8. 结束语
废话就不多说了,在这里再附上一句话“learn once write anywhere”,感谢🙏🙏🙏🙏🙏🙏关注,谢谢。