redux 原理浅析

在对 redux 的使用过程中,了解到 redux 中的一些核心概念和方法,为了达到 “知其然,也知其所以然” 的学习目标,尝试从应用层面出发,剖析原理,手撸 redux 和 react-redux 中的核心方法。

一. redux 中的基本概念

整个工作流如图所示,涉及到以下核心概念:

  • Store:状态树,存储对象状态的地一个容器

  • Action :操作 store 的行为载荷,通过 store.dispatch 传递到 store

  • Reducers:真正操作 store 的方法,可以查看之前的状态,也可以响应接收到的 action 并返回一个新的状态

以下概念为 React-redux 中才具备的:

  • Provider:一个外层容器,配合 connect 实现父子层级组件的通信

  • Connect:连接 React 组件与 redux store 的一个方法,接收 Provider 组件提供的 store,返回一个高阶组件,将响应的 state 和 dispatch 作为属性参数传给内部组件

redux 工作流

二. redux 核心方法实现

在手撸核心方法之前,先回忆一下使用 redux 的流程:

  1. 首先需要声明一个 reducer,然后调用 createStore 来实例化一个 store

  2. 紧接着如果要获取对象的状态,就需要调用 store.getState 方法来获取

  3. 如果要修改对象的状态,就需要调用 store.dispatch 方法来修改

  4. 最后不要忘了使用 store.subscribe 方法来添加监听函数

根据上述回顾,可以知道实例化的 store 包含三个方法 getState ,dispatch, subscribe,其次为了实现对对象的存储与事件的监听,还需要两个变量 currentStatecurrentListeners 分别用于存储当前对象状态和监听队列。于是可以 createStore 函数的基本结构就清楚了

export function createStore(reducer, enhancer) {
    // 先忽略 enhancer 的操作
    // ...
    let currentState, currentListeners = []
    function getState() {
        // todo
    }
    function dispatch(action) {
        // todo
    }
    function subscribe(listener) {
        // todo
    }
    return {getState, dispatch, subscribe}
}

有了基础框架之后,再来仔细思考一下三个方法都具备了哪些功能:

  1. getState 方法:没有参数,能够返回当前的对象状态,故而函数内部很简单,就是直接返回 currentState;

  2. dispatch 方法:传入一个 action,将其代理到 reducer 中,由 reducer 去执行真正的状态更改并得到新的状态,同时触发状态变更事件,也即需要执行所有的监听函数;

  3. subscribe 方法:传入一个 listener,将其添加到监听队列中,一旦 store 中的状态发生变更,listener 将被执行。

createStore 实现如下:
export function createStore(reducer, enhancer) {
    if(enhancer) {
        // 如果存在 enhancer,则对本函数进行转换,再将 reducer 传入
        return enhancer(createStore)(reducer)
    }
    let currentState, currentListeners = []
    
    function getState() {
        return currentState
    }

    function subscribe(listener) {
        currentListeners.push(listener)
        // 触发一个不可能存在的 action,使得 currentState 具备一个初始值
        dispatch('@roadlin/myRedux')
    }

    function dispatch(action) {
        currentState = reducer(currentState, action)
        // 遍历监听队列,依次执行监听事件
        currentListeners.forEach(v => v())
        return action
    }

    return {
        getState,
        subscribe,
        dispatch
    }
}

除了上述的 createStore 方法,redux 还提供了其它 API,比如当存在多个 reducer 时,需要调用 combineReducers 进行合并;当要引入中间件时,需要使用 applyMiddleware 进行注入。同样对这两个 API 的功能进行拆分分析,有助于理解其内部实现:

  1. combineReducers :传入的参数是一个对象,包含多个 reducers,最终经处理合并之后,返回一个合并之后的 reducer,代码描述如下:
combineReducers({
   count: countReducer,
   cart: cartReducer
})

// 上述代码合并之后得到一个整体的 reducer
function reducer(state = {}, action) {
   return {
       count: countReducer(state.count, action),
       cart: cartReducer(state.cart, action)
   }
}

合并之后,在调用 dispatch 的时候,会将 action 传递给合并后的 reducer, 合并后的reducer 中会遍历获取每个 key 值对应的状态,传递给相应的原始 reducer 执行,也即无论 action 是操作哪一个状态,所有的 reducer 都会执行一次

  1. applyMiddleware: 传入的参数是 n 个中间件(n ≥ 1),其主要作用是包装 store 原始的 dispatch 方法,使其支持中间件的功能,比如 applyMiddleware(thunk) 之后 dispatch 中支持异步操作。当 n > 1时,中间件是从右往左进行链式调用,也即最后的中间件处理之后的结果交给前一个中间件处理
