网站建设与维护制作网页上海关键词排名优化价格

此前一直在疑惑,明明 pushState()
、replaceState()
不触发 popstate
事件,可为什么 React Router 还能挂载对应路由的组件呢?
翻了一下 history.js 源码,终于知道原因了。
源码
假设项目路由设计如下:
import { render } from 'react-dom'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { Mine, About } from './routes'
import App from './App'const rootElement = document.getElementById('root')render(<BrowserRouter><Routes><Route path="/" exact element={<App />} /><Route path="/mine" element={<Mine />} /><Route path="/about" element={<About />} /></Routes></BrowserRouter>,rootElement
)
然后我们看下 <BrowserRouter />
的源码(react-router-dom/modules/BrowserRouter.js
),以下省略了一部分无关代码:
import React from 'react'
import { Router } from 'react-router'
import { createBrowserHistory as createHistory } from 'history'/*** The public API for a <Router> that uses HTML5 history.*/
class BrowserRouter extends React.Component {// 构建 history 对象history = createHistory(this.props)render() {// 将 history 对象等传入 <Router /> 组件return <Router history={this.history} children={this.props.children} />}
}// ...export default BrowserRouter
接着我们继续看下 <Router />
组件的源码(react-router/modules/Router.js
),如下:
import React from 'react'
import HistoryContext from './HistoryContext.js'
import RouterContext from './RouterContext.js'/*** The public API for putting history on context.*/
class Router extends React.Component {static computeRootMatch(pathname) {return { path: '/', url: '/', params: {}, isExact: pathname === '/' }}constructor(props) {super(props)this.state = {location: props.history.location}// 关键点:// 当触发 popstate 事件、// 或主动调用 props.history.push()、props.history.replace() 方法时,// 都会执行 history 对象的 listen 方法,使得执行 setState 强制更新当前组件this.unlisten = props.history.listen(location => {this.setState({ location })})}componentWillUnmount() {// 组件卸载时,解除监听if (this.unlisten) this.unlisten()}render() {return (// 由于 React Context 的特性,所有消费 RouterContext.Provider 的 Custom 组件// 在其 value 值发生变化时,都会重新渲染。// 当前 <Router /> 组件并没有做任何限制重新渲染的处理,// 因此每次 setState 都会引起 RouterContext.Provider 的 value 值发生变化。<RouterContext.Providervalue={{history: this.props.history,location: this.state.location,match: Router.computeRootMatch(this.state.location.pathname),staticContext: this.props.staticContext}}><HistoryContext.Provider children={this.props.children || null} value={this.props.history} /></RouterContext.Provider>)}
}export default Router
原因剖析
往下之前,如果对 History API 或者 URL Fragment 不了解的,可以看下这篇文章:History 对象及事件监听详解。
react-router-dom
引用了 history.js 库 ,它主要提供了三种方法:createBrowserHistory
、createHashHistory
、createMemoryHistory
。
它们用于构建对应模式的 history
对象(请注意,它有别于 window.history
对象),该对象的属性和方法可在 Devtools 中清晰地看到(如下图),也可查阅文档。这个太简单了,你们都懂,不说了。

本文讨论的是 History 模式,因而对应 createBrowserHistory
方法。
在构建项目路由时,选择 <BrowserRouter />
组件,它内部是将通过 createBrowserHistory()
方法构造的 history
对象传递给 <Router />
组件。
我们知道,在 React 应用中切换路由,它会加载对应的组件。我们知道 createBrowserHistory()
利用了 HTML5 History API 特性,但是主动调用 window.history.pushState()
和 window.history.replaceState()
方法都不会触发 popstate
事件,因此,如果仅通过监听 popstate
事件是不能完全实现路由切换的。
那么 React Router 是如何解决问题的呢?
在前面的源码部分,其实已经添加了一些注解,<Router />
组件它内部依赖于 Context 的 Provider/Comsumer 模式。因此,它只要做到 URL 发生变化时更新 Context.Provider
的 value
值即可,至于后续如何加载组件就交给 React 了(当然里面还包括 React Router 的路由匹配,但非本文讨论内容,不展开讲述)。
一般情况下,<BrowserRouter />
都会作为整个项目的根路由,它包裹了一层 <Router />
组件,<Router />
组件在实例化时,设置了一个监听函数:
// props.history 就是通过 createBrowserHistory(props) 生成的对象
this.unlisten = props.history.listen(location => {// 回调函数的作用是,通过 setState 触发 Router 组件更新,// 使得 Provider 的 value 值发生变化,以带动 Consumer 的更新。this.setState({ location })
})
// this.unlisten 是一个函数,执行它内部会移除 popstate 事件监听器
Q:history.js 是如何做到每当 URL 发生变化,会触发这个回调函数的?
在 React 中是通过调用组件的 props.history.push()
和 props.history.replace()
方法实现路由切换的。
我们来看一下 history.js 的源码(history/esm/history.js
):
里面省略了一部分代码,然后分析顺序已经按顺序标注出来。
function createTransitionManager() {// ...function confirmTransitionTo(location, action, getUserConfirmation, callback) {var result = typeof prompt === 'function' ? prompt(location, action) : promptif (typeof result === 'string') {if (typeof getUserConfirmation === 'function') {getUserConfirmation(result, callback)} else {process.env.NODE_ENV !== 'production' ? warning(false, 'A history needs a getUserConfirmation function in order to use a prompt message') : void 0callback(true)}} else {// Return false from a transition hook to cancel the transition.callback(result !== false)}}var listeners = []function appendListener(fn) {var isActive = truefunction listener() {if (isActive) fn.apply(void 0, arguments)}// 添加监听器listeners.push(listener)return function () {isActive = false// 过滤重复的监听器listeners = listeners.filter(function (item) {return item !== listener})}}// 6️⃣ 执行 listeners 中所有的 listener 监听器,// 最后触发 <Router /> 中的回调函数 this.unlisten = props.history.listen(location => { this.setState({ location }) }) 逻辑function notifyListeners() {// 将类数组 arguments 转换为数组形式for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {args[_key] = arguments[_key]}listeners.forEach(function (listener) {// 回调函数将获得 location、action 两个参数return listener.apply(void 0, args)})}return {setPrompt: setPrompt,confirmTransitionTo: confirmTransitionTo,appendListener: appendListener,notifyListeners: notifyListeners}
}/*** Creates a history object that uses the HTML5 history API including* pushState, replaceState, and the popstate event.*/
function createBrowserHistory(props) {// ...// 创建 location 对象function getDOMLocation(historyState) {var _ref = historyState || {},key = _ref.key,state = _ref.statevar _window$location = window.location,pathname = _window$location.pathname,search = _window$location.search,hash = _window$location.hashvar path = pathname + search + hashif (basename) path = stripBasename(path, basename)return createLocation(path, state, key)}// ...// 创建 transitionManager 对象var transitionManager = createTransitionManager()// 5️⃣ 主要更新 history 对象,并调用 notifyListeners 方法function setState(nextState) {_extends(history, nextState)history.length = globalHistory.length// 执行 transitionManager 中的所有 listenerstransitionManager.notifyListeners(history.location, history.action)}// 3️⃣ popstate 事件监听器的处理函数function handlePopState(event) {// getDOMLocation 方法用于生成 location 对象,location: { hash, pathname, search, state }// handlePop 方法,主要是用于触发 setState 方法handlePop(getDOMLocation(event.state))}// 4️⃣ 用于调用 setState 方法function handlePop(location) {var action = 'POP'transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {if (ok) {setState({action: action,location: location})}})}// 7️⃣// 这里的 push 和 replace 方法,是利用了 window.history.pushState() 和 window.history.replaceState()// 他们不会触发 popstate 事件,因此无法执行 handlePopState 方法,因此我们需要主动执行 setState() 方法,进而// 执行 notifyListeners() 以使得 <Router /> 组件的回调被执行,使得组件进行更新。function push(path, state) {// ...var action = 'PUSH'var location = createLocation(path, state, createKey(), history.location)// 将会执行 confirmTransitionTo 的 callback 函数transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {if (!ok) returnvar href = createHref(location)var key = location.key,state = location.stateif (canUseHistory) {globalHistory.pushState({key: key,state: state},null,href)if (forceRefresh) {window.location.href = href} else {var prevIndex = allKeys.indexOf(history.location.key)var nextKeys = allKeys.slice(0, prevIndex + 1)nextKeys.push(location.key)allKeys = nextKeys// 调用 setState() 方法,然后里面会执行 notifyListeners 方法,并触发 listeners 的所有监听器setState({action: action,location: location})}} else {window.location.href = href}})}function replace(path, state) {// 与 push 方法同理,省略...}// 8️⃣// 这里的 go()、goBack()、goForward() 全是利用了 History API 的能力,// 他们都会触发 popstate 事件,因此都会执行 handlePopState 方法。function go(n) {globalHistory.go(n)}function goBack() {go(-1)}function goForward() {go(1)}var listenerCount = 0// 2️⃣ 注册/移除 popstate 事件监听器function checkDOMListeners(delta) {listenerCount += deltaif (listenerCount === 1 && delta === 1) {// 添加 popstate 事件监听器,执行 handlePopState 时将会触发 setStatewindow.addEventListener(PopStateEvent, handlePopState)if (needsHashChangeListener) window.addEventListener(HashChangeEvent, handleHashChange)} else if (listenerCount === 0) {// 移除事件监听器window.removeEventListener(PopStateEvent, handlePopState)if (needsHashChangeListener) window.removeEventListener(HashChangeEvent, handleHashChange)}}// 1️⃣ 设置监听器,以触发 <Router /> 组件中的回调函数function listen(listener) {// 往 transitionManager 中的 listeners 数组添加新的监听器 listener,// 其中 transitionManager 对象有这些方法:{ setPrompt, confirmTransitionTo, appendListener, notifyListeners }var unlisten = transitionManager.appendListener(listener)// 负责添加、移除 popstate 事件监听器checkDOMListeners(1)// 执行回调函数移除 listener 监听器return function () {checkDOMListeners(-1)unlisten()}}var history = {length: globalHistory.length,action: 'POP',location: initialLocation,createHref: createHref,push: push,replace: replace,go: go,goBack: goBack,goForward: goForward,block: block,listen: listen}return history
}
以下是 history.js 中创建的 history
、location
对象的一些属性和方法:

先回到 <Router />
组件中的 history.listen(fn)
,它主要做几件事:
- 将
fn
保存在负责存储监听器的listeners
数组中,未来它将会被notifyListeners()
方法调用。- 注册
popstate
事件监听器,触发之后,会执行notifyListeners()
方法- 在 React 组件中调用
props.history.push()
等方法,也将会触发notifyListeners()
方法。- 执行
notifyListeners()
方法,会执行listeners
中所有的listener
,因此fn
将会被触发。- 执行
fn()
触发 Component 中的setState()
方法更新<Router />
组件,即Router.Provider
的value
发生改变,那么Router.Consumer
就会跟着更新
所以,React Router 是利用了 Context 的 Provider/Custom 特性,解决了 pushState/replaceState 不触发 popstate
事件时实现了路由切换的问题。
The end.

喜欢的朋友记得点赞、收藏、关注哦!!!