我又膨胀了,现在打算写一写
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,因为毕竟还有很多细节处处理的问题。