React Router原理

原文地址 http://blog.poetries.top/2018/12/20/react-router-anaylse/

一、React Router基礎(chǔ)之history

1.1 History介紹

history是一個(gè)獨(dú)立的第三方j(luò)s庫(kù),可以用來(lái)兼容在不同瀏覽器、不同環(huán)境下對(duì)歷史記錄的管理,擁有統(tǒng)一的API。具體來(lái)說里面的history分為三類

  • 老瀏覽器的history: 主要通過hash來(lái)實(shí)現(xiàn),對(duì)應(yīng)createHashHistory
  • 高版本瀏覽器: 通過html5里面的history,對(duì)應(yīng)createBrowserHistory
  • node環(huán)境下: 主要存儲(chǔ)在memeory里面,對(duì)應(yīng)createMemoryHistory

上面針對(duì)不同的環(huán)境提供了三個(gè)API,但是三個(gè)API有一些共性的操作,將其抽象了一個(gè)公共的文件createHistory


// 內(nèi)部的抽象實(shí)現(xiàn)
function createHistory(options={}) {
  ...
  return {
    listenBefore, // 內(nèi)部的hook機(jī)制,可以在location發(fā)生變化前執(zhí)行某些行為,AOP的實(shí)現(xiàn)
    listen, // location發(fā)生改變時(shí)觸發(fā)回調(diào)
    transitionTo, // 執(zhí)行l(wèi)ocation的改變
    push, // 改變location
    replace,
    go,
    goBack,
    goForward,
    createKey, // 創(chuàng)建location的key,用于唯一標(biāo)示該location,是隨機(jī)生成的
    createPath,
    createHref,
    createLocation, // 創(chuàng)建location
  }
}

上述這些方式是history內(nèi)部最基礎(chǔ)的方法,createHashHistorycreateBrowserHistory、createMemoryHistory只是覆蓋其中的某些方法而已。其中需要注意的是,此時(shí)的location跟瀏覽器原生的location是不相同的,最大的區(qū)別就在于里面多了key字段,history內(nèi)部通過key來(lái)進(jìn)行location的操作

function createLocation() {
  return {
    pathname, // url的基本路徑
    search, // 查詢字段
    hash, // url中的hash值
    state, // url對(duì)應(yīng)的state字段
    action, // 分為 push、replace、pop三種
    key // 生成方法為: Math.random().toString(36).substr(2, length)
  }
}

1.2 內(nèi)部解析

