如今的前端應用不少都是單頁應用。路由對於單頁應用來講,是一個重要的組成部分。本系列文章將講解前端路由的實現原理。這是系列文章的第三篇:React-Router源碼解析。javascript
前兩篇文章在這裏:
本文不會再介紹路由的基本原理,而是會結合React-Router的源碼,探索一下路由和React是如何結合的。
本文所用的React-Router的版本爲:5.1.2,react版本爲:16.12.0react
咱們用官方最簡單的代碼示例來分析一下React-Router的源碼。web
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; export default function App() { return ( <Router> <div> <nav> <ul> <li> <Link to="/">Home</Link> </li> <li> <Link to="/about">About</Link> </li> <li> <Link to="/users">Users</Link> </li> </ul> </nav> {/* A <Switch> looks through its children <Route>s and renders the first one that matches the current URL. */} <Switch> <Route path="/about"> <About /> </Route> <Route path="/users"> <Users /> </Route> <Route path="/"> <Home /> </Route> </Switch> </div> </Router> ); } function Home() { return <h2>Home</h2>; } function About() { return <h2>About</h2>; } function Users() { return <h2>Users</h2>; }
下面依次對 BrowserRouter、Switch、Route、Link進行解析。api
BrowserRouter很簡單:react-router
import { Router } from 'react-router'; ... _this.history = createBrowserHistory(_this.props); ... _proto.render = function render() { return React.createElement(Router, { history: this.history, children: this.props.children }); };
其中,Router來自react-router這個庫,是react路由的核心組件,其內部維護了一個state。當頁面的路由發生變化時,會更新state的location值,從而觸發react的re-render。框架
/* interface Location<S = LocationState> { pathname: Pathname; search: Search; state: S; hash: Hash; key?: LocationKey; } */ _this.state = { location: props.history.location }
props.history,是createBrowserHistory這個函數生成的,createBrowserHistory是來自history這個package,返回了一個對象:dom
// 請記住這個history var history = { length: globalHistory.length, action: 'POP', location: initialLocation, createHref: createHref, push: push, replace: replace, go: go, goBack: goBack, goForward: goForward, block: block, listen: listen }; return history
Router組件渲染時,會將上述的方法、對象,傳遞給須要的childern組件。傳遞是經過 context
完成的。Router會在childern外包一層 Router.Provider,來提供history對象等信息。ide
// 這裏的 context.Provider 是一個組件。使用的是 mini-create-react-context 這個 package. // 參考 React.createContext _proto.render = function render() { return React.createElement(context.Provider, { children: this.props.children || null, value: { history: this.props.history, location: this.state.location, match: Router.computeRootMatch(this.state.location.pathname), staticContext: this.props.staticContext } }); };
... _proto.render = function render() { var _this = this; return React.createElement(context.Consumer, null, function (context) { !context ? process.env.NODE_ENV !== "production" ? invariant(false, "You should not use <Switch> outside a <Router>") : invariant(false) : void 0; var location = _this.props.location || context.location; var element, match; // We use React.Children.forEach instead of React.Children.toArray().find() React.Children.forEach(_this.props.children, function (child) { if (match == null && React.isValidElement(child)) { element = child; var path = child.props.path || child.props.from; match = path ? matchPath(location.pathname, _extends({}, child.props, { path: path })) : context.match; } }); return match ? React.cloneElement(element, { location: location, computedMatch: match }) : null; }); };
Switch 會找到第一個匹配當前路由的Route,來進行渲染。Switch會在childern外面包一層Router.Comsumer。這個是爲了經過context,拿到外層Router組件傳遞的history對象相關信息傳遞給Route組件。
Route組件的邏輯也很好理解,首先是經過Router.Consumer拿到history對象等相關信息,通過一些處理後,在children外面包一層Router.Provider, 而後渲染children。之因此要包一層,我理解是爲了供children中的Link組件等消費。來看代碼:
... proto.render = function render() { var _this = this; return React.createElement(context.Consumer, null, function (context$1) { !context$1 ? process.env.NODE_ENV !== "production" ? invariant(false, "You should not use <Route> outside a <Router>") : invariant(false) : void 0; var location = _this.props.location || context$1.location; var match = _this.props.computedMatch ? _this.props.computedMatch // <Switch> already computed the match for us : _this.props.path ? matchPath(location.pathname, _this.props) : context$1.match; var props = _extends({}, context$1, { location: location, match: match }); var _this$props = _this.props, children = _this$props.children, component = _this$props.component, render = _this$props.render; // Preact uses an empty array as children by // default, so use null if that's the case. if (Array.isArray(children) && children.length === 0) { children = null; } return React.createElement(context.Provider, { value: props }, props.match ? children ? typeof children === "function" ? process.env.NODE_ENV !== "production" ? evalChildrenDev(children, props, _this.props.path) : children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null : typeof children === "function" ? process.env.NODE_ENV !== "production" ? evalChildrenDev(children, props, _this.props.path) : children(props) : null); }); ...
Link組件最終會render一個包裹着Router.Consumer的LinkAnchor組件。Router.Consumer是爲了獲取外層Router組件的history對象等信息,LinkAnchor綁定了特殊的點擊跳轉邏輯的標籤。這裏先不展開。
初次渲染後,示例代碼會造成下面這樣的組件樹:
分析了這麼久,能夠發現 :react-router-dom 主要的邏輯是處理history對象在整個react應用中的傳遞以及children的渲染等。history對象的傳遞是經過context完成的。history對象是由history 這個package中的createBrowserHistory函數生成的。而真正處理路由的核心邏輯, 是在 history 這個package中。
下面,咱們來看看點擊Link後,會發生什麼?
點擊link後,最終會調用到history對象中的方法來進行路由的切換。
function navigate() { var location = resolveToLocation(to, context.location); // 這裏的history,就是 createBrowserHistory 方法生成,而且在react組件樹中傳遞的對象 var method = replace ? history.replace : history.push; method(location); }
下面,進入history中的邏輯:
var href = createHref(location); var key = location.key, state = location.state; if (canUseHistory) { // 調用window.history.pushState globalHistory.pushState({ key: key, state: state }, null, href); if (forceRefresh) { window.location.href = href; } else { var prevIndex = allKeys.indexOf(history.location.key); var nextKeys = allKeys.slice(0, prevIndex + 1); nextKeys.push(location.key); allKeys = nextKeys; // 更新狀態。注意,這裏的setState可不是react中的setState setState({ action: action, location: location }); } }
咱們來看下history中的setState幹了什麼:
function setState(nextState) { // 更新history對象 _extends(history, nextState); history.length = globalHistory.length; // 通知訂閱者,history已更新。控制react組件從新渲染的關鍵就在這裏 transitionManager.notifyListeners(history.location, history.action); }
其實,在Router組件初始化的時候,就監聽了history的更新,下面是Router組件的代碼:
... if (!props.staticContext) { // 這裏的history.listen是history對象提供的方法,用於監聽頁面history的更新。 _this.unlisten = props.history.listen(function (location) { if (_this._isMounted) { _this.setState({ location: location }); } else { _this._pendingLocation = location; } }); } ...
能夠看到,Router組件監聽了history的更新。當頁面的history更新時,會調用Router組件的setState,從而完成頁面的re-render。
hashHistory的邏輯與BrowserHistory的邏輯相似,本文就再也不繼續展開了,
到這裏,能夠簡單總結一下,整個react-router的實現思路是:
使用一個第三方的、框架無關的history對象來控制頁面的路由變化邏輯。在react側,使用context來傳遞history對象,保證路由組件中能夠訪問到history對象、方便控制路由,而且將history對象與業務組件隔離。使用發佈訂閱模式,解耦了頁面路由的更新與react的更新。
在咱們本身的業務組件中,沒法直接訪問到history對象。若是想直接訪問到history對象,可使用withRouter這個HOC。
前端路由系列文章算是告一段落了。本系列文章從最基本的路由原理講起,到框架的路由實現結束,還算符合預期。不過路由中還涉及到很多的知識點 以及一些高級的功能(好比 keep-alive),值得繼續研究。