閱讀react-redux源碼 - 一

閱讀react-redux源碼零中準備了一些react、redux和react-redux的基礎知識。從使用的例子中可以看出來頂層的代碼中需要用一個來自react-redux的Provider組件提供redux的store,然後Provider的後代組件通過connect組件連接自己的業務組件就可以獲取到通過Provider組件跨組件傳遞過來的store中的state值。

所以我們先從Provider開始看源碼的實現。因爲源碼很短,直接先貼出來整個源碼如下:

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

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])

  useEffect(() => {
    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>
}

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

可以看到在頂部引入了 ReactReduxContext 這個Context也很簡單就是一個React.createContext創建出來的context,用於跨層級向後代組件提供 contextValue。這個contextValue將在下面被定義。

再下面引人注意的就是 Subscription 這個函數,從名字上可以看出是一個實現發佈訂閱的類。這將是本文的重點。

再往下就定義了我們今天的主角 Provider組件。

Provider組件指接收三個props,分別爲store、context和children。而它的返回值爲:

<Context.Provider value={contextValue}>
	{children}
</Context.Provider>

這個組件是React的context的Provider,用於向後代組件跨層級傳遞值,這裏的值就是contextValue。

獲取該值的方法就是通過<Context.Consumer>{contextValue => null}</Context.Consumer>或者使用hooks的useContext也可以拿到contextValue

contextValue

const contextValue = useMemo(() => {
    const subscription = new Subscription(store)
   
    subscription.onStateChange = subscription.notifyNestedSubs

    return {
      store,
      subscription
    }
  }, [store])

contextValue的值只依賴store,如果store沒變那麼contextValue的值則不會變。

可以看出來contextValue是一個對象其中有store和一個subscription對象。subscription對象是一個監聽器(監聽到某些事件然後通知自己的監聽者),監聽store中的state的變化,只要state變化了那麼subscription對象的onStateChange則會執行,由此可見onStateChange這個名字也是很能說明這個方法是做什麼的,就是監聽store的state改變事件。

subscription.onStateChange = subscription.notifyNestedSubs這樣就表示state發生變化則subscription.notifyNestedSubs則會被調用,用來通知自身的監聽者。

再下面得到 contextValue 的值爲 {store, subscription}。

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

暫存一下首次進來的state。(暫時只知道這裏做了什麼,但是爲什麼這麼做還不是很清楚)

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

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

一個執行副作用的鉤子,執行subscription.trySubscribe()嘗試監聽某個對象,當前上下文中是store中的state的改變。之後返回一個函數用於在卸載的時候做一些清理工作,例如卸載監聽和去除onStateChange的關聯。

const Context = context || ReactReduxContext

獲取Context,這個Context可以不用react-redux提供的默認Context也可以自己提供context。

return <Context.Provider value={contextValue}>{children}</Context.Provider>

返回Context.Provider包裹的組件。被包裹的組件可以通過對應的Context來獲取被傳入的value(contextValue)。

小結

組件Provider中一共使用了三個生命週期,useMemo、useMemo和useEffect,這三個生命週期都是直接或者間接監聽store的改變。所以可以看出來這些邏輯是爲了處理在運行過程中Provider的父組件改變store的行爲。在父組件改變store的時候可以及時卸載舊store上的監聽設置新store的監聽,並且通知後代組件有新的state產生。

./utils/Subscription.js

在組件Provider中使用Subscription類的實例來監聽store中state的改變,並且通知改變給自己的監聽者。

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
    }
  }
}

整個Subscription實現了一個事件鏈:

------ subscriptionA ------ subscriptionB\ ------ subscriptionC
-------------- |---------------------|---------------------------- |
-------------- | - listenerA1 — | - listenerB1 ----------- | - listenerB1
-------------- | - listenerA2 — | - listenerB2 ----------- | - listenerC2
-------------- | - listenerA3 — | - listenerB3 ----------- | - listenerC3

​使用Subscription實現上面的事件鏈:

const eventOrigin = {
  listeners: [],
  subscribe(fn) {
    eventOrigin.listeners.push(fn)
    return function() {
      eventOrigin.listeners = eventOrigin.listeners.filter(item => item !== fn)
    }
  },
  notify() {
    let i = 0
    while (i < eventOrigin.listeners.length) eventOrigin.listeners[i++]()
  }
}

