redux / react-redux 源码学习笔记

水平不够 这篇不知道怎么写 就纯粹当自己的笔记本
如果有人想看的话 得有一定的 redux / react-redux 使用基础才能看明白我写得这一坨
以后要是我水平上去了 会慢慢完善内容的

首先我们在 react 项目中使用 redux 和 react-redux 这两个库的时候基本会有这么几个必用的方法/组件:

redux

  • createStore
  • applyMiddleware
  • compose
  • combineReducers

react-redux

  • connect
  • Provider

这几个方法/组件我挨个抄了实现了 demo, 另外还有一些辅助实现的函数, 和涉及到的 context 的概念
都会写出来记录一下 方便下次理解


首先是 react-redux 里的 Provider

ReactDOM.render(
  (
    <Provider store={store}>
      <App />
    </Provider>
  ),
  document.getElementById('root'))

这个组件我最开始写 react 项目的时候有一个地方不明白 就是这个 store={store}
它涉及到的概念是 context, 我先写一下 context 的东西记一记

import React from 'react'
import PropTypes from 'prop-types'

class Sidebar extends React.Component {
    render() {
        return (
            <div>
                <p>Sidebar</p>
                <Navbar />
            </div>
        )
    }
}

class Navbar extends React.Component {
    static contextTypes = {
        user: PropTypes.string
    }
    render() {
        return (
            <div>
                {this.context.user}'s Navbar
            </div>
        )
    }
}

class Page extends React.Component {
    static childContextTypes = {
        user: PropTypes.string
    }
    constructor(props) {
        super(props)
        this.state = {
            user: 'admin'
        }
    }
    getChildContext() {
        return this.state
    }
    render() {
        return (
            <div>
                <p>I'm {this.state.user}</p>
                <Sidebar />
            </div>
        )
    }
}

export default Page

这段代码里 我们有三个存在依次调用关系的组件 Page > Sidebar > Navbar 目的是在最下层的 Navbar 里拿到最上层的 Page 的一个数据
不用 props 不用 redux 现成的库, 那就是手写 context

  • 写法就是最上层组件设置 childContextTypes 对象和 getChildContext 方法
    要调用 context 的组件设置 contextTypes 对象 然后就能在组件里 this.context 拿到最上级组件设置到 context 里的数据

具体看中文站官网 写得贼详细 http://www.css88.com/react/docs/context.html

我上面的例子就是写法不一样而已

另外现在 react 16 版本的 context API 也改了 官网同步了文档https://reactjs.org/blog/2018/03/29/react-v-16-3.html

怎么说呢 我觉得改了之后单纯从使用角度来讲并没有什么很大的意义 不如不改 不过无所谓了 反正日常 context 的需求都可以拿 redux 替代掉 我也不会主动去用这个东西

现在我们知道 context 是个什么东西 而 Provider 就是用到了它 下面就是一个最简单实现的 Provider

export class Provider extends React.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
    }
}

首先 childContextTypes 和 getChildContext 那边没什么好说的 就是在 Provider 这个组件里设置一下 context
constructor 里面第二个参数默认就是 context 可以直接拿来用
render 里面就是渲染一下 App (看下面)

ReactDOM.render(
  (
    <Provider store={store}>
      <App />
    </Provider>
  ),
  document.getElementById('root'))

接着我们看下 react-redux 里的 connect

这个就稍微麻烦一点 所以抄了个最简单的 demo
首先我们使用 connect 方法是有很多写法的, mapStateToProps 啊, 装饰器啊, 裸奔啊等等, 这里就举个最简单的例子

App = connect(
    state=>({num: state}),
    {addFun, removeFun, addFunAsync}
)(App)

以上面这个调用做为例子实现的最简单的 connect demo 如下

