SunShinewyf/issue-blog

react-router 源码解读

SunShinewyf opened this issue · 0 comments

在项目过程中,对 react-router 这个库了解甚少,只是停留在一些基础的用法层面,源码层面的东西还是云里雾里,对它如何运作的原理也是知之甚少,所以趁着最近修 bug 的空档期深入学习一下。

此文基于 react-router 4 进行讲解,如果对 react-router 还不太熟悉的童鞋可以移步 初探 React Router 4.0 和 react-router 的 API 文档地址 先学习一下。

react-router-dom 和 react-router

react-router-dom 中包含了 web 端所有路由相关的东西,它基于 react-router 进行了一些包装,所以项目中只需要引入 react-router-dom 即可。react-router 4 中强调万物即组件,其暴露的几乎所有东西都是一个 react 组件,正是因为这种 Just Components 的**使得开发者更容易在项目中使用路由。
一张图显示出他们之间的关系:

images

从上图可知,react-router-dom 中的很多元素都是直接从 react-router 中拿过来的,只是在它的基础上进行了一些功能上的扩展。

源码分析

Router

Router 是创建路由最外面的包裹层组件,有点类似 Provider 的感觉,它的作用是监听路由的变化,从而渲染的页面组件。其源码如下:

class Router extends React.Component {
  static propTypes = {
    history: PropTypes.object.isRequired,
    children: PropTypes.node
  };

  state = {
    match: this.computeMatch(this.props.history.location.pathname)
  };

  computeMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  }

  componentWillMount() {
    const { children, history } = this.props;

    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }

  componentWillUnmount() {
    this.unlisten();
  }

  render() {
    const { children } = this.props;
    return children ? React.Children.only(children) : null;
  }
}

源码部分中包含声明接受的 props 数据为 history 对象和要显示的 child 节点内容。并且在组件渲染之前(componentWillMount) 的时候,监听路由( history 是一个记录浏览器记录的一个库)的改变,当 url 发生更改时可以执行传递过去的 setState 回调,当 state 进行更新时,就会重新执行 render 对应的逻辑,从而引发页面的重新渲染。在 Router 组件销毁期间,取消对浏览器的监听。
history 有三种形式,分别是:

  • browser history
  • hash history
  • memory history
    关于 history 的一些基础知识,可以移步 React Router预备知识,关于history的那些事 进行学习,这里不赘述。有三种 history, 对应的就有三种 Router:
  • BrowserRouter
  • HashRouter
  • MemoryRouter
    这几种 Router 的源码只是改变了 history 的类型,然后传递给 Router 进行展示,以 BrowserRouter 为例,代码如下:
import { createBrowserHistory as createHistory } from "history";

class BrowserRouter extends React.Component {
  static propTypes = {
    basename: PropTypes.string,
    forceRefresh: PropTypes.bool,
    getUserConfirmation: PropTypes.func,
    keyLength: PropTypes.number,
    children: PropTypes.node
  };

  history = createHistory(this.props);

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

内容很简单,结合 Router 源码很好理解。HashRouter 和 MemoryRouter 的套路一样,在此不再赘述。

Route

Route 是嵌在 Router 里面的元素,如下:

  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <Route path="about" component={About}/>
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User}/>
      </Route>
    </Route>
  </Router>

它的作用是根据 url 进行匹配,如果匹配到当前路由,就展示对应的组件或者内容,否则显示 null。主要源码如下:

class Route extends React.Component {
  static propTypes = {
    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
  };

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

  computeMatch(
    { computedMatch, location, path, strict, exact, sensitive },
    router
  ) {
    if (computedMatch) return computedMatch; // <Switch> already computed the match for us

    const { route } = router;
    const pathname = (location || route.location).pathname;

    return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
  }

  componentWillReceiveProps(nextProps, nextContext) {
    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 };

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

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

    if (typeof children === "function") return children(props);

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

    return null;
  }
}

