「源碼解析 」這一次徹底弄懂react-router路由原理

2021-02-20 前端Sharing

寫在前面:為什麼要學習react-router底層源碼? 為什麼要弄明白整個路由流程?筆者個人感覺學習react-router,有助於我們學習單頁面應用(spa)路由跳轉原理,讓我們理解從history.push,到組件頁面切換的全套流程,使我們在面試的時候不再為路由相關的問題發怵,廢話不說,讓我們開啟深入react-router源碼之旅吧。

一 正確理解react-router1 理解單頁面應用

什麼是單頁面應用?

個人理解,單頁面應用是使用一個html下,一次性加載js, css等資源,所有頁面都在一個容器頁面下,頁面切換實質是組件的切換。

2 react-router初探,揭露路由原理面紗①react-router-dom和react-router和history庫三者什麼關係

history 可以理解為react-router的核心,也是整個路由原理的核心,裡面集成了popState,history.pushState等底層路由實現的原理方法,接下來我們會一一解釋。

react-router可以理解為是react-router-dom的核心,裡面封裝了Router,Route,Switch等核心組件,實現了從路由的改變到組件的更新的核心功能,在我們的項目中只要一次性引入react-router-dom就可以了。

react-router-dom,在react-router的核心基礎上,添加了用於跳轉的Link組件,和histoy模式下的BrowserRouter和hash模式下的HashRouter組件等。所謂BrowserRouter和HashRouter,也只不過用了history庫中createBrowserHistory和createHashHistory方法

react-router-dom 我們不多說了,這裡我們重點看一下react-router。

②來個小demo嘗嘗鮮?

import { BrowserRouter as Router, Switch, Route, Redirect,Link } from 'react-router-dom'

import Detail from '../src/page/detail'
import List from '../src/page/list'
import Index from '../src/page/home/index'

const menusList = [
{
name: '首頁',
path: '/index'
},
{
name: '列表',
path: '/list'
},
{
name: '詳情',
path: '/detail'
},
]
const index = () => {
return <div >
<div >

<Router >
<div>{
/* link 路由跳轉 */
menusList.map(router=><Link key={router.path} to={ router.path } >
<span className="routerLink" >{router.name}</span>
</Link>)
}</div>
<Switch>
<Route path={'/index'} component={Index} ></Route>
<Route path={'/list'} component={List} ></Route>
<Route path={'/detail'} component={Detail} ></Route>
{/* 路由不匹配,重定向到/index */}
<Redirect from='/*' to='/index' />
</Switch>
</Router>
</div>
</div>
}

效果如下

二 單頁面實現核心原理

單頁面應用路由實現原理是,切換url,監聽url變化,從而渲染不同的頁面組件。

主要的方式有history模式和hash模式。

1 history模式原理①改變路由

history.pushState

history.pushState(state,title,path)

1 state:一個與指定網址相關的狀態對象, popstate 事件觸發時,該對象會傳入回調函數。如果不需要可填 null。

2 title:新頁面的標題,但是所有瀏覽器目前都忽略這個值,可填 null。

3 path:新的網址,必須與當前頁面處在同一個域。瀏覽器的地址欄將顯示這個地址。

history.replaceState

history.replaceState(state,title,path)

參數和pushState一樣,這個方法會修改當前的 history 對象記錄, history.length 的長度不會改變。

②監聽路由

popstate事件

window.addEventListener('popstate',function(e){
/* 監聽改變 */
})

同一個文檔的 history 對象出現變化時,就會觸發 popstate 事件  history.pushState 可以使瀏覽器地址改變,但是無需刷新頁面。注意⚠️的是:用 history.pushState() 或者 history.replaceState() 不會觸發 popstate 事件。 popstate 事件只會在瀏覽器某些行為下觸發, 比如點擊後退、前進按鈕或者調用 history.back()、history.forward()、history.go()方法。

2 hash模式原理①改變路由

window.location.hash

通過window.location.hash 屬性獲取和設置 hash 值。

②監聽路由

onhashchange

window.addEventListener('hashchange',function(e){
/* 監聽改變 */
})

三 理解history庫

react-router路由離不開history庫,history專注於記錄路由history狀態,以及path改變了,我們應該做寫什麼, 在history模式下用popstate監聽路由變化,在hash模式下用hashchange監聽路由的變化。

接下來我們看 Browser模式下的createBrowserHistory 和 Hash模式下的 createHashHistory方法。

1 createBrowserHistory

