react-router源码分析

前言

React router,是声明式的React路由管理库,以react-router为核心库。本篇将分析react-router的实现原理。 在此基础上,扩展出react-router-dom作为Web环境下的路由管理库, react-router-native作为native环境下的路由管理库。(虽然react-native下可能不如react-navigation好用2333)

在这篇文章中,将分别对react-router库中各个API逐个分析

<Router>

<Router>组件是套在路由最外层的组件,主要作用他的作用有两点

  1. 接收一个history参数,提供一个包含historyhistorylocation和根路由matchReact legacy context
  2. 通过historylisten接口,监听history的变化,变化时更新Router组件的state中的match,在<Router>被卸载时取消监听

那么,我们开始阅读<Router>的代码

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

/**
 * The public API for putting history on context.
 */
class Router extends React.Component {
  static propTypes = {
    history: PropTypes.object.isRequired,
    children: PropTypes.node
  };

  // 这里的contextTypes是为了接收<StaticRouter>提供的context
  static contextTypes = {
    router: PropTypes.object
  };

  // `childContextTypes`和`getChildContext`提供了`<Router>`子孙组件的context
  static childContextTypes = {
    router: PropTypes.object.isRequired
  };

  getChildContext() {
    return {
      router: {
        ...this.context.router,
        history: this.props.history,
        route: {
          // 单独把history的location拎出来,
          // 因为location和match要作为额外的props传给`<Route>`和`<Switch>`的子组件。
          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;

    invariant(
      children == null || React.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>.
    // 在SSR的时候<Redirect>会在`componentWillMount`时执行`history.replace`或`history.push`
    // 因此需要在`componentWillMount`时就开始监听history的变化
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }

  componentWillReceiveProps(nextProps) {
    // 警告库的使用者不要改变传入·<Router>·的history属性
    warning(
      this.props.history === nextProps.history,
      "You cannot change <Router history>"
    );
  }

  componentWillUnmount() {
    // `<Router>`组件卸载时取消监听
    this.unlisten();
  }

  render() {
    const { children } = this.props;
    // 断言`<Router>`的children数量是1
    return children ? React.Children.only(children) : null;
  }
}

export default Router;

<Route><Switch>

<Route><Switch>中都调用了matchPath函数来返回match对象。当然,如果没有match到,它会返回nullmatchPath调用了一个compilePath的函数。 compilePath通过path-to-regexp来将path字符串转化为正则表达式对象,同时将path中的参数元信息存在变量key中。

compilePathmatchPath

import pathToRegexp from "path-to-regexp";

// 对以传入的`pattern`和`option`作为key, 缓存`path-to-regexp`返回的结果
// 这么说可能不严谨,准确的说是以`option`的`end`, `strict`,`sensitive`三个字段作为一级cache的key
// `pattern`作为二级cache的key保存Regexp
const patternCache = {};
// 缓存上限。一个猜想可能是在Native环境下如果页面太多会有问题(然而10000个页面的APP或Web项目规模是有多大2333)
const cacheLimit = 10000;

// 当前缓存的Regexp个数
let cacheCount = 0;

const compilePath = (pattern, options) => {
  // 上面说的一级key, 以`end`, `strict`, `sensitive`作为key
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  // 获得对应`cacheKey`所存在的cache,如果cache不存在创建一个这样的二级caches
  const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});

  // 如果有cache那么返回这个cache
  if (cache[pattern]) return cache[pattern];

  const keys = [];
  const re = pathToRegexp(pattern, keys, options);
  const compiledPattern = { re, keys };
  // `pathToRegexp`会返回一个`Regexp`对象同时修改传入`的keys`数组,往这个数组中写入`pattern`中参数的元信息

  // 若缓存未满,写入缓存,下一次就不需要通过`pathToRegexp`重新生成Regexp pattern
  if (cacheCount < cacheLimit) {
    cache[pattern] = compiledPattern;
    cacheCount++;
  }

  return compiledPattern;
};

/**
 * Public API for matching a URL pathname to a path pattern.
 */
const matchPath = (pathname, options = {}, parent) => {

  // 在`react-router-redux`库中有一处调用传入的options是string,此处做兼容处理
  if (typeof options === "string") options = { path: options };

  const { path, exact = false, strict = false, sensitive = false } = options;

  // <Route>没有path属性时,返回父组件的match
  if (path == null) return parent;

  // 获取正则表达式和path中的参数信息
  const { re, keys } = compilePath(path, { end: exact, strict, sensitive });
  const match = re.exec(pathname);

  // 没有匹配到返回null
  if (!match) return null;

  // 第一个匹配到的是url,后面是path中的各个参数
  const [url, ...values] = match;
  const isExact = pathname === url;

  // <Route>的`exact`为true且当前pathname未能精确匹配返回null
  if (exact && !isExact) return null;

  // 返回`match`对象
  return {
    path, // the path pattern used to match
    url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
    isExact, // whether or not we matched exactly
    params: keys.reduce((memo, key, index) => {
      // 将path中的参数(如`:id`这样的参数表达式)和pathname中匹配到的对应参数合并到一个params对象中
      memo[key.name] = values[index];
      return memo;
    }, {})
  };
};

export default matchPath;

<Route>组件

<Route>也许是react-router中最重要的组件。他最基本的职能就是,当location匹配时,他会渲染出相同的UI 他分别有component, render, children三个属性,通过他们可以让<Route>渲染一些东西出来。这三个属性对应下列三个场景下渲染内容的策略

  • <Route>传入一个组件作为component属性。这种情况下适用于传入一个无状态的函数式组件(React SFC)或者一个有状态React组件(用es6 class语法定义或React.createClass创建的组件)不适用在component中传入一个匿名函数作为组件。因为每次re-render的时候都会创建一个新的组件,之前的组件会被卸载,然后新的组件被挂载上去,即使他们的内容是一样的。
  • <Route>传入一个匿名函数作为render属性,这样做不会像传入component那样每次组件更新时,想让路由渲染的那个组件被挂载和卸载。在他的内部会直接调用render函数,这样就不会有上面一条的组件反复挂载卸载的情况。
  • <Route>传入children,即给<Route>子组件。 上面两条在路由不匹配时<Route>会返回null,第三条会将路由的match, location, history一并作为props传给children,由children自己根据matchlocation等信息决定渲染什么。比如在导航的Tab中渲染各个Tab item,给当前选中的Tab不同样式等情景
import warning from "warning";
import invariant from "invariant";
import React from "react";
import PropTypes from "prop-types";
import matchPath from "./matchPath";

// 用于判断`<Route>`有没有`children`
const isEmptyChildren = children => React.Children.count(children) === 0;

/**
 * The public API for matching a single path and rendering.
 */
class Route extends React.Component {
  static propTypes = {
    // 如果这个`<Route>`的父组件是`<Switch>`,
    // 那么他会被传入`computedMatch`。
    // 然后`<Route>`的match会直接使用`computedRoute`
    computedMatch: PropTypes.object, // private, from <Switch>
    path: PropTypes.string,
    exact: PropTypes.bool,
    strict: PropTypes.bool,
    sensitive: PropTypes.bool,
    component: PropTypes.func,
    render: PropTypes.func,
    children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
    location: PropTypes.object
  };

  // 接收context
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.object.isRequired,
      route: PropTypes.object.isRequired,
      staticContext: PropTypes.object
    })
  };

  // 提供一个新的context
  static childContextTypes = {
    router: PropTypes.object.isRequired
  };

  getChildContext() {
    return {
      router: {
        ...this.context.router,
        route: {
          // 默认`<Route>`会通过`history`的`location`来判断路由是否匹配上
          // 但也可以显式地给`<Route>`传入`location`的props,
          // 来使得在当前`history`的`location`不匹配时也能匹配上这个路由
          location: this.props.location || this.context.router.route.location,
          // 因为`<Route>`子孙的需要接收`<Route>`的`match`,
          // 而不是当前`<Route>`的父`<Route>`或`<Router>`的`match`
          match: this.state.match
        }
      }
    };
  }

  state = {
    match: this.computeMatch(this.props, this.context.router)
  };

  computeMatch(
    { computedMatch, location, path, strict, exact, sensitive },
    router
  ) {
    // 如果有由`<Switch>`传入的`computedMatch`参数,则直接返回`computedMatch`
    if (computedMatch) return computedMatch; // <Switch> already computed the match for us

    // 断言`<Route>`被`<Router>`包裹
    invariant(
      router,
      "You should not use <Route> or withRouter() outside a <Router>"
    );

    const { route } = router;
    // 从`props`的`location`或者`context.router.location`中取得`pathname`
    const pathname = (location || route.location).pathname;

    // 用上个section分析过的`matchPath`函数来获得`match`对象,如果未匹配会返回`null`
    return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
  }

  componentWillMount() {
    // 警告用户`component`、`render`、`children`只要传入一个给`<Route>`就够了
    warning(
      !(this.props.component && this.props.render),
      "You should not use <Route component> and <Route render> in the same route; <Route render> will be ignored"
    );

    warning(
      !(
        this.props.component &&
        this.props.children &&
        !isEmptyChildren(this.props.children)
      ),
      "You should not use <Route component> and <Route children> in the same route; <Route children> will be ignored"
    );

    warning(
      !(
        this.props.render &&
        this.props.children &&
        !isEmptyChildren(this.props.children)
      ),
      "You should not use <Route render> and <Route children> in the same route; <Route children> will be ignored"
    );
  }

  componentWillReceiveProps(nextProps, nextContext) {
    // 警告用户不要切换<Route>的受控和非受控状态
    warning(
      !(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.'
    );

    warning(
      !(!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.'
    );

    // 组件props改变时更新`<Route>`的匹配状态
    this.setState({
      match: this.computeMatch(nextProps, nextContext.router)
    });
  }

  render() {
    const { match } = this.state;
    const { children, component, render } = this.props;
    const { history, route, staticContext } = this.context.router;
    const location = this.props.location || route.location;
    // 额外需要传给`component`、`render`、`children`的props
    const props = { match, location, history, staticContext };

    if (component) return match ? React.createElement(component, props) : null;

    if (render) return match ? render(props) : null;

    // 当`children`是一个function(即无状态的函数式组件(React SFC)用或es6 `class`定义的有状态组件)时将上面的`props`传给`children`
    if (typeof children === "function") return children(props);

    // 否则当children非空时返回这个children。
    // 这种情况,`children`是类似`div`这种html原生的标签
    // 下面的断言排除了有多个`children`的情况
    if (children && !isEmptyChildren(children))
      return React.Children.only(children);

    return null;
  }
}

export default Route;

<Switch>组件

<Switch>组件和<Route>组件在许多地方代码都差不多,所以关于他的分析主要会在render函数上 与<Route>一样,也可以显式地给他传入一个location对象使得当前history.location不匹配时,也使得<Switch>匹配

import React from "react";
import PropTypes from "prop-types";
import warning from "warning";
import invariant from "invariant";
import matchPath from "./matchPath";

/**
 * The public API for rendering the first <Route> that matches.
 */
class Switch extends React.Component {

  // contextTypes,用于接收context
  static contextTypes = {
    router: PropTypes.shape({
      route: PropTypes.object.isRequired
    }).isRequired
  };

  static propTypes = {
    children: PropTypes.node,
    location: PropTypes.object
  };

  // 断言`<Switch>`被`<Router>`包裹
  componentWillMount() {
    invariant(
      this.context.router,
      "You should not use <Switch> outside a <Router>"
    );
  }

  componentWillReceiveProps(nextProps) {
  // 警告库使用者不要改变`<Switch>`的受控状态
    warning(
      !(nextProps.location && !this.props.location),
      '<Switch> 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.'
    );

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

  render() {
    const { route } = this.context.router;
    const { children } = this.props;
    // 同`<Route>`可以显式传入一个`location`的props,
    // 使得在`context.router.history.location`不匹配的情况下,
    // 使得`<Switch>`匹配上其中的某个`<Router>`
    const location = this.props.location || route.location;

    let match, child;
    React.Children.forEach(children, element => {
      // 没有匹配到且这个子组件是合法的React组件的情况下
      if (match == null && React.isValidElement(element)) {
        const {
          path: pathProp,
          exact,
          strict,
          sensitive,
          // `from`是`<Redirect>`组件的props, 是一个能被`path-to-regexp`解析的字符串
          // 仅有当前页面的路由匹配从`from`转换的正则表达式时, `<Redirect>`才会重定向url
          from
        } = element.props;

        // 对`<Route>`、·<Switch>、`<Redirect>`的”路径“做兼容处理
        const path = pathProp || from;

        // 保存当前子组件match和对当前子组件的引用
        child = element;
        match = matchPath(
          location.pathname,
          { path, exact, strict, sensitive },
          route.match
        );
      }
    });

    return match
      // `<Route>`组件会直接使用`computedMatch`作为它state中的`match`
      ? React.cloneElement(child, { location, computedMatch: match })
      : null;
  }
}

export default Switch

<Redirect>

<Redirect>适用于重定向。若在服务端渲染且使用<StaticRouter>,会在componentWillMount第一次改变historylocation,其它情况下会在componentDidMount改变historylocation

传入<Redirect>to是一个能被path-to-regexp解析的字符串,他的参数会被from属性中的匹配到的参数填入。

当传入<Redirect>to属性改变时,也会改变historylocation,这种情况只会在客户端渲染时发生

它还有一个from的属性,是一个path-to-regexp能解析的字符串,这个属性仅当<Redirect>组件的父组件是<Switch>时才有效。

form存在,当且仅当当前的pathname能匹配由form属性转化出的正则表达式,该重定向才会生效

<Redirect>组件

import React from "react";
import PropTypes from "prop-types";
import warning from "warning";
import invariant from "invariant";
import { createLocation, locationsAreEqual } from "history";
import generatePath from "./generatePath";

/**
 * The public API for updating the location programmatically
 * with a component.
 */
class Redirect extends React.Component {
  static propTypes = {
    // 由`<Switch>`传入的`computedMatch`属性
    computedMatch: PropTypes.object, // private, from <Switch>
    push: PropTypes.bool,
    from: PropTypes.string,
    to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
  };

  static defaultProps = {
    push: false
  };

  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,
        replace: PropTypes.func.isRequired
      }).isRequired,
      staticContext: PropTypes.object
    }).isRequired
  };

  // 判断是否是`<StaticRouter>`
  isStatic() {
    return this.context.router && this.context.router.staticContext;
  }

  componentWillMount() {
    invariant(
      this.context.router,
      "You should not use <Redirect> outside a <Router>"
    );
    // 有`<StaticRouter>`说明是服务端渲染或测试。没有`componentDidMount`生命周期。
    // 因此在`componentWillMount`中改变`history`
    if (this.isStatic()) this.perform();
  }

  componentDidMount() {
    // 其它情况,在componentDidMount中改变`history`
    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();
  }

  // 计算重定向的目标url
  computeTo({ computedMatch, to }) {

    // 当`<Switch>`匹配到`<Redirect>`的`from`属性时,`computedMatch`会作为属性传入
    // 没有匹配到`from`时,`<Redirect>`根本就不会渲染出来
    // computedMath.params中便是`from`的path中匹配到的参数
    // 通过`generatePath`函数可以将`path`和`params`组装成最终重定向的路径
    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;
    const to = this.computeTo(this.props);

    // 由传入`<Redirect>`的`push`属性来决定调用`history.push`或`history.replace`
    if (push) {
      history.push(to);
    } else {
      history.replace(to);
    }
  }

  render() {
    return null;
  }
}

