react-router 源代码学习笔记

阅读 2952
收藏 0
2018-04-10
原文链接: github.com

前言

使用 react-router 也有一段时间了,现在基于官方文档以及源码做一下总结。

文章略长 ^_^ 如果有不正确的地方,请务必指正。


1. Router

Router 是一个公共的接口组件,有一个必须的参数 history。通过 history 库中的不同的 createXXXHistory 实现。

官方文档点这里

源码:

class Router extends React.Component {
  static propTypes = {
    history: PropTypes.object.isRequired, // 由 history 库提供不同的实现
    children: PropTypes.node
  };
  // this.context.router
  static contextTypes = {
    router: PropTypes.object
  };

  static childContextTypes = {
    router: PropTypes.object.isRequired
  };
  // 将传入的 history mixin 到 this.context.router 中
  // 然后往下层传递
  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 返回的是一个 match 对象
  computeMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  }

  componentWillMount() {
    const { children, history } = this.props;
    // 要么没有 children, 要么只能有 1 个
    invariant(
      children == null || React.Children.count(children) === 1,
      "A <Router> may have only one child element"
    );

    // history 库中,发布-订阅模式实现的注册监听和触发通知
    // 当 history.push() 或者 replace() 执行 notifyListeners,
    // 因为注册监听函数的同时还开启了对 popstate 事件的监听
    // 所以回退、前进之类的操作同样会触发 notifyListeners

    // 这里在组件挂载前注册一个监听函数,触发后会更新 match 对象
    // 触发的时机就是一旦执行了 history.push() | history.replace() | popstate
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }

  componentWillReceiveProps(nextProps) {
    // 不能够改变 history 对象
    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;
  }
}

history 库中的相关代码片段:

发布-订阅模型

let listeners = [];
// 将监听函数加入到数组中
const appendListener = fn => {
  let isActive = true;
  // 柯里化
  const listener = (...args) => {
    if (isActive) fn(...args);
  };

  listeners.push(listener);
  // 执行返回的函数就会清除这个监听函数
  return () => {
    isActive = false;
    listeners = listeners.filter(item => item !== listener);
  };
};
// 通知函数,执行监听数组中的所有函数
const notifyListeners = (...args) => {
  listeners.forEach(listener => listener(...args));
};

监听函数(以 createBrowserRouter 为例)

const listen = listener => {
  // 加入到监听函数数组
  const unlisten = transitionManager.appendListener(listener);
  // 1 -> window.addEventListener('popstate', handlePop)
  // 0 -> window.removeEventListener...
  checkDOMListeners(1);
  // 返回取消函数
  return () => {
    checkDOMListeners(-1);
    unlisten();
  };
};

触发通知(以 createBrowserRouter 为例)

// 不管是 history.push() 或者是 replace()
// 亦或者是触发 popstate 事件的回调函数 handlePop 
// 最终都会执行一个 setState 函数
const setState = nextState => { 
  // nextState: {action, location}
  // 将 action 和 location 等 mixin 进 history 对象中。
  Object.assign(history, nextState);
  // 更新 history 堆栈的长度
  history.length = globalHistory.length;
  // 触发通知,执行监听函数
  transitionManager.notifyListeners(history.location, history.action);
};

将上面的过程串联起来(以 BrowserRouter 为例):

Link | Redirect 组件中使用了 history.replace() 或者 history.push(), 或者是点击浏览器的前进、回退按钮,最终都会触发 history 去通知监听函数的执行。

这里假设是点击了

根据之前的分析,这里会执行 history.push() = history.pushState + setState

history.pushState 会往历史堆栈中加入一条记录,但是不会更新页面,只是url变了。这部分可以参见这里

setState的作用就是 mixin 一个 location 进 history 中,然后触发 notifyListeners。 这时候注册的监听函数触发,会根据 history 更新 match 对象, 假设有
这时候 match 成功,就会马上 render A 组件。

2. BrowserRouter

利用公共 Router 接口,通过传入 createBrowserHistory 创建的历史记录实现。

import { createBrowserHistory as createHistory } from "history";
history = createHistory(this.props);
render() {
  return <Router history={this.history} children={this.props.children} />;
}

