我又膨胀了,现在打算写一写
Redux的原理了...
Redux原理
这里我们先讲redux最简单实现。
空讲原理,不如自己写一个redux,我们来一步步写一个my-redux.js,上代码:
//根据使用redux的几个关键方法,倒推出写法
export function createStore(reducer){
let currentState = {}
let currentListeners = []//有可能传入多个listener
function getState(){
return currentState
}
function subscribe(listener){
currentListeners.push(listener)
}
function dispatch(action){
currentState = reducer(currentState,action)
currentListeners.map(v=>v())//把监听器中的每个监听方法都执行一下
return action
}
//因为初始化的时候,我们就能getState到值,证明已经在内部自动执行过一次dispatch
dispatch({type:'@FOR-DEMO/MY-REDUX/INIT@'})//注意尽量把type写复杂些,不要和用户的type相同了,没有对应的type时,就会走reducer的默认return,即初始的state
return {getState ,subscribe ,dispatch}
}
其实看过代码后,大家就基本明白了,最关键的地方,就在于dispatch方法,在这一步的时候,执行了状态的修改,执行了订阅的监听事件,一般来写即变更了状态,也触发了渲染的更新。
到这里基本的redux的功能都实现了,原理也能理解了,那么,我们进一步完善下这个redux。
我们要真正使用redux其实还是要靠react-redux来优雅的实现,要理解react-redux,首先我们要先理解Context的概念。
Context
当我们有多个组件嵌套的时候,如果需要传递一个变量,那就要层层用属性的方式传递,非常繁琐,对性能也不友好,而Context的出现就很好的解决了这个问题。
Context是全局的,组件里声明,所有的子元素都可以直接获取。
值得强调的一点是,因为FB认为全局使用并不好,所以严格规范了其使用,要求如果使用了Context的话,必须实现类型校验
所以需要先npm install prop-types --save一下,在要声明和引用Context的组件中要校验类型:
//声明处
static childContextTypes = {
user:PropTypes.string
}
//引用处
static contextTypes = {
user:PropTypes.string
}
有点乱,还是上完整的代码吧:
import React , { Component } from 'react'
import PropTypes from 'prop-types'
class SideBar extends Component {
render(){
return(
<div>
<p>侧边栏</p>
<NavBar></NavBar>
</div>
)
}
}
class NavBar extends Component {
//NavBar要使用Context就要校验类型
static contextTypes = {
user:PropTypes.string
}
render(){
return <div>{this.context.user}的导航栏</div>
}
}
class Page extends Component {
//声明的地方也要进行类型校验
static childContextTypes = {
user:PropTypes.string
}
constructor(props){
super(props)
this.state={ user:'JW' }
}
//返回子组件要获取的Context
getChildContext(){
return this.state
}
render(){
return(
<div>
<p>我是{this.state.user}</p>
<SideBar></SideBar>
</div>
)
}
}
export default Page
看了代码就应该知道Context的作用和怎么用了吧。
那么,究竟Context和我们要说的react-redux有什么关系呢?
当然有关系了,记得react-redux里的Provider吗,它就是为全局提供Context的!
这么说吧:
connect负责连接组件,给到Redux里的数据放到组件的属性里面。
Provider负责把store放到context里,所有的子元素都可以直接取到store。
先不管connect,初步实现了Provider的react-redux代码如下:
import React , { Component } from 'react'
import PropTypes from 'prop-types'
export class Provider extends Component{
static childContextTypes = {
store : PropTypes.object
}
constructor(props,context){
super(props,context)
this.store = props.store
}
getChildContext(){
return {store : this.store}
}
render(){ return this.props.children }
}
Provider把store放到context后,全局都能获取到store中的属性。这时候connect可以继续使用react-redux提供的,你会发现,功能可以正常执行了。
下一步,我们就开始着手实现connect了。
connect主要有两个任务:负责接收一个组件,并且把state里的一些数据放进去,返回一个组件;再者就是当数据有变化的时候,能够通知组件。
首先,实现组件的数据接收:
export const connect = (mapStateToProps=state=>state,mapDispatchToProps={})=>(WrapComponent)=>{
return class connectCompnent extends Component {
static contextTypes={
store:PropTypes.object
}
constructor(props,context){
super(props,context)
this.state={
props:{}
}
}
componentDidMount(){
this.update()
}
//获取mapStateToProps和mapDispatchToProps放入this.props里
update(){
//重点就在这,Context的作用发挥出来了
const {store} = this.context
//通过这个方法给出对应的store中的数据
const stateProps = mapStateToProps(store.getState())
//新获取数据后,执行刷新wrapComponent的动作
this.setState({
props : {...stateProps , ...this.state.props}
})
//这样组件就有了获取props的功能
}
render(){
return <WrapComponent {...this.state.props}></WrapComponent>
}
}
}
第二步,实现组件的事件的传递:
为了实现事件的传递和绑定,需要在我们自己的redux中新增一个方法:
//相当于每个creator包一层dispatch
export function bindActionCreators(creators ,dispatch){
let bound = {}
Object.keys(creators).forEach(v=>{
let creator = creators[v]
bound[v] = bindActionCreator(creator ,dispatch)
})
return bound
}
function bindActionCreator(creator ,dispatch){
return (...args) => dispatch(creator(...args))
}
然后就是把connect传入的creators用dispatch包裹起来,并且再每次调用creator的时候,把新返回的状态赋值给connect的props,达到更新数据时刷新渲染,上面尚未实现事件传递的代码更新成这样:
componentDidMount(){
const {store} = this.context
//组件加载时重新调用监听和更新组件视图
store.subscribe(()=>this.update())
this.update()
}
//获取mapStateToProps和mapDispatchToProps放入this.props里
update(){
const {store} = this.context
//通过这个方法给出对应的数据
const stateProps = mapStateToProps(store.getState())
//获取组件对应的事件,不能直接赋值或者执行,没有意义,因为需要dispatch()来传入事件去执行才有用
const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch)
//新获取数据后,执行刷新wrapComponent的动作
this.setState({
props : {
...this.state.props ,
...stateProps ,
...dispatchProps
}
})
//这样组件就有了获取props的功能
}
至此,redux的基本功能就实现的差不多了,因为redux默认只处理同步事件,所以还需要中间件的形式来处理异步事件。
我们在这里先理一遍整体流程:
redux:
1、两个变量:
currentState(包含了所有的状态)
currentListeners(所有监听事件)。
2、三个函数:
getState(直接返回store的所有状态)
subscribe(把订阅监听事件push到currentListeners中)
dispatch(发起修改状态事件,把传入的reducer的事件执行并且接收事件执行返回的状态,把所有的监听事件都执行一遍,并返回一个dispatch)
3、默认执行一次dispatch,用一个生僻的action type完全不匹配到type直接返回默认的state已达到初始化的目的。
4、最后返回包含三个函数的一个对象当做store,就足够实现redux的逻辑了。
PS : React-Redux所需方法bindActionCreators提供给dispatch进行一次包裹,并返回包裹后的事件。
React-Redux:
1、Provider把传递进来的store属性放到context中以便子元素可以全局获取到,而render()事件什么都不用渲染,只需要渲染其子元素即可。
2、connect是一个高阶组件,接收4个参数,但是在这里我们只实现基础的功能,所以只需要2个参数:
mapStateToProps告诉组件我们需要state的哪个属性。
mapDispatchToProps告诉组件需要redux的哪些事件,并且自动为其包裹上一层dispatch。
3、connect首先返回一个WrapComponent高阶组件,再将其传递给内层函数,内层函数则渲染WrapComponent,并且返回一个connectCompnent组件。
4、connectCompnent里面有一个最核心的方法update(),首先执行mapStateToProps方法,获得store中所有的state并添加到connect组件的state中,再将它展开对应所需要的state以属性的方式一一传递给WrapComponent。
5、dispatchProps则是将mapDispatchToProps对象中的事件进行一次dispatch包裹,执行的方法就是上文中所讲到的redux中的bindActionCreators事件。
6、再在update()方法中,setState将对应属性和dispatch赋值给connect的state,并且自动刷新WrapComponent组件。
7、之后在组件加载后获取到context里的store;再订阅一下update()方法,保证每次dispatch都能进行刷新;最终达到redux数据修改,组件显示也同步更新的效果。
至此,一个简易的react-redux就实现了。
现在就来实现一下我们欠下的中间件,做一个真正支持异步操作的
react-redux。放一张中间件机制图,大家体会一下。