export default Redirect;

顺便看一下generatePath函数

这个函数的实现思路与matchPath类似。generatePath会缓存PathToRegexp.compile的结果

import pathToRegexp from "path-to-regexp";

const patternCache = {};
const cacheLimit = 10000;
let cacheCount = 0;


const compileGenerator = pattern => {
  // 一级cache的key是pattern
  const cacheKey = pattern;
  const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});

  if (cache[pattern]) return cache[pattern];

  // 通过`pathToRegexp.compile`来获得能生成路径的函数
  const compiledGenerator = pathToRegexp.compile(pattern);

  // 缓存这个函数(为什么还需要`pattern`作为二级缓存的key呢)
  if (cacheCount < cacheLimit) {
    cache[pattern] = compiledGenerator;
    cacheCount++;
  }

  return compiledGenerator;
};

/**
 * Public API for generating a URL pathname from a pattern and parameters.
 */
const generatePath = (pattern = "/", params = {}) => {
  // 没有pattern传入或者传入的pattern为"/"(等效于没有传入),不匹配,直接返回"/"作为重定向的url
  if (pattern === "/") {
    return pattern;
  }

  // 获得能从"path"到url的参数,并通过他生成url返回之
  const generator = compileGenerator(pattern);
  return generator(params, { pretty: true });
  
};

