react-router路由機(jī)制

原理

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

實(shí)現(xiàn)過(guò)程


當(dāng)我們點(diǎn)擊了Link組件或者調(diào)用了history.push跳轉(zhuǎn)路由時(shí),react-router做了上圖的處理流程。

假定使用BrowserRoutercreateBrowserHistory,使用history.push跳轉(zhuǎn)路由

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

分析

1、history.push
在react中,我們可以調(diào)用history.push(path,state)來(lái)跳轉(zhuǎn)路由,實(shí)際執(zhí)行的就是createBrowserHistory中的push方法
在這個(gè)方法中主要做三件事:
一、根據(jù)傳遞的path,state參數(shù)創(chuàng)建一個(gè)location,不同于window.location。(具體的可以看代碼,點(diǎn)擊上面的createBrowserHistory

location = {
  pathname, // 當(dāng)前路徑,即 Link 中的 to 屬性
  search, // search
  hash, // hash
  state, // state 對(duì)象
  action, // location 類型,在點(diǎn)擊 Link 時(shí)為 PUSH,瀏覽器前進(jìn)后退時(shí)為 POP,調(diào)用 replaceState 方法時(shí)為 REPLACE
  key, // 用于操作 sessionStorage 存取 state 對(duì)象
};

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

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

三、在setState({ action, location })中執(zhí)行Router中注冊(cè)的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事件觸發(fā)時(shí),可以得到event.state,createBrowserHistory中會(huì)根據(jù)這個(gè)state和當(dāng)前window.location重新生成一個(gè)location對(duì)象,執(zhí)行Router組件注冊(cè)的listener,同步UI。
3、<Router>組件
BrowserRouter組件中會(huì)實(shí)例化一個(gè)createBrowserHistory對(duì)象,傳遞給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組件中要注冊(cè)history.listen()的一個(gè)監(jiān)聽(tīng)函數(shù),并且保存一份子組件(Route)使用的數(shù)據(jù)

  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
        }
      })
    };
  };

當(dāng)調(diào)用history.push或觸發(fā)popstate事件時(shí),這里注冊(cè)的listener都會(huì)被createBrowserHistory執(zhí)行,觸發(fā)setState,然后Router的子組件中匹配的<Route>會(huì)重新渲染.
4、<Route>組件
在Route中有一個(gè)match狀態(tài),在父組件props發(fā)生變化時(shí)會(huì)重新計(jì)算

  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主要工作就是匹配當(dāng)前組件上指定的path和當(dāng)前瀏覽器的路徑是否一致,一致就渲染組件

  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);
  };

總結(jié)

總結(jié)一下,react-router的路由機(jī)制就是:
1、借助history庫(kù),history中實(shí)現(xiàn)了push、go、goBack等方法,注冊(cè)了popstate事件,當(dāng)路由跳轉(zhuǎn)時(shí),使用瀏覽器內(nèi)置的history api 操作 history棧。
2、history庫(kù)對(duì)外暴露的history對(duì)象提供了listen方法,<Router></Router>組件會(huì)注冊(cè)一個(gè)listener;
3、當(dāng)調(diào)用hsitory.push或popstate事件觸發(fā)時(shí),執(zhí)行l(wèi)istener。
4、<Router></Router>注冊(cè)的監(jiān)聽(tīng)函數(shù)內(nèi)部會(huì)setState更新?tīng)顟B(tài)
5、<Router></Router>的子組件<Route>的componentWillReceiveProps生命周期函數(shù)中能得到Router中context,根據(jù)當(dāng)前path和瀏覽器當(dāng)前l(fā)ocation來(lái)判斷當(dāng)前route是否match,匹配就渲染component。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。