const connect = (mapStateToProps=state=>state, mapDispatchToProps={}) =>
     (WrapComponent) => {
         return class ConnectComponent extends React.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()
            }
            update() {
                // 从 context 上下文里把 store 扒出来, 然后借用 mapStateToProps 和 mapDispatchToProps 装饰新的组件
                const {store} = this.context
                const stateProps = mapStateToProps(store.getState())
                const dispatchProps= bindActionCreators(mapDispatchToProps, store.dispatch)
                this.setState({
                    props: {
                        ...this.state.props,
                        ...stateProps,
                        ...dispatchProps
                    }
                })
            }
            render() {
                return (
                    <WrapComponent {...this.state.props}></WrapComponent>
                )
            }
         }
     }

首先 connect 是一个 HOC(高阶组件 Higher-Order Components)
这里的双箭头函数第一个箭头前面的参数就是 connect 调用时候第一个括号里的两个参数
第二个箭头前面的参数就是 App 这个组件
所以 connect 就是把 mapStateToProps 和 mapDispatchToProps 这俩东西塞进了 App 组件里产生了一个新的 App 组件给我们用(当然不止这俩 其他参数就不记了 太麻烦了)

然后看 renturn 的 ConnectComponent 组件里面

class ConnectComponent extends React.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()
        }
        update() {
            // 从 context 上下文里把 store 扒出来, 然后借用 mapStateToProps 和 mapDispatchToProps 装饰新的组件
            const {store} = this.context
            const stateProps = mapStateToProps(store.getState())
            const dispatchProps= bindActionCreators(mapDispatchToProps, store.dispatch)
            this.setState({
                props: {
                    ...this.state.props,
                    ...stateProps,
                    ...dispatchProps
                }
            })
        }
        render() {
            return (
                <WrapComponent {...this.state.props}></WrapComponent>
            )
        }
}

contextTypes 里面就是从 Provider 里拿数据 没什么讲的
constructor 里 state 等会讲
componentDidMount 的时候做了监听 等于每次数据变化都会调用一次 update 函数(subscribe 之后也会实现一下)
update 里面首先把给 mapStateToProps 这个函数传入了当前了 store 作为参数 返回过滤后的数据 num(看下面)

state=>({num: state}) // mapStateToProps

然后有个 bindActionCreators 函数, 代码如下

function bindActionCreator(creator, dispatch) {
    return (...args) => dispatch(creator(...args))
}
 
function bindActionCreators(creators, dispatch) {
    let bound = {}
    Object.keys(creators).forEach(v => {
        let creator = creators[v]
        bound[v] = bindActionCreator(creator, dispatch)
    })
    return bound
}

它把 mapDispatchToProps 里面所有的方法全都扒出来 然后拿 dispatch 给包一层(dispatch 一会也会实现一下) 然后返回出去
至于为什么要拿 dispatch 包一层 毕竟不做 dispatch 的话不会更新到 store 里去

setState 里面的按这种写法来的话 三个对象的解构顺序要排好 先把 this.state.props 也就是之前的数据解构出来 然后再解构新的有更改的数据 不然你页面UI读 store 里更新的数据是不会重新渲染的

最后 render 里面直接把所有处理好的东西全塞进 WrapComponent 也就是外面传的 App 组件的 props 里

这就是 react-redux 里干的比较重要的活 Provider 和 connect


现在我们把 redux 简单的实现一下

function createStore(reducer, middleware) {
    if(middleware) {
        return middleware(createStore)(reducer)
    }
    let currentState = {}
    let currentListeners = []
 
    function getState() {
        return currentState
    }
    function subscribe(listener) {
        currentListeners.push(listener)
    }
    function dispatch(action) {
        currreducer = reducer(currentState, action)
        currentListeners.forEach(v => v())
        return action
    }
    dispatch({
        type: '@THIS_REDUX-DEMO_INIT-TYPE'
    })
 
    return {getState, subscribe, dispatch}
}
 
function applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        const store = createStore(...args)
        let dispatch = store.dispatch
 
        const midApi = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }
        const middlewareChain = middlewares.map(middleware => middleware(midApi))
        dispatch = compose(...middlewareChain)(store.dispatch)
        return {
            ...store,
            dispatch
        }
    }
}
 