三個(gè)API的大致的技術(shù)實(shí)現(xiàn)如下

  • createBrowserHistory: 利用HTML5里面的history
  • createHashHistory: 通過hash來(lái)存儲(chǔ)在不同狀態(tài)下的history信息
  • createMemoryHistory: 在內(nèi)存中進(jìn)行歷史記錄的存儲(chǔ)`

1.2.1 執(zhí)行URL前進(jìn)

  • createBrowserHistory: pushState、replaceState
  • createHashHistory: location.hash=*** location.replace()
  • createMemoryHistory: 在內(nèi)存中進(jìn)行歷史記錄的存儲(chǔ)
// 偽代碼

// createBrowserHistory(HTML5)中的前進(jìn)實(shí)現(xiàn)
function finishTransition(location) {
  ...
  const historyState = { key };
  ...
  if (location.action === 'PUSH') ) {
    window.history.pushState(historyState, null, path);
  } else {
    window.history.replaceState(historyState, null, path)
  }
}
// createHashHistory的內(nèi)部實(shí)現(xiàn)
function finishTransition(location) {
  ...
  if (location.action === 'PUSH') ) {
    window.location.hash = path;
  } else {
    window.location.replace(
    window.location.pathname + window.location.search + '#' + path
  );
  }
}
// createMemoryHistory的內(nèi)部實(shí)現(xiàn)
entries = [];
function finishTransition(location) {
  ...
  switch (location.action) {
    case 'PUSH':
      entries.push(location);
      break;
    case 'REPLACE':
      entries[current] = location;
      break;
}

1.2.2 檢測(cè)URL回退

  • createBrowserHistory: popstate
  • createHashHistory: hashchange
  • createMemoryHistory:因?yàn)槭窃趦?nèi)存中操作,跟瀏覽器沒有關(guān)系,不涉及UI層面的事情,所以可以直接進(jìn)行歷史信息的回退
// 偽代碼

// createBrowserHistory(HTML5)中的后退檢測(cè)
function startPopStateListener({ transitionTo }) {
  function popStateListener(event) {
    ...
    transitionTo( getCurrentLocation(event.state) );
  }
  addEventListener(window, 'popstate', popStateListener);
  ...
}
 
// createHashHistory的后退檢測(cè)
function startPopStateListener({ transitionTo }) {
  function hashChangeListener(event) {
    ...
    transitionTo( getCurrentLocation(event.state) );
  }
  addEventListener(window, 'hashchange', hashChangeListener);
  ...
}
// createMemoryHistory的內(nèi)部實(shí)現(xiàn)
function go(n) {
  if (n) {
    ...
    current += n;
  const currentLocation = getCurrentLocation();
  // change action to POP
  history.transitionTo({ ...currentLocation, action: POP });
  }
}

1.2.3 state的存儲(chǔ)

為了維護(hù)state的狀態(tài),將其存儲(chǔ)在sessionStorage里面:

// createBrowserHistory/createHashHistory中state的存儲(chǔ)
function saveState(key, state) {
  ...
  window.sessionStorage.setItem(createKey(key), JSON.stringify(state));
}
function readState(key) {
  ...
  json = window.sessionStorage.getItem(createKey(key));
  return JSON.parse(json);
}
// createMemoryHistory僅僅在內(nèi)存中,所以操作比較簡(jiǎn)單
const storage = createStateStorage(entries); // storage = {entry.key: entry.state}
 
function saveState(key, state) {
  storage[key] = state
}
function readState(key) {
  return storage[key]
}

1.3 小結(jié)

路由原理

前端路由實(shí)現(xiàn)起來(lái)其實(shí)很簡(jiǎn)單,本質(zhì)就是監(jiān)聽 URL 的變化,然后匹配路由規(guī)則,顯示相應(yīng)的頁(yè)面,并且無(wú)須刷新。目前單頁(yè)面使用的路由就只有兩種實(shí)現(xiàn)方式

  • hash 模式
  • history 模式

www.test.com/##/ 就是 Hash URL,當(dāng) ## 后面的哈希值發(fā)生變化時(shí),不會(huì)向服務(wù)器請(qǐng)求數(shù)據(jù),可以通過 hashchange 事件來(lái)監(jiān)聽到 URL 的變化,從而進(jìn)行跳轉(zhuǎn)頁(yè)面。

image

History模式是 HTML5新推出的功能,比之 Hash URL 更加美觀

image

二、react-router的基本原理

實(shí)現(xiàn)URLUI界面的同步。其中在react-router中,URL對(duì)應(yīng)Location對(duì)象,而UI是由react components來(lái)決定的,這樣就轉(zhuǎn)變成locationcomponents之間的同步問題

image

2.1 優(yōu)點(diǎn)

  • React融為一體,專為react量身打造,編碼風(fēng)格與react保持一致,例如路由的配置可以通過component來(lái)實(shí)現(xiàn)
  • 不需要手工維護(hù)路由state,使代碼變得簡(jiǎn)單
  • 強(qiáng)大的路由管理機(jī)制,體現(xiàn)在如下方面
    • 路由配置: 可以通過組件、配置對(duì)象來(lái)進(jìn)行路由的配置
    • 路由切換: 可以通過<Link> Redirect進(jìn)行路由的切換
    • 路由加載: 可以同步記載,也可以異步加載,這樣就可以實(shí)現(xiàn)按需加載
  • 使用方式: 不僅可以在瀏覽器端的使用,而且可以在服務(wù)器端的使用

2.2 react-router具體實(shí)現(xiàn)

react-routerhistory庫(kù)的基礎(chǔ)上,實(shí)現(xiàn)了URLUI的同步,分為兩個(gè)層次來(lái)描述具體的實(shí)現(xiàn)。

組件層面描述實(shí)現(xiàn)過程

react-router中最主要的componentRouter RouterContext Link,history庫(kù)起到了中間橋梁的作用

image

browserHistory(一種history類型:一個(gè) history 知道如何去監(jiān)聽瀏覽器地址欄的變化, 并解析這個(gè) URL 轉(zhuǎn)化為 location 對(duì)象)為例 :

  • browserHistory進(jìn)行路由state管理,主要通過sessionStorage
//保存 路由state(router state)
function saveState(key, state) {
  ...
  window.sessionStorage.setItem(createKey(key), JSON.stringify(state));
}
//讀取路由state
function readState(key) {
  ...
  json = window.sessionStorage.getItem(createKey(key));
  return JSON.parse(json);
}

其中saveState函數(shù)傳進(jìn)來(lái)的state是個(gè)json對(duì)象,如:

{route: '/about'} ///假設(shè)此時(shí)的location為'/about'

進(jìn)行路由匹配,最終渲染對(duì)應(yīng)的組件

const About = React.createClass({/*...*/}) //About 組件
const Inbox = React.createClass({/*...*/}) //Inbox 組件
const Home = React.createClass({/*...*/}) //Home組件
 render() {
    let Child
    switch (this.state.route) {
      case '/about': Child = About; break;
      case '/inbox': Child = Inbox; break;
      default:      Child = Home;
    }

    return (
      <div>
        <h1>App</h1>
        <ul>
          <li><a href="#/about">About</a></li>
          <li><a href="#/inbox">Inbox</a></li>
        </ul>
        <Child/>
      </div>
    )
  }
})

React.render(<App />, document.body)

API層面描述實(shí)現(xiàn)過程

為了簡(jiǎn)單說明,只描述使用browserHistory的實(shí)現(xiàn),hashHistory的實(shí)現(xiàn)過程是類似的,就不在說明

image

2.3 用戶點(diǎn)擊了Link組件后路由系統(tǒng)中到底發(fā)生了哪些變化

Link 組件最終會(huì)渲染為 HTML 標(biāo)簽 <a>,它的 to、queryhash屬性會(huì)被組合在一起并渲染為 href 屬性。雖然 Link 被渲染為超鏈接,但在內(nèi)部實(shí)現(xiàn)上使用腳本攔截了瀏覽器的默認(rèn)行為,然后調(diào)用了history.pushState 方法

  • 系統(tǒng)會(huì)將上述 location對(duì)象作為參數(shù)傳入到 TransitionTo方法中,然后調(diào)用 window.location.hash 或者window.history.pushState() 修改了應(yīng)用的 URL,這取決于你創(chuàng)建history對(duì)象的方式。同時(shí)會(huì)觸發(fā)history.listen 中注冊(cè)的事件監(jiān)聽器。
  • 接下來(lái)請(qǐng)看路由系統(tǒng)內(nèi)部是如何修改UI 的。在得到了新的location對(duì)象后,系統(tǒng)內(nèi)部的 matchRoutes 方法會(huì)匹配出Route 組件樹中與當(dāng)前location對(duì)象匹配的一個(gè)子集,并且得到了 nextState,具體的匹配算法不在這里講解,感興趣的同學(xué)可以點(diǎn)擊查看,state 的結(jié)構(gòu)如下
nextState = {
  location, // 當(dāng)前的 location 對(duì)象
  routes, // 與 location 對(duì)象匹配的 Route 樹的子集,是一個(gè)數(shù)組
  params, // 傳入的 param,即 URL 中的參數(shù)
  components, // routes 中每個(gè)元素對(duì)應(yīng)的組件,同樣是數(shù)組
};

Router 組件的 componentWillMount 生命周期方法中調(diào)用了 history.listen(listener) 方法。listener 會(huì)在上述 matchRoutes 方法執(zhí)行成功后執(zhí)行listener(nextState),nextState對(duì)象每個(gè)屬性的具體含義已經(jīng)在上述代碼中注釋,接下來(lái)執(zhí)行this.setState(nextState) 就可以實(shí)現(xiàn)重新渲染 Router組件。舉個(gè)簡(jiǎn)單的例子,當(dāng) URL(準(zhǔn)確的說應(yīng)該是 location.pathname) 為 /archives/posts 時(shí),應(yīng)用的匹配結(jié)果如下圖所示

image.png

到這里,系統(tǒng)已經(jīng)完成了當(dāng)用戶點(diǎn)擊一個(gè)由 Link 組件渲染出的超鏈接到頁(yè)面刷新的全過程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容