const subscriptionA = new Subscription(eventOrigin)
subscriptionA.onStateChange = subscriptionA.notifyNestedSubs
subscriptionA.trySubscribe()
subscriptionA.addNestedSub(function listenerA1() {
  console.log('listenerA1')
})
subscriptionA.addNestedSub(function listenerA2() {
  console.log('listenerA2')
})
subscriptionA.addNestedSub(function listenerA3() {
  console.log('listenerA3')
})

const subscriptionB = new Subscription(undefined, subscriptionA)
subscriptionB.onStateChange = subscriptionB.notifyNestedSubs
subscriptionB.trySubscribe()
subscriptionB.addNestedSub(function listenerA1() {
  console.log('listenerB1')
})
subscriptionB.addNestedSub(function listenerA2() {
  console.log('listenerB2')
})
subscriptionB.addNestedSub(function listenerA3() {
  console.log('listenerB3')
})

const subscriptionC = new Subscription(undefined, subscriptionB)
subscriptionC.onStateChange = subscriptionC.notifyNestedSubs
subscriptionC.trySubscribe()
subscriptionC.addNestedSub(function listenerA1() {
  console.log('listenerC1')
})
subscriptionC.addNestedSub(function listenerA2() {
  console.log('listenerC2')
})
subscriptionC.addNestedSub(function listenerA3() {
  console.log('listenerC3')
})

// 測試,觸發事件源的notify
eventOrigin.notify()

打印出如下結果:

listenerA1
listenerA2
listenerA3
listenerB1
listenerB2
listenerB3
listenerC1
listenerC2
listenerC3

每個subscription實例就是一個事件的節點,每個節點上面有很多事件的監聽器,事件沿着subscription實例組成的鏈傳遞,挨個通知每個subscription節點,然後subscription節點的事件監聽器監聽到事件之後挨個執行回調。

可以想象成DOM的原生事件,事件沿着DOM傳遞,每個DOM上可以addEventListener多個事件回調。

createListenerCollection

其中用到的函數createListenerCollection創建的對象也是一個監聽器,監聽某個事件發生通知自己的監聽者。

createListenerCollection返回的是一個雙向鏈表,這個數據結構方便修改,刪除某一項十分快捷,不需要遍歷鏈表中的每一個,直接將上一個的next指針指向下一個就完成了自身的刪除。源碼如下:

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
        }
      }
    }
  }
}
const batch = getBatch()

這個batch表示的是批量更新的意思。這個是react的知識點,簡單描述已備忘。

react自身發起的更新是默認批量的。例如onClick函數裏setState兩次只會引起一次render,componentDidMount裏面setState兩次也只會引起一次render,但是setTimeout裏面兩次setState就會引起兩次跟新,想要避免這個事情就可以用batch來規避。

原理就類似於react自己張開了一個袋子,需要更新的組件都會被收到這個袋子裏,裝完之後一起處理組件的更新。那麼爲什麼setTimeout裏面的裝不到袋子裏裏呢?因爲setTimeout是異步的並不歸react管,不在一個調用棧內。或者說setTimeout的回調函數執行之前張開的袋子已經閉合了。

const batch = getBatch()
let first = null
let last = null

首先拿到batch函數,定義頭指針和尾指針,這是一個雙向鏈表,和單鏈表不同,不僅可以從first到last遍歷,也可以從last到first遍歷。

並且刪除雙向鏈表中的一個節點也十分方便,可以將當前節點的上一個節點的next指針指向當前節點的下一個節點就直接將當前節點刪除了。如果是單鏈表需要從頭開始遍歷鏈表纔可以。從空間上來說鏈表不需要連續的內存空間,相較於數組這方面也是更加靈活。

return {
  // 清理當前所有監聽者
  clear() {},
  // 通知監聽者事件發生
  notify() {},
  // 返回所有監聽者組成的數組
  get() {},
  // 設置監聽者
  subscribe(callback) {}
}
clear() {
  first = null
  last = null
}

清除的時候直接將first和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

設置事件發生的回調函數,notify通知的就是在這裏subscribe的回調函數,這個方法相對複雜點,整體來說做了兩件事情:

一件是將入參callback作爲一個節點添加到雙向鏈表中,以便notify的時候可以通知到

一件是返回一個函數用於在鏈表中刪除該節點

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
    }
  }
}