function compose(...funcs) {
    if(funcs.length == 0) {
        return arg => arg
    }
    if(funcs.length == 1) {
        return funcs[0]
    }
    return funcs.reduce((res, item) => (...args) => res(item(...args)))
}

先看下 createStore
getState 这个方法就是把一个存着数据的变量丢出去给调用者 没什么好讲的
subscribe 也是往一个内部维护的数组里丢一些函数(比如下面这种)

function listener(){
  const current = store.getState()
  console.log(`num is: ${current}`)
}

那么在每次 dispatch 的时候先把 store 通过调用 reducer 函数更新一下 然后把所有被监听的 listener 调用一下就完事了
然后把定义好的三个方法 return 出去, 就能在 Provider 里当 store 用了
至于 createStore 里面那个执行了一次的 dispatch 就是初始化 store 用的 不用管

然后我们看下 applyMiddleware
我们这里只用它就是为了加个 thunk 中间件 这个本来也是一个 npm 的库 用来让 action 可以返回函数 而不是只能返回对象(一会也会把 thunk 实现一下)(调用如下)

const store = createStore(counter,applyMiddleware(thunk))

那么 thunk 被传入 applyMiddleware 里面以后也是做了一遍 HOC 的操作
别的都没什么好讲 无外乎就是同样的 接参数/传参数/调方法 套路 唯一有个要提的就是 compose
它的场景是这样的

const store = createStore(counter,applyMiddleware(thunk1, thunk2, thunk3))

处理我们传入了多个中间件的情况 处理的关键就是这么一句话(如下)

funcs.reduce((res, item) => (...args) => res(item(...args)))

利用 Array.prototype.reduce 方法做了柯里化处理 最后达成的效果就是

thunk1(thunk2(thunk3))

当然这里的三个 thunk 都是被包装 store 处理后的(如下)

const midApi = {
    getState: store.getState,
    dispatch: (...args) => dispatch(...args)
}
// 所有的 thunk 包装后拼成一个 Array 返回在 middlewareChain 里
const middlewareChain = middlewares.map(middleware => middleware(midApi)) 

如果有人看到这里对柯里化不熟的话 可以去 mdn 看看 reduce 的文档 至于其他的语法糖不熟就随便百度一下就好 都没啥特别的


最后我们再实现一下 thunk

const thunk = ({dispatch, getState}) =>
    next => action => {
        if(typeof action == 'function') {
            return action(dispatch, getState)
        }
        return next(action)
    }

还是 HOC 那一套 注意一下这个 next 参数 它就是下一个 thunk
比如现在是 thunk1 那么 next 参数就是 thunk2


贴一下所有 demo 的调用代码给女朋友辅助阅读

// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore,applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import thunk2 from 'redux-thunk2'
import { counter } from 'redux'
import { Provider } from 'react-redux'
import App from './App'

const store = createStore(counter,applyMiddleware(thunk,thunk2))
ReactDOM.render(
  (
    <Provider store={store}>
      <App />
    </Provider>
  ),
document.getElementById('root'))
// App.js
import React from 'react'
import { connect } from 'react-redux'
import { addFun, removeFun, addFunAsync } from './index.redux'

class App extends React.Component{
  render(){
    return (
      <div>
        <h2>num is: {this.props.num}</h2>
        <button onClick={this.props.addGun}>add</button>
        <button onClick={this.props.removeGun}>reomve</button>
        <button onClick={this.props.addGunAsync}>addAsync</button>
      </div>
    )
  }
}
// 裸奔模式
App = connect(
    state=>({ num: state}),
    {addGun, removeGun, addGunAsync}
)(App)
// 装饰器模式
// @connect(
//   state=>({ num: state}),
//   {addGun, removeGun, addGunAsync,addTwice}
// )

export default App

(以后随缘完善 这个源码太烦了 看得头痛 我觉得一般了解一下最基本几个方法就够了 没事不要去读源码 太浪费时间了)

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

推荐阅读更多精彩内容