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(
    <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是一种JavaScript的状态管理容器,是一个独立的状态管理库,可配合其它框架使用,比如React。引...
    JS小子阅读 496评论 0 1
  • 一.引入: react本身是一个非常轻量级的视图层框架,因为组件传值太麻烦了。在react基础上配套一个数据层的框...
    Zlaojie阅读 249评论 0 0
  • 水平不够 这篇不知道怎么写 就纯粹当自己的笔记本如果有人想看的话 得有一定的 redux / react-redu...
    tcssin阅读 467评论 0 0
  • 1 redux使用步骤 React仅仅是一个前端View框架库,可以看做是MVC里面的V。没有解决组件间通信,MV...
    Dabao123阅读 436评论 0 2
  • 1. React简介 React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScrip...
    王蕾_fd49阅读 420评论 0 0