前言

花了一点时间把react-router系统的整理了一下,包括常用组件的功能原理以及基本实现方式, 文中所贴出来的代码都是每个组件的核心原理的实现,与源码会有略有不同,敬请注意,源码地址均已提供详细的连接,点击即可跳转。放心食用。

渲染方式

  • children
  • component
  • render

优先级:

这三种渲染方式是互斥的,同时存在的情况下: children > component > render;
这是源码中关于优先级部分的代码;

注意事项

  1. childrenrender是只能以匿名函数的形式传入要展示的节点,component则不需要。
  2. componentrender需要path匹配上以后才能展示,children则不论是否匹配都会展示。
  3. component不建议以匿名函数的形式传入要展示的节点,因为渲染的时候会调用React.createElement,如果使用匿名函数的形式,每次都会生成新的type,导致子组件出现频繁挂载和卸载的问题,childrenrender则不会;
    有兴趣的可以尝试运行一下代码;
'use strict';
import React, { useState, useEffect } from 'react';
import { Router, Route } from 'react-router';

const Child = (props) => {

  useEffect(() => {
    console.log("挂载");
    return () =>  console.log("卸载");
  }, []);

  return <div>Child - {props.count}</div>
}

class ChildFunc extends React.Component {
  componentDidMount() {
    console.log("componentDidMount");
  }

  componentWillUnmount() {
    console.log("componentWillUnmount");
  }
  render() {
    return <div>
      ChildFunc - {this.props.count}
    </div>
  }
}

const Index = (props) => {
  const [count, setCount] = useState(0);

  return <div>
     <button onClick={() => setCount((state) => state + 1)}>add</button>
    <p>chick change count{count}</p>
    <Router >
      {/* bad 观察一下挂载和卸载函数的log*/}
      <Route component={() => <Child count={count} />} />
      <Route component={() => <ChildFunc count={count} />} />

      {/* good 这才是正确的打开方式 观察一下挂载和卸载函数的log*/}
      <Route render={() => <Child count={count} />} />
      <Route render={() => <ChildFunc count={count} />} />

      {/* 观察一下挂载和卸载函数的log 这种也是可以的但是children不需要匹配path,慎用*/}
      <Route children={() => <ChildFunc count={count} />} />
      <Route children={() => <Child count={count} />} />
    </Router>
  </div>
};
export default Index;

Link组件

link 本质上就是一个a标签,但是直接使用href属性点击的时候会有抖动需要使用命令的方式跳转,源码中对其追加了部分属性和功能,并且对参数toclick事件进行了处理。

源码请移步

'use strict';
import React, { useContext } from 'react'
import RouterContext from './RouterContext'
export default function Link({ to, children }) {
  const { history } = useContext(RouterContext)
  const handle = e => {
    // 防止抖动所以禁掉默认行为命令形式跳转
    e.preventDefault();
    history.push(to)
  };
  return <a href={to} onClick={handle}>{children}</a>
};

BrowserRouter组件

这个组件是react-router的最上层组件,主要作用决定路由系统使用何种路由。

查看源码请移步

'use strict'
import React, { PureComponent } from 'react';
import { createBrowserHistory } from 'history'
import Router from "./Router"

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

RouterContext.js 文件

因为路由组件可以和普通元素节点进行嵌套,并不能很好的确定具体的层级关系,所以我们依旧选择跨层级数据残敌的方式来实现。声明并导出RouterContext拆分成独立文件会使逻辑更加清晰。
源码并没有直接使用createContext而是又包了一层createNamedContext为生成的context添加了一个displayName.

源码

'use strict';
import React from 'react'
const RouterContext = React.createContext();
export default RouterContext;

Router.js 文件

Router文件主要作用:

  • 通过RouterContext向下传递historylocationmatch等属性;
  • 通过history.listen监听页面的location的变化,并向下传递location方便Route组件以及Switch组件进行匹配;
    源码
'use strict'
import React, { PureComponent } from 'react';
import RouterContext from 'RouterContext'