其余 Router 组建同理,只是 history 的实现不同而已:

2.1 browser history 2.2 hash history 2.3 memory history

3. Route

这应该是最重要的组件了,当 location 与 Route's path 匹配的时候就会渲染组件。

参数介绍

3.1 render 方式

三种 render methods 的参数都是 props: ({match, location, history})

3.1.1 component

当 match 的时候渲染,因为使用了 React.createElement(component, props) 创建新的组件进行渲染,所以当你使用

<Route path='/a' component={()=>A}/>
这种内联函数的时候每一次渲染都会去创建一个新的组件,造成浪费。这种时候务必使用 render

PS: 优先级 component > render > children, 不要同时使用!

3.1.2 render

同样是 match 的时候才会渲染。与 component 的区别就是没有使用 createElement, 你可以放心的使用内联函数。

PS: 优先级 component > render > children, 不要同时使用!

3.1.3 children

children 可以是函数,也可以是子组件对象。 这里讨论是函数的情况,无论是否 match,都会执行。这种特性在动画中很有用处。

<Route children={({ match, ...rest }) => (
  {/* Animate will always render, so you can use lifecycles
      to animate its child in and out */}
  <Animate>
    {match && <Something {...rest}/>}
  </Animate>
)}/>

PS: 优先级 component > render > children, 不要同时使用!

3.2 path 的匹配配置

react-router 是使用正则进行路由的匹配的。所以可以配置一些匹配的规则,

exact 表示精确匹配,严格一致
strict 表示结尾斜线的一致
sensitive 表示大小写一致

3.3 location参数

其实 Switch 组件同样也可以传入 location 参数,这样就可以去 match 一个当前 history location 之外的 location,这种应用场景常见于动画中!

比如这个利用 react-motion 实现的动画路由组件:TransitionRoute

PS: 如果给 Switch 中传入了一个 location,其子组件 Route 中的 location 将会被覆盖,这个在 Switch 源码中可以看到。

3.4 源码

// 判断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 = {
    computedMatch: PropTypes.object, // 私有参数, 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
  };
  // this.context.router
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.object.isRequired,
      route: PropTypes.object.isRequired,
      staticContext: PropTypes.object
    })
  };

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

  getChildContext() {
    return {
      router: {
        ...this.context.router,
        // 如果有 this.props.location
        // minxin 进 this.conotext.router 中传递
        route: {
          location: this.props.location || this.context.router.route.location,
          match: this.state.match
        }
      }
    };
  }

  state = {
    match: this.computeMatch(this.props, this.context.router)
  };
  // 返回 match 对象
  computeMatch(
    { computedMatch, location, path, strict, exact, sensitive },
    router
  ) {
    // 如果 Switch 把这事儿给你干了!
    if (computedMatch) return computedMatch;

    invariant(
      router,
      "You should not use <Route> or withRouter() outside a <Router>"
    );

    const { route } = router;
    // 如果传了 location 参数,用之!
    const pathname = (location || route.location).pathname;
    // 使用 path-to-regexp 去匹配
    // strict, exact, sensitive 是在这里作为配置参数
    return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
  }

  componentWillMount() {
    // component 和 render 不能一起用,render 会被忽视
    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"
    );
    // component 和 children 不能一起用,children 会被忽视
    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"
    );
    // render 和 children 不能一起用,children 会被忽视
    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"
    );
  }
  // 为了让location从一而终
  componentWillReceiveProps(nextProps, nextContext) {
    // location 这个东西啊,你开始的时候没传,后边就不能突然又有了。
    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.'
    );
    // location 这个东西啊,你开始的时候传了一个,后边就不能突然没有了。
    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.'
    );
    // 如前面的分析,在监听到路由变化的时候,重新计算 match
    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;
    // 三种渲染函数的参数
    const props = { match, location, history, staticContext };
    // match && createElement()
    if (component) return match ? React.createElement(component, props) : null;
    // match && render()
    if (render) return match ? render(props) : null;
    // 不管 match 与否
    if (typeof children === "function") return children(props);

    if (children && !isEmptyChildren(children))
      return React.Children.only(children);

    return null;
  }
}