export default generatePath;

<StaticRouter>

<StaticRouter>封装了<Router>组件,正如他名字中的static,它的historylocation永远不会改变。在SSR(服务端渲染)和测试时,会变得非常有用

import warning from "warning";
import invariant from "invariant";
import React from "react";
import PropTypes from "prop-types";
import { createLocation, createPath } from "history";
import Router from "./Router";

// 用于在url开头加上"/"
const addLeadingSlash = path => {
  return path.charAt(0) === "/" ? path : "/" + path;
};

// 用于在`location`的`pathname`前加上`basename`
const addBasename = (basename, location) => {
  if (!basename) return location;

  return {
    ...location,
    pathname: addLeadingSlash(basename) + location.pathname
  };
};

// 返回移除了location的pathname前basename的的location
const stripBasename = (basename, location) => {
  if (!basename) return location;

  const base = addLeadingSlash(basename);

  if (location.pathname.indexOf(base) !== 0) return location;

  return {
    ...location,
    pathname: location.pathname.substr(base.length)
  };
};

// 将`location`转化为url
const createURL = location =>
  typeof location === "string" ? location : createPath(location);

// 虚设的staticHander,用于在`history.go`, `history.goBack`, `history.goForward`调用时提醒库的使用者这是一个静态的路由,不能这么做
const staticHandler = methodName => () => {
  invariant(false, "You cannot %s with <StaticRouter>", methodName);
};

