本文將用盡量容易理解的方式,實現最小可用的 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 項目點個⭐️吧!