Browser模式下路由的運行 ,一切都從createBrowserHistory開始。這裡我們參考的history-4.7.2版本,最新版本中api可能有些出入,但是原理都是一樣的,在解析history過程中,我們重點關注setState ,push ,handlePopState,listen方法

const PopStateEvent = 'popstate'
const HashChangeEvent = 'hashchange'
/* 這裡簡化了createBrowserHistory,列出了幾個核心api及其作用 */
function createBrowserHistory(){
/* 全局history */
const globalHistory = window.history
/* 處理路由轉換,記錄了listens信息。*/
const transitionManager = createTransitionManager()
/* 改變location對象,通知組件更新 */
const setState = () => { /* ... */ }

/* 處理當path改變後,處理popstate變化的回調函數 */
const handlePopState = () => { /* ... */ }

/* history.push方法,改變路由,通過全局對象history.pushState改變url, 通知router觸發更新,替換組件 */
const push=() => { /*...*/ }

/* 底層應用事件監聽器,監聽popstate事件 */
const listen=()=>{ /*...*/ }
return {
push,
listen,
/* .... */
}
}

下面逐一分析各個api,和他們之前的相互作用

const PopStateEvent = 'popstate'
const HashChangeEvent = 'hashchange'

popstate 和hashchange是監聽路由變化底層方法。

①setState

const setState = (nextState) => {
/* 合併信息 */
Object.assign(history, nextState)
history.length = globalHistory.length
/* 通知每一個listens 路由已經發生變化 */
transitionManager.notifyListeners(
history.location,
history.action
)
}

代碼很簡單:統一每個transitionManager管理的listener路由狀態已經更新。

什麼時候綁定litener, 我們在接下來的React-Router代碼中會介紹。

②listen

const listen = (listener) => {
/* 添加listen */
const unlisten = transitionManager.appendListener(listener)
checkDOMListeners(1)

return () => {
checkDOMListeners(-1)
unlisten()
}
}

checkDOMListeners

const checkDOMListeners = (delta) => {
listenerCount += delta
if (listenerCount === 1) {
addEventListener(window, PopStateEvent, handlePopState)
if (needsHashChangeListener)
addEventListener(window, HashChangeEvent, handleHashChange)
} else if (listenerCount === 0) {
removeEventListener(window, PopStateEvent, handlePopState)
if (needsHashChangeListener)
removeEventListener(window, HashChangeEvent, handleHashChange)
}
}

listen本質通過checkDOMListeners的參數 1 或 -1 來綁定/解綁 popstate 事件,當路由發生改變的時候,調用處理函數handlePopState 。

接下來我們看看push方法。

③push

const push = (path, state) => {
const action = 'PUSH'
/* 1 創建location對象 */
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) {
/* 改變 url */
globalHistory.pushState({ key, state }, null, href)
if (forceRefresh) {
window.location.href = href
} else {
/* 改變 react-router location對象, 創建更新環境 */
setState({ action, location })
}
} else {
window.location.href = href
}
})
}

push ( history.push ) 流程大致是 首先生成一個最新的location對象,然後通過window.history.pushState方法改變瀏覽器當前路由(即當前的path),最後通過setState方法通知React-Router更新,並傳遞當前的location對象,由於這次url變化的,是history.pushState產生的,並不會觸發popState方法,所以需要手動setState,觸發組件更新。

④handlePopState

最後我們來看看當popState監聽的函數,當path改變的時候會發生什麼,

/* 我們簡化一下handlePopState */
const handlePopState = (event)=>{
/* 獲取當前location對象 */
const location = getDOMLocation(event.state)
const action = 'POP'

transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({ action, location })
} else {
revertPop(location)
}
})
}

handlePopState 代碼很簡單 ,判斷一下action類型為pop,然後 setState ,從新加載組件。

2 createHashHistory

hash 模式和 history API類似,我們重點講一下 hash模式下,怎麼監聽路由,和push , replace方法是怎麼改變改變路徑的。

監聽哈希路由變化

const HashChangeEvent = 'hashchange'
const checkDOMListeners = (delta) => {
listenerCount += delta
if (listenerCount === 1) {
addEventListener(window, HashChangeEvent, handleHashChange)
} else if (listenerCount === 0) {
removeEventListener(window, HashChangeEvent, handleHashChange)
}
}

和之前所說的一樣,就是用hashchange來監聽hash路由的變化。

改變哈希路由


/* 對應 push 方法 */
const pushHashPath = (path) =>
window.location.hash = path

