react篇lesson4(react-router)知識點

machinist發表於2021-11-24

前言

花了一點時間把react-router系統的整理了一下,包括常用元件的功能原理以及基本實現方式, 文中所貼出來的程式碼都是每個元件的核心原理的實現,與原始碼會有略有不同,敬請注意,原始碼地址均已提供詳細的連線,點選即可跳轉。放心食用。

渲染方式

  • children
  • component
  • render

優先順序:

這三種渲染方式是互斥的,同時存在的情況下: children > component > render;
這是原始碼中關於優先順序部分的程式碼;
WechatIMG40.png

注意事項

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

注意:

  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 <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元件;

相關文章