圖解 React-router 源碼
首先,我們先不糾結於源碼細節。先用最簡單的話來概括一下 React-router 到底做了什麼?
本質上, React-Router 就是在頁面 URL 發生變化的時候,通過我們寫的 path 去匹配,然後渲染對應的組件。
那麼,從這句話,我們想一下如何分步驟實現:
-
如何監聽 url 的變化 ?
-
如何匹配 path,按什麼規則 ?
-
渲染對應的組件
瞭解好需要實現的關鍵步驟,我們來將倉庫源碼下載下來。
接下來我們看一下 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 個關鍵步驟,如下:
-
如何監聽 url 的變化 ?
-
如何匹配 path ?
-
渲染對應的組件
我們不自己來實現,直接看源碼,站在巨人的肩膀上來學習😄。接下來我們來看一下 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
從代碼中,我們可以觀察到下面幾點:
-
最外層包裹了
<BrowserRouter>
,它有什麼意義? -
在
<Route />
匹配的外層,包裹了<Switch>
,作用是如果匹配了一個,則不會再繼續渲染另外一個。如何實現? -
Route 中有 path 匹配路徑,包裹的則是渲染的組件。
整體設計
我們用一張圖來理解一下整個 react-router 是怎麼實現的:
接下來我們看看每一個步驟是怎麼實現的。
一、監聽 URL 的變化
正常情況下,當 URL 發生變化時,瀏覽器會像服務端發送請求,但使用以下 2 種辦法不會向服務端發送請求:
-
基於 hash
-
基於 history
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. 是否匹配
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 來寫,非要寫這麼多變態的 ?:,請不要學習,除非你的項目只有你一個前端😂。
一下子看不懂也沒關係,我們來看下面的流程圖。
從上面的代碼我們可以看出:
-
Router 渲染的優先級:children > component > render,三種方式互斥,只能使用一種。
-
不匹配的情況下,只要 children 是函數,也會渲染
-
component 是使用 createComponent 來創建的, 這會導致不再更新現有組件,而是直接卸載再去掛載一個新的組件。如果是使用匿名函數來傳入 component ,每次 render 的時候,這個 props 都不同,會導致重新渲染掛載組件,導致性能特別差。因此,當使用匿名函數的渲染時,請使用 render 或 children 。
1// 不要這麼使用
2<Route path="/user/:username" component={() => <User/> } />
3
4
結論
-
對於依賴關係大,同類型的包使用 lerna 來統一管理。儘量抽象出共用不可變的地方,比如 react-router 中的方法。
-
React-router 使用了 Compound components(複合組件模式),在這種模式中,組件將被一起使用,它們可以方便的共享一種隱式的狀態,比如 Switch , 可以在這裏通過 React.children 來控制包裹組件的渲染優先級,而無須使用者去控制。再比如我們經常使用的
<select />
和<option>
, 可以通過 React.children 和 React.cloneElement 來劫持修改子組件,讓組件使用者通過更少的 api 來觸發更強大的功能。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/OwyUU4ikmjQQpmSs4l3Bfg