乐趣区

关于react.js:从源码对reactrouter-v5进行原理分析重编制二

前言

此篇文章默认读者曾经把握 react-router 的 api, 或是对其有所理解;

在看这篇文章之前, 须要先对 react-router 和 react-router-dom 有一个简略的理解;

首先来看官网对两者的形容

The core of React Router (react-router)

DOM bindings for React Router (react-router-dom)

react-router 是 React Router 的 外围 , 实现了 路由的外围性能;

react-router-dom 是 React Router 的 DOM 绑定, 提供了浏览器环境下的性能, 比方 <Link>, <BrowserRouter> 等组件;

能够了解为:

react-router-dom 基于 react-router, 装置依赖的时候只须要装置 react-router-dom 就好了;

react-router 构造剖析

依据官网文档, 应用 react-router-dom 进行路由治理, 首先咱们须要抉择一个路由模式:

  • BrowserRouter: History 模式
  • HashRouter: Hash 模式
  • MemoryRouter: 在没有 url 的状况下, 应用 Memory 记住路由, 常见在 React Native 中应用, 这里不进行探讨

以下都以 create-react-app 为例 , 抉择 History 模式, 也就是在最外层应用<BrowserRouter> 组件:

index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter as Router} from 'react-router-dom';
import App from './App';

ReactDOM.render(
  <Router>
    <App />
  </Router>,
  document.getElementById('root')
);

而后在被 <BrowserHistory> 组件包裹的组件中能够应用 <Route> 进行路由划分:

App.tsx

import React from 'react';
import {Route} from 'react-router-dom';

const Page1.React.FC = props => {return <div>Page1</div>;};

const Page2.React.FC = props => {return <div>Page2</div>;};

function App() {
  return (
    <div className="App">
      <Route path="/page1" component={Page1}></Route>
      <Route path="/page2" component={Page2}></Route>
    </div>
  );
}

export default App;