要实现中间件,我们先要扩展一下前面自己写的redux的createStore方法,使其能够接收applyMiddleware()参数,并且实现applyMiddleware()方法。
export function createStore(reducer,enhancer){//enhancer增强器,扩展createStore方法
//如果传入了中间件,则套一层增强器再返回store
if (enhancer) {
return enhancer(createStore)(reducer)
}
.
.
.
return {getState ,subscribe ,dispatch}
}
除了套了一层enhancer返回一个被扩展了的store,其他并没有什么不同。
再来就实现enhancer方法了,也就是我们对外暴露的applyMiddleware了。
export function applyMiddleware(middleware){
return createStore=>(...args)=>{
//生成原生的store
const store = createStore(...args)
//获取原生的dispatch
let dispatch = store.dispatch
//定义中间件的接口
const midApi = {
getState : store.getState,
dispatch : (...args)=>dispatch(...args)
}
//dispatch用middleware做一次扩展
dispatch = middleware(midApi)(store.dispatch)
//返回被新的dispatch覆盖掉的store
return {
...store,
dispatch
}
}
}
至此,thunk先应用redux-thunk提供的,那么,redux的异步操作也就实现了。
现在来实现以下
thunk吧。
const thunk = ({dispatch,getState})=>next=>action=>{
//如果action是一个方法,执行一下,参数是dispatch和getState
if (typeof(action)==='function') {
return action(dispatch,getState)
}
//如果action是除方法之外的,例如对象{type : LOGIN}这样的,就直接返回,什么都不操作,按同步事件处理
return next(action)
}
export default thunk
是不是有点懵,打印一下action你就大概清楚了:
action: ƒ (dispatch) {
setTimeout(function () {
// 异步结束后,手动执行dispatch
dispatch(doSomething());
}, 2000);
}
其实就是等异步操作结束了,再执行对应的方法即可。
但是中间件可能不止有一个,我们经常要传入多个中间件,所以,我们要来做一个合并中间件的操作。
首先要支持传入多个中间件参数:
export function applyMiddleware(...middlewares){
return createStore=>(...args)=>{
.
.//重复代码我就不贴了,只贴有变动的
.
//dispatch用middleware做一次扩展(注释掉,大家可以自己对比看看)
// dispatch = middleware(midApi)(store.dispatch)
//多个中间件,就把他遍历一遍,接入中间件,放入middlewareChain数组中
const middlewareChain = middlewares.map(middleware=>middleware(midApi))
dispatch = compose(...middlewareChain)(store.dispatch)
//返回被新的dispatch覆盖掉的store
return {
...store,
dispatch
}
}
}
这里对组合中间件的dispatch我们用到了compose()方法
补上compose()方法:
export function compose(...funcs){
//如果传入的funcs长度为0
if (funcs.length === 0) {
//直接返回给入的参数,什么都不执行
return arg=>arg
}
//如果传入的funcs长度为1,代表当前传入了一个参数
if (funcs.length === 1) {
//直接返回第0个
return funcs[0]
}
//用reduce()方法组合所有的中间件
return funcs.reduce((ret,item)=>(...args)=>ret(item(...args)))
}
写一个数组类型的中间件处理
const arrayThunk = ({dispatch,getState})=>next=>action=>{
if (Array.isArray(action)) {
return action.forEach(v=>dispatch(v))
}
return next(action)
}
export default arrayThunk
以上就是一个基础的
react-redux的实现。上代码:
//my-redux.js
export function createStore(reducer,enhancer){//enhancer增强器,扩展createStore方法
//如果传入了中间件,则套一层增强器再返回store
if (enhancer) {
return enhancer(createStore)(reducer)
}
let currentState = {}
let currentListeners = []
function getState(){
return currentState
}
function subscribe(listener){
currentListeners.push(listener)
}
function dispatch(action){
currentState = reducer(currentState,action)
currentListeners.map(v=>v())//把监听器中的每个监听方法都执行一下
return action
}
//因为初始化的时候,我们就能getState到值,证明已经在内部自动执行过一次dispatch
dispatch({type:'@FOR-DEMO/MY-REDUX/INIT@'})//注意尽量把type写复杂些,不要和用户的type相同了,没有对应的type时,就会走reducer的默认return,即初始的state
return {getState ,subscribe ,dispatch}
}
export function applyMiddleware(...middlewares){
return createStore=>(...args)=>{
//生成原生的store
const store = createStore(...args)
//获取原生的dispatch
let dispatch = store.dispatch
//定义中间件的接口
const midApi = {
getState : store.getState,
dispatch : (...args)=>dispatch(...args)
}
//dispatch用middleware做一次扩展
// dispatch = middleware(midApi)(store.dispatch)
//多个中间件,就把他遍历一遍,接入中间件,放入middlewareChain数组中
const middlewareChain = middlewares.map(middleware=>middleware(midApi))
dispatch = compose(...middlewareChain)(store.dispatch)
//返回被新的dispatch覆盖掉的store
return {
...store,
dispatch
}
}
}
export function compose(...funcs){
//如果传入的funcs长度为0
if (funcs.length === 0) {
//直接返回给入的参数,什么都不执行
return arg=>arg
}
//如果传入的funcs长度为1,代表当前传入了一个参数
if (funcs.length === 1) {
//直接返回第0个
return funcs[0]
}
return funcs.reduce((ret,item)=>(...args)=>ret(item(...args)))
}
export function bindActionCreators(creators ,dispatch){
let bound = {}
Object.keys(creators).forEach(v=>{
let creator = creators[v]
bound[v] = bindActionCreator(creator ,dispatch)
})
return bound
}
function bindActionCreator(creator ,dispatch){
return (...args) => dispatch(creator(...args))
}
//my-react-redux.js
import React , { Component } from 'react'
import PropTypes from 'prop-types'
import { bindActionCreators } from './my-redux'
export const connect = (mapStateToProps=state=>state,mapDispatchToProps={})=>(WrapComponent)=>{
return class connectCompnent extends Component {
static contextTypes={
store:PropTypes.object
}
constructor(props,context){
super(props,context)
this.state={
props:{}
}
}
componentDidMount(){
const {store} = this.context
//组件加载时重新调用监听和更新组件视图
store.subscribe(()=>this.update())
this.update()
}
//获取mapStateToProps和mapDispatchToProps放入this.props里
update(){
const {store} = this.context
//通过这个方法给出对应的数据
const stateProps = mapStateToProps(store.getState())
//获取组件对应的事件,不能直接赋值或者执行,没有意义,因为需要dispatch()来传入事件去执行才有用
const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch)
//新获取数据后,执行刷新wrapComponent的动作
this.setState({
props : {
...this.state.props ,
...stateProps ,
...dispatchProps
}
})
//这样组件就有了获取props的功能
}
render(){
return <WrapComponent {...this.state.props}></WrapComponent>
}
}
}
class Provider extends Component{
static childContextTypes = {
store: PropTypes.object
}
getChildContext(){
return {store:this.store}
}
constructor(props, context){
super(props, context)
this.store = props.store
}
render(){
return this.props.children
}
}
export {Provider}
//my-redux-thunk.js
const thunk = ({dispatch,getState})=>next=>action=>{
console.log('action:',action,'\n','typeof action:',typeof action);
//如果action是一个方法,执行一下,参数是dispatch和getState
if (typeof(action)==='function') {
return action(dispatch,getState)
}
//如果action是除方法之外的,例如对象{type : LOGIN}这样的,就直接返回,什么都不操作,按同步事件处理
return next(action)
}
export default thunk
//my-redux-array.js
const arrayThunk = ({dispatch,getState})=>next=>action=>{
if (Array.isArray(action)) {
return action.forEach(v=>dispatch(v))
}
return next(action)
}
export default arrayThunk
写在最后:这里的redux只做原理分析所用,实际项目还请使用正式的redux,因为毕竟还有很多细节处处理的问题。