Redux原理分析以及使用详解(TS && JS)
Redux原理分析
image.png一、Reudx基本介绍
1.1、什么时候使用Redux?
简单说,如果你的UI层非常简单,没有很多互动,Redux 就是不必要的,用了反而增加复杂性。
-
用户的使用方式非常简单
-
用户之间没有协作
-
不需要与服务器大量交互,也没有使用 WebSocket
-
视图层(View)只从单一来源获取数据
从组件角度看,如果你的应用有以下场景,可以考虑使用 Redux。
-
某个组件的状态,需要共享
-
某个状态需要在任何地方都可以拿到
-
一个组件需要改变全局状态
-
一个组件需要改变另一个组件的状态
1.2、为什么要用Redux
在React中,数据在组件中是单向流动的,这是react的一个特点,单向数据流动,会让开发者阅读代码以及数据流向时更清楚,数据从一个方向父组件流向子组件(通过props),但是这也伴随着一个问题,两个非父子组件之间通信就相对麻烦,例如A页面用到了B页面产生的数据,redux的出现就是方便解决了这类问题。
1.3、Redux设计理念
Redux是将整个应用状态存储到一个地方上称为store,里面保存着一个状态树store tree,组件可以派发(dispatch)行为(action)给store,而不是直接通知其他组件,组件内部通过订阅store中的状态state来刷新自己的视图
1.4、Redux是什么?
很多人认为redux必须要结合React使用,其实并不是的,Redux 是 JavaScript 状态容器,只要你的项目中使用到了状态,并且状态十分复杂,那么你就可以使用Redux管理你的项目状态,它可以使用在react中,也可以使用中在Vue中,当然也适用其他的框架。
二、Redux的工作原理
image.png1、首先我们找到最上面的state
2、在react中state决定了视图(UI),state的变化就会调用React的render()方法,从而改变视图
3、用户通过一些事件(如点击按钮,移动鼠标)就会向reducer派发一个action
4、reducer接受到action后就会去更新state
5、store是包含了所有的state,可以把它看作所有状态的集合
Redux三大原则
-
1、唯一数据源
-
2、保持只读状态
-
3、数据改变只能通过纯函数来执行
1、唯一数据源
整个应用的state都被存储到一个状态树里面,并且这个状态树,只存在于唯一的store中
2、保持只读状态
state是只读的,唯一改变state的方法就是触发action,action会dispatch分发给reducer
3、数据改变只能通过纯函数来执行
使用纯函数来执行修改,也就是reducer
纯函数是什么,一个函数的返回结果只依赖其参数,并且执行过程中没有副作用。
返回结果只依赖其参数
// 非纯函数 返回值与a相关,无法预料
const a = 1
const foo = (b) => a + b
foo(2) // => 3
// 纯函数 返回结果只依赖于它的参数 x 和 b
const a = 1
const foo = (x, b) => x + b
foo(1, 2) // => 3
函数执行过程中没有副作用
函数执行的过程中对外部产生了可观察的变化,我们就说函数产生了副作用。 例如修改外部的变量、调用DOM API修改页面,发送Ajax请求、调用window.reload刷新浏览器甚至是console.log打印数据,都是副作用。
// 无副作用
const a = 1
const foo = (obj, b) => {
return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 3
counter.x // => 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
为什么要煞费苦心地构建纯函数?因为纯函数非常“靠谱”,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。不管何时何地,你给它什么它就会乖乖地吐出什么。如果你的应用程序大多数函数都是由纯函数组成,那么你的程序测试、调试起来会非常方便。
2.1、Action
action本质上就是一个对象,它一定有一个名为type的key如 {type: 'add'} , {type: 'add'} 就是一个action , 但是我们只实际工作中并不是直接用action ,而是使用 action创建函数 (千万别弄混淆), 顾名思义action创建函数就是一个函数,它的作用就是返回一个action,如:
function add() { return { type: 'add', money : 1 }}
2.2、Reducer
reducer其实就是一个函数,它接收两个参数,第一个参数是需要管理的状态state,第二个是action。reducer会根据传入的action的type值对state进行不同的操作,然后返回一个新的state,而不是在原有state的基础上进行修改,但是如果遇到了未知的(不匹配的)action,就会返回原有的state,不进行任何改变。
function reducer(state = {money: 0}, action) {
//返回一个新的state可以使用es6提供的Object.assign()方法,或扩展运算符
switch (action.type) {
case '+':
return Object.assign({}, state, {money: action.money + 1});
case '-':
return {...state, ...{money: action.money - 1}};
default:
return state;
}
}
2.3、store
可以把store想成一个状态树,它包含了整个redeux应用的所有状态。我们使用redux提供的createStore
方法生成store
import {createStore} from 'redux';
const store = createStore(reducer);
store提供了几个方法供我们使用,下面是我们常用的3个:
store.getState();//获取整个状态树
store.dispatch();//改变状态,改变state的唯一方法
store.subscribe();//订阅一个函数,每当state改变时,都会去调用这个函数
三、Redux中间件机制
Redux本身就提供了非常强大的数据流管理功能,但这并不是它唯一的强大之处,它还提供了利用中间件来扩展自身功能,以满足用户的开发需求。
image.png上面是很典型的一次 redux 的数据流的过程,但在增加了 middleware 后,我们就可以在这途中对 action 进行截获,并进行改变。且由于业务场景的多样性,单纯的修改 dispatch 和 reduce 人显然不能满足大家的需要,因此对 redux middleware 的设计是可以自由组合,自由插拔的插件机制。也正是由于这个机制,我们在使用 middleware 时,我们可以通过串联不同的 middleware 来满足日常的开发,每一个 middleware 都可以处理一个相对独立的业务需求且相互串联:
image.png如上图所示,派发给 redux Store 的 action 对象,会被 Store 上的多个中间件依次处理,值得注意的是这些中间件会按照指定的顺序一次处理传入的 action,只有排在前面的中间件完成任务之后,后面的中间件才有机会继续处理 action,同样的,每个中间件都有自己的“熔断”处理,当它认为这个 action 不需要后面的中间件进行处理时,后面的中间件也就不能再对这个 action 进行处理了。 换言之,中间件都是对store.dispatch()的增强
四、redux的异步流
在多种中间件中,处理 redux 异步事件的中间件,绝对占有举足轻重的地位。从简单的 react-thunk 到 redux-promise 再到 redux-saga等等,都代表这各自解决redux异步流管理问题的方案
4.1 、redux-thunk
redux-thunk最重要的思想,就是可以接受一个返回函数的action creator。如果这个action creator 返回的是一个函数,就执行它,如果不是,就按照原来的next(action)执行。 正因为这个action creator可以返回一个函数,那么就可以在这个函数中执行一些异步的操作,就比如网络请求。
export function addCount() {
return {type: ADD_COUNT}
}
export function addCountAsync() {
return dispatch => {
setTimeout( () => {
dispatch(addCount())
},2000)
}
}
addCountAsync函数就返回了一个函数,将dispatch作为函数的第一个参数传递进去,在函数内进行异步操作。
尽管redux-thunk很简单,而且也很实用,但人总是有追求的,都追求着使用更加优雅的方法来实现redux异步流的控制,这就有了redux-promise。
4.2、redux-promise
使用redux-promise中间件,允许action是一个promise,在promise中,如果要触发action,则通过调用resolve来触发
4.3、redux-sage
redux-saga将react中的同步操作与异步操作区分开来,以便于后期的管理与维护 ,redux-saga相当于在Redux原有数据流中多了一层,通过对Action进行监听,从而捕获到监听的Action,然后可以派生一个新的任务对state进行维护,通过更改的state驱动View的变更。
4.4、总结
总的来讲Redux Saga适用于对事件操作有细粒度需求的场景,同时它也提供了更好的可测试性,与可维护性,比较适合对异步处理要求高的大型项目 。一般项目redux-thunk就足以满足自身需求了。毕竟react-thunk对于一个项目本身而言,毫无侵入,使用极其简单,只需引入这个中间件就行了。而react-saga则要求较高,难度较大,我现在也并没有掌握和实践这种异步流的管理方式。
五、使用redux-dev-tools插件调试redux
5.1、下载插件
首先在谷歌商店搜索redux-dev-tools,下载这个插件,然后重启浏览器
image.png在redux中的store文件进行配置
若是JS则添加
const store = createStore(
reducers,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
若是TS则添加
const store = createStore(reducer, compose(
applyMiddleware(thunk),
(window as any).__REDUX_DEVTOOLS_EXTENSION__ && (window as any).__REDUX_DEVTOOLS_EXTENSION__()))
Tip :原来我使用JS+Redux,添加这个插件配置,部署到服务器上用户访问以及别人启动我的项目,都没有报错,但是当我使用TS+hooks+Redux,没有测试部署到服务器会怎么样,但是当别人启动这个项目,若没有安装这个插件则会报错。若想避免这个问题,则可在webpack配置启动项目或者打包项目不同的环境。可使用 process.env.NODE_ENV === 'production' 判断不同环境,或者使用 window.location.host 获取url地址来进行判断是否开启这个插件。
下面则是工具的图,该工具,可以查看action的触发过程,以及state的变化。非常方便进行调试。
image.png image.png六、实际开发中使用redux
6.1、目录结构,在项目src里面创建即可
image.png6.1.1、store
store则是配置redux总仓库,createStore()则需要把reducer传进来,以及上文介绍到的中间件,以及设置调试工具则都是在此文件进行配置
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const store = createStore(reducer, compose(
applyMiddleware(thunk),
))
export default store
6.1.2、action
action则是view用来调用的,action通过dispatch来触发reducer,然后来更新state
image.png6.1.3、reducer
store文件需要配置reducer,所以reducer文件夹中则需要一个index文件,来引入所有的reducer,并且暴露出去,供store文件使用。
import {combineReducers} from 'redux'
import manage from './manage/manage'
import submit from './submit'
import saveName from './manage/saveName'
export default combineReducers({
manage,
submit,
saveName
})
例如我现在需要存储上面action文件里面key为ALL_NAME的值,我reducer文件则需要这么写
const init = {
userNameData : []
}
export default (state = init, action : any) => {
switch (action.type) {
case 'ALL_NAME':
return {...state,userNameData : action.allName}
default:
return state
}
}
6.1.4、项目入口文件,index.ts
import React from 'react';
import ReactDOM from 'react-dom';
import store from './redux/store'
import {Provider} from 'react-redux'
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
6.2、在组件中取出store仓库的值,和如果触发action(JS && TS + hooks)
6.2.1、JS的用法(取值以及触发action)
import React, {Component} from 'react'
import {connect} from 'react-redux'
import {GetAllClass,SaveScroll} from '../../redux/action/product'
class Home extends Component {
componentDidMount() {
//取出值
const {user,productAllClass,productScroll} = this.props
//触发action
this.props.getAllClass()
const scroll = document.scrollingElement.scrollTop
//触发action
this.props.SaveScroll(scroll)
}
}
//取值
//其实mapStateToProps接收了state,但是此处这么写,是使用了ES6的解构,会简化代码
const mapStateToProps = ({user, productAllClass,productScroll}) => ({
user, productAllClass,productScroll
})
//调用action
const mapDispatchToProps = (dispatch) => ({
getAllClass: () => dispatch(GetAllClass()),
SaveScroll : (scroll) => dispatch(SaveScroll(scroll))
})
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Home))
大家可能看到这就有疑问,mapStateToProps和mapDispatchToProps是干嘛的?有什么作用?
首先我们在组件当中使用redux,就需要使用react-redux中的connect将该组件与store连接起来,而connect又可以接受两个参数,分别是mapStateToProps和mapDispatchToProps,前者则是获取store里面的状态,用于建立组件跟store的state的映射关系,后者则是用于建立组件跟store.dispatch的映射关系。
TS的用法(取值以及触发action)
import { useDispatch, useSelector } from 'react-redux'
const ManageTable: React.FC<{}> = () => {
const dispatch = useDispatch()
const userNameRedux = useSelector((state: any) => state.saveName.userNmae);
return (
useEffect(() => {
//调用action,传一个值name
dispatch(saveSearchUserName(name))
//获取store的值
console.log(userNameRedux)
},[])
)
}
BUG分享
需求:一个接口,需要在多个页面调用,而且多个页面互相没有关联,我在每个页面都去调用这个接口,显然这是浪费性能的,我就想在react入口文件去调用action,然后分发给reducer,存储到store,页面就能获取到值。
大家可以先观察观察这份代码。大家觉得我能如愿在第一次加载的时候能拿到数据吗?
export const test = () => {
console.log("1")
return async (dispatch: any) => {
console.log("2")
const data = await getAllNameApi()
console.log("3")
dispatch({
type: 'ALL_NAME',
allName: data
})
}
}
//拆分一下上面的代码
export const test = () => {
console.log("1")
return new Promise(async (resolve) => {
console.log("2")
resolve(await getAllNameApi())
}).then(() => {
console.log("3")
dispatch({
type: 'ALL_NAME',
allName: data
})
})
}
useEffect(() => {
const manage: any = useSelector((state: any) => state.manage);
console.log(manage.userNameData)
},[])
最终正确打印顺序应该是1,2,数据,4。
最后经过反复研究,并且请教各路大神,最终总结了两个原因。
从同步异步的角度来说这个问题:想让异步变成类似同步的操作我们应该怎么办,大家想到的肯定是async/await,阻塞代码,我开始一直陷入一个误区,我内部的确造成了阻塞,等到data有值了,才会dispatch,但是,这整个Action方法,返回的是一个async,async其实本质也就是promise对象,那么又是一个异步对象,所以它的外部不会等待,当代码执行到await这块, 因为需要时间来调用接口,所以会跳出去,页面第一次会渲染,而不会说等待这个数据成功存入redux里面才会渲染页面。
从React页面渲染来说:页面肯定是先渲染,不会关心dispatch,也不会关心action,只会关心我store里面数据的变化,其实也就是我第一次useEffect的时候,数据取得其实是初始值。
对于这个问题,在我这份代码里面,目前我想到了三个解决方法:
1、定义初始值loading为true,当我们dispatch成功把数据存入的时候,才将loading改为false,写一个加载动画,用这个loading来控制。
2、在useEffect监听store里面这个值的变化,当有值的时候,才绑定到页面上
const [autoData,setAutoData] = useState<Array[item]>([]) //此处item是我写的定义类型的接口
useEffect(() => {
if(manage.userNameData !== []){
setAutoData(manage.userNameData)
}
},[manage.userNameData])
- 3、因为我这个组件可以直接绑定数据源,其实我直接数据源头,写上这个store里面的值就好
<Auto
dataSource={manage.userNameData}
allowClear={true}
style={{ width: 250 }}
filterOption={(inputValue, option: any) =>
option.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
notFoundContent={<Empty />}
placeholder="Please input or select"
onChange={(e) => setUserNameValue(e)}
value={autoValue}
/>
大家可以再看看下面这个小demo
let test = async() => {
let data = await test1()
console.log(data)
console.log(3)
}
let test1 = () => {
return test2().then(data=>{ return data })
}
let test2 = async() => {
return await test3()
}
let test3 = () => {
return new Promise(reslove=>{
setTimeout(()=>{
reslove('hello')
}, 1000)
})
}
console.log(1)
test()
console.log(2)