React-Router源碼解讀

原因前端

目前負責的項目中有一個微信網頁,用的是react技術棧。在該項目中增長了一個微信分享功能後,線上ios出現了問題,經排查,定位到了react的路由系統。 react

此次線上bug,讓我決定,先拿react-router-dom開刀,看看它內部到底幹了點啥,以解心頭之恨。ios

前端目前用到的就是react-router-dom這個庫,它提供了兩個高級路由器,分別是BrowserRouterHashRouter,它兩的區別就是一個用的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上。網絡

1.先從一個簡單demo開始

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

2. BrowersRouter

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有什麼區別?讓咱們接着往下走。

3. Router

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更新等動做。

4. histroy庫

在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方法

這兩個方法主要用的是pushStatereplaceState這兩個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 ,來決定是否渲染該頁面組件,到此整個的路由跳轉的過程就結束了。

總結

第一次閱讀源碼,儘管刪減了不少,但仍是寫了很多。

但願你們能夠沿着這個思路,本身也去看看,仍是有不少細節值得推敲的。

最後,覺的有幫助,但願給個贊。

相關文章
相關標籤/搜索