Redux源码阅读(三)——connect

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

不用connect

之前有说到过,当dispatch发生之后,所有使用subscribe订阅的listener将会被执行。所以在React项目里可以这么来将组件渲染与store更新绑定

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import Counter from './components/Counter'
import counter from './reducers'

const store = createStore(counter)
const rootEl = document.getElementById('root')

const render = () => {
  return ReactDOM.render(
     store.dispatch({ type: 'INCREMENT' })}
      onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
    />,
    rootEl
  )
}

render()
store.subscribe(render)

使用connect

使用connect可以在组件props处理store相关操作

// index.js
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './components/App'
import rootReducer from './reducers'

const store = createStore(rootReducer)

render(
  
    
  ,
  document.getElementById('root')
)
// FilterLink.js
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.visibilityFilter
})

const mapDispatchToProps = (dispatch, ownProps) => ({
  onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
})

export default connect(mapStateToProps, mapDispatchToProps)(Link)

源码

Provider

解释问题:当store发生改变时,是如何触发组件重新渲染的?

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

function Provider({ store, context, children }) {
  const contextValue = useMemo(() => {
    const subscription = new Subscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription,
    }
  }, [store])

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

  useIsomorphicLayoutEffect(() => {
    const { subscription } = contextValue
    subscription.trySubscribe()

    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs()
    }
    return () => {
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])

  const Context = context || ReactReduxContext

  return {children}
}
……

export default Provider

Provider的作用是处理store相关的listeners,当store发生了变更之后去执行订阅的listener,即触发组件渲染。

1. 初始化

从第一行开始看起

  • Subscription类就是用观察者模式去实现依赖收集和事件派发的,观察者模式的代码实现都大同小异。
import { getBatch } from './batch'

// encapsulates the subscription logic for connecting a component to the redux store, as
// well as nesting subscriptions of descendant components, so that we can ensure the
// ancestor components re-render before descendants

const nullListeners = { notify() {} }

function createListenerCollection() {
  const batch = getBatch()
  let first = null
  let last = null

  return {
    clear() {
      first = null
      last = null
    },

    notify() {
      batch(() => {
        let listener = first
        while (listener) {
          listener.callback()
          listener = listener.next
        }
      })
    },

    get() {
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners
    },

    subscribe(callback) {
      let isSubscribed = true

      let listener = (last = {
        callback,
        next: null,
        prev: last,
      })

      if (listener.prev) {
        listener.prev.next = listener
      } else {
        first = listener
      }

      return function unsubscribe() {
        if (!isSubscribed || first === null) return
        isSubscribed = false

        if (listener.next) {
          listener.next.prev = listener.prev
        } else {
          last = listener.prev
        }
        if (listener.prev) {
          listener.prev.next = listener.next
        } else {
          first = listener.next
        }
      }
    },
  }
}

export default class Subscription {
  constructor(store, parentSub) {
    this.store = store
    this.parentSub = parentSub
    this.unsubscribe = null
    this.listeners = nullListeners

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

  addNestedSub(listener) {
    this.trySubscribe()
    return this.listeners.subscribe(listener)
  }

  notifyNestedSubs() {
    this.listeners.notify()
  }

  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange()
    }
  }

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

  trySubscribe() {
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper)

      this.listeners = createListenerCollection()
    }
  }

  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()
      this.listeners = nullListeners
    }
  }
}

特别的地方在于存储listener的结构是链表,可能是因为频繁插入删除的原因而没有使用数组(记得在vue2的发布订阅模式用的是array)

  subscription.onStateChange = subscription.notifyNestedSubs

这行就是onStateChange=this.listeners.notify()

  • store发生变更时,将会重新计算得到两个memoized 值。一个是contextValue,一个是previousState
  • 使用上文说的两个memoizeduseIsomorphicLayoutEffect也就是useLayoutEffect来做数据更新时的重新订阅,即store.subscribe
  useIsomorphicLayoutEffect(() => {
    const { subscription } = contextValue

    // 即this.store.subscribe(this.handleChangeWrapper),将handleChangeWrapper订阅成为store的listener;
    //显然,当store发生变化时,redux将会触发这个listener。
    subscription.trySubscribe() 

    if (previousState !== store.getState()) { // 如果store发生变更
      subscription.notifyNestedSubs() // 即this.listeners.notify(),触发subscription的listener
    }
    return () => {
      subscription.tryUnsubscribe() // 清除订阅
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])

当store发生变化时会触发this.listeners的所有listener.callback()。


Redux源码阅读(三)——connect_第1张图片
listeners

可以看到listeners链表里面装的callback叫handleChangeWrapper

  • 然后利用Context.Provider把得到的contextValue传递下去


    Redux源码阅读(三)——connect_第2张图片
    contextValue
2. 更新
  • 当触发了store的更新时


    Redux源码阅读(三)——connect_第3张图片
    listener.notify

    Redux源码阅读(三)——connect_第4张图片
    callback

    调用datchedUpdate就可以触发组件的渲染

小结
Redux源码阅读(三)——connect_第5张图片
Provider

Provider做的主要的事情其实就是

this.store.subscribe(this.listeners.notify)

Connect

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

It does not modify the component class passed to it; instead, it returns a new, connected component class that wraps the component you passed in.

  • connect不会改变传入的Component而是返回包了一层的新组件。
function ConnectFunction(props){
……
      const ContextToUse = useMemo(() => {
        // Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
        // Memoize the check that determines which context instance we should use.
        return propsContext &&
          propsContext.Consumer &&
          isContextConsumer()
          ? propsContext
          : Context
      }, [propsContext, Context])
……
      const renderedWrappedComponent = useMemo(
        () => (
          
        ),
        [reactReduxForwardedRef, WrappedComponent, actualChildProps]
      )

      const renderedChild = useMemo(() => {
        if (shouldHandleStateChanges) {
          // If this component is subscribed to store updates, we need to pass its own
          // subscription instance down to our descendants. That means rendering the same
          // Context instance, and putting a different value into the context.
          return (
            
              {renderedWrappedComponent}
            
          )
        }

        return renderedWrappedComponent
      }, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
……

  • 新的使用useMemo包过的Component是renderedWrappedComponent,actualChildProps就是传入的处理过的新增props

通过这种方式,在store发生变化时触发了renderedWrappedComponent的重新渲染达到刷新页面的作用

参考

https://segmentfault.com/a/1190000010416732
https://react-redux.js.org/api/connect
https://www.redux.org.cn/docs/react-redux/api.html
https://axiu.me/coding/react-batchedupdates-and-transaction/

你可能感兴趣的:(Redux源码阅读(三)——connect)