/* 對應replace方法 */
const replaceHashPath = (path) => {
const hashIndex = window.location.href.indexOf('#')

window.location.replace(
window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path
)
}

在hash模式下 ,history.push 底層是調用了window.location.href來改變路由。history.replace底層是掉用 window.location.replace改變路由。

總結

我們用一幅圖來描述了一下history庫整體流程。

四 核心api1 Router-接收location變化,派發更新流

Router 作用是把 history location 等路由信息 傳遞下去

Router

/* Router 作用是把 history location 等路由信息 傳遞下去 */
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: '/', url: '/', params: {}, isExact: pathname === '/' };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
//記錄pending位置
//如果存在任何<Redirect>,則在構造函數中進行更改
//在初始渲染時。如果有,它們將在
//在子組件身上激活,我們可能會
//在安裝<Router>之前獲取一個新位置。
this._isMounted = false;
this._pendingLocation = null;
/* 此時的history,是history創建的history對象 */
if (!props.staticContext) {
/* 這裡判斷 componentDidMount 和 history.listen 執行順序 然後把 location複製 ,防止組件重新渲染 */
this.unlisten = props.history.listen(location => {
/* 創建監聽者 */
if (this._isMounted) {

this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
/* 解除監聽 */
if (this.unlisten) this.unlisten();
}
render() {
return (
/* 這裡可以理解 react.createContext 創建一個 context上下文 ,保存router基本信息。children */
<RouterContext.Provider
children={this.props.children || null}
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
/>
);
}
}

總結:

初始化綁定listen, 路由變化,通知改變location,改變組件。react的history路由狀態是保存在React.Content上下文之間, 狀態更新。

一個項目應該有一個根Router , 來產生切換路由組件之前的更新作用。 如果存在多個Router會造成,會造成切換路由,頁面不更新的情況。

2 Switch-匹配正確的唯一的路由

根據router更新流,來渲染當前組件。

/* switch組件 */
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{/* 含有 history location 對象的 context */}
{context => {
invariant(context, 'You should not use <Switch> outside a <Router>');
const location = this.props.location || context.location;
let element, match;
//我們使用React.Children.forEach而不是React.Children.toArray().find()
//這裡是因為toArray向所有子元素添加了鍵,我們不希望
//為呈現相同的兩個<Route>s觸發卸載/重新裝載
//組件位於不同的URL。
//這裡只需然第一個 含有 match === null 的組件
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
// 子組件 也就是 獲取 Route中的 path 或者 rediect 的 from
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}

找到與當前path,匹配的組件進行渲染。通過pathname和組件的path進行匹配。找到符合path的router組件。

matchPath

function matchPath(pathname, options = {}) {
if (typeof options === "string" || Array.isArray(options)) {
options = { path: options };
}

const { path, exact = false, strict = false, sensitive = false } = options;

const paths = [].concat(path);

return paths.reduce((matched, path) => {
if (!path && path !== "") return null;
if (matched) return matched;

const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
const match = regexp.exec(pathname);
/* 匹配不成功,返回null */
if (!match) return null;

const [url, ...values] = match;
const isExact = pathname === url;

if (exact && !isExact) return null;

return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}

匹配符合的路由。

3 Route-組件頁面承載容器

/**
* The public API for matching a single path and rendering.
*/
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
/* router / route 會給予警告警告 */
invariant(context, "You should not use <Route> outside a <Router>");
// computedMatch 為 經過 swich處理後的 path
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;

if (Array.isArray(children) && children.length === 0) {
children = null;
}

return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}

匹配path,渲染組件。作為路由組件的容器,可以根據將實際的組件渲染出來。通過RouterContext.Consume 取出當前上一級的location,match等信息。作為prop傳遞給頁面組件。使得我們可以在頁面組件中的props中獲取location ,match等信息。

4 Redirect-沒有符合的路由,那麼重定向

重定向組件, 如果來路由匹配上,會重定向對應的路由。

function Redirect({ computedMatch, to, push = false }) {
return (
<RouterContext.Consumer>
{context => {
const { history, staticContext } = context;
/* method就是路由跳轉方法。*/
const method = push ? history.push : history.replace;
/* 找到符合match的location ,格式化location */
const location = createLocation(
computedMatch
? typeof to === 'string'
? generatePath(to, computedMatch.params)
: {
...to,
pathname: generatePath(to.pathname, computedMatch.params)
}
: to
)
/* 初始化的時候進行路由跳轉,當初始化的時候,mounted執行push方法,當組件更新的時候,如果location不相等。同樣會執行history方法重定向 */
return (
<Lifecycle
onMount={() => {
method(location);
}}
onUpdate={(self, prevProps) => {
const prevLocation = createLocation(prevProps.to);
if (
!locationsAreEqual(prevLocation, {
...location,
key: prevLocation.key
})
) {
method(location);
}
}}
to={to}
/>
);
}}
</RouterContext.Consumer>
);
}

初始化的時候進行路由跳轉,當初始化的時候,mounted執行push方法,當組件更新的時候,如果location不相等。同樣會執行history方法重定向。

五 總結 + 流程分析總結

history提供了核心api,如監聽路由,更改路由的方法,已經保存路由狀態state。

react-router提供路由渲染組件,路由唯一性匹配組件,重定向組件等功能組件。

流程分析

當地址欄改變url,組件的更新渲染都經歷了什麼?😊😊😊 拿history模式做參考。當url改變,首先觸發histoy,調用事件監聽popstate事件, 觸發回調函數handlePopState,觸發history下面的setstate方法,產生新的location對象,然後通知Router組件更新location並通過context上下文傳遞,switch通過傳遞的更新流,匹配出符合的Route組件渲染,最後有Route組件取出context內容,傳遞給渲染頁面,渲染更新。

當我們調用history.push方法,切換路由,組件的更新渲染又都經歷了什麼呢?

我們還是拿history模式作為參考,當我們調用history.push方法,首先調用history的push方法,通過history.pushState來改變當前url,接下來觸發history下面的setState方法,接下來的步驟就和上面一模一樣了,這裡就不一一說了。

我們用一幅圖來表示各個路由組件之間的關係。

希望讀過此篇文章的朋友,能夠明白react-router的整個流程,代碼邏輯不是很難理解。整個流程我給大家分析了一遍,希望同學們能主動看一波源碼,把整個流程搞明白。紙上得來終覺淺,絕知此事要躬行。

相關焦點

  • 深入淺出解析React Router 源碼
    這是由於 pushState 的 url 必須與當前的 url 同源,而 file:// 形式打開的頁面沒有 origin ,導致報錯。如果想正常運行體驗,可以使用http-server為文件啟動一個本地服務。
  • 手寫React-Router源碼,深入理解其原理
    BrowserRouter源碼 我們代碼裡面最外層的就是BrowserRouter,我們先去看看他的源碼幹了啥,地址傳送門:https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/BrowserRouter.js
  • 我一定把 React-Router 的真諦給你整地明明白白的
    BrowserRouter源碼我們代碼裡面最外層的就是BrowserRouter,我們先去看看他的源碼幹了啥,地址傳送門:https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/BrowserRouter.js
  • 淺析 vue-router 源碼和動態路由權限分配
    vue-router 源碼分析首先閱讀源碼之前最好是將 Vue 和 vue-router 的源碼克隆下來,然後第一遍閱讀建議先跟著 官方文檔 (https://router.vuejs.org/zh/) 先走一遍基礎用法,然後第二遍開始閱讀源碼,先理清楚各層級目錄的作用和抽出一些核心的文件出來,過一遍代碼的同時寫個小的
  • React系列十二:React - Router
    (點擊文章底部左下角閱讀原文來訪問我們官網)前言 這節我們將介紹 React 中 react - router,路由跳轉的配置以及使用本文會向你介紹以下內容:認識 react - router 如果你是第一次接觸 router
  • Vue全家桶&vue-router原理
    原理 Vue全家桶:vue + vue-router(路由)+ vuex(狀態管理)+ axios(請求)本次主要分析並實現簡版vue-router,從源碼中學習借鑑,更好的理解源碼vue-routerVue Router 是 Vue.js 官⽅的路由管理器。
  • 前端必備:React的生命周期和路由
    使用React構建的單頁面應用,要想實現頁面間的跳轉,首先想到的就是使用路由。在React中,常用的有兩個包可以實現這個需求,那就是react-router和react-router-dom。>先簡單說下各自的功能:react-router: 實現了路由的核心功能 react-router-dom: 基於react-router,加入了在瀏覽器運行環境下的一些功能。
  • React SSR 同構入門與原理
    那就讓我們更加深入的學習它,手寫一個同構框架,徹底理解同構渲染的原理。既然需要路由我們就先安裝下: npm install react-router-dom之前我們只定義了一個 Home 組件,為了演示路由,我們再定義一個 Login 組件:...
  • webpack4+react16+react-router-dom4從零配置到優化,實現路由按需...
    上一篇介紹了下webpack的一些配置,接下來講下reactRouter4裡面關於路由的一些配置,如何做到模塊的按需加載,這也是常用的一種優化網站性能的方式
  • React Router 5 完整指南
    接下來切換到該項目目錄下:cd react-router-demo複製代碼React Router 庫包含三個包:react-router、react-router-dom 和 react-router-native
  • React-Router v6新特性解讀及遷移指南
    嵌套路由變得更簡單。  用useNavigate代替useHistory。  新鉤子useRoutes代替react-router-config。  大小減少:從20kb到8kb1.
  • Vue-Router源碼學習之index.js(vue-router類)
    今天,帶來Vue-Router源碼解析系列的第二篇文章:index.js。正文vue-router類裡面都做了什麼?index.js是vue-router這個類的主構造函數,所以內容上算是比較關鍵的:從圖片中我們可以看出來,這是一個ES6聲明類的方法,vue-router源碼中類的聲明都是使用類ES的語法,constructor (options: RouterOptions
  • vue-router 實現分析
    是 Vue.js 官方的路由庫,本著學習的目的,我對 vue-router 的源碼進行了閱讀和分析,分享出來給其他感興趣的同學做個參考吧。})源碼(https://github.com/vuejs/vue-router/blob/v2.2.1/src/history/hash.js#L21)這個動作是什麼時候執行的呢?是在 router.init()(源碼)中調用的,而 router.init() 則是在根組件創建時(源碼)調用的。
  • 超實用的十條 VueRouter 高級技巧!
    這些回調將會在導航成功完成 (在所有的異步鉤子被解析之後) 或終止 (導航到相同的路由、或在當前導航完成之前導航到另一個不同的路由) 的時候進行相應的調用。在 3.1.0+,可以省略第二個和第三個參數,此時如果支持 Promise,router.push 或 router.replace 將返回一個 Promise。
  • React + Redux + React-router 全家桶
    web端開發需要搭配React-dom使用import React from 'react';import ReactDOM from 'react-dom';const App = () => (  <div>      <
  • React 中後臺系統多 Tab 頁緩存功能實現(keep-alive)
    改寫 React Router 源碼,切換路由不卸載,改為隱藏。使用社區的輪子,當時選了GitHub 裡的兩個產品:React Keeper  和 react-router-cache-route其實每種方案都存在一些問題,最終的選擇是使用了排除法。
  • 5000字的React-native源碼解析
    /app.json';AppRegistry.registerComponent(appName, () => App);默認使用AppRegistry.registerComponent幫我們註冊了一個組件(今天不對原理做過多講解,有興趣的可以自己搭建一個React-native腳手架,你會對整套運行原理、流程有一個真正的了解)
  • 一名 Vue 程式設計師總結的 React 基礎
    : 實現了路由的核心功能, react-router 3.x  版本還包括操作 dom 的方法,4.x 以上就沒有了。react-router-dom: 基於 react-router,加入了在瀏覽器運行環境下的一些功能,例如:Link 組件,會渲染一個 a 標籤,Link 組件源碼 a 標籤行; BrowserRouter 和 HashRouter 組件,前者使用 pushState 和 popState 事件構建路由,後者使用 window.location.hash 和 hashchange 事件構建路由。
  • 使用React-Router 創建單頁應用
    在單頁應用開發中,redux 並不是必須的,所以今天只講講 前端的路由系統以及 React-Router的簡單使用。 什麼是路由 以下來自維基百科:: 路由(routing)就是通過互聯的網絡把信息從源地址傳輸到目的地址的活動。路由發生在OSI網絡參考模型中的第三層即網路層。路由引導分組轉送,經過一些中間的節點後,到它們最後的目的地。
  • 【翻譯】基於 Create React App路由4.0的異步組件加載(Code...
    當然這個操作不是完全必要的,但如果你好奇的話,請隨意跟隨這篇文章一起用Create React App和 react路由4.0的異步加載方式來幫助react.js構建大型應用。 代碼分割(Code Splitting) 當我們用react.js寫我們的單頁應用程式時候,這個應用會變得越來越大,一個應用(或者路由頁面)可能會引入大量的組件,可是有些組件是第一次加載的時候是不必要的,這些不必要的組件會浪費很多的加載時間。