4. Link

参数说明可以参见这里

4.1 to: (string | obj)

可以是对象{pathname, search, hash, state} 可以拼接成字符串 '/a?sort=name#foo'

4.2 replace: bool

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

4.3 innerRef: func

获取 DOM 节点

refCallBack = node => { ... }
<Link to='/a' innerRef={refCallBack} />

4.4 源码

// 判断这几个键是不是被摁了
const isModifiedEvent = event =>
  !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);

/**
 * The public API for rendering a history-aware <a>.
 */
class Link extends React.Component {
  static propTypes = {
    onClick: PropTypes.func,
    target: PropTypes.string,
    replace: PropTypes.bool,
    to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
    innerRef: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
  };

  static defaultProps = {
    replace: false
  };
  // this.context.router
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,
        replace: PropTypes.func.isRequired,
        createHref: PropTypes.func.isRequired
      }).isRequired
    }).isRequired
  };

  handleClick = event => {
    // 执行用户定义的点击事件
    if (this.props.onClick) this.props.onClick(event);

    if (
      !event.defaultPrevented && // 没有阻止默认事件
      event.button === 0 && // 左点击
      !this.props.target && // 没有 "target=_blank" 之类的
      !isModifiedEvent(event) // ignore clicks with modifier keys
    ) {
      event.preventDefault(); // 阻止默认事件

      const { history } = this.context.router;
      const { replace, to } = this.props;
      // 这里会触发 notifyListeners 去通知注册的监听事件执行
      // replace -> replaceState
      // push -> pushState
      if (replace) {
        history.replace(to);
      } else {
        history.push(to);
      }
    }
  };

  render() {
    const { replace, to, innerRef, ...props } = this.props;
    invariant(
      this.context.router,
      "You should not use <Link> outside a <Router>"
    );

    invariant(to !== undefined, 'You must specify the "to" property');

    const { history } = this.context.router;
    const location =
      typeof to === "string"
        ? createLocation(to, null, null, history.location)
        : to;

    const href = history.createHref(location);
    return (
      <a {...props} onClick={this.handleClick} href={href} ref={innerRef} />
    );
  }
}

5. Switch

参数说明可以参见这里

Switch 的作用是只渲染第一个匹配到的 Route 或者 Redirect 组件。 可以传入一个 location 给 Switch,这种方式在动画场景很常见。 比如: TransitionSwitch

PS: Switch 匹配的是 Redirect 中的 from , 这个 from 和 Route's path 是差不多的,同样可以设置 exact, and strict。

5.1 源码

/**
 * The public API for rendering the first <Route> that matches.
 */
class Switch extends React.Component {
  // this.context.router
  static contextTypes = {
    router: PropTypes.shape({
      route: PropTypes.object.isRequired
    }).isRequired
  };

  static propTypes = {
    children: PropTypes.node,
    // 可以传入一个 location, 
    // 使之不再监听当前的 history location (usually the current browser URL)
    location: PropTypes.object 
  };

  componentWillMount() {
    invariant(
      this.context.router,
      "You should not use <Switch> outside a <Router>"
    );
  }
  // 确保 location 从一而终
  componentWillReceiveProps(nextProps) {
    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;
    const location = this.props.location || route.location;

    let match, child;
    React.Children.forEach(children, element => {
      // 这里保证只会执行第一次匹配到的
      if (match == null && React.isValidElement(element)) {
        const {
          path: pathProp,
          exact,
          strict,
          sensitive,
          from // Redirect 中的 from
        } = element.props;
        const path = pathProp || from;

        child = element;
        match = matchPath(
          location.pathname,
          { path, exact, strict, sensitive },
          route.match
        );
      }
    });
    // 覆盖了子组件的 location
    // 传递 computedMatch 给子组件,使其渲染组件。 
    return match
      ? React.cloneElement(child, { location, computedMatch: match })
      : null;
  }
}

6. Redirect

会覆盖当前的 history.location, 就像HTTP's 3XX.

参数说明可以参见这里

