纯函数
这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战
函数式编程中有一个概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;
如果一个函数需要成为纯函数,必须满足以下两点:
- 确定的输入,一定会产生确定的输出
- 函数在执行过程中,不能产生副作用 (也就是不可以修改函数作用域以外的值)
// 纯函数
function sum(num1, num2) {
return num1 + num2
}
// 不是纯函数
let num = 30
function add(param) {
// num是变量,在程序运行过程中,num的值可能会发生改变
// 传入相同的param值,返回的结果可能是不一样的
// 所以add函数不是一个纯函数
return param + num
}
// 纯函数
const num = 30
function add(param) {
// 因为num是常量
// 在整个代码运行过程中,num的值不会发生改变
// 所以相同的param传入一定会产生相同的返回值
return param + num
}
纯函数对于确定的输入一定会有确定的输出,而且不会产生任何的副作用
所以对于纯函数而言,纯函数和其它函数之间的耦合度是非常低的
所以React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改
这样就可以保证react的单向数据流,也就是所有对于父组件提供的状态的修改都一定在父组件中
不会出现子组件修改父组件中状态的情况,这样可以避免bug的出现,也减低了后期维护的难度
Redux
JavaScript开发的应用程序,已经变得越来越复杂了:
- JavaScript需要管理的状态越来越多,越来越复杂
- 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,(比如某些元素是否被选中, 是否显示加载动效,当前分页)
而管理不断变化的state是非常困难的
- 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化
- 所以当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪
虽然React在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理
Redux就是一个帮助我们管理State的容器: Redux是JavaScript的状态容器,提供了可预测的状态管理
我们可以通过Redux来统一管理项目中的所有需要在多个组件间共享的状态,并统一对对应的状态进行操作
从而取代原本的在项目中不同的地方对对应的状态进行修改的方式。
以便于对这些需要在多组件间共享的状态的操作更好的维护,和调试
Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)
三大原则
单一数据源:
- 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中
- 虽然Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护
- 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改
State是只读的
-
唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State
-
这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改stat
-
这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题
也就是多个地方同时操作同一个store,而产生冲突问题
使用纯函数来执行修改
- 通过reducer将 旧state和 actions联系在一起,并且返回一个新的State
- 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分
- 但是所有的reducer都应该是纯函数,不应该产生任何的副作用
基本使用
// Redux可以单独使用,不需要和React进行捆绑
// 所以先尝试在node环境下 单独使用Redux
const redux = require('redux')
// 实际管理数据的地方
const initialStore = {
count: 0
}
// redux 三要素之一: reducer
// 必须是一个纯函数
// 用于关联store和action
// reducer做的事情就是将传入的state和action结合起来生成一个新的state
// 对于参数store 每执行一次action,store就会传入最新的那个store
// 而对于第一次执行的时候, store就是undefined,所以会使用initialStore
// reducer函数的功能就是根据不同的action更新store,并返回最新的store
function reducer(store = initialStore, action) {
switch (action.type) {
case 'INCREMENT':
// count属性会覆盖原本store中的count属性
return { ...store, count: store.count + 1 }
case 'ADD':
return { ...store, count: store.count + action.num }
default:
return store
}
}
// redux 三要素之一: store
// 用于对需要共享的状态进行统一存储
const store = redux.createStore(reducer)
// redux 三要素之一: action
// 所有数据的变化,必须通过派发(dispatch)action来更新
// 而action本质其实就是一个JS对象
// 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的
const incrementAction = {
type: 'INCREMENT' // type是必填项 一般大写 表示你需要进行什么样的操作
}
const addNumAction = {
type: 'ADD',
num: 10 // 这里传递的就是参数
}
// 订阅store的变化
// 一定要在redux派发对应的action之前就订阅store的变化
store.subscribe(() => {
// 我们可以通过store.getState()来获取最新的那个store对象
console.log('store', store.getState().count)
})
store.dispatch(incrementAction)
store.dispatch(addNumAction)
但是在实际开发过程中,我们在使用的时候,使用的是ESM,而且需要将代码进行拆分操作
代码拆分
package.json
{
"name": "tmp",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
// 在node中使用ESM
// 在node中使用ESM后,node不会再自动为对应的js文件添加js后缀,需要手动添加
"type": "module",
"dependencies": {
"redux": "^4.1.2"
}
}
index.js --- 业务代码
import store from './store/index.js'
import {
incAction,
addAction
} from './store/actions.js'
store.subscribe(() => {
console.log('store', store.getState())
})
store.dispatch(incAction())
store.dispatch(addAction(10))
store/index.js --- store的入口文件
import redux from 'redux'
import reducer from './reducer.js'
export default redux.createStore(reducer)
store/consts.js
// 将所有的常量都定义在这里
// 以确保某一个常量值发生改变的时候
// 所有使用这个常量的地方都会发生对应的改变
export const INCREMENT = 'INCREMENT'
export const ADD_NUMBER = 'ADD_NUMBER'
store/reducer.js
// reducer函数
import {
INCREMENT,
ADD_NUMBER
} from './consts.js'
const initialStore = {
count: 0
}
export default function reducer(store = initialStore, action) {
switch(action.type) {
case INCREMENT:
return { ...store, count: store.count + 1 }
case ADD_NUMBER:
return { ...store, count: store.count + action.num }
default:
return store
}
}
store/actions.js
// 定义所有的action
import {
ADD_NUMBER,
INCREMENT
} from './consts.js' // js后缀不可以省略
export const addAction = num => ({
type: ADD_NUMBER,
num
})
// 虽然以下action不需要传递参数
// 但是为了整体统一,我们依旧会将其定义为函数形式
export const incAction = () => ({
type: INCREMENT
})
使用流程
结合React
需求
当点击+5或-5按钮的时候,Home和Profile后面的数值都会发生相同的改变
实现
业务代码
App.js
import { PureComponent } from 'react'
import Home from './components/Home'
import Profile from './components/Profile'
import store from '../store'
import {
add_number,
sub_number
} from '../store/action'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
count: store.getState().count
}
}
render() {
return (
<>
<Home />
<Profile />
<button onClick={() => this.add(5)}>+5</button>
<button onClick={() => this.sub(5)}>-5</button>
</>
)
}
add(num) {
store.dispatch(add_number(num))
}
sub(num) {
store.dispatch(sub_number(num))
}
}
Home/Profile组件
import { PureComponent } from 'react'
import store from '../../store'
export default class Profile extends PureComponent {
constructor(props) {
super(props)
this.state = {
count: store.getState().count
}
}
// 在componentDidMount中订阅store
// 确保store发生更新后,界面发生更新
componentDidMount() {
store.subscribe(() => {
this.setState({
count: store.getState().count
})
})
}
render() {
return (
<div>
Home: { this.state.count }
</div>
)
}
}
store
index.js
import { createStore } from 'redux'
import reducer from './reducer'
export default createStore(reducer)
store.js
export const ADD_NUMBER = 'ADD_NUMBER'
export const SUB_NUMBER = 'SUB_NUMBER'
action.js
import {
ADD_NUMBER,
SUB_NUMBER
} from './consts'
export const add_number = num => ({
type: ADD_NUMBER,
num
})
export const sub_number = num => ({
type: SUB_NUMBER,
num
})
reducer.js
import {
ADD_NUMBER,
SUB_NUMBER
} from './consts'
const initialStore = {
count: 0
}
export default function reducer(store = initialStore, action) {
switch (action.type) {
case ADD_NUMBER:
return { ...store, count: store.count + action.num }
case SUB_NUMBER:
return { ...store, count: store.count - action.num }
default:
return store
}
}