美文网首页React
Redux源码阅读(三)——connect

Redux源码阅读(三)——connect

作者: 景阳冈大虫在此 | 来源:发表于2021-04-20 21:07 被阅读0次

    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(
        <Counter
          value={store.getState()}
          onIncrement={() => 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(
      <Provider store={store}>
        <App />
      </Provider>,
      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 <Context.Provider value={contextValue}>{children}</Context.Provider>
    }
    ……
    
    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()。


    listeners

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

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


      contextValue
    2. 更新
    • 当触发了store的更新时


      listener.notify
      callback

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

    小结
    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.Consumer />)
              ? propsContext
              : Context
          }, [propsContext, Context])
    ……
          const renderedWrappedComponent = useMemo(
            () => (
              <WrappedComponent
                {...actualChildProps}
                ref={reactReduxForwardedRef}
              />
            ),
            [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 (
                <ContextToUse.Provider value={overriddenContextValue}>
                  {renderedWrappedComponent}
                </ContextToUse.Provider>
              )
            }
    
            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

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