3. react-router-dom原始碼揭祕 - BrowserRouter

今天開始,我們開始揭開react-router-dom神祕的頭蓋骨,哦不,面紗。 在此之前,我們需要了解一些預備知識:React的context和react-router-dom的基本使用。需要複習的同學請移步:

下面是我跟小S同學一起閱讀原始碼的過程。 大家可以參照這個思路,進行其他開源專案原始碼的學習。

我:小S,今天我們來一起學習React-router-dom的原始碼吧

好呀!
好呀!

我:首先,react-router的官網上,有基本的使用方法。這裡 (中文點選這裡) 列出了常用的元件,以及它們的用法

  1. Router (BrowserRouter, HashRouter)
  2. Route
  3. Switch
  4. Link
好的, 繼續
好的, 繼續

我:先從這些元件的原始碼入手,那肯定第一個就是BrowserRouter,或者HashRouter

那應該怎麼入手呢?
那應該怎麼入手呢?

我:首先,從 github 上,得到與文件版本對應的程式碼。

我:接著看路徑結構。是這樣的:

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter
接下來我一般就是找教程先簡單過一遍,程式碼下下來然後把node__modules複製出來debugger 然後看不懂了就放棄
接下來我一般就是找教程先簡單過一遍,程式碼下下來然後把node__modules複製出來debugger 然後看不懂了就放棄

我:不,你進入細節之前,要先搞清楚程式碼的結構

恩啊, 不然怎麼找程式碼
恩啊, 不然怎麼找程式碼

我:你看到這個路徑之後,第一步,應該看一看,這些文件夾都是幹啥的,哪個是你需要的

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

script是build, website是doc, packges是功能

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

這個都差不多

我:對。開啟各個文件夾,會發現,packages裡面的東西,是我們想要的原始碼。

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

我:我們肯定先從原始碼看起,因為這次讀原始碼首先要學習的是 實現原理 ,並不是如何構建

我:那咱們就從 react-router-dom 開始唄

我:開啟react-router-dom,奔著modules去

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter
直接從github上下載master的分支麼
直接從github上下載master的分支麼

我: 嗯

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

為啥看modules

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

不應該先看package.json和rollup麼

我:核心程式碼,肯定是在modules裡了。我要先看看整個的結構,有個大致的印象

恩恩
恩恩

我:開啟modules就看到了我們剛剛文件中提及的幾個元件了

我:我們先從 BrowserRouter.js 入手

嗯哼
嗯哼

我:那我要開啟這個文件,開始看程式碼了

我:我先不關注package.json這些配置文件

殘暴
殘暴

我:因為我這次是要看原理,不是看整個原始碼如何build

我:配置文件也是輔助而已

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

嗯啊。

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

可是有時候還是很重要的

我:那就用到了再說

是不是至少看一下都用了什麼和幾個入口
是不是至少看一下都用了什麼和幾個入口

我:用到了什麼也不需要在package.json中看,因為我關注的那幾個元件,用到啥會import的。所以看原始碼,最重要的是focus on。你要有關注點,因為有的原始碼,是非常龐大的。一不小心就掉進了細節的海洋出不來了。

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

有道理

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

比如react

我:對,你不可能一次就讀懂他裡面的東西,所以你要看很多次

我:每次的關注點可以不同

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

恩啊

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

確實如此

我:都揉到一起,會覺得非常亂,最後就放棄了

我:而且,我們學習原始碼,也不一定要把原始碼中的每個特性都在同一個專案中都用到,還是要分開學,分開用

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

有道理

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

我就總忍不住亂看

我:那就先看 BrowserRouter.js 了。

我:開啟文件,看了一下,挺開心,程式碼沒幾行

import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";

/**
 * The public API for a <Router> that uses HTML5 history.
 */
class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

if (__DEV__) {
  //此處省略若干行程式碼
}

export default BrowserRouter;
複製程式碼
然後一臉懵逼記不住, 看不懂
然後一臉懵逼記不住, 看不懂

