前言
React Router
中很大程度上地依赖了 history 的功能,如 useNavigate
、useHref
、Router
等都直接或间接地用到了 history,所以我们在分析 React Router 源码之前,有必要深入了解下 history 的用法,相信您看完本篇文章之后,能学到很多之前不知道的东西。写下本篇文章时的 history 版本是 latest 的,为 5.0.1
,那废话不多说,让我们开始探索之旅吧~
history 分类
history 又分为 browserHistory
和 hashHistory
,对应 react-router-dom
中的 BrowserRouter
和 HashRouter
, 这两者的绝大部分都相同,我们先分析下 browserHistory,后面再点出 hashHistory 的一些区别点。
createBrowserHistory
顾名思义,createBrowserHistory 自然是用于创建 browserHistory
的工厂函数,我们先看下类型
export interface BrowserHistory<S extends State = State> extends History<S> {}
export interface History<S extends State = State> {
/**
* @description 上一个修改当前 location的action,有 `POP`、`PUSH`、`REPLACE`,初始创建为POP
*/
readonly action: Action;
/**
* @description 当前location
*/
readonly location: Location<S>;
/**
* @description 返回一个新的href, to为string则返回to,否则返回 `createPath(to)` => pathname + search + hash
*/
createHref(to: To): string;
/**
* @description push一个新的location到历史堆栈,stack的length会+1
*/
push(to: To, state?: S): void;
/**
* @description 将历史堆栈中当前location替换为新的,被替换的将不再存在
*/
replace(to: To, state?: S): void;
/**
* @description 历史堆栈前进或后退delta(可正负)
*/
go(delta: number): void;
/**
* @description 同go(-1)
*/
back(): void;
/**
* @description 同go(1)
*/
forward(): void;
/**
* @description 设置路由切换的监听器,`listener`为函数
*
* @example
*
* const browserHistory = createBrowserHistory()
* browserHistory.push('/user')
* const unListen = browserHistory.listen(({action, location}) => {
* // 切换后新的action和location,上面push后,这里的action为PUSH, location为 { pathname: '/user', ... }
* console.log(action, location)
* })
*/
listen(listener: Listener<S>): () => void;
/**
* @description 改变路由时阻塞路由变化
*/
block(blocker: Blocker<S>): () => void;
}
即是说,createBrowserHistory 最终肯定要返回一个上面形状的 browserHistory 实例,我们先看下函数总体概览(这里大致看下框架就好,后面会具体分析)。
export function createBrowserHistory(
options: BrowserHistoryOptions = {}
): BrowserHistory {
const { window = document.defaultView! } = options;
const globalHistory = window.history;
function getIndexAndLocation(): [number, Location] {}
let blockedPopTx: Transition | null = null;
function handlePop() {}
window.addEventListener(PopStateEventType, handlePop);
let action = Action.Pop;
let [index, location] = getIndexAndLocation();
const listeners = createEvents<Listener>();
const blockers = createEvents<Blocker>();
if (index == null) {
index = 0;
globalHistory.replaceState({ ...globalHistory.state, idx: index }, '');
}
function createHref(to: To) {
return typeof to === 'string' ? to : createPath(to);
}
function getNextLocation(to: To, state: State = null): Location {}
function getHistoryStateAndUrl(
nextLocation: Location,
index: number
): [HistoryState, string] {}
function allowTx(action: Action, location: Location, retry: () => void): boolean {}
function applyTx(nextAction: Action) {}
function push(to: To, state?: State) {}
function replace(to: To, state?: State) {}
function go(delta: number) {
globalHistory.go(delta);
}
const history: BrowserHistory = {
get action() {
return action;
},
get location() {
return location;
},
createHref,
push,
replace,
go,
back() {
go(-1);
},
forward() {
go(1);
},
listen(listener) {
return listeners.push(listener);
},
block(blocker) {
const unblock = blockers.push(blocker);
if (blockers.length === 1) {
window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
}
return function() {
unblock();
if (!blockers.length) {
window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
}
};
}
};
return history;
}
可以看到函数里面又有一些内部函数和函数作用域内的顶层变量,我们先来看下这些顶层变量的作用。
createBrowserHistory 函数作用域内的顶层变量
export function createBrowserHistory(
options: BrowserHistoryOptions = {}
): BrowserHistory {
const { window = document.defaultView! } = options;
const globalHistory = window.history;
let blockedPopTx: Transition | null = null;
let action = Action.Pop;
let [index, location] = getIndexAndLocation();
const listeners = createEvents<Listener>();
const blockers = createEvents<Blocker>();
...
}
window
createBrowserHistory 接收一个 options,默认为空对象,而其中的window
默认为document.defaultView
,即是 Window 对象罢了
history
上面获取到 window,然后会从 window 上获取到 history
blockedPopTx
用于存储下面的 blockers.call 方法的参数,即每一个 blocker 回调函数的参数,类型如下
export interface Transition<S extends State = State> extends Update<S> {
/**
* 被阻塞了后调用retry可以尝试继续跳转到要跳转的路由
*/
retry(): void;
}
export interface Update<S extends State = State> {
/**
* 改变location的action,有POP、PUSH、REPLACE
*/
action: Action;
/**
* 新location
*/
location: Location<S>;
}
index 与 location
index
为当前 location
的索引,即是说,history 会为每个 location 创建一个idx
,放在state
中, 会用于在 handlePop
中计算 delta
,这里稍微提下,后面分析 handlePop 如何阻止路由变化会讲到
// 初始调用
const [index, location] = getIndexAndLocation();
// handlePop中
const [nextIndex, nextLocation] = getIndexAndLocation();
const delta = index - nextIndex;
go(delta)
action
blockers
、listeners
的回调会用到 action
,其是通过 handlePop
、 push
、replace
三个函数修改其状态,分别为 POP
、PUSH
和REPLACE
,这样我们就可以通过判断 action 的值来做出不同的判断了。
listeners 与 blokers
我们先看下 创建 listeners 与 blokers 的工厂函数 createEvents
function createEvents<F extends Function>(): Events<F> {
let handlers: F[] = [];
return {
get length() {
return handlers.length;
},
push(fn: F) {
// 其实就是一个观察者模式,push后返回unsubscribe
handlers.push(fn);
return function() {
handlers = handlers.filter(handler => handler !== fn);
};
},
call(arg) {
// 消费所有handle
handlers.forEach(fn => fn && fn(arg));
}
};
}
其返回了一个对象,通过 push
添加每个 listener,通过 call
通知每个 listener,代码中叫做 handler
。
listeners 通过 call 传入 { action, location }
, 这样每个 listener 在路由变化时就能接收到,从而做出对应的判断。
listener 类型如下
export interface Update<S extends State = State> {
action: Action;
location: Location<S>;
}
export interface Listener<S extends State = State> {
(update: Update<S>): void;
}
blockers 通过 call 传入 { action, location, retry }
,比listeners多了一个 retry
,从而判断是否要阻塞路由,不阻塞的话需要调用函数 retry
。
blocker 类型如下
export interface Transition<S extends State = State> extends Update<S> {
retry(): void;
}
export interface Blocker<S extends State = State> {
(tx: Transition<S>): void;
}
知道了顶层变量的作用,那我们接下来一一分析下返回 history
实例对象的每个属性。
action 与 location
const history: BrowserHistory = {
get action() {
return action;
},
get location() {
return location;
},
...
}
这两个属性都通过 修饰符 get
,那么我们每次要获取最新的 action 或 location,就可以通过 history.action
或 history.location
。
避免了只能拿到第一次创建的值,如
const history: BrowserHistory = {
action,
location,
}
或需要每次调用函数才能拿到:
const history: BrowserHistory = {
action: () => action,
location: () => location,
}
action 我们上面已经分析了,这里我们看下获取 location 的函数。
getIndexAndLocation
即获取当前索引和 location
function getIndexAndLocation(): [number, Location] {
const { pathname, search, hash } = window.location;
const state = globalHistory.state || {};
return [
state.idx,
readOnly<Location>({
pathname,
search,
hash,
state: state.usr || null,
key: state.key || 'default'
})
];
}
...
// createBrowserHistory创建的时候获取初始当前路径index和location
let [index, location] = getIndexAndLocation();
createBrowserHistory 调用的时候会获取初始当前路径 index 和 location,这个时候的 index 肯定是 undefined(请注意要打开新页面才会,否则刷新后还是有历史堆栈,导致 state.idx 有值,即 index 不为空)
所以下面会通过判断 index 是否为空,空的话会给个默认值 0
if (index == null) {
// 初始index为空,那么给个0
index = 0;
// 这里replaceState后,history.state.idx就为0了
globalHistory.replaceState({ ...globalHistory.state, idx: index }, '');
}
通过 replaceState
初始重置了历史堆栈,从而就能获取到 state 中的 idx 了。
这个时候我们再通过 history.state.idx 就能获取到
history.createHref
history.createHref
来自 createBrowserHistory 的内部函数,接收一个 To
类型的参数,返回值为字符串 href
type To = string | Partial<Path>;
interface Path {
pathname: string;
search: string;
hash: string;
}
function createHref(to: To) {
return typeof to === 'string' ? to : createPath(to);
}
如果 to 不为字符串,会通过 createPath
函数转为字符串,即是把 pathname、search 和 hash 拼接起来罢了
export function createPath({
pathname = '/',
search = '',
hash = ''
}: PartialPath) {
return pathname + search + hash;
}
history.push
function push(to: To, state?: State) {
const nextAction = Action.Push;
const nextLocation = getNextLocation(to, state);
// 跳过,后面blockers会讲到
function retry() {
push(to, state);
}
// 跳过,后面blockers会讲到,这里我们先默认为true
if (allowTx(nextAction, nextLocation, retry)) {
const [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
// try...catch because iOS limits us to 100 pushState calls :/
// 用try catch的原因是因为ios限制了100次pushState的调用
try {
globalHistory.pushState(historyState, '', url);
} catch (error) {
// They are going to lose state here, but there is no real
// way to warn them about it since the page will refresh...
window.location.assign(url);
}
// 跳过,后面listeners会讲到
applyTx(nextAction);
}
}
首先会通过 getNextLocation,根据 to
和 state
获取到新的 location,注意这时候路由还没切换
const nextLocation = getNextLocation(to, state);
/**
* @description 获取新的Location
* @param to 新的path
* @param state 状态
*/
function getNextLocation(to: To, state: State = null): Location {
return readOnly<Location>({
...location,
...(typeof to === 'string' ? parsePath(to) : to),
state,
key: createKey()
});
}
如果 to 是字符串的话,会通过parsePath
解析对应的 pathname、search、hash(三者都是可选的,不一定会出现在返回的对象中)
/**
* @example
* parsePath('https://juejin.cn/post/7005725282363506701?utm_source=gold_browser_extension#heading-2')
* {
* "hash": "#heading-2",
* "search": "?utm_source=gold_browser_extension",
* "pathname": "https://juejin.cn/post/7005725282363506701"
* }
* 从结果可看到,去掉 `hash` 、 `search` 就是 `pathname` 了
*
* parsePath('?utm_source=gold_browser_extension#heading-2')
* {
* "hash": "#heading-2",
* "search": "?utm_source=gold_browser_extension",
* }
* parsePath('') => {}
* 而如果只有search和hash,那么parse完也没有pathname,这里要特别注意
*
* @see https://github.com/ReactTraining/history/tree/master/docs/api-reference.md#parsepath
*/
export function parsePath(path: string) {
const partialPath: PartialPath = {};
if (path) {
const hashIndex = path.indexOf('#');
if (hashIndex >= 0) {
partialPath.hash = path.substr(hashIndex);
path = path.substr(0, hashIndex);
}
const searchIndex = path.indexOf('?');
if (searchIndex >= 0) {
partialPath.search = path.substr(searchIndex);
path = path.substr(0, searchIndex);
}
if (path) {
partialPath.pathname = path;
}
}
return partialPath;
}
再根据新的 location 获取新的 state 和 url,而因为是 push,所以这里的 index 自然是加一
const [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
/** 获取state和url */
function getHistoryStateAndUrl(
nextLocation: Location,
index: number
): [HistoryState, string] {
return [
{
usr: nextLocation.state,
key: nextLocation.key,
idx: index
},
createHref(nextLocation)
];
}
最后调用history.pushState
成功跳转页面,这个时候路由也就切换了
globalHistory.pushState(historyState, '', url);
history.replace
replace
和 push 类似,区别只是 index 不变以及调用 replaceState
function replace(to: To, state?: State) {
const nextAction = Action.Replace;
const nextLocation = getNextLocation(to, state);
// 跳过,后面blockers会讲到
function retry() {
replace(to, state);
}
if (allowTx(nextAction, nextLocation, retry)) {
const [historyState, url] = getHistoryStateAndUrl(nextLocation, index);
globalHistory.replaceState(historyState, '', url);
// 跳过,后面listeners会讲到
applyTx(nextAction);
}
}
history.go、history.back、history.forward
用于历史堆栈的前进后退,back
和 forward
分别是 是 go(-1)
和 go(1)
,delta 可正负,代表前进后退
function go(delta: number) {
globalHistory.go(delta);
}
history.listen
const history: HashHistory = {
...
listen(listener) {
return listeners.push(listener);
},
...
}
history.listen
可以往 history 中添加 listener,返回值是 unListen,即取消监听。这样每当成功切换路由,就会调用 applyTx(nextAction);
来通知每个 listener,applyTx(nextAction);
在 push
、 replace
和 handlePop
三个函数中成功切换路由后调用。
function push(to: To, state?: State) {
...
// 跳过,后面blockers会讲到,这里我们先默认为true
if (allowTx(nextAction, nextLocation, retry)) {
...
// 下面会讲到
applyTx(nextAction);
}
}
function replace(to: To, state?: State) {
...
if (allowTx(nextAction, nextLocation, retry)) {
...
// 下面会讲到
applyTx(nextAction);
}
}
function handlePop() {
if (blockedPopTx) {
...
} else {
...
if (blockers.length) {
...
} else {
// // 下面会讲到
applyTx(nextAction);
}
}
}
function applyTx(nextAction: Action) {
action = nextAction;
// 获取当前index和location
[index, location] = getIndexAndLocation();
listeners.call({ action, location });
}
即只要满足 allowTx
返回 true(push 和 replace 函数中) 或没有 blocker(handlePop 函数中) 就能通知每个 listener。那我们看下 allowTx
function allowTx(action: Action, location: Location, retry: () => void): boolean {
return (
!blockers.length || (blockers.call({ action, location, retry }), false)
);
}
allowTx
的作用是判断是否允许路由切换,有 blockers 就不允许,逻辑如下:
- blockers 不为空,那么通知每个 blocker,然后返回 false
- blockers 为空,返回 true
那么要返回 true 的话就必须满足 blockers 为空,也即是说,listener 能否监听到路由变化,取决于当前页面是否被阻塞了(block)。
history.block
上面我们说有 blocker 就会导致 listener 收不到监听,且无法成功切换路由,那我们看下 block 函数:
const history: BrowserHistory = {
...
block(blocker) {
// push后返回unblock,即把该blocker从blockers去掉
const unblock = blockers.push(blocker);
if (blockers.length === 1) {
// beforeunload
// 只在第一次block加上beforeunload事件
window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
}
return function() {
unblock();
// 移除beforeunload事件监听器以便document在pagehide事件中仍可以使用
// Remove the beforeunload listener so the document may
// still be salvageable in the pagehide event.
// See https://html.spec.whatwg.org/#unloading-documents
if (!blockers.length) {
// 移除的时候发现blockers空了那么就移除`beforeunload`事件
window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
}
};
}
};
beforeunload
我们发现添加第一个 blocker 时会添加 beforeunload
事件,也就是说只要 block 了,那么我们刷新、关闭页面,通过修改地址栏 url 后 enter 都会弹窗提示:
刷新会询问重新加载此网站?,而关闭 tab 或修改地址栏后 enter 是提示离开此网站?,这两种要注意区别。
这功能常用在表单提交的页面,避免用户不小心关闭 tab 导致表单数据丢失。
当然如果 unblock 时发现 blockers 为空就会移除 beforeunload
事件了。
history 如何阻止路由切换
说完上面的beforeunload
事件,我们关注下上面跳过的 block 方面的代码
对于 push
和 replace
,其中都会有一个 retry
和 allowTx
,这里我们再看下
function retry() {
push(to, state);
}
if (allowTx(nextAction, nextLocation, retry)) {
globalHistory.pushState(historyState, '', url);
// or
globalHistory.replaceState(historyState, '', url);
}
function allowTx(action: Action, location: Location, retry: () => void): boolean {
return (
!blockers.length || (blockers.call({ action, location, retry }), false)
);
}
如果我们通过 block 添加了一个 blocker,那么每次 push 或 replace 都会判断到 blocker.length 不为 0,那么就会传入对应的参数通知每个 blocker,之后会返回 false,从而无法进入条件,导致无法触发 pushState
或 replaceState
,所以点击 Link 或调用 navigate 无法切换路由。
还有另外一个是在 handlePop 中,其在一开始调用 createBrowserHistory 的时候就往 window 上添加监听事件:
// PopStateEventType = 'popstate'
window.addEventListener(PopStateEventType, handlePop);
只要添加了该事件,那我们只要点击浏览器的前进、后退按钮、在 js 代码中调用 history.back()、history.forward()、history.go 方法,点击 a 标签都会触发该事件。
比如我们在 useEffect 中添加一个 blocker(详细代码可查看blocker) ,这段代码的意思是只要触发了上面的行为,那么第一和第二次都会弹窗提示,等到第三次才会调用 retry 成功切换路由
const countRef = useRef(0)
const { navigator } = useContext(UNSAFE_NavigationContext)
useEffect(() => {
const unblock = navigator.block((tx) => {
// block两次后调用retry和取消block
if (countRef.current < 2) {
countRef.current = countRef.current + 1
alert(`再点 ${3 - countRef.current}次就可以切换路由`)
} else {
unblock();
tx.retry()
}
})
}, [navigator])
我们看下 popstate
的回调函数 handlePop:
function handlePop() {
if (blockedPopTx) {
blockers.call(blockedPopTx);
blockedPopTx = null;
} else {
const nextAction = Action.Pop;
const [nextIndex, nextLocation] = getIndexAndLocation();
if (blockers.length) {
if (nextIndex != null) {
const delta = index - nextIndex;
if (delta) {
// Revert the POP
blockedPopTx = {
action: nextAction,
location: nextLocation,
retry() {
go(delta * -1);
}
};
go(delta);
}
} else {
// Trying to POP to a location with no index. We did not create
// this location, so we can't effectively block the navigation.
warning(
false,
// TODO: Write up a doc that explains our blocking strategy in
// detail and link to it here so people can understand better what
// is going on and how to avoid it.
`You are trying to block a POP navigation to a location that was not ` +
`created by the history library. The block will fail silently in ` +
`production, but in general you should do all navigation with the ` +
`history library (instead of using window.history.pushState directly) ` +
`to avoid this situation.`
);
}
} else {
applyTx(nextAction);
}
}
}
这里我们举个?比较容易理解:比如当前 url 为 http://localhost:3000/blocker
,其 index 为 2,我们点击后退(其他能触发popstate的事件都可以),这个时候就立即触发了 handlePop,而此时地址栏的 url 实际上已经变化为http://localhost:3000
了,其获取到的 nextIndex 为 1(注意,这里的 index 只是我们举例用到,实际上不一定是上面的值,下面的 delta 也是)。
而由于有 blocker,所以会进行 blockedPopTx
的赋值,从上面的 index 和 nextIndex 能获取到对应的 delta 为 1,那么 retry 中的 delta * -1
即为-1 了
const delta = index - nextIndex;
retry() {
go(delta * -1)
}
然后继续走到下面的 go(delta)
,由于 delta
是 1,那么又重新回到 http://localhost:3000/blocker
了
注意!!注意!!集中精神了!!此处是真正触发前进后退时保持当前 location 不变的关键所在,也就是说其实 url 已经切换了,但是这里又通过go(delta)
把 url 给切换回去。
还有需要特别注意一点的是,这里调用 go(delta)
后又会触发 handlePop,那么 if (blockedPopTx)
就为 true 了,自然就会调用 blockers.call(blockedPopTx)
,blocer 可以根据 blockedPopTx 的 retry 看是否允许跳转页面,然后再把blockedPopTx = null
。
那么当点击第三次后,由于我们 unblock 后 blockers 为空,且调用了 retry
,即 go(-1)
,这个时候就能成功后退了。
也就是说,我点击后退,此时 url 为/
,触发了handlePop
,第一次给blockedPopTx
赋值,然后go(delta)
又返回了/blocker
,随即又触发了handlePop
,再次进入发现blockedPopTx
有值,将 blockedPopTx 回调给每个 blocker,blocker 函数中 unblock 后调用 retry,即go(delta * -1)
又切回了/
,真是左右横跳啊~
由于 blockers 已经为空,那么 push
、 replace
和 handlePop
中就可以每次都调用 applyTx(nextAction);
,从而成功通知到对应的 listeners,这里透露下,BrowserRouter
、HashRouter
就是通过 history.listen(setState)
收听到每次 location 变化从而 setState 触发 render 的。
export function BrowserRouter({
basename,
children,
window
}: BrowserRouterProps) {
const historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
// 如果为空,则创建
historyRef.current = createBrowserHistory({ window });
}
const history = historyRef.current;
const [state, setState] = React.useState({
action: history.action,
location: history.location
});
React.useLayoutEffect(() => {
/**
* popstate、push、replace时如果没有blokcers的话,会调用applyTx(nextAction)触发这里的setState
* function applyTx(nextAction: Action) {
* action = nextAction;
* // 获取当前index和location
* [index, location] = getIndexAndLocation();
* listeners.call({ action, location });
* }
*/
history.listen(setState)
}, [history]);
// 一般变化的就是action和location
return (
<Router
basename={basename}
children={children}
action={state.action}
location={state.location}
navigator={history}
/>
);
}
这也解释了为何block后路由虽然有切换,但是当前页面没有卸载,就是因为 applyTx(nextAction)
没有执行,导致 BrowserRouter
中没有收到通知。
重新看整个createBrowserHistory
我们上面一一解析了每个函数的作用,下面我们全部合起来再看一下,相信经过上面的分析,再看这整个函数就比较容易理解了
export function createBrowserHistory(
options: BrowserHistoryOptions = {}
): BrowserHistory {
// 默认值是document.defaultView,即浏览器的window
const { window = document.defaultView! } = options;
const globalHistory = window.history;
/** 获取索引和当前location */
function getIndexAndLocation(): [number, Location] {
const { pathname, search, hash } = window.location;
const state = globalHistory.state || {};
return [
state.idx,
readOnly<Location>({
pathname,
search,
hash,
state: state.usr || null,
key: state.key || 'default'
})
];
}
/** 用于存储下面的 blockers.call方法的参数,有 { action,location,retry } */
let blockedPopTx: Transition | null = null;
/** popstate的回调, 点击浏览器 ← 或 → 会触发 */
function handlePop() {
// 第一次进来`blockedPopTx`没有值,然后下面的else判断到有blockers.length就会给`blockedPopTx`赋值,之后判断到if (delta)就会调用go(delta),
// 从而再次触发handlePop,然后这里满足条件进入blockers.call(blockedPopTx)
if (blockedPopTx) {
// 如果参数有值,那么将参数传给blockers中的handlers
blockers.call(blockedPopTx);
// 然后参数置空
blockedPopTx = null;
} else {
// 为空的话,给blockPopTx赋值
// 因为是popstate,那么这里的nextAction就是pop了
const nextAction = Action.Pop;
// 点击浏览器前进或后退后的state.idx和location
// 比如/basic/about的index = 2, 点击后退后就会触发handlePop,后退后的nextLocation.pathname = /basic, nextIndex = 1
const [nextIndex, nextLocation] = getIndexAndLocation();
if (blockers.length) {
if (nextIndex != null) {
// 这里的index是上一次的getIndexAndLocation得到了,下面有
// 从上面例子 delta = index - nextIndex = 2 - 1 = 1
const delta = index - nextIndex;
if (delta) {
// Revert the POP
blockedPopTx = {
action: nextAction,
location: nextLocation,
retry() {
// 由于下面的go(delta)阻塞了当前页面的变化,那么retry就可以让页面真正符合浏览器行为的变化了
// 这个在blocker回调中可以调用,但下面的go(delta)会触发handlePop,可是go(delta * -1)不会,为何????
go(delta * -1);
}
};
// 上面/basic/about => /basic,delta为1,那么go(1)就又到了/basic/about
// 此处是真正触发前进后退时保持当前location不变的关键所在
// 还有需要特别注意一点的是,这里调用go后又会触发handleProp,那么if (blockedPopTx)就为true了,那么
// 就会调用blockers.call(blockedPopTx),blocer可以根据blockedPopTx的retry看是否允许跳转页面,然后再把blockedPopTx = null
go(delta);
}
} else {
// Trying to POP to a location with no index. We did not create
// this location, so we can't effectively block the navigation.
warning(
false,
// TODO: Write up a doc that explains our blocking strategy in
// detail and link to it here so people can understand better what
// is going on and how to avoid it.
`You are trying to block a POP navigation to a location that was not ` +
`created by the history library. The block will fail silently in ` +
`production, but in general you should do all navigation with the ` +
`history library (instead of using window.history.pushState directly) ` +
`to avoid this situation.`
);
}
} else {
// blockers为空,那么赋值新的action,然后获取新的index和location,然后
// 将action, location作为参数消费listeners
applyTx(nextAction);
}
}
}
/**
* 监听popstate
* 调用history.pushState()或history.replaceState()不会触发popstate事件。
* 只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的前进、后退按钮、在Javascript代码中调用history.back()
* 、history.forward()、history.go方法,此外,a 标签的锚点也会触发该事件
*
* @see https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event
*/
window.addEventListener(PopStateEventType, handlePop);
let action = Action.Pop;
// createBrowserHistory创建的时候获取初始当前路径index和location
let [index, location] = getIndexAndLocation();
// blockers不为空的话listeners不会触发
const listeners = createEvents<Listener>();
const blockers = createEvents<Blocker>();
if (index == null) {
// 初始index为空,那么给个0
index = 0;
// 这里replaceState后,history.state.idx就为0了
globalHistory.replaceState({ ...globalHistory.state, idx: index }, '');
}
/** 返回一个新的href, to为string则返回to,否则返回 `createPath(to)` => pathname + search + hash */
function createHref(to: To) {
return typeof to === 'string' ? to : createPath(to);
}
/**
* @description 获取新的Location
* @param to 新的path
* @param state 状态
*/
function getNextLocation(to: To, state: State = null): Location {
return readOnly<Location>({
...location,
...(typeof to === 'string' ? parsePath(to) : to),
state,
key: createKey()
});
}
/** 获取state和url */
function getHistoryStateAndUrl(
nextLocation: Location,
index: number
): [HistoryState, string] {
return [
{
usr: nextLocation.state,
key: nextLocation.key,
idx: index
},
createHref(nextLocation)
];
}
/**
* @description 判断是否允许路由切换,有blockers就不允许
*
* - blockers有handlers,那么消费handlers,然后返回false
* - blockers没有handlers,返回true
* */
function allowTx(action: Action, location: Location, retry: () => void): boolean {
return (
!blockers.length || (blockers.call({ action, location, retry }), false)
);
}
/** blocker为空才执行所有的listener, handlePop、push、replace都会调用 */
function applyTx(nextAction: Action) {
debugger
action = nextAction;
// 获取当前index和location
[index, location] = getIndexAndLocation();
listeners.call({ action, location });
}
/** history.push,跳到哪个页面 */
function push(to: To, state?: State) {
debugger
const nextAction = Action.Push;
const nextLocation = getNextLocation(to, state);
/**
* retry的目的是为了如果有blockers可以在回调中调用
* @example
* const { navigator } = useContext(UNSAFE_NavigationContext)
* const countRef = useRef(0)
* useEffect(() => {
* const unblock = navigator.block((tx) => {
* // block两次后调用retry和取消block
* if (countRef.current < 2) {
* countRef.current = countRef.current + 1
* } else {
* unblock();
* tx.retry()
* }
* })
* }, [navigator])
*
* 当前路径为/blocker
* 点击<Link to="about">About({`<Link to="about">`})</Link>
* 第三次(countRef.current >= 2)因为unblock了,随后调用rety也就是push(to, state)判断到下面的allowTx返回true,
* 就成功pushState了,push到/blocker/about了
*/
function retry() {
push(to, state);
}
// 只要blockers不为空下面就进不去
// 但是blockers回调里可以unblock(致使blockers.length = 0),然后再调用retry,那么又会重新进入这里,
// 就可以调用下面的globalHistory改变路由了
if (allowTx(nextAction, nextLocation, retry)) {
const [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
// TODO: Support forced reloading
// try...catch because iOS limits us to 100 pushState calls :/
// 用try catch的原因是因为ios限制了100次pushState的调用
try {
globalHistory.pushState(historyState, '', url);
} catch (error) {
// They are going to lose state here, but there is no real
// way to warn them about it since the page will refresh...
window.location.assign(url);
}
applyTx(nextAction);
}
}
function replace(to: To, state?: State) {
const nextAction = Action.Replace;
const nextLocation = getNextLocation(to, state);
/**
* retry的目的是为了如果有blockers可以在回调中调用
* @example
* const { navigator } = useContext(UNSAFE_NavigationContext)
* const countRef = useRef(0)
* useEffect(() => {
* const unblock = navigator.block((tx) => {
* // block两次后调用retry和取消block
* if (countRef.current < 2) {
* countRef.current = countRef.current + 1
* } else {
* unblock();
* tx.retry()
* }
* })
* }, [navigator])
*
* 当前路径为/blocker
* 点击<Link to="about">About({`<Link to="about">`})</Link>
* 第三次(countRef.current >= 2)因为unblock了,随后调用rety也就是push(to, state)判断到下面的allowTx返回true,
* 就成功pushState了,push到/blocker/about了
*/
function retry() {
replace(to, state);
}
// 只要blockers不为空下面就进不去
// 但是blockers回调里可以unblock(致使blockers.length = 0),然后再调用retry,那么又会重新进入这里,
// 就可以调用下面的globalHistory改变路由了
if (allowTx(nextAction, nextLocation, retry)) {
const [historyState, url] = getHistoryStateAndUrl(nextLocation, index);
// TODO: Support forced reloading
globalHistory.replaceState(historyState, '', url);
applyTx(nextAction);
}
}
/** eg: go(-1),返回上一个路由,go(1),进入下一个路由 */
function go(delta: number) {
globalHistory.go(delta);
}
// 这里创建一个新的history
const history: BrowserHistory = {
get action() {
return action;
},
get location() {
return location;
},
createHref,
push,
replace,
go,
back() {
go(-1);
},
forward() {
go(1);
},
listen(listener) {
return listeners.push(listener);
},
block(blocker) {
// push后返回unblock,即把该blocker从blockers去掉
const unblock = blockers.push(blocker);
if (blockers.length === 1) {
// beforeunload
// 只在第一次block加上beforeunload事件
window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
}
return function() {
unblock();
// 移除beforeunload事件监听器以便document在pagehide事件中仍可以使用
// Remove the beforeunload listener so the document may
// still be salvageable in the pagehide event.
// See https://html.spec.whatwg.org/#unloading-documents
if (!blockers.length) {
// 移除的时候发现blockers空了那么就移除`beforeunload`事件
window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
}
};
}
};
return history;
}
createHashHistory 与 createBrowserHistory 的不同点
两个工厂函数的绝大部分代码是一模一样的,以下是稍微不同之处:
getIndexAndLocation
。createBrowserHistory 是直接获取 window.location,而 createHashHistory 是parsePath(window.location.hash.substr(1))
function getIndexAndLocation(): [number, Location] {
const { pathname = '/', search = '', hash = '' } = parsePath(
window.location.hash.substr(1)
);
const state = globalHistory.state || {};
return [
state.idx,
readOnly<Location>({
pathname,
search,
hash,
state: state.usr || null,
key: state.key || 'default'
})
];
}
parsePath
我们上面已经讲了,这个给个例子
即 url 中有多个#
,但是会取第一个#后面的来解析对应的 pathname、search 和 hash
- createHashHistory 多了监听
hashchange
的事件
window.addEventListener(HashChangeEventType, () => {
const [, nextLocation] = getIndexAndLocation();
// Ignore extraneous hashchange events.
// 忽略无关的hashchange事件
// 检测到hashchange,只有前后pathname + search + hash不一样才执行handlePop
if (createPath(nextLocation) !== createPath(location)) {
handlePop();
}
});
- createHref 会在前面拼接
getBaseHref() + '#'
function getBaseHref() {
// base一般为空,所以下面的href一般返回空字符串
// 如果有 类似<base href="http://www.google.com"/>,那么获取到的href就为 "http://www.google.com/",可看下面示意图
const base = document.querySelector('base');
let href = '';
if (base && base.getAttribute('href')) {
const url = window.location.href;
const hashIndex = url.indexOf('#');
// 有hash的话去掉#及其之后的
href = hashIndex === -1 ? url : url.slice(0, hashIndex);
}
return href;
}
// 后面部分和createBrowserHistory的createHref相同
function createHref(to: To) {
return getBaseHref() + '#' + (typeof to === 'string' ? to : createPath(to));
}
结语
我们总结一下:
history
有browserHistory
和hashHistory
,两个工厂函数的绝大部分代码相同,只有parsePath
的入参不同和 hashHistory 增加了hashchange
事件的监听- 通过 push、replace 和 go 可以切换路由
- 可以通过 history.listen 添加路由监听器 listener,每当路由切换可以收到最新的 action 和 location,从而做出不同的判断
- 可以通过 history.block 添加阻塞器 blocker,会阻塞 push、replace 和浏览器的前进后退。且只要判断有 blockers,那么同时会加上
beforeunload
阻止浏览器刷新、关闭等默认行为,即弹窗提示。且只要有 blocker,那么上面的 listener 就监听不到 - 最后我们也透露了
BrowserRouter
中就是通过history.listen(setState)
来监听路由的变化,从而管理所有的路由
最后
history
是react-router
的基础,只有知道了其作用,才能在后续分析 react router 的过程中更加游刃有余,那我们下篇文章就开始真正的 react router 之旅,敬请期待~
往期文章
翻译翻译,什么叫 ReactDOM.createRoot
翻译翻译,什么叫 JSX
什么,React Router已经到V6了 ??
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!