本文将用尽量容易理解的方式,实现最小可用的 react-router v4 和 history,目的为了了解 react-router 实现原理。html
本文首发于 daweilv.comreact
在开始阅读本文以前,但愿你至少使用过一次 react-router,知道 react-router 的基本使用方法。git
先来看一段代码,咱们须要实现的逻辑是:当 location.pathname = '/'
时,页面渲染 Index 组件,当 location.path = '/about/'
时,页面渲染 About 组件。github
import React from 'react';
import { BrowserRouter as Router, Route, Link } from "./react-router-dom";
function Index(props) {
console.log('Index props', props);
return <h2>Home</h2>;
}
function About() {
return <h2>About</h2>;
}
function Users() {
return <h2>Users</h2>;
}
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>
<Route path="/" exact component={Index} />
<Route path="/about/" component={About} />
<Route path="/users/" component={Users} />
</div>
</Router>
);
}
export default App;
复制代码
其实,Route 组件内部的核心逻辑就是判断当前 pathname 是否与自身 props 上的 path 相等,若是相等,则渲染自身 props 上的 component,不等的时候不渲染,返回 null。浏览器
好,来看下 Route 的实现:react-router
import React from 'react';
import { RouterContext } from './BrowserRouter';
export default class Route extends React.Component {
render() {
const { path, component } = this.props;
if (this.context.location.pathname !== path) return null;
return React.createElement(component, { ...this.context })
}
}
Route.contextType = RouterContext
复制代码
Route 主要就是一个 render() 函数,内部经过 context 得到当前 pathname。那么这个 context 是哪来的呢?dom
import React from 'react';
import { createBrowserHistory } from '../history';
const history = createBrowserHistory()
export const RouterContext = React.createContext(history)
export default class BrowserRouter extends React.Component {
constructor(props) {
super(props)
this.state = {
location: {
pathname: window.location.pathname
}
}
}
render() {
const { location } = this.state;
return (
<RouterContext.Provider value={{ history, location }}> {this.props.children} </RouterContext.Provider> ) } }; 复制代码
这里仅贴出了首次渲染的逻辑代码。BrowserRouter 在 constructor 中根据 window.location 初始化 location,而后将 location 传入 RouterContext.Provider 组件,子组件 Route 接收到含有 location 的 context,根据 1. Route 的实现
完成首次渲染。ide
注意到传入 RouterContext.Provider 组件的对象不光有 location,还有 history 对象。这个 history 是作什么用的呢?实际上是暴露 history.push 和 history.listen 方法,提供给外部作跳转和监听跳转事件使用的。Link 组件的实现也是用到了 history,咱们接着往下看。函数
import React from 'react';
import { RouterContext } from './BrowserRouter';
export default class Link extends React.Component {
constructor(props) {
super(props)
this.clickHandler = this.clickHandler.bind(this)
}
clickHandler(e) {
console.log('click', this.props.to);
e.preventDefault()
this.context.history.push(this.props.to)
}
render() {
const { to, children } = this.props;
return <a href={to} onClick={this.clickHandler}>{children}</a>
}
}
Link.contextType = RouterContext
复制代码
Link 组件其实就是一个 a 标签,与普通 a 标签不一样,点击 Link 组件并不会刷新整个页面。组件内部把 a 标签的默认行为 preventDefault 了,Link 组件从 context 上拿到 history,将须要跳转的动做告诉 history,即 history.push(to)
。ui
以下面代码所示,BrowserRouter 在 componentDidMount 中,经过 history.listen 监听 location 的变化。当 location 变化的时候,setState 一个新的 location 对象,触发 render,进而触发子组件 Route 的从新渲染,渲染出对应 Route。
// BrowserRouter
componentDidMount() {
history.listen((pathname) => {
console.log('history change', pathname);
this.setState({ location: { pathname } })
})
}
复制代码
history 的内部实现是怎么样的呢?请看下面的代码:
let globalHistory = window.history;
export default function createBrowserHistory() {
let listeners = []
const push = function (pathname) {
globalHistory.pushState({}, '', pathname)
notifyListeners(pathname)
}
const listen = function (listener) {
listeners.push(listener)
}
const notifyListeners = (...args) => {
listeners.forEach(listener => listener(...args))
}
window.onpopstate = function () {
notifyListeners(window.location.pathname)
}
return {
listeners,
listen,
push
}
};
复制代码
history 经过 listen 方法收集外部的监听事件。当外部调用 history.push 方法时,使用 window.history.pushState 修改当前 location,执行 notifyListeners 方法,依次回调全部的监听事件。注:这里为了让代码更加容易理解,简化了 listener this 上下文的处理。
另外,history 内部增长了 window.onpopstate 用来监听浏览器的前进后退事件,执行 notifyListeners 方法。
咱们使用了 100 多行代码,实现了 react-router 的基本功能,对 react-router 有了更深刻的认识。想更加深刻的了解 react-router,建议看一下 react-router 的源码,快速走读一遍,再对比下本文的实现细节,相信你会有一个更清晰的理解。
以为本文帮助到你的话,请给个人 build-your-own-react-router 项目点个⭐️吧!