// 什么都不坐的空函数
const noop = () => {};

/**
 * The public top-level API for a "static" <Router>, so-called because it
 * can't actually change the current location. Instead, it just records
 * location changes in a context object. Useful mainly in testing and
 * server-rendering scenarios.
 */
class StaticRouter extends React.Component {
  static propTypes = {
    basename: PropTypes.string,
    context: PropTypes.object.isRequired,
    location: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
  };

  static defaultProps = {
    basename: "",
    location: "/"
  };

  static childContextTypes = {
    router: PropTypes.object.isRequired
  };

  // 他的子孙组件可以通过context接收staticContext来得知路由是否是静态的
  getChildContext() {
    return {
      router: {
        staticContext: this.props.context
      }
    };
  }
  // 调用`createHref`以获得以`baseName`开头的URL
  createHref = path => addLeadingSlash(this.props.basename + createURL(path));

  // 在调用`history.push`时,将这个动作相关的信息记录在传入`<StaticRouter>`的context属性中
  // 那么外部就可以通过context来进行测试
  handlePush = location => {
    const { basename, context } = this.props;
    context.action = "PUSH";
    context.location = addBasename(basename, createLocation(location));
    context.url = createURL(context.location);
  };

  // 同上,写入context来记录路由的replace
  handleReplace = location => {
    const { basename, context } = this.props;
    context.action = "REPLACE";
    context.location = addBasename(basename, createLocation(location));
    context.url = createURL(context.location);
  };

