react篇lesson4(react-router)知识点

前言

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

渲染方式

  • children
  • component
  • render

优先级:

这三种渲染方式是互斥的,同时存在的情况下: children > component > render;
这是源码中关于优先级部分的代码;
react篇lesson4(react-router)知识点_第1张图片

注意事项

  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 
Child - {props.count}
} class ChildFunc extends React.Component { componentDidMount() { console.log("componentDidMount"); } componentWillUnmount() { console.log("componentWillUnmount"); } render() { return
ChildFunc - {this.props.count}
} } const Index = (props) => { const [count, setCount] = useState(0); return

chick change count{count}

{/* bad 观察一下挂载和卸载函数的log*/} } /> } /> {/* good 这才是正确的打开方式 观察一下挂载和卸载函数的log*/} } /> } /> {/* 观察一下挂载和卸载函数的log 这种也是可以的但是children不需要匹配path,慎用*/} } /> } />
}; 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 {children}
};

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 {this.props.children}
  }
};

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 (
      
        {this.props.children}
      
    )
  }
}

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 
      {(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 
          {
            match
              ? children
                ? typeof children === "function" ? children(props) : children
                : component ? React.createElement(component, props)
                  : render ? render(props) : null
              : typeof children === "function" ? children(props) : null
          }
        
      }}
    
  }
}

注意:

  1. 上述代码中反复提到的match就是我们路由挂载参数的那个match;
  2. 我们在人returncomponent的地方给返回值有包裹了一层RouterContext.Provider,原因是我们在外部使用useRouteMatchuseParams获取match的时候,context获取到的match其实是Router.js文件传递下来的初始值,但是我们这里需要获取Route组件里面的match值,所以要在包一层,这里是利用了context就近取值的特性;

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

redirect

redirect是路由重定向,作用:

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

源码

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

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

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的时候需要用 包裹一次并传入新的match,以及context的就近取值特性;
  • Switch组件寓意为独占路由,也就是只渲染匹配到的第一个Route组件;

你可能感兴趣的