combineReducersapplyMiddleware 实现如下:
function combineReducers(reducersObj) {
    // 1. 首先做了去重和判断,去除重复的 key 值,保证传入的每一个属性值都是 reducer 函数,保证一个 key 只对应一个 reducer
    let finnalReducers = {}
    for(let key in reducersObj) {
        if(typeof reducersObj[key] === 'function') {
            finnalReducers[key] = reducersObj[key]
        }
    }
    // 2. 中间还做了下判断,保证传入的 reducers 中不包括复合型的 reducer,也就是 combineReudcers 处理后的结果不能用于再一次的 combineReducers
    // ... 该部分省略
    // 3. 返回一个合并后的 reducer 函数 combination(state ={}, action)
    //  3.1 内部遍历了所有的 reducers,获取上一次 key 值对应的 state,调用对应的 reducer 函数,执行 action
    //  3.2 循环过程中判断是否更新了 state 值,如果更新了,则返回新的 state,否则依旧返回旧的 state
    return function combination(state = {}, action) {
        let hasChange = false, nextState = {}
        for(let key in finnalReducers) {
            let previousStateForKey = state[key]
            let nextStateForKey = reducersObj[key](previousStateForKey, action)
            nextState[key] = nextStateForKey
            hasChange = hasChange || nextStateForKey !== previousStateForKey
        }
        return hasChange ? nextState : state
    }
}

export function applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        const store = createStore(...args)
        let dispatch = store.dispatch
        let midApi = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }
        let middleChain = middlewares.map(middleware => middleware(midApi))
        dispatch = compose(middleChain)(store.dispatch)
        return {
            ...store,
            dispatch
        }
    }
}

// 遵循从右到左的链式调用,所以 compose(f, h, g) 等价于 (...args) => f(h(g(...args)))
export function compose(...funcs) {
    if(funcs.length === 0) {
        return arg => arg
    }
    if(funcs.length === 1) {
        return funcs[0]
    }
    return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}


/*******************华丽分割线*************************/
// 除此之外,redux 中还有别的函数,比如:
/* 
    调用 connect 时,如果 mapDispatchToProps 是对象时执行 bindActionCreators 如

    @connect(
        state => ({goods: state.goods}), 
        {addGood, deleteGood, asyncAdd}
    )
*/
function bindActionCreator(creator, dispatch) {
    return (...args) => dispatch(creator(...args))
}
export function bindActionCreators(creators, dispatch) {
    return Object.keys(creators).reduce((ret, item) => {
        ret[item] = bindActionCreator(creators[item], dispatch)
        return ret
    }, {})
}

三. react-redux 核心方法实现

在应用 react-redux 时,主要用到了 <Provider></Provider> 组件以及 connect 函数,同样的,先分析它们的主要功能:

  1. Provider 组件:通过属性传参的方式传入 store 参数,并将其传给子组件,同时渲染内部组件。为了实现子组件中可以读取到 store 参数,需要借助 context 上下文来传递,作为父组件,其需要设置 childContextTypes

  2. connect 函数:传入的是两个 map,返回一个高阶组件,将 redux 中的 statedispatch 变成组件的 props

    • 为了从 Provider 中获取到 store,需要设置 contextTypes

    • 高阶组件最后返回的新组件中,也即经 connect 装饰之后的组件可以直接通过 this.props[key] 读取到 redux 中的数据和方法,故而需要执行两个 map,获取到 redux 数据和方法,最后以 props 的方式传给新组件

    • 此外,还需要设置监听事件,当 redux 中的数据发生更改时,更新当前组件的 props

import React from 'react'
import PropTypes from 'prop-types'
import {bindActionCreators} from './myRedux'

export class Provider extends React.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 const connect = (mapStateToProps = state => state, mapDispatchToProps = {}) => WrapComponent =>{

    return class NewComponent extends React.Component {
        static contextTypes = {
            store: PropTypes.object
        }

        constructor(props, context) {
            super(props, context)
            this.state = {
                props: {}
            }
        }

        // 在组件渲染前更新,否则因为读不到对应的 props 属性值而报错
        componentWillMount() {
            const {store} = this.context
            // 注册监听事件
            store.subscribe(() => this.update())
            this.update()
        }

        update() {
            const {store} = this.context
            const stateProps = mapStateToProps(store.getState())
            const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch)
            // 整合所有的参数,包括组件自身的参数、redux 中的 state 及 dispatch
            this.setState({
                props: {
                    ...this.state.props,
                    ...stateProps,
                    ...dispatchProps
                }
            })
        }

        render() {
            return <WrapComponent {...this.state.props}></WrapComponent>
        }
    }
}
最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容