阅读 200

react-redux 源码解析(1) -- Provider

react-redux 源码解析(1) -- Provider
react-redux 源码解析(2) -- Connect(上)
react-redux 源码解析(3) -- Connect(下)

---------------------------------------分割线-----------------------------------------

写了很多react代码,然鹅还没看过react-redux源码,只知道大概流程,就寻思着看看源码真正的了解其中原理。然后就去网上先看看别人的解析,看了很多篇文章后,大多都是只点到几个关键点,还是一脸懵逼。没办法自己建个工程慢慢调试吧。下面附上源码理解之后的解析,都在代码上标了注释,也会都解释一下。看源码的应该对react-redux大致流程都了解了,咱们就尽量细细的分析每个API,当做个分享也当个自己的存档。

Provider应用

在使用react-redux 的过程中,首先是使用的肯定就是 Provider。那么我们先来看看怎么使用它。

index.tsx

import AppContainer from './container/AppContainer';
import createStore from './store/createStore';
// redux 创建 store
const store = createStore();
// 这里将store传入Provider
// 创建一个Provider,将store传入Provider,作为 Provider 组件的第一个参数
ReactDOM.render(<AppContainer store={store} />, document.getElementById('root'));
复制代码

AppContainer.tsx

render() {
    const { store } = this.props;
    return (
      <Provider store={store}>
        <Router>
          <AppLayout />
        </Router>
      </Provider>
    );
}
复制代码

在代码中看到咱们使用Provider 还是很简单的,传入store, 作为当前context的值,便于组件通过context获取Redux store。

store

我们继续来看,上代码:
createStore.tx

import { applyMiddleware, compose, createStore } from 'redux';
import createSagaMiddleware  from 'redux-saga';
import rootReducer from '../reducers/index';
import rootSaga  from '../saga';

export default function configureStore(preloadedState?:any) {
  // 创建saga中间件
  const sagaMiddleware = createSagaMiddleware();
  const middleWares = [sagaMiddleware];
  // middleWares 传入 applyMiddleware, 拓展 store 的 dispatch,返回一个函数,给 createStore 调用
  const middlewareEnhancer = applyMiddleware(...middleWares);

  // 创建存储容器
  // rootReducer 自定义的 reducer
  // preloadedState 默认的 state
  // 包装过的 enhancers
  // middlewareEnhancer参数有值,则 createStore执行的是 middlewareEnhancer 函数,返回 store 和 包装过的 dispatch
  const store = createStore(rootReducer, preloadedState, middlewareEnhancer);
  sagaMiddleware.run(rootSaga);
  return store;
}
复制代码

代码里的注释大致能看出来干了什么,那么我们再来一步步分析。
首先createSagaMiddleware创建了redux-saga中间件来扩展dispatch,那么为什么要扩展dispatch? 这里稍微说下,原因就是我们需要处理异步。 在原始的redux,dispatch的action必须是一个纯对象(plain object),而且必须是同步的。但是我们的应用里面经常会有定时器,网络请求等等异步操作,扩展dispatch让它能接受函数,这个函数会传入dispatch本身作为参数,这就是一种异步解决方案(Redux-Thunk)。有兴趣的可以再深入了解redux以及redux-thunk/redux-saga。
继续往下看,我们看到它把生成的中间件传入了applyMiddleware,那么这个API又干了什么?

applyMiddleware 和 compose

从名称上就能看出来是使用中间件的意思,那么它是怎么直接使用的呢?参数可是一个数组(不止一个中间件)。我们来分析下代码:

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 创建store
    const store = createStore(...args)
    // 新建变量 dispatch 函数
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }
    
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 把 store.getState 和 dispatch 注入到每个中间件
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 调用compose, 重新赋值经过中间件扩展后的dispatch
    dispatch = compose(...chain)(store.dispatch)
    // 返回 store, 扩展后的dispatch
    return {
      ...store,
      dispatch
    }
  }
}
复制代码

这里看到 applyMiddleware 关键部分就是compose函数调用后重新赋值dispatch。我们再来看看 compose

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
复制代码

关键代码就在最后一句,说白了就是循环调用函数,就像下面一样

const func = [f1, f2, f3];
compose(...func)        //return f1(f2(f3(...args)))
//注意函数调用的顺序是从左到右,即:f1 -> f2 -> f3
复制代码

那么这里有个问题,applyMiddleware返回的store和dispatch又在什么地方用到了?留下这问题我们继续看
基本上面的细节我们已经分析完毕。这边我们到下一行,

const store = createStore(rootReducer, preloadedState, middlewareEnhancer);
复制代码