6.1 props

to: string | object

同 Link's to.

from: string

这个只能在 Switch 标签包裹的情况下使用。

push: bool

if(push) history.push(to);

6.2 源码

/**
 * The public API for updating the location programmatically
 * with a component.
 */
class Redirect extends React.Component {
  static propTypes = {
    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
  };
  
  // staticContext 只在 <StaticRouter> 也就是 SSR 的情况下存在。
  // 判断是不是服务端渲染
  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(); // SSR
  }

  componentDidMount() {
    if (!this.isStatic()) this.perform(); // 非 SSR
  }
  // re-render 之后触发,判断是不是重定向到当前的 route。
  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();
  }
  // 计算路径
  // 带上 Switch 中传过来的 match 对象参数
  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;
    const to = this.computeTo(this.props);

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

  render() {
    return null;
  }
}

7. Prompt

参数说明可以参见这里

官方demo

相当于 window.confirm 的功效。

用于在导航离开页面之前提示用户。 当你的应用程序进入一个应该阻止用户导航的状态时(比如一个表单被填满了一半,你就想跳转到别的地方去),呈现一个<Prompt>

7.1 message: string | func

将在用户尝试导航到的下一个位置和操作中调用.

返回一个字符串以向用户显示提示,或者返回 true 以允许转换。

message 函数的参数是 ({location, action})

<Prompt message={location => (
  location.pathname.startsWith('/app') ? true : `Are you sure you want to go to ${location.pathname}?`
)}/>

7.2 when: bool

when 为 true 的时候就会 always render it! 默认值就是 true.

通过 when 来允许或者阻止用户的导航。

7.3 源码

7.3.1 history 库中的相关函数代码片段

let isBlocked = false;
// history.block() 函数
// 设置 prompt, 其实就是上边说的 message
const block = (prompt = false) => {
  const unblock = transitionManager.setPrompt(prompt);

  if (!isBlocked) {
    checkDOMListeners(1); // 监听 popstate 事件
    isBlocked = true;
  }
  // 返回一个取消函数
  return () => {
    if (isBlocked) {
      isBlocked = false;
      checkDOMListeners(-1); // 取消监听 popstate
    }

    return unblock(); // 设置 prompt = null
  };
};
// transition to 函数
// 执行 push replace 或者 popstate 事件触发,都会走这个函数
// 所以设置了 prompt 之后,点击别的 link 或者浏览器的回退按钮之类的都会
// 触发这个函数,弹出用户提示信息或者直接允许
// 而 history.push 或者 replace 中触发监听的逻辑则是在 callback 中实现的
// 这种编程设计很是巧妙!是不是有 AOP 的感觉?
const confirmTransitionTo = (
    location,
    action,
    getUserConfirmation, // 这个其实就是 window.confirm(message: string)
    callback
  ) => {
    // prompt 就是 Prompt 组件中的 message!
    if (prompt != null) {
      // message 可以是函数也可以是字符串
      const result =
        typeof prompt === "function" ? prompt(location, action) : prompt;

      if (typeof result === "string") {
        if (typeof getUserConfirmation === "function") {
          // getUserConfirmation 其实就是封装的 window.confirm
          getUserConfirmation(result, callback);
        } else {
          warning(
            false,
            "A history needs a getUserConfirmation function in order to use a prompt message"
          );

          callback(true);
        }
      } else {
        // message 函数返回 bool 的情况,
        // 如果返回 true,表示允许,执行 cb(true)
        callback(result !== false);
      }
    } else {
      callback(true);
    }
  };

7.3.2 Prompt 源码

/**
 * The public API for prompting the user before navigating away
 * from a screen with a component.
 */
class Prompt extends React.Component {
  static propTypes = {
    when: PropTypes.bool,
    message: PropTypes.oneOfType([PropTypes.func, PropTypes.string]).isRequired
  };