  // 调用用history.listen什么事情也不会发生。返回的`unlisten`函数也应该是个空函数
  handleListen = () => noop;

  // 静态页面不需要在页面离开时发出提示。因为根本没有这样的场景
  handleBlock = () => noop;

  componentWillMount() {
    warning(
      !this.props.history,
      "<StaticRouter> ignores the history prop. To use a custom history, " +
        "use `import { Router }` instead of `import { StaticRouter as Router }`."
    );
  }

  render() {
    const { basename, context, location, ...props } = this.props;

    // 一个静态的history对象
    // `<Redirect>`的重定向会被记录到context中
    // 正如上文分析,会在使用静态路由时,`<Redirect>`会在`componentWillMount`中
    // 调用`history.push`或`history.replace`,这两个行为就被记录在了`context`中
    const history = {
      createHref: this.createHref,
      action: "POP",
      location: stripBasename(basename, createLocation(location)),
      push: this.handlePush,
      replace: this.handleReplace,
      go: staticHandler("go"),
      goBack: staticHandler("goBack"),
      goForward: staticHandler("goForward"),
      listen: this.handleListen,
      block: this.handleBlock
    };

    return <Router {...props} history={history} />;
  }
}

export default StaticRouter;

有时我们需要在用户离开页面时提示用户。如果我们需要阻止用户离开当前页面时,可以使用<Prompt>组件 <Prompt>组件通过history.block来实现这样的功能

