圖解 React-router 源碼


首先,我們先不糾結於源碼細節。先用最簡單的話來概括一下 React-router 到底做了什麼?

本質上, React-Router 就是在頁面 URL 發生變化的時候,通過我們寫的 path 去匹配,然後渲染對應的組件。

那麼,從這句話,我們想一下如何分步驟實現:

  1. 如何監聽 url 的變化 ?

  2. 如何匹配 path,按什麼規則 ?

  3. 渲染對應的組件

瞭解好需要實現的關鍵步驟,我們來將倉庫源碼下載下來。

接下來我們看一下 GitHub, 它使用 lerna 管理同時管理多個包. 也就是 Multirepo 概念。

react-router 使用 lerna 來同時管理多個包. (lerna  的好處特別多,對於依賴關係大,同類型的包推薦使用 lerna 來統一管理。)

核心庫是 react-router. react-router-dom 是在瀏覽器中使用的,react-router-native 是在 rn 中使用的。

如果不理解,直接看一下源碼就懂了。其實 react-router-dom 只是多了下面四個組件 BrowserRouter、 Link、NavLink、HashRouter, 其他其實都是直接引用 react-router 的。

瞭解完多包的組織關係之後,我們回到前面如何實現 react-router 的 3 個關鍵步驟,如下:

  1. 如何監聽 url 的變化 ?

  2. 如何匹配 path ?

  3. 渲染對應的組件

我們不自己來實現,直接看源碼,站在巨人的肩膀上來學習😄。接下來我們來看一下 react-router-dom 官方文檔 的基本使用。

 1export default function App() {
 2  return (
 3    <BrowserRouter>
 4      <div>
 5         <Link to="/">Home</Link>
 6         <Link to="/about">About</Link>
 7         <Link to="/topics">Topics</Link>
 8        <Switch>
 9          <Route path="/about">
10            <About />
11          </Route>
12          <Route path="/">
13            <Home />
14          </Route>
15        </Switch>
16      </div>
17    </BrowserRouter>
18  );
19}
20
21

從代碼中,我們可以觀察到下面幾點:

  1. 最外層包裹了<BrowserRouter> ,它有什麼意義?

  2. <Route />匹配的外層,包裹了<Switch>,作用是如果匹配了一個,則不會再繼續渲染另外一個。如何實現?

  3. Route 中有 path 匹配路徑,包裹的則是渲染的組件。

整體設計

我們用一張圖來理解一下整個 react-router 是怎麼實現的:

接下來我們看看每一個步驟是怎麼實現的。

一、監聽 URL 的變化

正常情況下,當 URL 發生變化時,瀏覽器會像服務端發送請求,但使用以下 2 種辦法不會向服務端發送請求:

react-router 使用了 history 這個核心庫。

1. 選擇方式:  history 或 hash

HashRouter 先是從 history 中引用 createBrowserHistory ,然後將 history 和 children 傳入到 Router 。BrowseHistory 同理。

BrowseHistory 必須依賴服務器讓 url 都映射到 index.html ,否則會 404 。

2. 監聽 URL 的變化,拿到對應的 history,location,match 等通過 Provider 注入到子組件中。

二、Route 中匹配渲染組件

這代碼可以分兩部分理解:

  1. 是否匹配

  2. 渲染組件

1. 是否匹配

computedMatch 是使用 Switch 包裹的子組件纔有的值,Switch 的作用是從上到下開始渲染,只要匹配到一個,其他的就不匹配。所以這裏會先判斷 computedMatch 。

匹配解析 path ,這裏使用了第三方庫  path-to-regexp
1// Make sure you consistently `decode` segments.
2const fn = match("/user/:id", { decode: decodeURIComponent });
3
4fn("/user/123"); //=> { path: '/user/123', index: 0, params: { id: '123' } }
5fn("/invalid"); //=> false
6fn("/user/caf%C3%A9"); //=> { path: '/user/caf%C3%A9', index: 0, params: { id: 'café' } }
7
2. 組件渲染方式

從文檔來看,它支持三種方式的渲染,如下:

 1// children 方式
 2<Route exact path="/">
 3   <HomePage />
 4</Route>
 5
 6/
 7
 8/ func 方式
 9<Route
10   path="/blog/:slug"
11   render={({ match }) => {
12     // Do whatever you want with the match...
13     return <div />;
14   }}
15/>
16
17// component 方式
18<Route path="
19
20/user/:username" component={User} />
21
22

源碼部分如下:

吐槽一下,作者怎麼就不能好好用 if else 來寫,非要寫這麼多變態的 ?:,請不要學習,除非你的項目只有你一個前端😂。

一下子看不懂也沒關係,我們來看下面的流程圖。

從上面的代碼我們可以看出:

  1. Router 渲染的優先級:children >  component > render,三種方式互斥,只能使用一種。

  2. 不匹配的情況下,只要 children 是函數,也會渲染

  3. component 是使用 createComponent 來創建的, 這會導致不再更新現有組件,而是直接卸載再去掛載一個新的組件。如果是使用匿名函數來傳入 component ,每次 render 的時候,這個 props 都不同,會導致重新渲染掛載組件,導致性能特別差。因此,當使用匿名函數的渲染時,請使用 render 或 children 。

1// 不要這麼使用
2<Route path="/user/:username" component={() => <User/> } />
3
4

結論

  1. 對於依賴關係大,同類型的包使用 lerna 來統一管理。儘量抽象出共用不可變的地方,比如 react-router 中的方法。

  2. React-router 使用了 Compound components(複合組件模式),在這種模式中,組件將被一起使用,它們可以方便的共享一種隱式的狀態,比如 Switch , 可以在這裏通過 React.children 來控制包裹組件的渲染優先級,而無須使用者去控制。再比如我們經常使用的 <select /><option>, 可以通過 React.children 和 React.cloneElement 來劫持修改子組件,讓組件使用者通過更少的 api 來觸發更強大的功能。



本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/OwyUU4ikmjQQpmSs4l3Bfg