對 react-router
的分析,目前準備主要集中在三點:javascript
a. `history` 的分析。
b. `history` 與 `react-router` 的聯繫。
c. `react-router` 內部匹配及顯示原理。
複製代碼
這篇文章準備着重理解 history
.html
推薦:★★★☆java
一段顯而易見出如今各大 react v16+ 項目中的代碼是這樣的:node
import React, {Component} from 'react'
import { render } from 'react-dom'
import { Router, Route } from 'react-router'
import { createBrowserHistory } from 'history'
const history = createBrowserHistory()
const App = () => (
<Router history={history} /> <div id="app"> {/* something */} </div> </Router>
)
render(<App/>, document.body.querySelector('#app'))
複製代碼
在 react
v16+ 版本里,一般 react-router
也升級到了 4 以上。react
而 react-router
v4+ 一般是配合 history
v4.6+ 使用的。git
下面就先從 history
開始,讓咱們一步一步走近 react-router
的神祕世界。github
history
在內部主要導出了三個方法:
createBrowserHistory
, createHashHistory
, createMemoryHistory
.設計模式
它們分別有着本身的做用:api
createBrowserHistory
是爲現代主流且支持 HTML5 history 瀏覽器提供的 API.createHashHistory
是爲不支持 history
功能的瀏覽器提供的 API.createMemoryHistory
則是爲沒有 DOM 環境例如 node
或 React-Native
或測試提供的 API.咱們就先從最接地氣的 createBrowserHistory
也就是咱們上文中使用的方法開始看起。瀏覽器
話很少說,直接走進 createBrowserHistory源碼
/**
* Creates a history object that uses the HTML5 history API including
* pushState, replaceState, and the popstate event.
*/
複製代碼
在該方法的註釋裏,它說明了是它基於 H5 的 history
建立的對象,對象內包括了一些經常使用的方法譬如
pushState
,replaceState
,popstate
等等。history
對象那麼它具體返回了什麼內容呢,下面就是它目前全部的方法和屬性:
const globalHistory = window.history;
const history = {
length: globalHistory.length, // (number) The number of entries in the history stack
action: "POP", // (string) The current action (`PUSH`, `REPLACE`, or `POP`)
location: initialLocation, // (object) The current location. May have the following properties.
createHref,
push, // (function) Pushes a new entry onto the history stack
replace, // (function) Replaces the current entry on the history stack
go, // (function) Moves the pointer in the history stack by `n` entries
goBack, // (function) Equivalent to `go(-1)`
goForward, // (function) Equivalent to `go(1)`
block, // (function) Prevents navigation
listen
}
複製代碼
globalHistory.length
顯而易見是當前存的歷史棧的數量。
createHref
根據根路徑建立新路徑,在根路徑上添加原地址所帶的 search
, pathname
, path
參數, 推測做用是將路徑簡化。
location
當前的 location
, 可能含有如下幾個屬性。
path
- (string) 當前 url
的路徑 path
.search
- (string) 當前 url
的查詢參數 query string
.hash
- (string) 當前 url
的哈希值 hash
.state
- - (object) 存儲棧的內容。僅存在瀏覽器歷史和內存歷史中。block
阻止瀏覽器的默認導航。用於在用戶離開頁面前彈窗提示用戶相應內容。the history docs
其中,go
/goBack
/goForward
是對原生 history.go
的簡單封裝。
剩下的方法相對複雜些,所以在介紹 push
, replace
等方法以前,先來了解下 transitionManager
. 由於下面的不少實現,都用到了這個對象所提供的方法。
transitionManager
方法介紹首先看下該對象返回了哪些方法:
const transitionManager = {
setPrompt,
confirmTransitionTo,
appendListener,
notifyListeners
}
複製代碼
在後續 popstate
相關的方法中,它就應用了 appendListener
和與之有關的 notifyListeners
方法,咱們就先從這些方法看起。
它們的設計體現了常見的訂閱-發佈模式,前者負責實現訂閱事件邏輯,後者負責最終發佈邏輯。
let listeners = [];
/** * [description 訂閱事件] * @param {Function} fn [description] * @return {Function} [description] */
const appendListener = fn => {
let isActive = true;
// 訂閱事件,作了函數柯里化處理,它實際上至關於運行了 `fn.apply(this, ...args)`
const listener = (...args) => {
if (isActive) fn(...args);
};
// 將監聽函數一一保存
listeners.push(listener);
return () => {
isActive = false;
listeners = listeners.filter(item => item !== listener);
};
};
/** * [發佈邏輯] * @param {[type]} ..args [description] */
const notifyListeners = (..args) => {
listeners.forEach(listener => listener(..args))
}
複製代碼
介紹了上面兩個方法的定義,先別急。後續再介紹它們的具體應用。
而後來看看另外一個使用的較多的方法 confirmTransitionTo
.
const confirmTransitionTo = (
location,
action,
getUserConfirmation,
callback
) => {
if (prompt != null) {
const result =
typeof prompt === "function" ? prompt(location, action) : prompt;
if (typeof result === "string") {
if (typeof getUserConfirmation === "function") {
getUserConfirmation(result, callback);
} else {
callback(true);
}
} else {
// Return false from a transition hook to cancel the transition.
// 若是已經在執行,則暫時中止執行
callback(result !== false);
}
} else {
callback(true);
}
};
複製代碼
實際上執行的就是從外部傳進來的 callback
方法,只是多了幾層判斷來作校驗,並且傳入了布爾值來控制是否須要真的執行回調函數。
transitionManager
調用再而後咱們來看看上述方法appendListener
, notifyListeners
的具體應用。前者體如今了 popstate
事件的訂閱中。
那麼就先簡單談談 popstate
事件。
popstate
事件, 也就是說,popstate
自己並非像 pushState
或 replaceState
同樣是 history
的方法。history.popState
這樣的方式來調用。history.pushState
或 history.replaceState
不會觸發 popstate
事件。在事件監聽方法 listen
中涉及了 popstate
的使用,在源碼中能夠看到如下兩個方法 listen
和 checkDOMListeners
.
它們就是上述訂閱事件的具體調用方。
// 首先天然是初始化
const transitionManager = createTransitionManager();
const PopStateEvent = "popstate";
const HashChangeEvent = "hashchange";
// 當 URL 的片斷標識符更改時,將觸發 hashchange 事件(跟在 # 後面的部分,包括 # 符號)
// https://developer.mozilla.org/zh-CN/docs/Web/Events/hashchange
// https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onhashchange
const checkDOMListeners = delta => {
listenerCount += delta;
if (listenerCount === 1) {
// 其實也是最多見最簡單的訂閱事件, handlePopState 對應的內容在下文有說明
window.addEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
window.addEventListener(HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
window.removeEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
window.removeEventListener(HashChangeEvent, handleHashChange);
}
};
/** * [訂閱事件的具體調用方] * @param {Function} listener [description] * @return {Function} [description] */
const listen = listener => {
// 返回一個解綁函數
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
// 返回的函數負責取消
return () => {
checkDOMListeners(-1);
unlisten();
};
};
複製代碼
簡言之,調用 listen
就是給 window
綁定了相應方法,再次調用以前 listen
返回的函數則是取消。
而後來看看發佈事件的具體調用方 setState
。它在 createBrowserHistory.js
中定義,在 popstate
、 push
與 replace
中均有調用。
/** * 在該方法中發佈 * @param {*} nextState [入參合併到 history] */
const setState = nextState => {
Object.assign(history, nextState);
history.length = globalHistory.length;
// 執行全部的監聽函數
transitionManager.notifyListeners(history.location, history.action);
};
複製代碼
以上是 setState
的定義。咱們來看看它在 popstate
中的使用。
window.addEventListener(PopStateEvent, handlePopState);
const handlePopState = (event) => {
handlePop(getDOMLocation(event.state))
}
let forceNextPop = false
const handlePop = (location) => {
if (forceNextPop) {
forceNextPop = false
setState()
} else {
const action = 'POP'
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({ action, location })
} else {
revertPop(location)
}
})
}
}
複製代碼
瀏覽器註冊了 popstate
事件,對應的 handlePopState
的方法則最終調用了 setState
方法。
翻譯成白話就是瀏覽器回退操做的時候,會觸發 setState
方法。它將在下文以及後一篇博文裏起到重要做用。
confirmTransitionTo
.push
, replace
這兩個上文提到的重要方法,是原生方法的擴展。它們都用到了上述分析過的方法,都負責實現跳轉,所以內部有較多邏輯相同。
後面會以 push
爲例, 它其實就是對原生的 history.pushState
的強化。
那麼這裏就先從原生的 history.pushState
開始熟悉瞭解。
history.pushState
接收三個參數,第一個爲狀態對象,第二個爲標題,第三個爲 Url.
react-router
用來標識頁面的變化,以此渲染組件)history.pushState({ foo: 'bar'}, 'page1', 'bar.html')
.popstate
事件。只有在上述操做後,訪問了其餘頁面,而後點擊返回,或者調用 history.go(-1)/history.back()
時,popstate
會被觸發。// 定義一個 popstate 事件
window.onpopstate = function(event) {
console.info(event.state)
}
const page1 = { page: 'page1' }
const page2 = { page: 'page2' }
history.pushState(page1, 'page1', 'page1.html')
// 頁面地址由 www.google.com => www.google.com/page1.html
// 但不會刷新或從新渲染
history.pushState(page2, 'page2', 'page2.html')
// 頁面地址由 www.google.com/page2.html => www.google.com/page2.html
// 但不會刷新或從新渲染
// 此時執行
history.back() // history.go(-1)
// 會觸發 popstate 事件, 打印出 page1 對象
// { page: 'page1' }
複製代碼
介紹完 pushState
後,看看 history
中是怎樣實現它的。
const push = (path, state) => {
const action = "PUSH";
const location = createLocation(path, state, createKey(), history.location);
// 過渡方法的應用
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
// 布爾值,用於判斷是否須要執行
if (!ok) return;
const href = createHref(location);
const { key, state } = location;
// 在支持 history 的地方則使用 history.pushState 方法實現
if (canUseHistory) {
globalHistory.pushState({ key, state }, null, href);
if (forceRefresh) {
window.location.href = href
} else {
// 若是是非強制刷新時,會更新狀態,後續在 react-router 中起到重要做用
// 上文提到過的發佈事件調用處
setState({ action, location })
}
} else {
window.location.href = href;
}
}
);
};
複製代碼
關鍵代碼:globalHistory.pushState({ key, state }, null, href);
和上文分析的一致。
pushState
和 push
方法講完,replaceState
和 replace
也就很好理解了。
replaceState
只是把推動棧的方式改成替換棧的行爲。它接收的參數與 pushState
徹底相同。只是方法調用後執行的效果不一樣。
補:原本若是僅僅是介紹當前的 history
. 我以前覺得找到 pushState
這個核心就已經足夠了。但當我繼續深刻,探究 react-router
原理的時候,才發現這裏遺漏了重要的一點。那就是 setState
方法。
那麼這個方法具體作了什麼呢。在上文中已經作了簡單介紹,這裏再重申一遍:就是將當前 state
存入 history
, 同時發佈事件,也就是調用以前訂閱時的保存的全部方法。參數則是 [history.location, history.action]
. 或許如今,咱們可能對該方法的重要性沒有那麼深的理解,當你再結合後一篇分析 react-router
的文章,就知道它起的做用了。
react-router
倉庫裏是有 history
的介紹的。此時我一臉茫然。這裏面內容雖然很少,卻很是值得參考。這裏作部分翻譯和理解,看成對上文的補充。history is mutable
在原文檔中,說明了 history
對象是可變的。所以建議在 react-router
中獲取 location
時可使用 Route
的 props
的方式來替代 history.location
的方式。這樣的方式會確保你的流程處於 React
的生命週期中。例如:
class Comp extends React.Component {
componentWillReceiveProps(nextProps) {
// 正確的打開方式
const locationChanged = nextProps.location !== this.props.location
// 錯誤的打開方式,由於 history 是可變的,因此這裏老是不等的 // will *always* be false because history is mutable.
const locationChanged = nextProps.history.location !== this.props.history.location
}
}
<Route component={Comp}/>
複製代碼
更多內容請查看the history documentation.
history
這個庫。它是一個對 HTML5 原生 history
的拓展,它對外輸出三個方法,用以在支持原生 api 的環境和不兼容的環境,還有 node 環境中調用。而該方法返回的就是一個加強的 history
api.react-router
,到發現它依賴的主要的庫 history
. 再進行細化,到 history
主要提供的對象方法。裏面涉及的發佈訂閱設計模式、思路、以及具體的實現使用了柯里化方式。一步一步探究下去能夠發現不少有趣的地方。彷佛又喚起往日的技術熱情。react-router
.createBrowserHistory
基本一致,只是具體的實現有部分差異。有時間補上。createHashHistory
createMemoryHistory