Preface

It took a while to organize the react-router system, including the functional principles and basic implementation methods of commonly used components. The code posted in the article is the implementation of the core principles of each component, which is slightly different from the source code. Please note that the source address has provided a detailed link, click to jump. Eat with confidence.

Rendering method

  • children
  • component
  • render

priority:

These three rendering methods are mutually exclusive, when they exist at the same time: children > component > render ;
This is the priority part of the code in the source code;
WechatIMG40.png

Precautions

  1. children and render can only be passed to the node to be displayed in the form of anonymous function component is not required.
  2. component and render need path after the match to be shown, children regardless of whether the match will show.
  3. component not recommended to pass in the node to be displayed in the form of anonymous function React.createElement will be called when rendering. If the anonymous function is used, a new type will be generated every time, causing frequent mounting and unmounting of subcomponents. Problem, children and render will not;
    Those who are interested can try to run the code;
'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 component

The link is essentially an a tag, but when you click directly using the href attribute, there will be jitters and you need to use commands to jump. Some attributes and functions are added to it in the source code, and the parameters to and click events are processed.

Source code please move to

'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 component

This component is the react-router component of 0619e0d328eafb, and its main function is to determine what kind of routing the routing system uses.

View the source code , please move to

'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 file

Because routing components can be nested with ordinary element nodes, and the specific hierarchical relationship cannot be determined well, we still choose the cross-level data to achieve this. Declaring and exporting the RouterContext into separate files will make the logic clearer.
The source code does not directly use createContext but a layer of createNamedContext is added to the generated context to add a displayName.

source code

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

Router.js file

The main function of the Router file:

  • Pass down the attributes such as history , location , match
  • Monitor location on the page through history.listen , and pass location facilitate the Route component and the Switch component;
    source code
'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 component

route component mainly responsible for match processing and returns need to be rendered component components, this match may be the upper Switch assembly passed down computedMatch , if the upper layer is not used Switch component is determined Route component received path property exists and which is the location.pathname is compared. If it matches, it will not be displayed if it is not displayed. path can also be empty. If it is empty, use context.match directly;

source code

matchPath source code

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

Notice:

  1. The code repeatedly mentioned match is our route to mount the parameters match ;
  2. We in the return of component place to return values are wrapped in a layer of RouterContext.Provider , because we use external useRouteMatch and useParams get match time, context acquired match fact Router.js file passed down from the initial value, but we are here You need to get the match Route component, so you need to wrap it in one layer, here is the use of the context nearest value of 0619e0d328f047;

switch component

Switch means exclusive routing, and its function: match routing and only render the first route or redirect ;

For the above reasons, components such as 404 that do not write the path attribute must be placed at the end, otherwise once the 404 component is matched, the subsequent sub-components will no longer be matched;

The difference with the Route component is that the Switch controls which Route component is displayed, and the Route component is empty is whether the component under the current Route component is displayed

source code

'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 is a route redirection, and its function:

  1. Return an empty component.
  2. Jump to the execution page

source code

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

Several commonly used hooks

Just post the code, I won’t describe these simple ones anymore.

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 is simpler than writing, just set a high-level component, then get the context and pass it in.

Summarize

The knowledge points are basically written in the front, here is a brief summary:

  • The BrowserRouter component determines what type of history is used in the routing system at the top level;
  • Then define the context in the Router file, use cross-level communication to pass attributes such as history, match, and loaction, and use history.listen to monitor changes in loaction;
  • Compare the path and location between the Router component and the Switch component, and render the corresponding components. The Switch component determines which Route component to render, and the Route component determines whether the current component is rendered;
  • The Route component has three rendering methods, mutually exclusive and children > component > render . It is necessary to pay attention to the input standard of the three attributes, and it is not recommended to use the anonymous function method for the component;
  • Another point to note in Route is to allow us to accurately obtain the match in subsequent use. Here, when <RouterContext.Provider value={props}> </RouterContext.Provider> , we need to wrap it with 0619e0d328f3ce and pass in the new match, as well as the nearest value of the context;
  • The Switch component implies an exclusive route, that is, only the first Route component that is matched is rendered;

machinist
460 声望33 粉丝

javaScript、typescript、 react全家桶、vue全家桶、 echarts、node、webpack