用 react-router 也用了比較久了,對他的內部工做方式卻只是瞭解皮毛,並且大部分仍是經過別人的博客。最近兩週打算本身探究一下他的實現。 注意!由於我只使用過 v3 版本的 react-router,由於對他的使用方式比較熟悉,因此此次解析也是基於這個版本。javascript
聊到這個話題就離不開前端路由。關於前端路由的一些演變過程和現有的方式能夠看這篇文章。前端路由的重點就是不刷新頁面,現有的解決方案有 hashChange 和 popState 兩種。 React 提供API也是圍繞這兩種方式。 共同點都是發佈訂閱的模式,讓瀏覽器事件觸發的時候本身添加的 listener 被調用。Router 組件包裹着 Route 組件,Route 組件負責描述每條路由的展現組件和匹配的路徑。這樣 Router 組件實際上會格式化出一個映射的路由表。
然而這是在頁面路由更新的時候,最開始進入頁面的時候怎麼辦呢?其實剛進入頁面的時候也會進行一次匹配,詳細分析見下一部分。前端
首先解答上面的遺留問題,剛進入頁面的狀態如何帶入?這個問題咱們能夠和 "Router 組件是在何時添加的事件監聽"放在一塊兒解答。java
componentWillMount() {//來源:modules/Router.js
this.transitionManager = this.createTransitionManager()
this.router = this.createRouterObject(this.state)
this._unlisten = this.transitionManager.listen((error, state) => {
if (error) {
this.handleError(error)
} else {
// Keep the identity of this.router because of a caveat in ContextUtils:
// they only work if the object identity is preserved.
assignRouterState(this.router, state)
this.setState(state, this.props.onUpdate)
}
})
},
複製代碼
Router 組件在 willMount 生命週期添加了 listener,而添加 listener 自己就會觸發一次匹配路由展現的過程。匹配的過程有 match 方法,用於各類嵌套路由的匹配。
可是注意,若是使用的是 browserHistory,這種路由方式通常是/a/b 這種方式,可能須要後端同窗的配合。react
在上面的代碼中,咱們會注意到添加監聽器的 listen方法來自於 transitionManager 這個生成以後被賦值到 this.router 實例的屬性。實際上 react-router 的事件監聽過程是用 transitionManager 套了 history 這個庫,抹平各類前端路由方式的調用差別。history庫自己暴露了一些API 好比監聽、取消監聽、跳轉等一系列方法。有基於我們剛纔提到的 hash 和 state 兩種方式。咱們傳給 Router 組件 history 屬性的值其實就是他的實例。(拿hashHistory 舉慄,下面的文件是 reate-router export 的 hashHistory 的來源,也就是咱們用的 hashHistory 的來源)。segmentfault
//來源:modules/hashHistory.js
import createHashHistory from 'history/lib/createHashHistory'
import createRouterHistory from './createRouterHistory'
export default createRouterHistory(createHashHistory)
複製代碼
而 transitionManager 作的事情是針對當前的 router 實例和開發者指定的 history 對象,對 history 庫給的 API 作一次二次封裝,加上修改路由狀態等等操做。而後開發者拿着 transitionManager 封裝以後暴露出的 listen 等方法操做路由。後端
createTransitionManager() {//來源:modules/Router.js
const { matchContext } = this.props
if (matchContext) {
return matchContext.transitionManager
}
const { history } = this.props
const { routes, children } = this.props
invariant(
history.getCurrentLocation,
'You have provided a history object created with history v4.x or v2.x ' +
'and earlier. This version of React Router is only compatible with v3 ' +
'history objects. Please change to history v3.x.'
)
return createTransitionManager(//注意這個createTransitionManager纔是
history,
createRoutes(routes || children)
)
},
複製代碼
渲染過程不是放在 Route 組件中負責渲染,而是把狀態都放在 Router 中保存,詳細可見第一部分的代碼添加 listener 的部分。瀏覽器
assignRouterState(this.router, state)
this.setState(state, this.props.onUpdate)
複製代碼
而 Router 組件的 render 是這樣寫的:bash
const { location, routes, params, components } = this.state
const { createElement, render, ...props } = this.props
return render({
...props,
router: this.router,
location,
routes,
params,
components,
createElement
})
複製代碼
而 props 的值是當前 Router 組件的狀態,他如今要展現的組件,對應的地址,當前跳轉攜帶的參數 params 等等。下面是調用 render 的部分。react-router
return <RouterContext {...props} /> 複製代碼
RouterContext 包裝組件的主要做用就是把 props參數中存有當前路由狀態的對象router存到全局。相似於 Redux 的 Provider 組件。ide
這裏小夥伴們能夠猜想一下,Link是怎麼作的呢?
咱們知道 Link最後渲染完是個 a 標籤,咱們一般會給 Link 組件幾個參數,最經常使用的是跳轉的路由地址和攜帶的參數。經過上面的講解不難猜出,Link 在點擊的時候應該是調用了一個跳轉的操做(八成也是 history 庫裏給的),而後禁止掉默認跳轉就好了。
事實也是如此,history 暴露了一個 push方法,來 push 進瀏覽器的歷史訪問棧中。 這裏再提一句另一種用法:*history.push() 的方式。這種其實就至關於直接點擊了 a 標籤同樣的道理,只不過用 js 的方式實現了。
咱們可不能夠嘗試把展現交給 route 組件管理?router 只控制激活當前的 route?可是這樣就不能支持經過 props 傳給 router 路由配置的方式使用了,這是其一;
其二,這樣其實 route 其實負責了組件的渲染工做,而不是把全部的狀態和路由信息所有放在 router 中管理了,不方便集中維護和擴展。 不知道小夥伴們還有別的見解嗎?
(原本想寫個淺析的……噼裏啪啦寫了一大堆……還捎帶點語無倫次……) (可是雖說了一堆……不過確實挺淺的……各位有興趣能夠嘗試本身扒一下源碼。建議 react-router 和 history 庫一塊兒 debug,更有助於咱們融會貫通) ( 各位見笑了)