export default class Router extends PureComponent {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }
  constructor() {
    super(props)
    this.state = {
      location: props.history.location
    }
    this.unlinsten = props.history.listen((location) => {
      this.setState({ location })
    })
  }
  componentWillUnmount() {
    this.unlinsten();
  }

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

Route 组件

route组件主要是负责match的处理并返回需要渲染的component组件,这个match可以是上层Switch组件传下来的computedMatch, 如果上层没有使用Switch组件,则判定Route组件接收到的path属性是否存在 这在则与location.pathname进行比对如果匹配上就展示展示不上就不不展示,path也可以为空,如果为空就直接使用context.match

源码

matchPath源码

'use strict'
import React, { PureComponent } from 'react';
import matchPath from './matchPath';
import RouterContext from './RouterContext';

export default class Route extends PureComponent {
  render() {
    return <RouterContext.Consumer >
      {(context) => {
        const { path, children, component, render, computedMatch } = this.props;
        const { location } = context;
        // 当match时,说明当前匹配成功
        const match = computedMatch
          ? computedMatch
          : path
            ? matchPath(location.pathname, this.props)
            : context.match;
        const props = { ...context, match }
        // 匹配成功以后要根据children > component > render的优先级来渲染

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

switch组件

Switch寓意为独占路由,作用:匹配路由并且只渲染匹配到的第一个route或者redirect

因为以上原因,例如404这样不写path属性的组件一定要放在最后,不然404组件一旦被匹配,那之后的子组件都不会再匹配了;

和Route组件的区别在于,Switch是控制显示哪一个Route 组件,而Route 组件空的是当前这个Route组件下的component是否展示

源码

'use strict'
import React, { PureComponent } from 'react';
import matchPath from './matchPath';
import RouterContext from './RouterContext';

export default class Switch extends PureComponent {
  render() {
    return <RouterContext.Consumer>
      {
        (context) => {
          let match; // 标记是否匹配
          let element; // 匹配到的元素
          /**
           * 这里接受到的props.children有可能是一个也有可能是多个
           * 理论上我们需要自行去做if判断,但是React提供了一个api,React.Children
           * 它当中的forEach会帮助我们完成这样的事情
           */
          React.Children.forEach(this.props.children, child => {
            // isValidElement判断是不是一个React节点
            if (match == null && React.isValidElement(child)) {
              element = child;
              match = child.props.path
              ? matchPath(context.location.pathname, child.props)
              : context.match
            }
          });

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

redirect

redirect是路由重定向,作用:

  1. 返回一个空组件。
  2. 跳转到执行页面

源码

'use strict'
import React, { useEffect, PureComponent } from 'react';
import RouterContext from './RouterContext';

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

const LifeCycle = ({ onMount }) => {
  useEffect(() => {
    if (onMount) onMount()
  }, [])
  return null
}

常用的几个hook

直接贴代码吧,这几个简单的我已经不会描述了。

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

withRouter就不写了比较简单,就是套个高阶组件,然后获取下context然后传进去就行可以了。

总结

知识点基本都写在前面了这里做一个简单总结:

  • BrowserRouter组件在最上层决定路由体系使用什么类型的history;
  • 然后在Router文件中定义context,使用跨层级通信的方式传递history,match以及loaction等属性,并使用history.listen监听loaction的变化;
  • 在Router组件和Switch组件中比对path和location,并渲染对应的组件,Switch组件决定渲染哪一个Route组件,而Route组件决定当前组件是否渲染;
  • Route组件有三种渲染方式,互相是互斥的且children > component > render,需要注意是的三个属性的入参标准,以及不建议component使用匿名函数方式入参;
  • Route里还有一点需要注意就是为了让我们后续使用中可以准确的获取match,这里在return的时候需要用<RouterContext.Provider value={props}> </RouterContext.Provider>包裹一次并传入新的match,以及context的就近取值特性;
  • Switch组件寓意为独占路由,也就是只渲染匹配到的第一个Route组件;
03-05 20:06