本系列将会根据一个简单的项目来学习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......