前言
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
的工厂函数,我们先看下类型
即是说,createBrowserHistory 最终肯定要返回一个上面形状的 browserHistory 实例,我们先看下函数总体概览(这里大致看下框架就好,后面会具体分析)。
可以看到函数里面又有一些内部函数和函数作用域内的顶层变量,我们先来看下这些顶层变量的作用。
createBrowserHistory 函数作用域内的顶层变量
window
createBrowserHistory 接收一个 options,默认为空对象,而其中的window
默认为document.defaultView
,即是 Window 对象罢了
history
上面获取到 window,然后会从 window 上获取到 history
blockedPopTx
用于存储下面的 blockers.call 方法的参数,即每一个 blocker 回调函数的参数,类型如下
index 与 location
index
为当前 location
的索引,即是说,history 会为每个 location 创建一个idx
,放在state
中, 会用于在 handlePop
中计算 delta
,这里稍微提下,后面分析 handlePop 如何阻止路由变化会讲到
action
blockers
、listeners
的回调会用到 action
,其是通过 handlePop
、 push
、replace
三个函数修改其状态,分别为 POP
、PUSH
和REPLACE
,这样我们就可以通过判断 action 的值来做出不同的判断了。
listeners 与 blokers
我们先看下 创建 listeners 与 blokers 的工厂函数 createEvents
其返回了一个对象,通过 push
添加每个 listener,通过 call
通知每个 listener,代码中叫做 handler
。
listeners 通过 call 传入 { action, location }
, 这样每个 listener 在路由变化时就能接收到,从而做出对应的判断。
listener 类型如下
blockers 通过 call 传入 { action, location, retry }
,比listeners多了一个 retry
,从而判断是否要阻塞路由,不阻塞的话需要调用函数 retry
。
blocker 类型如下
知道了顶层变量的作用,那我们接下来一一分析下返回 history
实例对象的每个属性。
action 与 location
这两个属性都通过 修饰符 get
,那么我们每次要获取最新的 action 或 location,就可以通过 history.action
或 history.location
。
避免了只能拿到第一次创建的值,如
或需要每次调用函数才能拿到:
action 我们上面已经分析了,这里我们看下获取 location 的函数。
getIndexAndLocation
即获取当前索引和 location
createBrowserHistory 调用的时候会获取初始当前路径 index 和 location,这个时候的 index 肯定是 undefined(请注意要打开新页面才会,否则刷新后还是有历史堆栈,导致 state.idx 有值,即 index 不为空)
所以下面会通过判断 index 是否为空,空的话会给个默认值 0
通过 replaceState
初始重置了历史堆栈,从而就能获取到 state 中的 idx 了。
这个时候我们再通过 history.state.idx 就能获取到
history.createHref
history.createHref
来自 createBrowserHistory 的内部函数,接收一个 To
类型的参数,返回值为字符串 href
如果 to 不为字符串,会通过 createPath
函数转为字符串,即是把 pathname、search 和 hash 拼接起来罢了
history.push
首先会通过 getNextLocation,根据 to
和 state
获取到新的 location,注意这时候路由还没切换
如果 to 是字符串的话,会通过parsePath
解析对应的 pathname、search、hash(三者都是可选的,不一定会出现在返回的对象中)
再根据新的 location 获取新的 state 和 url,而因为是 push,所以这里的 index 自然是加一
最后调用history.pushState
成功跳转页面,这个时候路由也就切换了
history.replace
replace
和 push 类似,区别只是 index 不变以及调用 replaceState
history.go、history.back、history.forward
用于历史堆栈的前进后退,back
和 forward
分别是 是 go(-1)
和 go(1)
,delta 可正负,代表前进后退
history.listen
history.listen
可以往 history 中添加 listener,返回值是 unListen,即取消监听。这样每当成功切换路由,就会调用 applyTx(nextAction);
来通知每个 listener,applyTx(nextAction);
在 push
、 replace
和 handlePop
三个函数中成功切换路由后调用。
即只要满足 allowTx
返回 true(push 和 replace 函数中) 或没有 blocker(handlePop 函数中) 就能通知每个 listener。那我们看下 allowTx
allowTx
的作用是判断是否允许路由切换,有 blockers 就不允许,逻辑如下:
- blockers 不为空,那么通知每个 blocker,然后返回 false
- blockers 为空,返回 true
那么要返回 true 的话就必须满足 blockers 为空,也即是说,listener 能否监听到路由变化,取决于当前页面是否被阻塞了(block)。
history.block
上面我们说有 blocker 就会导致 listener 收不到监听,且无法成功切换路由,那我们看下 block 函数:
beforeunload
我们发现添加第一个 blocker 时会添加 beforeunload
事件,也就是说只要 block 了,那么我们刷新、关闭页面,通过修改地址栏 url 后 enter 都会弹窗提示:
刷新会询问重新加载此网站?,而关闭 tab 或修改地址栏后 enter 是提示离开此网站?,这两种要注意区别。
这功能常用在表单提交的页面,避免用户不小心关闭 tab 导致表单数据丢失。
当然如果 unblock 时发现 blockers 为空就会移除 beforeunload
事件了。
history 如何阻止路由切换
说完上面的beforeunload
事件,我们关注下上面跳过的 block 方面的代码
对于 push
和 replace
,其中都会有一个 retry
和 allowTx
,这里我们再看下
如果我们通过 block 添加了一个 blocker,那么每次 push 或 replace 都会判断到 blocker.length 不为 0,那么就会传入对应的参数通知每个 blocker,之后会返回 false,从而无法进入条件,导致无法触发 pushState
或 replaceState
,所以点击 Link 或调用 navigate 无法切换路由。
还有另外一个是在 handlePop 中,其在一开始调用 createBrowserHistory 的时候就往 window 上添加监听事件:
只要添加了该事件,那我们只要点击浏览器的前进、后退按钮、在 js 代码中调用 history.back()、history.forward()、history.go 方法,点击 a 标签都会触发该事件。
比如我们在 useEffect 中添加一个 blocker(详细代码可查看blocker) ,这段代码的意思是只要触发了上面的行为,那么第一和第二次都会弹窗提示,等到第三次才会调用 retry 成功切换路由
我们看下 popstate
的回调函数 handlePop:
这里我们举个?比较容易理解:比如当前 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 了
然后继续走到下面的 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 的。
这也解释了为何block后路由虽然有切换,但是当前页面没有卸载,就是因为 applyTx(nextAction)
没有执行,导致 BrowserRouter
中没有收到通知。
重新看整个createBrowserHistory
我们上面一一解析了每个函数的作用,下面我们全部合起来再看一下,相信经过上面的分析,再看这整个函数就比较容易理解了
createHashHistory 与 createBrowserHistory 的不同点
两个工厂函数的绝大部分代码是一模一样的,以下是稍微不同之处:
getIndexAndLocation
。createBrowserHistory 是直接获取 window.location,而 createHashHistory 是parsePath(window.location.hash.substr(1))
parsePath
我们上面已经讲了,这个给个例子
即 url 中有多个#
,但是会取第一个#后面的来解析对应的 pathname、search 和 hash
- createHashHistory 多了监听
hashchange
的事件
- createHref 会在前面拼接
getBaseHref() + '#'
结语
我们总结一下:
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介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!