react-router-dom 源码分析&简单实现

源码地址:
https://github.com/ReactTraining/react-router

使用教程:
https://reactrouter.com/web/guides/quick-start

基本使用

import React from "react";

import {
  BrowserRouter as Router,
  Route,
  Link,
  Switch,
  withRouter
} from "./my-react-router-dom/";

import HomePage from "./pages/HomePage";
import UserPage from "./pages/UserPage";
import LoginPage from "./pages/LoginPage";
import ProductPage from './pages/ProductPage';
import _404Page from "./pages/_404Page";

function App() {
  return (
    <div className="App">
      <Router>
          <Link to="/">首页</Link>
          <Link to="/user">用户中心</Link>
          <Link to="/login">登录</Link>
          <Link to="/product/123">商品</Link>

          <Switch>
            <Route
              path="/"
              exact
              component={HomePage}
            />
            <Route
              path="/product/:id"
              render={() => <Product />}
            />
            <Route path="/user" component={UserPage} />
            <Route path="/login" component={LoginPage} />
            <Route component={_404Page} />
          </Switch>
        </Router>
    </div>
  );
}

export default App;

接下来本文将根据使用方式从外向内逐一介绍,并简单实现

本文的例子将只考虑 BrowserRouter,HashRouter 可以自己扩展或到源码处阅读,其实道理都差不多


基本概念快速介绍

  1. Route 是怎么监听路由然后根据路由跳转后的地址实施不同组件的渲染?

    react-router-dom 使用了 React 的 ContextAPI 进行值的传递,其中 context 的来源如图所示
    react-router-dom提供源解析

  2. history 对象哪来的?

    一般来说可以通过 window 对象来获取,但是在 React 中,引入了 react-router-dom 的时候就会引入一个 history依赖,所以我们只要使用这个依赖就可以了

BrowserRouter

BrowserRouter 完成的事

  1. 通过 historycreateBrowserRouter 创建 history对象
  2. 通过参数形式传递给通用的 <Router/>

Router

  1. 挂载时通过传入的 historylisten() 监听路由
  2. componentWillUnmount 中执行取消监听方法
  3. 在返回的 Provider 中的 value 传入一个对象,键值对分别为 historylocation根match

注意:
    熟悉 contextAPI 的同学都知道,Provider 所处的组件发生更新时,Provider 会对其 value 进行一次浅比较,当 value 传入的是引用数据类型的时候,更新组件就相当于给 value 赋值了一个新的引用数据,而引用数据类型比较的是地址,所以当这个 Provider 所处的组件发生更新时,Provider 及其后代组件都会发生更新,这样会造成性能浪费。。然而为什么这里传入对象是合适的呢?

回想一下我们平时使用 <Router> 的位置,通常都是放在最外层的组件,所以在这个情况下不用担心这种情况

BrwoserRouter.js

import React, {Component} from "react";
import {createBrowserHistory} from "history";
import Router from "./Router";

export default class BrowserRouter extends Component {
  constructor(props) {
    super(props);
    this.history = createBrowserHistory();
  }
  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

Router

import React, {Component} from "react";
import {RouterContext} from "./RouterContext";

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

  constructor(props) {
    super(props);
    this.state = {
      location: props.history.location
    };
    // 监听路由
    this.unlisten = props.history.listen(location => {
      this.setState({location});
    });
  }

  componentWillUnmount() {
    if (this.unlisten) {
      this.unlisten();
    }
  }

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

这里将 Router 分离封装出来是因为方便 BrowserRouterHashRouter 的使用


Link

本质是一个 a标签

点击时:

  1. 阻止默认跳转
  2. 通过 BrowserRouter 传递的 contexthistory 跳转
    context.history.push(to)
import React, {useCallback} from "react";
import {RouterContext} from "./RouterContext";

export default function Link({to, children, ...restProps}) {
  const context = React.useContext(RouterContext);
  const handleClick = useCallback(event => {
    event.preventDefault();
    context.history.push(to);
  }, [context.history, to])

  return (
    <a href={to} {...restProps} onClick={handleClick}>
      {children}
    </a>
  );
}


Switch

Swtich 做的事:

  1. 得到 根Router 传递下来的 context,去其中的 location
  2. 遍历 children,选出正确的 element(Route)

Switch.js

import React, {Component} from "react";
import {RouterContext} from "./RouterContext";
import matchPath from "./matchPath";

// 独占路由
// 渲染与该地址匹配的第一个子节点 <Route> 或者 <Redirect>。
class Switch extends Component {
  render() {
    console.log('Switch rendered');
    return (
      <RouterContext.Consumer>
        {context => {
          const location = this.props.location || context.location;
          let match; //标记匹配
          let element; //记录匹配的元素

          React.Children.forEach(this.props.children, child => {
            if (match == null && React.isValidElement(child)) {
              element = child;

              match = child.props.path
                ? matchPath(location.pathname, child.props)
                : context.match;
            }
          });
          
          return match
            ? React.cloneElement(element, {computedMatch: match})
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}
export default Switch;

注意:
    路由更新时只会执行 Consumer 中的内容,不会重新触发 Switch组件render()


Route

Route 渲染方式 优先级从高到低:

  1. children (func)
  2. component (component)
  3. render (func)

Route 渲染逻辑
Route渲染逻辑
Route.js

import React, {Component} from "react";
import {RouterContext} from "./RouterContext";
import matchPath from "./matchPath";

export default class Route extends Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          const {location} = context;
          const {path, children, component, render, computedMatch} = this.props;
          const match = computedMatch
            ? computedMatch
            : path
            ? matchPath(location.pathname, this.props)
            : context.match; 

          const props = {
            ...context,
            match
          };
          //match children, component, render, null
          // 不匹配 children(function), null
          return (
            <RouterContext.Provider value={props}>
              {match
                ? children
                  ? typeof children === "function"
                    ? children(props)
                    : children
                  : component
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? children(props)
                : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

matchPath.js

大概知道这个文件的作用就行,第一次看可以不用深究

import pathToRegexp from "path-to-regexp";

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

function compilePath(path, options) {
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  const pathCache = cache[cacheKey] || (cache[cacheKey] = {});

  if (pathCache[path]) return pathCache[path];

  const keys = [];
  const regexp = pathToRegexp(path, keys, options);
  const result = { regexp, keys };

  if (cacheCount < cacheLimit) {
    pathCache[path] = result;
    cacheCount++;
  }

  return result;
}

/**
 * Public API for matching a URL pathname to a path.
 */
function matchPath(pathname, options = {}) {
  if (typeof options === "string" || Array.isArray(options)) {
    options = { path: options };
  }

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

  const paths = [].concat(path);

  return paths.reduce((matched, path) => {
    if (!path && path !== "") return null;
    if (matched) return matched;

    const { regexp, keys } = compilePath(path, {
      end: exact,
      strict,
      sensitive
    });
    const match = regexp.exec(pathname);

    if (!match) return null;

    const [url, ...values] = match;
    const isExact = pathname === url;

    if (exact && !isExact) return null;

    return {
      path, // the path 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) => {
        memo[key.name] = values[index];
        return memo;
      }, {})
    };
  }, null);
}

export default matchPath;


其他组件

Redirect

import React, {Component} from "react";
import {RouterContext} from "./RouterContext";

export default class Redirect extends Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          const {history} = context;
          const {to, push = false} = this.props;
          return (
            <LifeCycle
              onMount={() => {
                push ? history.push(to) : history.replace(to);
              }}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

class LifeCycle extends Component {
  componentDidMount() {
    if (this.props.onMount) {
      this.props.onMount.call(this, this);
    }
  }
  render() {
    return null;
  }
}

注意:
    这里用到了一个工具人函数,借用了一下它的 componentDidMount 去做路由跳转

Prompt

Prompt.js

import React from 'react';
import {RouterContext} from './RouterContext';
import LifeCycleFragment from './LifeCycleFragment';

export default function Prompt({message, when = true}) {
  return (
    <div>
      <RouterContext.Consumer>
        {
          context => {
            if (!when) return null;
            let method = context.history.block;
         
            return (
              <LifeCycleFragment
                onMounted={self => {
                  self.release = method(message);
                }}
                onUnmount={self => {
                  self.release();
                }}
              />
            )
          }
        }
      </RouterContext.Consumer>
    </div>
  )
}

LifeCycleFragment.js

import React from 'react';

export default class LifeCycleFragment extends React.Component {
  componentDidMount() {
    if (this.props.onMounted) {
      this.props.onMounted.call(this, this);
    }
  }

  // 在源码中这里还考虑了组件更新的情况
  // xxx
  
  componentWillUnmount() {
    if (this.props.onUnmount) {
      this.props.onUnmount.call(this, this);
    }
  }
  
  render () {
    return null
  }
}

钩子函数

import {RouterContext} from "./RouterContext";
import {useContext} from "react";

export function useHistory() {
  return useContext(RouterContext).history;
}

export function useLocation() {
  return useContext(RouterContext).location;
}
export function useRouteMatch() {
  return useContext(RouterContext).match;
}
export function useParams() {
  const match = useContext(RouterContext).match;
  return match ? match.params : {};
}

感谢你能看到这里,有任何疑问欢迎评论或私信~

希望更深一步研究请直接研究源码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React Router是一个用于构建单页面应用程序的库。它提供了一种在React应用程序中管理路由的方式。React Router DOMReact Router的一个扩展,用于在Web应用程序中进行路由。 React Router DOM 5和React Router DOM 6之间有几个重要的区别: 1. 安装方式:React Router DOM 5使用npm包管理器进行安装,命令为`npm install react-router-dom`。而React Router DOM 6使用yarn进行安装,命令为`yarn add react-router-dom@next`。 2. 路由组件:在React Router DOM 5中,使用`<Route>`组件来定义路由。而在React Router DOM 6中,使用`<Route>`组件的替代方案`<Routes>`来定义路由。 3. 路由匹配方式:React Router DOM 5使用基于路径的匹配方式来确定哪个路由应该被渲染。而React Router DOM 6引入了新的匹配方式,称为元素匹配(element matching),它可以根据组件的类型来匹配路由。 4. 嵌套路由:在React Router DOM 5中,嵌套路由需要使用嵌套的`<Route>`组件来定义。而在React Router DOM 6中,可以使用嵌套的`<Routes>`组件来定义嵌套路由。 5. 动态路由:在React Router DOM 5中,可以通过在路径中使用参数来定义动态路由。而在React Router DOM 6中,可以使用`<Route>`组件的新属性`element`来定义动态路由。 6. 错误处理:React Router DOM 5使用`<Switch>`组件来处理路由匹配错误。而React Router DOM 6使用`<Routes>`组件的新属性`fallback`来处理路由匹配错误。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值