我:哈哈,程式碼這麼少,那肯定是有依賴元件了

我:先看看依賴了哪些元件

我:我最感興趣的是history和react-router。如下:

import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
複製程式碼
3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

history是庫啊

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

等等

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

我有點沒跟上

我:等待了30秒......

為啥我感興趣這倆呢

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

你的興趣點對

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

我以前看過原始碼相關教程,瞭解一點history

我:嗯。官網說了啊。

Routers

At the core of every React Router application should be a router component. For web projects, react-router-dom provides and routers. Both of these will create a specialized history object for you.

我:在實現路由的時候,肯定是用到history的

我:所以,這個可能會作為讀原始碼的預備知識。( 如果夥伴們有需求,請在評論中說明,我們可以再加一篇關於history的文章 )

我:但是我先不管他,看看影響react-router的閱讀不

我:另外,之前說過,這個文件原始碼行數很少,肯定依賴了其他的元件。看起來,這個react-router擔當了重要職責。

我:所以現在有兩個Todos: historyreact-router

嗯

我:那一會需要關注的就是react-router這個包了

我:我暫時先不管剛才的兩個todos,我把這個元件( BrowserRouter )先看看,反正程式碼又不多

class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

if (__DEV__) {
  //此處省略若干行程式碼
}
複製程式碼

我:我要把if(__DEV__)的分支略過,因為我現在要看的是最最核心的東西

我:切記過早的進入__DEV__,那個是方便開發用的,通常與核心的概念關係不大

我:那就只剩倆東西了

//......
class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}
//......
複製程式碼

我:所以現在BrowserRouter的任務,就是建立一個history物件,傳給react-router的<Router>元件

這個時候
這個時候

我:嗯,你說

你會選擇看react-router還是history
你會選擇看react-router還是history

我:哈哈,這個時候,我其實想看一眼HashRouter

我也是
我也是

我:因為import的那句話

import { createBrowserHistory as createHistory } from "history";
複製程式碼

所以我有理由懷疑,HashRouter的程式碼類似,只是從history包中匯入了不同的函式

HashRouter.js

import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
複製程式碼

還真是

我:那我就把關注點,放在react-router上了

我:因為

  1. history我猜出他是幹啥了,跟瀏覽器路徑有關
  2. router裡面如果用到history了,我等到在讀碼時,遇到了阻礙,再去看history,這樣行不行
恩啊
恩啊

我:回到這個路徑

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

我:去看react-router

為什麼是他
為什麼是他

我:因為它匯入包時,沒加相對路徑啊

我:說明這是一個已經發布的node包,匯入時需要在node_modules路徑下找

import { Router } from "react-router";
複製程式碼

我:我就往上翻一翻唄,當然,估計在配置文件中,應該會有相關配置

恩恩
恩恩

我:進這個路徑,文件真tmd多,mmp的

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter
我:

匯入的是一個包,包下有index.js,我是不是應該先看這個js

我是這個習慣,先看index是不是隻做了import
我是這個習慣,先看index是不是隻做了import

我:但是其實我們在使用recat-router-dom的時候,網上會有一些與react-router的比較的討論,

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

沒太注意

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

稀裡糊塗

我:所以,react-router是一個已經發布的node包。但是,我並不確定他的程式碼在哪,如果找不到,我可能會從github上其他的位置找,或者從npm的官網找連結了

恩啊
恩啊

我:進index.js吧

"use strict";

if (process.env.NODE_ENV === "production") {
  module.exports = require("./cjs/react-router.min.js");
} else {
  module.exports = require("./cjs/react-router.js");
}
複製程式碼

我:程式碼不多,分成production和else倆分支

我:我會選擇else分支

我:但是發現一個問題啊,我艹

我:當前路徑下,沒有cjs文件夾

我:因為BrowserRouter匯入的是一個包

我:所以這個包,得是build之後的

