react-router路由机制

原理

无刷新的更改地址栏地址 ,保证视图和URL的同步。基本原理是H5History 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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345