React-Router底層原理分析與實現

用技術改變生活,用生活完善技術。來自攜程(旅悅)一枚向全棧方向努力奔跑的前端工程師。
微信同步:wDxKn89javascript

React-Router基本瞭解

對於React-Router是針對React定義的路由庫,用於將URL和component進行匹配。 React Router的簡單使用教程html

React-Router源碼分析

簡單前端路由的實現

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>router</title>
</head>
<body>
    <ul> 
        <li><a href="#/">turn white</a></li> 
        <li><a href="#/blue">turn blue</a></li> 
        <li><a href="#/green">turn green</a></li> 
    </ul> 
<script> function Router() { this.routes = {}; this.currentUrl = ''; } <!-- //針對不一樣的地址進行回調的匹配 //1:用戶在調用Router.route('address',function),在this.routes對象中進行記錄或者說address與function的匹配 --> Router.prototype.route = function(path, callback) { this.routes[path] = callback || function(){}; }; <!-- //處理hash的變化,針對不一樣的值,進行頁面的處理 //1:在init中註冊過事件,在頁面load的時候,進行頁面的處理 //2:在hashchange變化時,進行頁面的處理 --> Router.prototype.refresh = function() { this.currentUrl = location.hash.slice(1) || '/'; this.routes[this.currentUrl](); }; <!-- //1:在Router的prototype中定義init //2:在頁面load/hashchange事件觸發時,進行回調處理 //3:利用addEventListener來添加事件,注意第三個參數的用處 //4:bind的使用區別於apply/call的使用 --> Router.prototype.init = function() { window.addEventListener('load', this.refresh.bind(this), false); window.addEventListener('hashchange', this.refresh.bind(this), false); } window.Router = new Router();//在window對象中構建一個Router對象 window.Router.init();//頁面初始化處理 var content = document.querySelector('body'); // change Page anything function changeBgColor(color) { content.style.backgroundColor = color; } Router.route('/', function() { changeBgColor('white'); }); Router.route('/blue', function() { changeBgColor('blue'); }); Router.route('/green', function() { changeBgColor('green'); }); </script>
</body>
</html>
複製代碼

上面的路由系統主要由三部分組成前端

  1. Router.protopyte.init 用於頁面初始化(load)/頁面url變化 的事件註冊
  2. Router.protopyte.route 對路徑(address)和回調函數(function)的註冊並存放於Router中,爲load/hashchange使用
  3. Router.protopyte.refresh 針對不一樣的路徑(address)進行回調的處理

React-Router簡單實現

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>包裝方式</title>
</head>
<body>
<script> var body = document.querySelector('body'), newNode = null, append = function(str){ newNode = document.createElement("p"); newNode.innerHTML = str; body.appendChild(newNode); }; // 原對象(這裏能夠是H5的history對象) var historyModule = { listener: [], listen: function (listener) { this.listener.push(listener); append('historyModule listen.') }, updateLocation: function(){ append('historyModule updateLocation tirgger.'); this.listener.forEach(function(listener){ listener('new localtion'); }) } } // Router 將使用 historyModule 對象,並對其包裝 var Router = { source: {}, //複製historyModule到Router中 init: function(source){ this.source = source; }, //處理監聽事件,在Router對頁面進行處理時,利用historyModule中處理頁面 listen: function(listener) { append('Router listen.'); // 對 historyModule的listen進行了一層包裝 return this.source.listen(function(location){ append('Router listen tirgger.'); listener(location); }) } } // 將 historyModule 注入進 Router 中 Router.init(historyModule); // Router 註冊監聽 Router.listen(function(location){ append(location + '-> Router setState.'); }) // historyModule 觸發監聽回調(對頁面進行渲染等處理) historyModule.updateLocation(); </script>
</body>
</html>
複製代碼

其實上訴的操做就是隻是針對前端簡單路由+historyModule的升級處理。 其中的操做也是相似的。java

  1. Router.init(historyModule) ==> Router.protopyte.init
  2. Router.listen(function()) ==> Router.protopyte.route
  3. Router.updateLocation ==> Router.protopyte.refresh

React-Router代碼實現分析

因爲React-Router版本之間的處理方式有些差異,因此就按最新版本來進行分析。react

historyModule(history)的實現

//這裏針對react-router-dom中的BrowserRouter.js進行分析git

import warning from "warning";
import React from "react";
import PropTypes from "prop-types";
import { createBrowserHistory as createHistory } from "history";//這裏的history就是上面第二個例子中的historyModule
import Router from "./Router"; //對應第二個例子中的Router對象

/** * 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} />; } } export default BrowserRouter; 複製代碼

追蹤一下history的實現 文件路徑在源碼中的history中index.tsgithub

//定義一個接口
export interface History {
    length: number;
    action: Action;
    location: Location;
    push(path: Path, state?: LocationState): void;
    push(location: LocationDescriptorObject): void;
    replace(path: Path, state?: LocationState): void;
    replace(location: LocationDescriptorObject): void;
    go(n: number): void;
    goBack(): void;
    goForward(): void;
    block(prompt?: boolean): UnregisterCallback;
    listen(listener: LocationListener): UnregisterCallback;
    createHref(location: LocationDescriptorObject): Href;
}
複製代碼

除去interface這種類型,是否是對History中定義的屬性有點熟悉。window.history微信

listen函數的註冊

React-Router/Router.js前端工程師

/** * The public API for putting history on context. //這裏的道理相似於例子二中第二步 */
class Router extends React.Component {
  
  static childContextTypes = {
    router: PropTypes.object.isRequired
  };

  getChildContext() {
    return {
      router: {
        ...this.context.router,
        history: this.props.history,
        route: {
          location: this.props.history.location,
          match: this.state.match
        }
      }
    };
  }

