聊聊BrowserRouter的内部实现
最近开始翻源码,我们都知道react-router有两种模式,一种是BrowserRouter、一种是HashRouter。所以今天主要聊聊 BrowserRouter 的内部实现。
入口
BrowserRouter
将会监听 URL 的变化,当 URL 变更时,它将使浏览器显示相应的页面。
- BrowserRouter本身是一个类组件,还是一个高阶组件,在内部创建一个全局的 history 对象(可以监听整个路由的变化),并将 history 作为 props 传递给 react-router 的 Router 组件(Router 组件再会将这个 history 的属性作为 context 传递给子组件)
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
export default BrowserRouter;
- 接受一个props,具体入参见 BrowserRouter props文档 。
<BrowserRouter basename="/calendar">
<Link to="/today"/> // renders <a href="/calendar/today">
<Link to="/tomorrow"/> // renders <a href="/calendar/tomorrow">
</BrowserRouter>
<BrowserRouter
getUserConfirmation={(message, callback) => {
// this is the default behavior
const allowTransition = window.confirm(message);
callback(allowTransition);
<BrowserRouter forceRefresh={true} />
<BrowserRouter keyLength={12} />
结合上述示例我们可以看出Router所接受的props主要就是history、children。
createHistory 创建history对象
那么接下来我们主要关注的是history是如何创建的。
import { createBrowserHistory as createHistory } from "history";
createHistory方法来自history库的 createBrowserHistory 方法。方法。
这里顺便讲述下history、react-router、react-router-dom三者之间关系。
- react-router: 是底层核心库,里面封装了Router,Route,Switch等核心组件,实现了从路由的改变到组件的更新的核心功能
- react-router-dom: 在react-router的核心基础上,添加了用于跳转的Link、NavLink组件,和histoy模式下的BrowserRouter和hash模式下的HashRouter组件等。所谓BrowserRouter和HashRouter,也只不过用了history库中createBrowserHistory和createHashHistory方法
- history:是整个路由原理的核心,里面集成了各类history等底层路由实现的原理方法
这里有挺多细节可以慢慢看, 看源码建议先看入参、反参、知道大致流程,再深挖细节。
function createBrowserHistory(props = {}) {
// props来自BrowserRouter组件 this.props
const history = {
// window.history属性长度
length: globalHistory.length,
// history 当前行为(包含PUSH-进入、POP-弹出、REPLACE-替换)
action: 'POP',
// location对象(与地址有关)
location: initialLocation,
// 当前地址(包含pathname)
createHref,
// 跳转的方法
push,
replace,
goBack,
goForward,
// 截取
block,
// 监听
listen
// 返回一个history对象
return history;
export default createBrowserHistory;
createBrowserHistory方法的目的就是:根据HTML5 history API来创建一个history对象。
判断当前环境、是否可以操作DOM
我们都知道react-router根据不同平台拆分为了不同包,react-router-dom、react-router-native。
我们这里指的是浏览器端环境,所有createBrowserHistory方法内需要先判断当前是否是浏览器环境,是否可以操作DOM。
function createBrowserHistory(props = {}) {
invariant(canUseDOM, 'Browser history needs a DOM');
export const canUseDOM = !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
判断是否支持HTML5 history API
function createBrowserHistory(props = {}) {
invariant(canUseDOM, 'Browser history needs a DOM');
// 挂载history对象
const globalHistory = window.history;
// 判断是否支持H5 history
const canUseHistory = supportsHistory();
* Returns true if the HTML5 history API is supported. Taken from Modernizr.
* https://github.com/Modernizr/Modernizr/blob/master/LICENSE
* https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js
* changed to avoid false negatives for Windows Phones: https://github.com/reactjs/react-router/issues/586
export function supportsHistory() {
const ua = window.navigator.userAgent;
// 通过userAgent排除一部分终端
if (
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1
return false;
// 根据API判断
return window.history && 'pushState' in window.history;
另外这里也建议大家养成良好习惯,边界判断逻辑要封装好、并且置于主函数作用域顶部执行。
接下来处理basename
function createBrowserHistory(props = {}) {
invariant(canUseDOM, 'Browser history needs a DOM');
// 挂载history对象
const globalHistory = window.history;
// 判断是否支持H5 history
const canUseHistory = supportsHistory();
const needsHashChangeListener = !supportsPopStateOnHashChange();
// 从props取出对应值、并且提供一些默认值
const {
forceRefresh = false,
getUserConfirmation = getConfirmation,
keyLength = 6
} = props;
// 对basename处理 斜杠/的位置
const basename = props.basename
? stripTrailingSlash(addLeadingSlash(props.basename))
: '';
* Returns true if browser fires popstate on hash change.
* IE10 and IE11 do not.
export function supportsPopStateOnHashChange() {
return window.navigator.userAgent.indexOf('Trident') === -1;
// 处理basename使其最终形成的path是带有`/${basename}`
export function addLeadingSlash(path) {
return path.charAt(0) === '/' ? path : '/' + path;
// 去除path尾部多余的"/"
export function stripTrailingSlash(path) {
return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;
createTransitionManager
创建一个路由切换的管理器。返回一个集成对象,对象中包含了关于history地址或者对象改变时候的监听函数。
function createTransitionManager() {
// 把用户设置的提示信息存储在prompt变量
let prompt = null;
// 用于设置url跳转时弹出的文字提示
function setPrompt(nextPrompt) {
// 提示prompt只能存在一个
warning(prompt == null, 'A history supports only one prompt at a time');
prompt = nextPrompt;
// 解除
return () => {
if (prompt === nextPrompt) prompt = null;
function confirmTransitionTo(
location,
action,
getUserConfirmation,
callback
// TODO: If another transition starts while we're still confirming
// the previous one, we may end up in a weird state. Figure out the
// best way to handle this.
if (prompt != null) {
// prompt 可以是一个函数,如果是一个函数返回执行的结果
const result =
typeof prompt === 'function' ? prompt(location, action) : prompt;
if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
// 默认 调用window.confirm来显示提示信息
// callback接收用户 选择了true或者false
getUserConfirmation(result, callback);
} else {
// 提示开发者 getUserConfirmatio应该是一个function来展示阻止路由跳转的提示
warning(
false,
'A history needs a getUserConfirmation function in order to use a prompt message'
// 相当于用户选择true 不进行拦截
callback(true);
} else {
// Return false from a transition hook to cancel the transition.
callback(result !== false);
} else {
// 当不存在prompt时,直接执行回调函数,进行路由的切换和rerender
callback(true);
// 被subscribe的列表,即在Router组件添加的setState方法,每次push replace 或者 go等操作都会触发
let listeners = [];
// 将回调函数添加到listeners,一个发布订阅模式
function appendListener(fn) {
let isActive = true;
function listener(...args) {
// 为什么要在这里判断isActive
// 其实是为了避免一种情况,比如注册了多个listeners: a,b,c 但是在a函数中注销了b函数
// 理论上来说b函数应该不能在执行了,但是注销方法里使用的是数组的filter,每次返回的是一个新的listeners引用
// 故每次解绑如果不添加isActive这个开关,那么当前循环还是会执行b的事件。加上isActive后,原始的liteners中
// 的闭包b函数的isActive会变为false,从而阻止事件的执行,当循环结束后,原始的listeners也会被gc回收
if (isActive) fn(...args);
listeners.push(listener);
// 返回的是一个函数
return () => {
isActive = false;
// listeners是未加入之前的队列
listeners = listeners.filter(item => item !== listener);
// 通知被订阅的事件开始执行
function notifyListeners(...args) {
listeners.forEach(listener => listener(...args));
return {
setPrompt,
confirmTransitionTo,
appendListener,
notifyListeners
export default createTransitionManager;
任何对地址栏的更新都会经过confirmTransitionTo 这个方法进行验证。
// getUserConfirmation默认是getConfirmation
export function getConfirmation(message, callback) {
callback(window.confirm(message)); // eslint-disable-line no-alert
上述的边界判断、基础状态处理完之后,我们挨个看看history对象具体的实现逻辑。
const history = {
// 返回一个整数,该整数表示会话历史中元素的数目,包括当前加载的页。
// 例如,在一个新的选项卡加载的一个页面中,这个属性返回1。只读属性
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
goBack,
goForward,
block,
listen
location
看看history.location对象是如何创建的。
const initialLocation = getDOMLocation(getHistoryState());
// 获取原生的histoty.state 只读属性
// 返回在 history 栈顶的 任意 值的拷贝。
// 通过这种方式可以查看 state 值,不必等待 popstate事件发生后再查看。
function getHistoryState() {
try {
return window.history.state || {};
} catch (e) {
// IE 11 sometimes throws when accessing window.history.state
// See https://github.com/ReactTraining/history/pull/289
return {};
关于window.history.state的值默认是null,只有调用了pushState、replaceState才有值。
function getDOMLocation(historyState) {
// 取出key state
const { key, state } = historyState || {};
// 从location获取pathname、search、hash
const { pathname, search, hash } = window.location;
// 拼接除了origin外完整path,
// 类似/search?type=content&q=BrowserRouter
let path = pathname + search + hash;
warning(
!basename || hasBasename(path, basename),
'You are attempting to use a basename on a page whose URL path does not begin ' +
'with the basename. Expected path "' +
path +
'" to begin with "' +
basename +
// 如果basename有传,拼接path带上basename
if (basename) path = stripBasename(path, basename);
// 到了这一步 path 已经是除了origin外完整path
return createLocation(path, state, key);
// 判断path是否在开头包含了前缀prefix
export function hasBasename(path, prefix) {
return new RegExp('^' + prefix + '(\\/|\\?|#|$)', 'i').test(path);
// 处理path已经包括了basename作为前缀的场景
export function stripBasename(path, prefix) {
return hasBasename(path, prefix) ? path.substr(prefix.length) : path;
createLocation
创建真正的location对象。
export function createLocation(path, state, key, currentLocation) {
let location;
// 根据path类型分别处理为一致的接口
if (typeof path === 'string') {
// Two-arg form: push(path, state)
// 解析处理hash search pathname
location = parsePath(path);
location.state = state;
} else {
// path是一个对象时候
// One-arg form: push(location)
location = { ...path };
if (location.pathname === undefined) location.pathname = '';
if (location.search) {
if (location.search.charAt(0) !== '?')
location.search = '?' + location.search;
} else {
location.search = '';
if (location.hash) {
if (location.hash.charAt(0) !== '#') location.hash = '#' + location.hash;
} else {
location.hash = '';
if (state !== undefined && location.state === undefined)
location.state = state;
// 解码pathname
try {
location.pathname = decodeURI(location.pathname);
} catch (e) {
if (e instanceof URIError) {
throw new URIError(
'Pathname "' +
location.pathname +
'" could not be decoded. ' +
'This is likely caused by an invalid percent-encoding.'
} else {
throw e;
// 挂载key
if (key) location.key = key;
// currentLocation指的是window.history.location
if (currentLocation) {
// Resolve incomplete/relative pathname relative to current location.
if (!location.pathname) {
location.pathname = currentLocation.pathname;
} else if (location.pathname.charAt(0) !== '/') {
location.pathname = resolvePathname(
location.pathname,
currentLocation.pathname
} else {
// When there is no prior location and pathname is empty, set it to /
if (!location.pathname) {
location.pathname = '/';
return location;
根据path字符串,生成对象{pathname, search, hash}。
export function parsePath(path) {
let pathname = path || '/';
let search = '';
let hash = '';
// 先拆分hash
const hashIndex = pathname.indexOf('#');
if (hashIndex !== -1) {
hash = pathname.substr(hashIndex);
// 剔除了hash部分
pathname = pathname.substr(0, hashIndex);
// 再拆分search
const searchIndex = pathname.indexOf('?');
if (searchIndex !== -1) {
search = pathname.substr(searchIndex);
// 再次剔除search部分
pathname = pathname.substr(0, searchIndex);
// 返回拆分后的对象
return {
pathname,
search: search === '?' ? '' : search,
hash: hash === '#' ? '' : hash
生成location过程结束,我们对比下history.location与window.location区别。
createHref
createHref
函数的作用是返回当前路径名。
function createHref(location) {
return basename + createPath(location);
// 根据window.location拼接path
export function createPath(location) {
const { pathname, search, hash } = location;
let path = pathname || '/';
if (search && search !== '?')
path += search.charAt(0) === '?' ? search : `?${search}`;
if (hash && hash !== '#') path += hash.charAt(0) === '#' ? hash : `#${hash}`;
return path;
push
function push(path, state) {
warning(
typeof path === 'object' &&
path.state !== undefined &&
state !== undefined
'You should avoid providing a 2nd state argument to push when the 1st ' +
'argument is a location-like object that already has state; it is ignored'
// 动作标示位
const action = 'PUSH';
// 创建一个location对象
const location = createLocation(path, state, createKey(), history.location);
// 进行路由切换
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
// ok 为 true表示不拦截
if (!ok) return;
const href = createHref(location);
const { key, state } = location;
if (canUseHistory) {
// 调用HTML5 history pushState history.pushState(state, title[, url])
// https://developer.mozilla.org/zh-CN/docs/Web/API/History/pushState
// pushState可以改变url但是不会刷新页面、也不会发起请求
globalHistory.pushState({ key, state }, null, href);
if (forceRefresh) {
// 强制刷新页面
window.location.href = href;
} else {
const prevIndex = allKeys.indexOf(history.location.key);
const nextKeys = allKeys.slice(
prevIndex === -1 ? 0 : prevIndex + 1
nextKeys.push(location.key);
allKeys = nextKeys;
setState({ action, location });
} else {
warning(
state === undefined,
'Browser history cannot push state in browsers that do not support HTML5 history'
// 降级方案
window.location.href = href;
// 随机生成路由key
function createKey() {
return Math.random()
.toString(36)
.substr(2, keyLength);
setState({ action, location })作用是根据当前地址信息(location)更新history,以及执行所有的listener。
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
以push为例,path可以是一个对象、或者字符串。
我们一般可以这样用:
push('/pointPartyGiftList?age=2')
push({
pathname: '/anniversaryAddress',
state: {
orderNo,
type: 'MONTHLY_INTEGRAL',
otherParams: {
type: '2'
每次调用push或者replace都会随机生成一个key,代表这个路径的唯一hash值,并将用户传递的state和key作为state,注意这部分state会被保存到 浏览器 中是一个长效的缓存,将拼接好的path作为传递给history的第三个参数,调用history.pushState(state, null, path),这样地址栏的地址就得到了更新。
地址栏地址得到更新后,页面在不使用foreceRefrsh的情况下是不会自动更新的。
replace
function replace(path, state) {
warning(
typeof path === 'object' &&
path.state !== undefined &&
state !== undefined
'You should avoid providing a 2nd state argument to replace when the 1st ' +
'argument is a location-like object that already has state; it is ignored'
const action = 'REPLACE';
const location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const href = createHref(location);
const { key, state } = location;
if (canUseHistory) {
globalHistory.replaceState({ key, state }, null, href);
if (forceRefresh) {
window.location.replace(href);
} else {
const prevIndex = allKeys.indexOf(history.location.key);
if (prevIndex !== -1) allKeys[prevIndex] = location.key;
setState({ action, location });
} else {
warning(
state === undefined,
'Browser history cannot replace state in browsers that do not support HTML5 history'
window.location.replace(href);
go、goBack、goForward
function go(n) {
globalHistory.go(n);
function goBack() {
go(-1);
function goForward() {
go(1);
block、listen
/**
* Returns true if a given popstate event is an extraneous WebKit event.
* Accounts for the fact that Chrome on iOS fires real popstate events
* containing undefined state when pressing the back button.
export function isExtraneousPopstateEvent(event) {
event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1;
// popstate事件的handler
function handlePopState(event) {
// 忽略WebKit中无关的popstate事件
if (isExtraneousPopstateEvent(event)) return;
// getDOMLocation(event.state): 根据event.state构建location
handlePop(getDOMLocation(event.state));
function handleHashChange() {
handlePop(getDOMLocation(getHistoryState()));
function handlePop(location) {
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
const action = 'POP';
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (ok) {
setState({ action, location });
} else {
revertPop(location);
// 记录是否有添加listener
let listenerCount = 0;
// 监听历史条目记录的改变
function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1 && delta === 1) {
// 监听popstate事件
window.addEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
// 监听hash change事件
window.addEventListener(HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
// 移除popstate事件
window.removeEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
// 移除hash change事件
window.removeEventListener(HashChangeEvent, handleHashChange);
let isBlocked = false;
// prompt可以是字符串或者返回字符串的函数
function block(prompt = false) {
const unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
return () => {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
return unblock();
function listen(listener) {
const unlisten = transitionManager.appendListener(listener);
// 开启监听
checkDOMListeners(1);
return () => {
// 卸载监听
checkDOMListeners(-1);
unlisten();
checkDOMListeners的功能主要是:监听 / 移除
"https://www.jianshu.com/p/ddb7fcdf5962">popstate事件
(监听用户在浏览器点击后退、前进,点击a标签锚点,或者在js中调用histroy.back(),history.go(),history.forward()等,但监听不到pushState、replaceState方法)。
needsHashChangeListener
:为了兼容
hash
改变不触发
popstate
事件的浏览器,所以需要额外增加了监听
hashchange
事件。
history.listen主要做两件事情:
- 绑定我们要设置的监听函数listener
- 当历史记录条目发生改变,触发执行对应的监听函数listener
// 监听当前location的更改。
const unlisten = history.listen((location, action) => {
console.log(action, location.pathname, location.state);
// 若要停止监听,请调用listen()返回的函数.
unlisten();
history.block的功能主要是当历史记录条目发生改变时候,触发提示信息。
history.block(function (location, action) {