美文网首页
redux / react-redux 源码学习笔记

redux / react-redux 源码学习笔记

作者: tcssin | 来源:发表于2018-05-31 17:48 被阅读49次

    水平不够 这篇不知道怎么写 就纯粹当自己的笔记本
    如果有人想看的话 得有一定的 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
    

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

    相关文章

      网友评论

          本文标题:redux / react-redux 源码学习笔记

          本文链接:https://www.haomeiwen.com/subject/nwhxsftx.html