  state = {
    match: this.computeMatch(this.props.history.location.pathname)
  };

  computeMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  }

  componentWillMount() {
    const { children, history } = this.props;
    // Do this here so we can setState when a <Redirect> changes the
    // location in componentWillMount. This happens e.g. when doing
    // server rendering using a <StaticRouter>.
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }

  componentWillReceiveProps(nextProps) {
    warning(
      this.props.history === nextProps.history,
      "You cannot change <Router history>"
    );
  }

  componentWillUnmount() {
    this.unlisten();
  }

  render() {
    const { children } = this.props;
    return children ? React.Children.only(children) : null;
  }
}

export default Router;

複製代碼

上面須要有幾處須要注意的地方react-router

  1. React-Router是利用React的Context進行組件間通訊的。childContextTypes/getChildContext
  2. 須要特別主要componentWillMount,也就是說在Router組件還未加載以前,listen已經被註冊。其實這一步和第一個例子中的init道理是相似的。
  3. 在componentWillUnmount中將方法進行註銷,用於內存的釋放。
  4. 這裏提到了 ,其實就是 用於url和組件的匹配。

瞭解Redirect.js

react-router/Redirect.js

//這裏省去其餘庫的引用
import generatePath from "./generatePath";
/** * The public API for updating the location programmatically * with a component. */
class Redirect extends React.Component {
//這裏是從Context中拿到history等數據
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,
        replace: PropTypes.func.isRequired
      }).isRequired,
      staticContext: PropTypes.object
    }).isRequired
  };

  isStatic() {
    return this.context.router && this.context.router.staticContext;
  }

  componentWillMount() {
    invariant(
      this.context.router,
      "You should not use <Redirect> outside a <Router>"
    );

    if (this.isStatic()) this.perform();
  }

  componentDidMount() {
    if (!this.isStatic()) this.perform();
  }

  componentDidUpdate(prevProps) {
    const prevTo = createLocation(prevProps.to);
    const nextTo = createLocation(this.props.to);

    if (locationsAreEqual(prevTo, nextTo)) {
      warning(
        false,
        `You tried to redirect to the same route you're currently on: ` +
          `"${nextTo.pathname}${nextTo.search}"`
      );
      return;
    }

    this.perform();
  }

  computeTo({ computedMatch, to }) {
    if (computedMatch) {
      if (typeof to === "string") {
        return generatePath(to, computedMatch.params);
      } else {
        return {
          ...to,
          pathname: generatePath(to.pathname, computedMatch.params)
        };
      }
    }

    return to;
  }
 //進行路由的匹配操做
  perform() {
    const { history } = this.context.router;
    const { push } = this.props;
    //Router中拿到須要跳轉的路徑,而後傳遞給history
    const to = this.computeTo(this.props);

    if (push) {
      history.push(to);
    } else {
      history.replace(to);
    }
  }

  render() {
    return null;
  }
}

export default Redirect;

複製代碼

note :

  1. 針對h5的history來說,push/replace只是將url進行改變,可是不會觸發popstate事件

generatePath函數的處理

//該方法只是對路徑進行處理
/** * Public API for generating a URL pathname from a pattern and parameters. */
const generatePath = (pattern = "/", params = {}) => {
  if (pattern === "/") {
    return pattern;
  }
  const generator = compileGenerator(pattern);
  return generator(params);
};
複製代碼

針對路徑進行頁面渲染處理

須要看一個Router的結構

//這裏的Router只是一個容器組件,用於從Redux/react中獲取數據,而真正的路徑/組件信息存放在Route中
 <Router>
      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/topics" component={Topics}/>
  </Router>
複製代碼

看一下Route對組件的處理

/** * The public API for matching a single path and rendering. */
class Route extends React.Component {
    //從Router中獲取信息
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.object.isRequired,
      route: PropTypes.object.isRequired,
      staticContext: PropTypes.object
    })
  };
//本身定義了一套Contex用於子組件的使用
  static childContextTypes = {
    router: PropTypes.object.isRequired
  };
//本身定義了一套Contex用於子組件的使用
  getChildContext() {
    return {
      router: {
        ...this.context.router,
        route: {
          location: this.props.location || this.context.router.route.location,
          match: this.state.match
        }
      }
    };
  }

  state = {
    match: this.computeMatch(this.props, this.context.router)// matching a URL pathname to a path pattern.若是不匹配,返回null,也就是找不到頁面信息
  };
  render() {
    const { match } = this.state;
    const { children, component, render } = this.props;//從Router結構中獲取對應的處理方法
    const { history, route, staticContext } = this.context.router;//從Context中獲取數據
    const location = this.props.location || route.location;
    const props = { match, location, history, staticContext };
    //若是頁面匹配成功,進行createElement的渲染。在這裏就會調用component的render===>頁面刷新 這是處理第一次頁面渲染
    if (component) return match ? React.createElement(component, props) : null;
    //這裏針對首頁已經被渲染,在進行路由處理的時候,根據props中的信息,進行頁面的跳轉或者刷新
    if (render) return match ? render(props) : null;

    return null;
  }
}

export default Route;

複製代碼

Buzzer

針對React-Router來說,其實就是對H5的History進行了一次封裝,使可以識別將url的變化與componet渲染進行匹配。

  1. 根據BrowserRouter等不一樣的API針對H5的history的重構
  2. 結構的構建,同時對history屬性進行註冊。
  3. 在Router的componentWillMount中註冊history的事件回調。
  4. 在Redirect中進行路徑的計算,調用history.push/history.replace等更新history信息。
  5. Route中根據計算的匹配結果,進行頁面首次渲染/頁面更新渲染處理。
相關文章
相關標籤/搜索