react-router路由機制

原理

無刷新的更改地址欄地址 ,保證視圖和URL的同步?;驹硎荋5History API 。
瀏覽器的歷史記錄,以棧的形式存儲,后進先出,按照棧的規律,必須有的方法:進棧(pushstate)、出棧(popstate)、替換當前的(replacestate) 。這里對H5History API就不做詳細解釋了,可以查看這篇文章。

實現過程


當我們點擊了Link組件或者調用了history.push跳轉路由時,react-router做了上圖的處理流程。

假定使用BrowserRoutercreateBrowserHistory,使用history.push跳轉路由

1、調用history.push跳轉路由時,內部執行window.history.pushState在瀏覽器history棧中新增一條記錄,修改了應用的 URL,執行<Router></Router>組件注冊的回調函數。
2、createBrowserHistory中注冊popstate事件,用戶點擊瀏覽器前進、回退時,在popstate事件中獲取當前的event.state,重新組裝一個location,執行<Router></Router>組件注冊的回調函數。
3、history庫對外暴露createBrowserHistory方法,react-router中實例化createBrowserHistory方法對象,在<Router>組件中注冊history.listen()回調函數,當路由有變化時,<Route>組件中匹配location,同步UI。

分析

1、history.push
在react中,我們可以調用history.push(path,state)來跳轉路由,實際執行的就是createBrowserHistory中的push方法
在這個方法中主要做三件事:
一、根據傳遞的path,state參數創建一個location,不同于window.location。(具體的可以看代碼,點擊上面的createBrowserHistory

location = {
  pathname, // 當前路徑,即 Link 中的 to 屬性
  search, // search
  hash, // hash
  state, // state 對象
  action, // location 類型,在點擊 Link 時為 PUSH,瀏覽器前進后退時為 POP,調用 replaceState 方法時為 REPLACE
  key, // 用于操作 sessionStorage 存取 state 對象
};

 const location = createLocation(path, state, createKey(), history.location);
//生成location
createLocation(path, state, key, currentLocation) {
  let location;
  if (typeof path === 'string') {
    // Two-arg form: push(path, state)
    location = parsePath(path);
    location.state = state;
  } else {
    // One-arg form: push(location)
    location = { ...path };

    if (location.pathname === undefined) location.pathname = '';

    if (location.search) {
      if (location.search.charAt(0) !== '?')
        location.search = '?' + location.search;
    } else {
      location.search = '';
    }

    if (location.hash) {
      if (location.hash.charAt(0) !== '#') location.hash = '#' + location.hash;
    } else {
      location.hash = '';
    }

    if (state !== undefined && location.state === undefined)
      location.state = state;
  }

  if (key) location.key = key;

  if (currentLocation) {
    // Resolve incomplete/relative pathname relative to current location.
    if (!location.pathname) {
      location.pathname = currentLocation.pathname;
    } else if (location.pathname.charAt(0) !== '/') {
      location.pathname = resolvePathname(
        location.pathname,
        currentLocation.pathname
      );
    }
  } else {
    // When there is no prior location and pathname is empty, set it to /
    if (!location.pathname) {
      location.pathname = '/';
    }
  }

  return location;
}

這個location會在<Router><Route>組件中使用,來根據location中的值和<Route path='xxx'></Route>中的path匹配,匹配成功的Route組件渲染指定的component;
二、執行globalHistory.pushState({ key, state }, null, href),添加一條記錄;并執行 setState({ action, location })

三、在setState({ action, location })中執行Router中注冊的listener,transitionManager.notifyListeners(history.location, history.action)。

  function setState(nextState) {
    Object.assign(history, nextState);
    history.length = globalHistory.length;
    transitionManager.notifyListeners(history.location, history.action);
  }
  function notifyListeners(...args) {
    listeners.forEach(listener => listener(...args));
  }

2、popstate事件
popstate事件觸發時,可以得到event.state,createBrowserHistory中會根據這個state和當前window.location重新生成一個location對象,執行Router組件注冊的listener,同步UI。
3、<Router>組件
BrowserRouter組件中會實例化一個createBrowserHistory對象,傳遞給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";

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

Router組件中要注冊history.listen()的一個監聽函數,并且保存一份子組件(Route)使用的數據

  Router.prototype.componentWillMount = function componentWillMount() {
    var _this2 = this;

    var _props = this.props,
        children = _props.children,
        history = _props.history;
    (0, _invariant2.default)(children == null || _react2.default.Children.count(children) === 1, "A <Router> may have only one child element");

    // 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(function () {
      _this2.setState({
        match: _this2.computeMatch(history.location.pathname)
      });
    });
  };
  Router.prototype.getChildContext = function getChildContext() {
    return {
      router: _extends({}, this.context.router, {
        history: this.props.history,
        route: {
          location: this.props.history.location,
          match: this.state.match
        }
      })
    };
  };

當調用history.push或觸發popstate事件時,這里注冊的listener都會被createBrowserHistory執行,觸發setState,然后Router的子組件中匹配的<Route>會重新渲染.
4、<Route>組件
在Route中有一個match狀態,在父組件props發生變化時會重新計算

  Route.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps, nextContext) {
    (0, _warning2.default)(!(nextProps.location && !this.props.location), '<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.');

    (0, _warning2.default)(!(!nextProps.location && this.props.location), '<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.');

    this.setState({
      match: this.computeMatch(nextProps, nextContext.router)
    });
  };

//computeMatch主要工作就是匹配當前組件上指定的path和當前瀏覽器的路徑是否一致,一致就渲染組件

  Route.prototype.computeMatch = function computeMatch(_ref, router) {
    var computedMatch = _ref.computedMatch,
        location = _ref.location,
        path = _ref.path,
        strict = _ref.strict,
        exact = _ref.exact,
        sensitive = _ref.sensitive;

    if (computedMatch) return computedMatch; // <Switch> already computed the match for us

    (0, _invariant2.default)(router, "You should not use <Route> or withRouter() outside a <Router>");

    var route = router.route;

    var pathname = (location || route.location).pathname;

    return (0, _matchPath2.default)(pathname, { path: path, strict: strict, exact: exact, sensitive: sensitive }, route.match);
  };

總結

總結一下,react-router的路由機制就是:
1、借助history庫,history中實現了push、go、goBack等方法,注冊了popstate事件,當路由跳轉時,使用瀏覽器內置的history api 操作 history棧。
2、history庫對外暴露的history對象提供了listen方法,<Router></Router>組件會注冊一個listener;
3、當調用hsitory.push或popstate事件觸發時,執行listener。
4、<Router></Router>注冊的監聽函數內部會setState更新狀態
5、<Router></Router>的子組件<Route>的componentWillReceiveProps生命周期函數中能得到Router中context,根據當前path和瀏覽器當前location來判斷當前route是否match,匹配就渲染component。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。