Route 在 componentWillReceiveProps 这个生命周期通过接受父级组件传来的 router 数据,从而来不断更新自己的 state 数据,进而达到重新渲染组件的效果。
其中 matchPath 这个方法,主要是用 path-to-regexp 这个库来匹配路由参数,并且将已经访问过的路由进行缓存,下次再匹配到这个路由的时候直接返回匹配的数据,其中缓存限制为 10000,具体的逻辑可以看 matchPath 源码,这里不贴出。
其中, strict, exact, sensitive 只是限定了一些匹配的规则,然后在 render 中进行渲染对应的渲染函数,根据传入的渲染规则不同,执行不同的渲染方式。

Link

Link 在浏览器中渲染出来就是一个 a 标签,只是动态赋给 a 一个 href 进行页面跳转,源码如下:

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

  handleClick = event => {
    if (this.props.onClick) this.props.onClick(event);

    if (
      !event.defaultPrevented && // onClick prevented default
      event.button === 0 && // ignore everything but left clicks
      !this.props.target && // let browser handle "target=_blank" etc.
      !isModifiedEvent(event) // ignore clicks with modifier keys
    ) {
      event.preventDefault();

      const { history } = this.context.router;
      const { replace, to } = this.props;

      if (replace) {
        history.replace(to);
      } else {
        history.push(to);
      }
    }
  };
  render() {
    const { replace, to, innerRef, ...props } = this.props; // eslint-disable-line no-unused-vars

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

如上所示:渲染一个 a 标签,并获取一个 href,当点击的时候,触发点击的回调函数,为了防止页面刷新,需要禁掉浏览器的默认行为,所以在 handleClick 中执行了 event.preventDefault(),并根据传进的 props 是 to 还是 replace 进行不同的操作。

NavLink

NavLink 是基于 Link 的一个特殊版本,会在匹配当前 url 的元素添加一些传递的属性和样式:activeClassName 和 activeStyle ,源码部分是 Link 和 Route 的组合版本,这里就不深入了。

Redirect

Redirect 在渲染时将会跳转到一个新的 url,这个新的 url 将会覆盖掉历史信息里面本该访问的那个地址。源码如下:

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

  isStatic() {
    return this.context.router && this.context.router.staticContext;
  }
  componentWillMount() {
    if (this.isStatic()) this.perform();
  }
  componentDidMount() {
    if (!this.isStatic()) this.perform();
  }

  componentDidMount() {
    if (!this.isStatic()) this.perform();
  }

  componentDidUpdate(prevProps) {
    //省略校验
    this.perform();
  }

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

如上源码,Redirect 在组件的几个生命周期中都去执行了 perform 函数,perform 的功能就是通过判断是跳转路由还是覆盖路由从而进行 history 的相应操作,computeTo 功能主要是返回一个 url 进行页面跳转或者替换。和 matchPath 方法类似, computeTo 里调用的 generatePath 也有缓存的操作,对新产生的路由进行缓存,下次再进行 redirect 的时候直接返回,不需要再次通过 pathToRegexp 构造。

Switch

Switch 用来嵌套在 Route 外面,找出第一个匹配的 Route 进行渲染,其他的 Route 就不会去渲染。源码如下:

class Switch extends React.Component {
  static propTypes = {
      children: PropTypes.node,
      location: PropTypes.object
  };

  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
        } = element.props;
        const path = pathProp || from;

        child = element;
        match = matchPath(
          location.pathname,
          { path, exact, strict, sensitive },
          route.match
        );
      }
    });

    return match
      ? React.cloneElement(child, { location, computedMatch: match })
      : null;
  }
}

首先会对 Children 进行遍历,然后使用上文提到的 matchPath 方法进行匹配,然后在 render 时只渲染匹配的那个 child。

总结

整个 react-router 的源码还是比较简单的,没有什么特别晦涩的地方,基本上都能看懂,了解其内部源码的原理,在项目中就可以比较得心应手地使用了~