這個時候就要看packge的script了
這個時候就要看packge的script了

我:嗯,可以的

我:不過我感覺略微跑偏了

我:我要回到router本身上

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

好好

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

繼續

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

怎麼回到router本身

我:/react-router下,有一個router.js文件

我:開啟看,只有那兩行程式碼,不是我要的東西啊

我:它匯出的,還是index.js編譯之後的

看modules
看modules

我:對,看modules

我:開啟modules下的Router.js

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

要是我的話, 這個時候就跑偏了

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

直接去看rollup了

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

然後最後找到router

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

router.js

我:我也可能會跑偏

我:我之前就跑到history上去了

我:但是後來想想,這樣不太好

我:從看原始碼角度說,直接找到modules下的Router.js很容易

我:因為其他文件,一看就不是原始碼實現

嗯啊
嗯啊

我:現在開啟它,一看,挺像啊,那先看看有多少行

我:百十來行,有信心了,哈哈

import React from "react";
import PropTypes from "prop-types";
import warning from "tiny-warning";

import RouterContext from "./RouterContext";
import warnAboutGettingProperty from "./utils/warnAboutGettingProperty";

function getContext(props, state) {
  return {
    history: props.history,
    location: state.location,
    match: Router.computeRootMatch(state.location.pathname),
    staticContext: props.staticContext
  };
}