以上就是 react-router 的大略构造, 上面将对 `react-router-dom 的组件进行源码剖析;

BrowserHistory

<BrowserHistory><HashHistory> 的代码构造和逻辑类似, 这里只对 <BrowserHistory> 作剖析;

以下是 <BrowserHistory> 外围代码逻辑剖析:

定义 <BrowserHistory> 的 prop-types

import PropTypes from "prop-types";

class BrowserRouter extends React.Component {// 此处代码略去}

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

<BrowserHistory>的外围逻辑

应用 historycreateBrowserHistory办法, 将 props 作为参数, 创立一个 history 实例, 并将 history 传入 Router 组件中:

import {Router} from "react-router";
import {createBrowserHistory as createHistory} from "history";

class BrowserRouter extends React.Component {history = createHistory(this.props);

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

从源码中能够看出, <BrowserHistory>是一个注入了 history<Router>组件;

Router

react-router-dom 中的 <Router> 实际上就是 react-router 的 Router, 此处间接对 react-router 的<Router> 进行源码剖析:

定义 <Router> 的 prop-types

import PropTypes from "prop-types";

Router.propTypes = {
  children: PropTypes.node,
  history: PropTypes.object.isRequired,
  staticContext: PropTypes.object
};

此处的 staticContext<staticRouter>中传入 <Router> 的属性, 这里不做剖析;

<Router>的路由渲染逻辑

<Router> 构造函数 中, 申明 this.state.location, 应用history 的监听函数对 history.location 进行监听, 并将 history.listen 的返回值赋值给this.unlisten:

this.state = {location: props.history.location};

this._isMounted = false;
this._pendingLocation = null;

this.unlisten = props.history.listen(location => {if (this._isMounted) {this.setState({ location});
  } else {this._pendingLocation = location;}
});

之所以在构造函数中就对 history.location 进行监听, 而不是在 componentDidMount 中进行监听, 官网是这么解释的:

This is a bit of a hack. We have to start listening for location changes here in the constructor in case there are any <Redirect>s on the initial render. If there are, they will replace/push when they mount and since cDM fires in children before parents, we may get a new location before the <Router> is mounted.

大略意思就是, 因为子组件会比父组件更早渲染实现, 以及 <Redirect> 的存在, 若是在 <Router>componentDidMount生命周期中对 history.location 进行监听, 则有可能在监听事件注册之前, history.location曾经因为 <Redirect> 产生了屡次扭转, 因而咱们须要在 <Router>constructor中就注册监听事件;

接下来, 在 componentWillUnmount 生命周期中进行移除监听函数操作:

componentWillUnmount() {if (this.unlisten) {this.unlisten();
    this._isMounted = false;
    this._pendingLocation = null;
  }
}

react-router 中应用 context 进行组件通信

<Router> 中, 应用 <RouterContext.Provider> 进行路由数据传递 (history,location, match 以及 staticContext), 应用 <HistoryContext.Provider> 进行 history 数据传递, 子组件 (<Route> 或是 <Redirect> 等)能够通过 <RouterContext.Consumer> 以及 <HistoryContext.Consumer> 对下层数据进行接管; HistoryContextRouterContext 都是应用 mini-create-react-context 创立的 context, 而mini-create-react-context 工具库定义如下:

(A smaller) Polyfill for the React context API

mini-create-react-context是 React context API 的 Polyfill, 因而能够间接将 mini-create-react-context 当成 React 的 context;

import React from "react";
import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js";

class Router extends React.Component {static computeRootMatch(pathname) {return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  render() {
    return (
      <RouterContext.Provider
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext
        }}
        >
        <HistoryContext.Provider
          children={this.props.children || null}
          value={this.props.history}
        />
      </RouterContext.Provider>
    );
  }
}

Switch

<Switch> is unique in that it renders a route exclusively

即便有多个路由组件胜利匹配, Switch也只展现一个路由

<Switch>必须作为 <Router> 的子组件进行应用, 若是脱离<Router>, 则会报错:

"You should not use <Switch> outside a <Router>"

定义 <Switch> 中传入的 prop-types

import PropTypes from "prop-types";

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

<Switch>的渲染逻辑

<Switch>应用 <RouterContext.Consumer> 进行路由数据接管; <Switch>对路由组件进行程序匹配, 应用 React.Children.forEach<Switch>的子组件进行遍历, 每次遍历逻辑如下:

应用 React.isValidElement 判断子组件是否为无效的 element:

  • 无效: 则进入 下个步骤;
  • 有效: 完结此轮循环, 进行下一轮循环;

申明path:

const path = child.props.path || child.props.from;

注: <Route>应用 path 进行路由地址申明, <Redirect>应用 from 进行重定向起源地址申明;

接着判断 path 是否存在:

  • 存在 path: 示意子组件存在路由映射关系, 应用 matchPath 对 path 进行匹配, 判断路由组件的门路与以后 location.pathname 是否匹配:

    • 若是匹配, 则对子组件进行渲染, 并将 matchPath 返回的值作为 computedMatch 传递到子组件中, 并且不再对其余组件进行渲染;
    • 若是不匹配, 则间接进行下次循环; 留神: location能够是内部传入的props.location, 默认为context.location;
  • 不存在 path: 示意子组件不存在路由映射关系, 间接渲染该子组件, 并将 context.match 作为 computedMatch 传入子组件中;

matchPath 是 react-router 的一个公共 api, 源码中正文对 matchPath 的介绍如下:

Public API for matching a URL pathname to a path.

次要用于匹配路由, 匹配胜利则返回一个 match 对象, 若是匹配失败, 则返回null;

import React from 'react';
import RouterContext from "./RouterContext.js";
import matchPath from "./matchPath.js";

/**
 * The public API for rendering the first <Route> that matches.
 */
class Switch extends React.Component {render() {
    return (
      <RouterContext.Consumer>
        {context => {invariant(context, "You should not use <Switch> outside a <Router>");

          const location = this.props.location || context.location;

          let element, match;

          // We use React.Children.forEach instead of React.Children.toArray().find()
          // here because toArray adds keys to all child elements and we do not want
          // to trigger an unmount/remount for two <Route>s that render the same
          // component at different URLs.
          React.Children.forEach(this.props.children, child => {if (match == null && React.isValidElement(child)) {
              element = child;

              const path = child.props.path || child.props.from;

              match = path
                ? matchPath(location.pathname, { ...child.props, path})
                : context.match;
            }
          });

          return match
            ? React.cloneElement(element, { location, computedMatch: match})
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}

Route

The Route component is perhaps the most important component in React Router to understand and learn to use well. Its most basic responsibility is to render some UI when its path matches the current URL

<Route>可能是 react-router 中 最重要的组件, 它最根本的职责是在其门路与以后 URL 匹配时出现对应的 UI 组件;

与其余非 <Router> 组件一样, 若是不被 <RouterContext.Provider> 包裹, 则会报错:

"You should not use <Switch> outside a <Router>"

定义 <Route> 的 prop-types:

import PropTypes from "prop-types";

Route.propTypes = {children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
  component: (props, propName) => {if (props[propName] && !isValidElementType(props[propName])) {
      return new Error(`Invalid prop 'component' supplied to 'Route': the prop is not a valid React component`);
    }
  },
  exact: PropTypes.bool,
  location: PropTypes.object,
  path: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.arrayOf(PropTypes.string)
  ]),
  render: PropTypes.func,
  sensitive: PropTypes.bool,
  strict: PropTypes.bool
};

<Route>的渲染逻辑

与其它路由组件一样, 应用 <RouterContext.Consumer> 接管全局路由信息; <Route>的逻辑比较简单, 次要判断 path 与以后路由是否匹配, 若是匹配则进行渲染对应路由组件, 若是不匹配则不进行渲染, 外围代码如下:

const match = this.props.computedMatch
  ? this.props.computedMatch // <Switch> already computed the match for us
  : this.props.path
  ? matchPath(location.pathname, this.props)
  : context.match;

...

<RouterContext.Provider value={props}>
  {
    props.match
    ? children
      ? typeof children === "function"
          ? __DEV__
              ? evalChildrenDev(children, props, this.props.path)
                : children(props)
            : children
        : component
          ? React.createElement(component, props)
            : render
              ? render(props)
                : null
    : typeof children === "function"
      ? __DEV__
          ? evalChildrenDev(children, props, this.props.path)
            : children(props)
        : null
  }
</RouterContext.Provider>

注: 依据下面代码, 不管 props.match 是否为 true, 当 <Route>children为函数时都会进行渲染;

总结

本篇文章对 react-router 的局部外围组件进行源码解读; react-router 应用 <Context.Provider> 向路由树传递路由信息, <Route>等组件通过 <Context.Consumer> 接管路由信息, 匹配门路并渲染路由组件, 以及与上篇文章讲到的 history 的紧密配合, 才让 react-router 如此优良; 下一篇文章将对残余组件以及 react-router 的 hooks 进行源码解读!

退出移动版