这一行很清楚,调用createStore生成store。 这里提一下上面的问题,其实在传入middlewareEnhancer的情况下,这里生成的store 其实就是 applyMiddleware返回的值。这样我们的疑问也得到了解答。
到这里我们终于分析完了store的生成过程,对store有了大致的了解。说白了store就是createStore内置函数subscribe、getState等以及包装过的dispatch。

Provider干了什么

前面说了这么多,终于进入主题,哈哈。各位客官不要急,咱们慢慢道来。先上代码,

import React, { useMemo } from 'react'
import PropTypes from 'prop-types'
import { ReactReduxContext } from './Context'
import Subscription from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'

// store 为 redux 创建的 store
function Provider({ store, context, children }) {
  // 定义 contextValue
  const contextValue = useMemo(() => {
    // 新建订阅类
    const subscription = new Subscription(store)
    // 通知类 createListenerCollection 的通知方法赋值到订阅类的回调方法上,以便后面回调通知
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription,
    }
  }, [store])

  const previousState = useMemo(() => store.getState(), [store])

  // useIsomorphicLayoutEffect 其实就是 useLayoutEffect 或者 useEffect, 这里使用的是 useLayoutEffect,为了在渲染之前调用
  useIsomorphicLayoutEffect(() => {
    const { subscription } = contextValue
    // 订阅类增加订阅
    subscription.trySubscribe()
    //前后的state不一样,那么就去通知订阅者更新,onStateChange就会执行
    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs()
    }
    return () => {
      // 卸载订阅
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])
  // 默认使用 ReactReduxContext, 其实就是 React.createContext(null)
  const Context = context || ReactReduxContext
  // 返回react的 Context.Provider,说白了 react-redux其实是在 react Provider Consumer 上的拓展, 
  // 当然得升级到16.3 才是用的 react  Context,要是项目小,react 版本高的话 不必用 redux, react Context 够用了
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

if (process.env.NODE_ENV !== 'production') {
  Provider.propTypes = {
    store: PropTypes.shape({
      subscribe: PropTypes.func.isRequired,
      dispatch: PropTypes.func.isRequired,
      getState: PropTypes.func.isRequired,
    }),
    context: PropTypes.object,
    children: PropTypes.any,
  }
}

export default Provider
复制代码

首先看到定义了contentValue,使用useMemo包装监听store变化返回store和subscription,我们看倒了一个新东西Subscription,这又是个什么玩意,老规矩上源码,看看里面究竟是什么:

Subscription

export default class Subscription {
  constructor(store, parentSub) {
    // 初始化store
    this.store = store
    // 获取来自父级的Subscription实例,connect会用到
    this.parentSub = parentSub
    this.unsubscribe = null
    this.listeners = nullListeners

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
  }

  addNestedSub(listener) {
    this.trySubscribe()
    // 这里是被parentSub调用的,所以listener也会被订阅到parentSub上
    return this.listeners.subscribe(listener)
  }

  notifyNestedSubs() {
    // 通知listeners 去执行
    this.listeners.notify()
  }

  handleChangeWrapper() {
    if (this.onStateChange) {
      // onStateChange会在外部的被实例化成subcription实例的时候,被赋值为不同的更新函数,被赋值的地方分别的Provider和connect中
      // 一般就是这行 notifyNestedSubs方法 this.listeners.notify()
      this.onStateChange()
    }
  }

  isSubscribed() {
    return Boolean(this.unsubscribe)
  }

  trySubscribe() {
    if (!this.unsubscribe) {
      // 如果parentSub没传,那么使用store订阅,否则,调用context中获取的subscrption来订阅,保证都订阅到一个地方
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper)
      // 创建listener集合
      this.listeners = createListenerCollection()
    }
  }
  // 取消订阅
  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()
      this.listeners = nullListeners
    }
  }
}
复制代码

Subscription就是将页面的更新工作和状态的变化联系起来。通过trySubscribe方法,根据情况被分别订阅到store或者Subscription内部。放入到listeners数组,当state变化的时候,listeners循环执行每一个监听器,触发页面更新。 trySubscribe中根据不同情况判断直接使用store订阅,还是调用addNestedSub来实现订阅的原因。者的场景是Provider将listener订阅到store中。后者是connect内部将checkForUpdates放到listeners数组中,实际上是利用Provider中传过来的Subscrption实例来订阅,保证所有被connect的组件都订阅到一个Subscrption实例上。

总结

到此Provider基本分析完毕。Provider最主要的功能是从props中获取我们传入的store,并将store作为context的其中一个值,向下层组件下发。一旦store变化,Provider要有所反应,以此保证将始终将最新的store放入context中。
那么如何向组件中注入state和dispatch,就要再分析分析connect。

文章分类
前端
文章标签