<Prompt>

import React from "react";
import PropTypes from "prop-types";
import invariant from "invariant";

/**
 * The public API for prompting the user before navigating away
 * from a screen with a component.
 */
class Prompt extends React.Component {
  static propTypes = {
    // 当`when`为`true`时,`<Prompt>`才会生效
    when: PropTypes.bool,
    // 需要提示的信息,可以是一个字符串,或者一个形如`(history: History, action: string) => string`这样的函数
    message: PropTypes.oneOfType([PropTypes.func, PropTypes.string]).isRequired
  };

  static defaultProps = {
    when: true
  };

  // 接收context
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        block: PropTypes.func.isRequired
      }).isRequired
    }).isRequired
  };

  // 取消之前添加到`history`的`block`
  // 调用`history.block`,当用户离开时给予提示。将`history.block`返回的用于unblock的函数绑定到`this.unblock`上
  enable(message) {
    if (this.unblock) this.unblock();

    this.unblock = this.context.router.history.block(message);
  }

  // 取消添加到`history`的`block`
  disable() {
    if (this.unblock) {
      this.unblock();
      this.unblock = null;
    }
  }

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

    // 如果`when`为`true`时,让用户再离开页面时收到提示
    if (this.props.when) this.enable(this.props.message);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.when) {
      // 当之前传入的`when`为`false`或之前传入的`message`和即传入的`message`不同时
      // 重新设置用户离开页面时的提示
      if (!this.props.when || this.props.message !== nextProps.message)
        this.enable(nextProps.message);
    } else {
      this.disable();
    }
  }

  componentWillUnmount() {
    // 组件卸载后取消离开时的提示
    this.disable();
  }

  render() {
    return null;
  }
}

export default Prompt;

<MemoryRouter>

<MemoryRouter>适用于无DOM的情景下,比如React-Native和单元测试 他通过history.createMemoryHistory来创建history

import warning from "warning";
import React from "react";
import PropTypes from "prop-types";
import { createMemoryHistory as createHistory } from "history";
import Router from "./Router";

/**
 * The public API for a <Router> that stores location in memory.
 */
class MemoryRouter extends React.Component {
  static propTypes = {
    // 初始的history stack
    initialEntries: PropTypes.array,
    // 初始history stack的index, 页面初始的location由它来决定
    initialIndex: PropTypes.number,
    // 当用户离开当前页面时的弹窗。
    // 在没有DOM的情况下不能使用`window.confirm`来弹窗,因此需要库的使用者自己去定义这种行为
    getUserConfirmation: PropTypes.func,
    // `location.key`的长度
    keyLength: PropTypes.number,
    children: PropTypes.node
  };

  history = createHistory(this.props);// 用`props`中的`initialEntries`, `initialIndex`, `getUserConfirmation`, `keyLength`来创建`memoryHistory`

  componentWillMount() {
    warning(
      !this.props.history,
      "<MemoryRouter> ignores the history prop. To use a custom history, " +
        "use `import { Router }` instead of `import { MemoryRouter as Router }`."
    );
  }

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

export default MemoryRouter;

参考资料

React Router官方文档

React Router git repo

history git repo