  static defaultProps = {
    when: true
  };

  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        block: PropTypes.func.isRequired
      }).isRequired
    }).isRequired
  };
  // 设置 message
  enable(message) {
    if (this.unblock) this.unblock();

    this.unblock = this.context.router.history.block(message);
  }
  // 取消
  disable() {
    if (this.unblock) {
      this.unblock();
      this.unblock = null;
    }
  }

  componentWillMount() {
    invariant(
      this.context.router,
      "You should not use <Prompt> outside a <Router>"
    );
    // 初始化设置 prompt
    if (this.props.when) this.enable(this.props.message);
  }
  // 当接收到新的 props 的时候
  // 比如上面说的官方 demo 中, when 的值改变的时机触发
  componentWillReceiveProps(nextProps) {
    if (nextProps.when) {
      if (!this.props.when || this.props.message !== nextProps.message)
        this.enable(nextProps.message);
    } else {
      this.disable();
    }
  }

  componentWillUnmount() {
    this.disable();
  }

  render() {
    return null;
  }
}

8. withRouter

常用的一个高阶组件。可以随时随地的把 updated match, location, and history props

传递给 wrapped component. 这也是它可以解决 update blocking 问题的原因。

官方文档了解一下?

源码:

/**
 * A public higher-order component to access the imperative API
 */
const withRouter = Component => {
  // a stateless component
  const C = props => {
    const { wrappedComponentRef, ...remainingProps } = props;
    return (
      // inject history, location, match 到 Component 中
      <Route
        children={routeComponentProps => (
          <Component
            {...remainingProps}
            {...routeComponentProps}
            ref={wrappedComponentRef}
          />
        )}
      />
    );
  };
  // for debug
  C.displayName = `withRouter(${Component.displayName || Component.name})`;
  C.WrappedComponent = Component; // 可以用来测试
  C.propTypes = {
    wrappedComponentRef: PropTypes.func
  };
  // Copies non-react specific statics from a child component to a parent component
  // hoistStatics(target, source)
  // 类似于一个排除了 react static keywords 的 Object.assign 方法
  return hoistStatics(C, Component);
};

9. matchPath

一个工具函数,参数是 pathname 和 Route 中接收到的 path, exact 等参数,

返回一个 match 对象。服务端渲染的时候 path 可能就是 req.path。

文档了解一下?

源码:

path-to-regexp

var keys = []
var re = pathToRegexp('/foo/:bar', keys, {end, sensitive, strict})
// re = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]

matchPath:

import pathToRegexp from "path-to-regexp";

// 缓存
const patternCache = {};
const cacheLimit = 10000;
let cacheCount = 0;

// 从缓存中取出 compiledPattern
const compilePath = (pattern, options) => {

  // 缓存 options 配置一样的,存到 cache 中
  // 所以 patternCache 最多只能缓存 2*2*2 个对象。
  // 因为 key 只有 8 种组合,'truefalsefalse', 'truetruetrue', ...

  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});

  // 再从 cache 中根据 pattern 取数据
  if (cache[pattern]) return cache[pattern];

  // 如果 cache 中没有就存进去
  // 这时候可能就会有很多情况了,因为 path 是不确定的,所以要有 limit
  const keys = [];
  const re = pathToRegexp(pattern, keys, options);
  const compiledPattern = { re, keys };

  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) => {
  // 封装 options 对象
  if (typeof options === "string") options = { path: options };
  const { path, exact = false, strict = false, sensitive = false } = options;

  // parent 就是指最近的 parent's match
  if (path == null) return parent;

  const { re, keys } = compilePath(path, { end: exact, strict, sensitive });
  // match 是一个数组 like: ['/test/route', 'test', 'route']
  const match = re.exec(pathname);

  if (!match) return null;

  const [url, ...values] = match;
  const isExact = pathname === url;
  // 严格匹配
  if (exact && !isExact) return null;
  // 返回一个 match 对象
  return {
    path, 
    url: path === "/" && url === "" ? "/" : url, 
    isExact, 
    // 这里获取参数
    // 比如 path: '/:foo', pathname: '/a'
    // 匹配得到的 keys: [{name: 'foo', ...}], match: ['/a', 'a'], values: ['a']
    params: keys.reduce((memo, key, index) => {
      memo[key.name] = values[index];
      return memo;
    }, {})
  };
};