/**
 * 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
    };

    // This is a bit of a hack. We have to start listening for location
    // changes here in the constructor in case there are any <Redirect>s
    // on the initial render. If there are, they will replace/push when
    // they mount and since cDM fires in children before parents, we may
    // get a new location before the <Router> is mounted.
    this._isMounted = false;
    this._pendingLocation = null;

    if (!props.staticContext) {
      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() {
    const context = getContext(this.props, this.state);

    return (
      <RouterContext.Provider
        children={this.props.children || null}
        value={context}
      />
    );
  }
}

// TODO: Remove this in v5
if (!React.createContext) {
  Router.childContextTypes = {
    router: PropTypes.object.isRequired
  };

  Router.prototype.getChildContext = function() {
    const context = getContext(this.props, this.state);

    if (__DEV__) {
      const contextWithoutWarnings = { ...context };

      Object.keys(context).forEach(key => {
        warnAboutGettingProperty(
          context,
          key,
          `You should not be using this.context.router.${key} directly. It is private API ` +
            "for internal use only and is subject to change at any time. Instead, use " +
            "a <Route> or withRouter() to access the current location, match, etc."
        );
      });

      context._withoutWarnings = contextWithoutWarnings;
    }

    return {
      router: context
    };
  };
}

if (__DEV__) {
  Router.propTypes = {
    children: PropTypes.node,
    history: PropTypes.object.isRequired,
    staticContext: PropTypes.object
  };

  Router.prototype.componentDidUpdate = function(prevProps) {
    warning(
      prevProps.history === this.props.history,
      "You cannot change <Router history>"
    );
  };
}

export default Router;
複製程式碼
3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

然後這麼少的程式碼

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

第一反應看一下引入

我:對

我:但是你看,一共五個

import React from "react";
import PropTypes from "prop-types";
import warning from "tiny-warning";

import RouterContext from "./RouterContext";
import warnAboutGettingProperty from "./utils/warnAboutGettingProperty";
複製程式碼
前三個忽略,一看就沒用
前三個忽略,一看就沒用

我:是的

我:我現在其實有點關注第五個了

我會看render
我會看render

我:先不著急

我:因為如果第五個的名字叫做warnXXXX

我:是警告的意思

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

恩恩

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

搜一下

我:警告通常都是開發版本的東西,如果能排除,那就剩第四個依賴了

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

可能沒用

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

再一看,是在__DEV__裡面的

我:對,當前文件搜尋了一下,在__DEV__分支下,不看了,哈哈

我:那就剩一個context.js了唄

過分
過分

我:我覺得我現在想掃一眼這個文件,如果內容不多,我就先搞他,如果多的話,那就先放那

恩恩
恩恩

我:那我去看一看吧,哈哈

我:進RouterContext.js這個文件了

// TODO: Replace with React.createContext once we can assume React 16+
import createContext from "create-react-context";

const context = createContext();

context.Provider.displayName = "Router.Provider";
context.Consumer.displayName = "Router.Consumer";

export default context;
複製程式碼

我:我次奧了

狗

我:十行不到,我把他搞定,我就可以專注Router.js那個文件了。那個文件裡面的內容,就是全部Router的核心了

我:這裡是標準context用法,店長推薦的,參見這個

我:返回Router.js了哈

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

然後呢

3. react-router-dom原始碼揭祕 - BrowserRouter
3. react-router-dom原始碼揭祕 - BrowserRouter

看createContext麼

我:createContex就是最新的context用法,參見這個

我:所以,需要有準備知識,哈哈

我:簡單點說,就是一個提供者(Provider),一個是消費者(Consumer)

我:我這次看的是react-router

我:別跑偏了

我:回到router.js去了

我:這個時候,可以稍微進入細節一些了

我:從第一個函式定義開始

function getContext(props, state) {
  return {
    history: props.history,
    location: state.location,
    match: Router.computeRootMatch(state.location.pathname),
    staticContext: props.staticContext
  };
}
複製程式碼

我:從名字看,是獲取context的,每次呼叫返回一個 新建立 的物件,多餘的不知道,先放著,往後看

嗯

我:我先大概掃一眼元件都有哪些方法。另外發現,除了元件,還有其他程式碼

我:除了元件內容,元件下面有一個判斷,看起來應該是處理老版本react的相容問題的。那我就先不看了

// TODO: Remove this in v5
if (!React.createContext) {
  Router.childContextTypes = {
    router: PropTypes.object.isRequired
  };

  Router.prototype.getChildContext = function() {
    const context = getContext(this.props, this.state);

    if (__DEV__) {
      const contextWithoutWarnings = { ...context };

      Object.keys(context).forEach(key => {
        warnAboutGettingProperty(
          context,
          key,
          `You should not be using this.context.router.${key} directly. It is private API ` +
            "for internal use only and is subject to change at any time. Instead, use " +
            "a <Route> or withRouter() to access the current location, match, etc."
        );
      });

      context._withoutWarnings = contextWithoutWarnings;
    }

    return {
      router: context
    };
  };
}
複製程式碼

我:所以,重點就是在這個元件裡面了。元件裡面就是一些生命週期函式

我:constructor、componentDidMount

我:這倆,是初始化的地方

嗯嗯
嗯嗯

我:一個一個看

我:重點是那個判斷

if (!props.staticContext) {
  this.unlisten = props.history.listen(location => {
    if (this._isMounted) {
      this.setState({ location });
    } else {
      this._pendingLocation = location;
    }
  });
}
複製程式碼

我:if (!props.staticContext) {}的作用,是保證Router裡面再巢狀Router時,使用的是相同的history

我:裡面是一個監聽,監聽history中的location的改變,也就是說,當通過這個history改變路徑時,會統一監聽,統一處理

嗯嗯
嗯嗯

我:那裡面就呼叫了setState了唄,接著render就執行了

嗯

我:render非常簡單,就是把context的value值,修改了一下

嗯啊
嗯啊

我:我們知道,只要context的value一變化,對應的consumer的函式,就會被呼叫,是吧

嗯嗯
嗯嗯

我:那現在Router就結束了

我:接下來,我們好奇的是,哪些元件使用了Consumer

找route
找route

我:對。根據React-router的使用,估計就是每個<Route>,都會監聽這個context,然後進行路徑匹配,決定是否要渲染自己的component屬性所指定的內容

我:接下來,我們就可以繼續看這個元件了。先吃飯去吧,<Route>解讀,且聽下回分解。

嗯,好的。拜拜。
嗯,好的。拜拜。