原因前端
目前負責的項目中有一個微信網頁,用的是react技術棧。在該項目中增長了一個微信分享功能後,線上ios出現了問題,經排查,定位到了react的路由系統。 react
此次線上bug,讓我決定,先拿react-router-dom開刀,看看它內部到底幹了點啥,以解心頭之恨。ios
前端目前用到的就是react-router-dom這個庫,它提供了兩個高級路由器,分別是BrowserRouter和HashRouter,它兩的區別就是一個用的history API ,一個是使用URL的hash部分,接下來我以BrowserRouter爲例,作一個解讀。web
簡易過程圖數組
解讀(只摘取核心代碼進行展現)瀏覽器
首先看看整個react-router-dom提供了點啥?微信
export {
MemoryRouter, Prompt, Redirect, Route, Router, StaticRouter, Switch, generatePath, matchPath, withRouter, useHistory, useLocation, useParams, useRouteMatch } from "react-router"; export { default as BrowserRouter } from "./BrowserRouter.js"; export { default as HashRouter } from "./HashRouter.js"; export { default as Link } from "./Link.js"; export { default as NavLink } from "./NavLink.js"; 複製代碼
除了下面它本身實現的四個組件外,其他的都是將react-router提供的組件作了一個引入再導出,那看來底層核心的東西仍是在react-router上。網絡
import { BrowserRouter, Route, Switch, Link } from "react-router-dom"
function App() { return ( <BrowserRouter> <div>主菜單</div> <Link to="/home">home</Link> <br /> <Link to="/search">search</Link> <hr /> <Switch> <Route path="/home" component={Home} /> <Route path="/search" component={Search} /> </Switch> </BrowserRouter> ) } ReactDOM.render(<App />, document.getElementById('root')); 複製代碼
須要經過路由跳轉來實現UI變化的組件,要用BrowserRouter做爲一個根組件來包裹起來,Route用來盛放頁面級的組件。react-router
那按照這種層級關係,咱們先來看下BrowersRouter裏實現了什麼功能。app
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history"; class BrowserRouter extends React.Component { history = createHistory(this.props); render() { return <Router history={this.history} children={this.props.children} />; } } 複製代碼
很是少許的幾行代碼,很清晰的看到,核心點是history這個庫所提供的函數。組件在render前執行了createHistory這個函數,而後它會返回一個history的對象實例,而後經過props傳給Router這個路由器,另外其中包裹的全部子組件,通通傳給Router。
這裏其實官網上已經說的很清楚,你們用的時候能夠多留意下。
那麼思路就很清楚,重點放在Router和history庫上,看看Router是怎麼用這個history對象的,以及這個history對象裏又包含了啥,和window.history有什麼區別?讓咱們接着往下走。
import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js"; 複製代碼
Router是核心的路由器,上面咱們已經看到BrowsRouter傳遞給它了一個history對象。
首先引入了兩個context,這裏其實就是建立的普通context,只不過擁有特定的名稱而已。
它的內部實現是這樣
const createNamedContext = name => {
const context = createContext(); context.displayName = name; return context; }; // 上述的引用就至關於 HistoryContext = createNamedContext("Router-History") 複製代碼
引入了這兩個context後,在來看它的構造函數。
constructor(props) { super(props); this.state = { location: props.history.location }; this.unlisten = props.history.listen(location => { this.setState({ location }); }); } 複製代碼
Router組件維護了一個內部狀態location對象,初始值爲上面提到的在BrowsRouter中建立的history提供的。
以後,執行了history對象提供的listen函數,這個函數須要一個回調函數做爲入參,傳入的回調函數的功能就是來更新當前Router內部狀態中的location的,關於何時會執行這個回調,以及listen函數,後面會詳細剖析。
componentWillUnmount() {
if (this.unlisten) { this.unlisten(); } } 複製代碼
等這個Router組件將要卸載時,就取消對history的監聽。
render() {
return ( <RouterContext.Provider value={{ history: this.props.history, location: this.state.location, match: Router.computeRootMatch(this.state.location.pathname), staticContext: this.props.staticContext }} > <HistoryContext.Provider children={this.props.children || null} value={this.props.history} /> </RouterContext.Provider> ); } 複製代碼
最後生成的react樹,就是由最開始引入的context組成的,而後傳入history、location這些值。
總結就是整個Router就是一個傳入了history、locaiton和其它一些數據的context的提供者,而後它的子組件做爲消費者就能夠共享使用這些數據,來完成後面的路由跳轉、UI更新等動做。
在Router組件能夠看到已經用到了createBrowserHistory函數返回的history實例了,如:history.location和history.listen,這個庫裏的封裝的函數那是至關多了,細節也不少,我仍然挑最重要的解讀。
首先是我們這個出鏡率較高的history提供了哪些屬性和方法
看起來都是些熟悉的東西,如push、replace、go這些,都是window對象屬性history所提供的。但有些屬性實際上是重寫了的,如push、replace,其它的是作了一個簡單封裝。
function goBack() {
go(-1); } function goForward() { go(1); } 複製代碼
Router內部狀態location的初始數據,是使用window.location與window.history.state作的重組。
路由系統最爲重要的兩個切換頁面動做,一個是push,一個是replace,咱們平時只用Link組件的話,並無確切的感覺,其中Link接受一個props屬性,to :string 或者to : object
<link to='/course'>跳轉</link>
此時點擊它時,調用的就是props.history中重寫的push方法。
<Link to='/course' replace>跳轉</Link>
若是增長replace屬性,則用的就是replace方法
這兩個方法主要用的是pushState和replaceState這兩個API,它們提供的能力就是能夠增長新的window.history中的歷史記錄和瀏覽器地址欄上的url,可是又不會發起真正的網絡請求。
這是實現單頁面應用的關鍵點。
而後讓咱們看一下這兩個路由跳轉方法
精簡後,代碼仍是很多,我解讀下。
push中的入參path,是接下來準備要跳轉的路由地址。createLocation方法先將這個path,與當前的location作一個合併,返回一個更新的loation。
而後就是重頭戲,transitionManager這個對象,讓咱們先關注下成功回調裏面的內容。
經過更新後的location,建立出將要跳轉的href,而後調用pushState方法,來更新window.history中的歷史記錄。
若是你在BrowserRouter中傳了forceRefresh這個屬性,那麼以後就會直接修改window.lcoation.href,來實現頁面跳轉,但這樣就至關於要從新刷新來進行網絡請求你的文件資源了。
若是沒有傳的話,就是調用setState這個函數,注意這個setState並非react提供的那個,而是history庫本身實現的。
function setState(nextState) {
history.length = globalHistory.length; transitionManager.notifyListeners(history.location, history.action); } 複製代碼
仍是用到了transitionManager對象的一個方法。
另外當咱們執行了pushState後,接下來所獲取到的window.history都是已經更新的了。
接下來就剩transitionManager這最後的一個點了。
transitionManager是經過createTransitionManager這個函數實例出的一個對象
function createTransitionManager() {
var listeners = []; function appendListener(fn) { var isActive = true; function listener() { if (isActive) fn.apply(void 0, arguments); } listeners.push(listener); return function () { isActive = false; listeners = listeners.filter(function (item) { return item !== listener; }); }; } function notifyListeners() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } listeners.forEach(function (listener) { return listener.apply(void 0, args); }); } return { appendListener: appendListener, notifyListeners: notifyListeners }; } 複製代碼
還記的開始時咱們在Router組件中已經用過一個history.listen方法,其中內部實現就是用的transitionManager.appendListener方法
function listen(listener) {
var unlisten = transitionManager.appendListener(listener); checkDOMListeners(1); return function () { checkDOMListeners(-1); unlisten(); }; } 複製代碼
當時咱們給listen傳入了一個回調函數,這個回調函數是用來經過React的setState來更新組件內部狀態的locaton數據,而後又由於這個lcoation傳入了Router-context的value中,因此當它發生變化時,全部的消費組件,都會從新render,以此來達到更新UI的目的。
listen的執行細節是,把它的入參函數(這裏指更新Rrouter的state.location的函數)會傳入到appendListener中。
執行appendListener後,appendListener將這個入參函數推到listeners這個數組中,保存起來。而後返回一個函數用來刪除掉推動該數組的那個函數,以此來實現取消監聽的功能。
因此當咱們使用push,切換路由時,它會執行notifyListeners並傳入更新的location。
而後就是遍歷listeners,執行咱們在listen傳入的回調,此時就是最終的去更新Router的location的過程了。
後面的流程,簡單說下,Router裏面的Route組件經過匹配pathname 和 更新的location ,來決定是否渲染該頁面組件,到此整個的路由跳轉的過程就結束了。
第一次閱讀源碼,儘管刪減了不少,但仍是寫了很多。
但願你們能夠沿着這個思路,本身也去看看,仍是有不少細節值得推敲的。
最後,覺的有幫助,但願給個贊。