本系列將會根據一個簡單的項目來學習React-Router 源碼,要到達的目的有以下:node
能夠下載項目源碼,並按照以下步驟,將項目運行起來react
git clone git@github.com:bluebrid/react-router-learing.git
git
npm i
github
npm start
npm
運行的項目只是一個簡單的React-Router 項目。bash
咱們經過應用,一步步去解析React-Router 源代碼.react-router
在咱們clone 項目後,咱們先找到入口文件, 也就是src/index 文件app
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>
, document.getElementById('root'));
複製代碼
其實很簡單,就是React 的入口文件寫法,ReactDOM.render 去Render 整個頁面,可是咱們發現一個問題,Render 的根組件是一個BrowserRouter 組件,查看其import 路徑import { BrowserRouter } from './react-router/packages/react-router-dom/modules';
, 發現其實React-Router 的一個組件,也就是咱們學習React-Router 的入口文件,下面咱們就來分析這個組件。dom
查看源碼,發現這個組件,只是從新render 了Router組件, 可是傳了一個history 的props. history 是一個獨立的第三方庫,是實現路由的一個關鍵所在,咱們後續會深刻分析它.函數
import { createBrowserHistory as createHistory } from "../../../../history/modules";
複製代碼
history = createHistory(this.props);
複製代碼
render() {
return <Router history={this.history} children={this.props.children} />;
}
複製代碼
由於React-Router 是基於history這個庫,來實現對路由變化的監聽,因此咱們先對這個庫進行簡單的分析.
import createBrowserHistory from "./createBrowserHistory";
import createHashHistory from "./createHashHistory";
import createMemoryHistory from "./createMemoryHistory";
import { createLocation, locationsAreEqual } from "./LocationUtils";
import { parsePath, createPath } from "./PathUtils";
export {
createBrowserHistory,
createHashHistory,
createMemoryHistory,
createLocation,
locationsAreEqual,
parsePath,
createPath
}
複製代碼
咱們上一節分析BrowserRouter , 其history 引用的是 createBrowserHistory 方法,因此咱們接下來主要分析這個方法.
若是咱們用VS Code 打開源碼,咱們能夠用Ctrl + k
Ctrl + 0(數字0)
組合鍵,咱們能夠查看這個文件源碼結構. 這個文件暴露出了一個對象, 也就是咱們能夠用的方法:
const history = {
length: globalHistory.length,
action: "POP",
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
};
複製代碼
咱們接下來咱們會分析其中幾個重要的方法
listen 是一個最主要的方法,在Router 組件中有引用,其是實現路由監聽的功能,也就是觀察者 模式.下面咱們來分析這個方法:
const listen = listener => {
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return () => {
checkDOMListeners(-1);
unlisten();
};
};
複製代碼
其中checkDOMListeners 方法,是真正實現了路由切換的事件監聽:
//註冊路由監聽事件
const checkDOMListeners = delta => {
// debugger
listenerCount += delta;
if (listenerCount === 1) {
window.addEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
window.addEventListener(HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
window.removeEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
window.removeEventListener(HashChangeEvent, handleHashChange);
}
};
複製代碼
其中window 監聽了兩種事件: popstate 和hashchange,這兩個事件都是HTML5中的API,也就是原生的監聽URL變化的事件.
分析事件監聽的回調函數handlePopState ,其最終是聽過setState 來出發路由監聽者,
const setState = nextState => {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
};
複製代碼
其中notifyListeners 會調用全部的listen 的回調函數,從而達到通知監聽路由變化的監聽者
在下面的Router 組件的componentWillMount 生命週期中就調用了history.listen
調用,從而達到當路由變化, 會去調用setState
方法, 從而去Render 對應的路由組件。
render() {
const { children } = this.props;
return children ? React.Children.only(children) : null;
}
複製代碼
很簡單,只是將chiildren 給render 出來
componentWillMount() {
const { children, history } = this.props;
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
});
});
}
複製代碼
在這個方法中,註冊了對history 路由變動的監聽,而且在監聽後去變動狀態
componentWillUnmount() {
this.unlisten();
}
複製代碼
當組件卸載時,註銷監聽.
由此分析,Router最主要的功能就是去註冊監聽history 路由的變動,而後從新render 組件。
分析到此,咱們發現跟React-Router已經斷開了聯繫,由於後面全部的事情都是去render children, 咱們接下來繼續返回到index.js文件中:
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>
, document.getElementById('root'));
複製代碼
咱們發現children 就是<App/>
組件了,咱們去查看APP的代碼,仍是先查看render:
class App extends Component {
render() {
return (
<div>
<nav className="navbar navbar">
<ul className="nav navbar-nav">
<li><Link to="/">Homes</Link></li>
<li><Link to="/category">Category</Link></li>
<li><Link to="/products">Products</Link></li>
<li><Link to="/admin">Admin area</Link></li>
</ul>
</nav>
<Switch>
<Route path="/login" render={(props) => <Login {...props} />} />
<Route exact path="/" component={Home}/>
<Route path="/category" component={Category}/>
<PrivateRoute path='/admin' component = {Admin} />
<Route path="/products" component={Products}/>
</Switch>
</div>
);
}
}
複製代碼
很是顯眼的是:Switch 組件,經查看是React-Router 的一個組件,咱們接下來就分析Switch組件
render() {
const { route } = this.context.router;
const { children } = this.props;
const location = this.props.location || route.location;
let match, child;
React.Children.forEach(children, element => {
if (match == null && React.isValidElement(element)) {
const {
path: pathProp,
exact,
strict,
sensitive,
from
} = element.props;
const path = pathProp || from;
child = element;
match = matchPath(
location.pathname,
{ path, exact, strict, sensitive },
route.match
);
}
});
return match
? React.cloneElement(child, { location, computedMatch: match })
: null;
}
複製代碼
其中最明顯的一塊代碼就是: React.Children.forEach , 去遍歷Switch 下面的全部的children, 而後根據path 去匹配對應的children, 而後將匹配到的children render 出來。
而Switch 的全部的Children 是一個Route 組件,咱們接下來就要分析這個組件的源代碼
Switch 的主要功能就是根據path 匹配上對應的children, 而後去Render 一個元素React.cloneElement(child, { location, computedMatch: match })
從app.js 中,發現 Route 使用方式是<Route exact path="/" component={Home}/>
render() {
const { match } = this.state;
const { children, component, render } = this.props;
const { history, route, staticContext } = this.context.router;
const location = this.props.location || route.location;
const props = { match, location, history, staticContext };
if (component) return match ? React.createElement(component, props) : null;
if (render) return match ? render(props) : null;
if (typeof children === "function") return children(props);
if (children && !isEmptyChildren(children))
return React.Children.only(children);
return null;
}
複製代碼
從render 方法能夠知道,其中有三個重要props, 決定了怎麼去render 一個路由。
在render組件的時候,都會將props 傳遞給子組件
props = {match, location, history, staticContext} 這些屬性在組件中會有很大的用途
從上面的代碼能夠發現Route的使用方式有四種:
<Route exact path="/" component={Home}/>
直接傳遞一個組件<Route path="/login" render={(props) => <Login {...props} />} />
使用render 方法<Route path="/category"> <Category/><Route/>
<Route path="/category" children={(props) => <Category {...props} />} />
跟render 使用方式同樣上面咱們已經分析了render 方法,咱們如今須要分析props, 由於理解了render 方法,也就是知道這個組件的實現原理, 理解了props, 就會理解這個組件能夠傳遞哪些屬性,從而能夠達到更好的使用Route組件.
static propTypes = {
computedMatch: PropTypes.object, // private, from <Switch>
path: PropTypes.string,
exact: PropTypes.bool,
strict: PropTypes.bool,
sensitive: PropTypes.bool,
component: PropTypes.func,
render: PropTypes.func,
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
location: PropTypes.object
};
複製代碼
下面咱們一一來分析這些屬性的用途:
上面咱們已經分析了咱們使用過的四個props, 咱們接下來分析咱們沒有使用過的幾個props,可是其實在特殊環境中是頗有做用:
<Switch>
<Route path="/login" render={(props) => <Login {...props} />} />
<Route path="/" component={Home}/>
<Route path="/category" children={(props) => <Category {...props} />} />
<PrivateRoute path='/admin' component = {Admin} />
<Route path="/products" component={Products}/>
</Switch>
複製代碼
上面"/" 路徑會匹配全部的路徑若是:/login /category ...., 可是咱們須要"/" 只匹配 Home , 咱們須要變動如:<Route exact path="/" component={Home}/>
<Route strict path="/one/" component={About}/>
只會匹配/one/ 或者/one/two 不會匹配 /one<Route sensitive path="/one" component={About}/>
只會匹配/one 不會匹配/One or /ONeRoute 組件最主要的功能,是匹配URL 而後render出組件, 其中匹配的邏輯是是其核心的功能,咱們後續會分析其匹配的過程,源碼.
TODO......