一個標識符表示callback是否在鏈表中,爲了防止返回的卸載監聽的函數多次被調用:

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

定義鏈表中當前節點的值。從右往左看,新加進去的節點一定是最後一個節點,所以新加入節點的上一個節點一定是當前的last節點,所以prev的值是last。

而新加入節點的next值就是null了。

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

如果有上一個節點,表示新加入的節點不是第一個,也就是加入前的last節點不是null。新加入的節點是第一個就需要將當前節點作爲頭結點賦值給first變量,以便隨時可以從頭開始遍歷鏈表。

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
  }
}

可以改變鏈表的不僅僅是被返回的函數,還有一個clear方法,會刪除整個鏈表,所以在刪除節點的時候首先要檢查下這個鏈表是否還存在,然後看下當前節點是否還存在,如果鏈表存在並且還在監聽中,那麼可以執行卸載流程了。

首先修改剛纔的訂閱標識符,修改標識符爲未訂閱,因爲馬上要卸載了。

如果當前節點有下一個節點,就將當前節點的下一個節點的prev指針指向當前節點的prev。

如果當前節點沒有下一個幾點,直接將閉包中的last指向當前節點的prev,就刪除了當前節點。

還需要改下節點的next指針,因爲是雙向鏈表。

注:subscribe函數設置監聽的同時還會返回一個卸載監聽的函數,這種風格和redux的store的subscribe的風格如出一轍。

小結

createListenerCollection()返回一個對象,這個對象有方法subscribe和notify,一個用來訂閱事件的發生,一個用來通知訂閱的回調事件發生了,內部的實現則是通過雙向鏈表來完成的。

class Subscription

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
    }
  }
}

使用案例:


const subscriptionB = new Subscription(undefined, subscriptionA)
subscriptionB.onStateChange = subscriptionB.notifyNestedSubs
subscriptionB.trySubscribe()
subscriptionB.addNestedSub(function listenerA1() {
  console.log('listenerB1')
})
subscriptionB.addNestedSub(function listenerA2() {
  console.log('listenerB2')
})
subscriptionB.addNestedSub(function listenerA3() {
  console.log('listenerB3')
})

這個類做的事情在上面大體上說過,類似於瀏覽器的時間冒泡,現在看下具體是怎麼實現的。


constructor(store, parentSub) {
  this.store = store
  this.parentSub = parentSub
  this.unsubscribe = null
  this.listeners = nullListeners

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

Subscription的構造函數有兩個入參,一個是store,一個是parentSub是同一個類的不同實例。

上面在使用這個類的時候,new了之後緊接着會關聯實例的onStateChange到notifyNestedSubs中去,表示onStateChange執行的時候,實際上執行的是notifyNestedSubs。

緊接着調用實例trySubscribe方法,嘗試訂閱:

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

    this.listeners = createListenerCollection()
  }
}

如果沒有訂閱,那麼就去訂閱事件。訂閱的時候以parentSub優先,如果沒有提供parentSub,那麼就訂閱store的事件。

parentSub和subscribe方法有同樣的簽名,需要一個入參函數,會返回一個取消訂閱的函數。

返回的取消訂閱的函數在Subscription會被用作是否訂閱事件的標識符。

調用createListenerCollection初始化字段listeners,後面Subscription實例的監聽函數都會被委託到listeners上。

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

畢竟onStateChange到notifyNestedSubs的關聯是調用方手動關聯的,如果沒有關聯的話直接調用會報錯,爲了不報錯,做一次檢查也是有必要的。

爲什麼要onStateChange = notifyNestedSubs?做一次關聯?主觀感覺應該是語義上的考慮。

這個類實例化出來的對象主要是監聽store中state的改變的,所以對外onStateChange這個名字一聽就懂。但是對內的話,其實響應事件之後是要通知自身的監聽者,所以是notifyNestedSubs。

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

添加Subscription實例的監聽函數,被委託給listeners。

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

通知自身的訂閱者們,事件發生了,你們要做點什麼了。

總結

Provider組件跨組件向後代組件(主要是後面要提到的connect)提供了一個contextValue對象,其中包括了Subscription類的實例和store自身,其中Subscription的實例在監聽到store中state的變化的時候就會通知自身的監聽者,store的state變化了你們需要重新store.getState()重新渲染組件了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章