3. react-router-dom源碼揭祕 - BrowserRouter

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

下面是我跟小S同窗一塊兒閱讀源碼的過程。 你們能夠參照這個思路,進行其餘開源項目源碼的學習。react

我: 小S,今天咱們來一塊兒學習React-router-dom的源碼吧git

好呀!

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

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

我: 先從這些組件的源碼入手,那確定第一個就是BrowserRouter,或者HashRouterweb

那應該怎麼入手呢?

我: 首先,從github上,獲得與文檔版本對應的代碼。
我: 接着看路徑結構。是這樣的:npm

接下來我通常就是找教程先簡單過一遍,代碼下下來而後把node__modules複製出來debugger 而後看不懂了就放棄

我: 不,你進入細節以前,要先搞清楚代碼的結構json

恩啊, 否則怎麼找代碼

我: 你看到這個路徑以後,第一步,應該看一看,這些文件夾都是幹啥的,哪一個是你須要的瀏覽器

script是build, website是doc, packges是功能
這個都差很少

我: 對。打開各個文件夾,會發現,packages裏面的東西,是咱們想要的源碼。react-router

我: 咱們確定先從源碼看起,由於此次讀源碼首先要學習的是實現原理,並非如何構建
我: 那我們就從react-router-dom開始唄
我: 打開react-router-dom,奔着modules去app

直接從github上下載master的分支麼

我:

爲啥看modules
不該該先看package.json和rollup麼

我: 核心代碼,確定是在modules裏了。我要先看看整個的結構,有個大體的印象

恩恩

我: 打開modules就看到了咱們剛剛文檔中說起的幾個組件了
我: 咱們先從BrowserRouter.js入手

嗯哼

我: 那我要打開這個文件,開始看代碼了
我: 我先不關注package.json這些配置文件

殘暴

我: 由於我此次是要看原理,不是看整個源碼如何build
我: 配置文件也是輔助而已

嗯啊。
但是有時候仍是很重要的

我: 那就用到了再說

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

我: 用到了什麼也不須要在package.json中看,由於我關注的那幾個組件,用到啥會import的。因此看源碼,最重要的是focus on。你要有關注點,由於有的源碼,是很是龐大的。一不當心就掉進了細節的海洋出不來了。

有道理
好比react

我: 對,你不可能一次就讀懂他裏面的東西,因此你要看不少次
我: 每次的關注點能夠不一樣

恩啊
確實如此

我: 都揉到一塊兒,會以爲很是亂,最後就放棄了
我: 並且,咱們學習源碼,也不必定要把源碼中的每一個特性都在同一個項目中都用到,仍是要分開學,分開用

有道理
我就總忍不住亂看

我: 那就先看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";
複製代碼
history是庫啊
等等
我有點沒跟上

我: 等待了30秒......
爲啥我感興趣這倆呢

你的興趣點對
我之前看過源碼相關教程,瞭解一點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

我: 哈哈,這個時候,我其實想看一眼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,這樣行不行
恩啊

我: 回到這個路徑

我: 去看react-router

爲何是他

我: 由於它導入包時,沒加相對路徑啊
我: 說明這是一個已經發布的node包,導入時須要在node_modules路徑下找

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

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

恩恩

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

我: 導入的是一個包,包下有index.js,我是否是應該先看這個js

我是這個習慣,先看index是否是隻作了import

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

沒太注意
稀裏糊塗

我: 因此,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了

我: 嗯,能夠的
我: 不過我感受略微跑偏了
我: 我要回到router自己上

好好
繼續
怎麼回到router自己

我: /react-router下,有一個router.js文件
我: 打開看,只有那兩行代碼,不是我要的東西啊
我: 它導出的,仍是index.js編譯以後的

看modules

我: 對,看modules
我: 打開modules下的Router.js

要是個人話, 這個時候就跑偏了
直接去看rollup了
而後最後找到router
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;
複製代碼
而後這麼少的代碼
第一反應看一下引入

我:
我: 可是你看,一共五個

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

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

我: 是的
我: 我如今其實有點關注第五個了

我會看render

我: 先不着急
我: 由於若是第五個的名字叫作warnXXXX
我: 是警告的意思

恩恩
搜一下

我: 警告一般都是開發版本的東西,若是能排除,那就剩第四個依賴了

可能沒用
再一看,是在__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了哈

而後呢
看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

我: 對。根據React-router的使用,估計就是每一個<Route>,都會監聽這個context,而後進行路徑匹配,決定是否要渲染本身的component屬性所指定的內容
我: 接下來,咱們就能夠繼續看這個組件了。先吃飯去吧,<Route>解讀,且聽下回分解。

嗯,好的。拜拜。
相關文章
相關標籤/搜索