系好安全带,带你遨游 React Router v6 源码

·  阅读 1133
系好安全带,带你遨游 React Router v6 源码

前言

在真正进入 React Router 源码之前我们已经做了两期的准备了,即通过文章 什么,React Router 已经到 V6 了 ?? 介绍了 v6 各种 api 的用法,又通过 React Router 源码解析之 history 详细解析了 history 每个 api 的作用,如果还没看的话,强烈建议先看完前两期,再看本篇文章。本篇文章所有示例代码都来自react-router-source-analysis,如果哪里不太清楚可以点击查看。那接下来,系好安全带,让我们开始 React Router 源码之旅~

BrowserRouter

我们选择 BrowserRouter 来进行讲解,BrowserRouter 一般是作为 App 的 container

ReactDOM.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>,
  document.getElementById('root')
)
复制代码

我们从BrowserRouter 入口开始,看看其做了哪些初始化工作:

export function BrowserRouter({
  basename,
  children,
  window
}: BrowserRouterProps) {
  const historyRef = React.useRef<BrowserHistory>();
  if (historyRef.current == null) {
    // 如果为空,则创建
    historyRef.current = createBrowserHistory({ window });
  }

  const history = historyRef.current;
  const [state, setState] = React.useState({
    action: history.action,
    location: history.location
  });

  React.useLayoutEffect(() => {
    /**
     * popstate、push、replace时如果没有blokcers的话,会调用applyTx(nextAction)触发这里的setState
     * function applyTx(nextAction: Action) {
     *   action = nextAction;
     * //  获取当前index和location
     *   [index, location] = getIndexAndLocation();
     *   listeners.call({ action, location });
     * }
     */
    history.listen(setState)
  }, [history]);
  // 一般变化的就是action和location
  return (
    <Router
      basename={basename}
      children={children}
      action={state.action}
      location={state.location}
      navigator={history}
    />
  );
}
复制代码

BrowserRouter 初始化会生成 history 实例,并把 setState<{action; location}> 放入对应的 listeners,那么路由切换就会 setState 了,这个我们在上面文章 React Router 源码解析之 history 的末尾有提过。

Router

BrowserRouter 最后返回了 Router ,其接收的 prop 有变化的一般就是 actionlocation,其他的一般都在初始化的时候就不变了。当然,我们一般也不直接渲染<Router>,而是具体环境的 Router,如浏览器环境的 <BrowserRouter><HashRouter>,也就是说这个一般是内部使用的

export function Router({
  action = Action.Pop,
  basename: basenameProp = "/",
  children = null,
  location: locationProp,
  /** 实质上就是history */
  navigator,
  static: staticProp = false
}: RouterProps): React.ReactElement | null {

  const basename = normalizePathname(basenameProp);
  const navigationContext = React.useMemo(
    () => ({ basename, navigator, static: staticProp }),
    [basename, navigator, staticProp]
  );

  if (typeof locationProp === "string") {
    locationProp = parsePath(locationProp);
  }

  const {
    pathname = "/",
    search = "",
    hash = "",
    state = null,
    key = "default"
  } = locationProp;
  // 替换传入location的pathname
  const location = React.useMemo(() => {
    // 获取pathname中basename后的字符串
    const trailingPathname = stripBasename(pathname, basename);

    if (trailingPathname == null) {
      // 1.pathname不是以basename开头的
      // 2.pathname以basename开头的,但不是以`${basename}/`开头
      return null;
    }
    // 到了这里则:
    // 1.basename === "/"
    // 2.pathname以`${basename}/`开头
    return {
      pathname: trailingPathname,
      search,
      hash,
      state,
      key
    };
  }, [basename, pathname, search, hash, state, key]);


  if (location == null) {
    return null;
  }

  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider
        children={children}
        value={{ action, location }}
      />
    </NavigationContext.Provider>
  );
}
复制代码

Router 最后返回了两个Context.Provider,那么其子组件就可以:

通过

const { basename, navigator } = React.useContext(NavigationContext);
复制代码

拿到NavigationContext的 basename 和 navigator(一般是 history 实例)

通过

const { location } = React.useContext(LocationContext)
复制代码

拿到 location 信息。

上面到了两个 Context 后就完了,那我们看下其 children,如<App/>

示例

我们以下面的 代码实例,看看http://localhost:3000/about/child(后面简写为/about/child)是如何匹配到<Route path="about/*" element={<About />}/>中的 element,从而进一步匹配到<Route path='child' element={<AboutChild/> }/>中的 element 的。

function App() {
  return (
    <Routes>
      <Route element={<BasicLayout />}>
        <Route index element={<Home />} />
        {* 注意这里的尾缀必须写成 '/*' *}
        <Route path="about/*" element={<About />} />
        <Route path="dashboard" element={<Dashboard />} />
        <Route path="*" element={<NoMatch />} />
      </Route>
    </Routes>
  );
}

function BasicLayout() {
  return (
    <div>
      <h1>Welcome to the app!</h1>
      <li>
        <Link to=".">Home</Link>
      </li>
      <li>
        <Link to="about">About</Link>
      </li>
      ...
      <hr />
      <Outlet />
    </div>
  );
}
function Home() {
  return (
    <h2>Home</h2>
  );
}

function About() {
  return (
    <div>
      <h2>About</h2>
      <Link to='child'>about-child</Link>
      <Routes>
        <Route path='child' element={<AboutChild/>  }/>
      </Routes>
    </div>
  );
}
function AboutChild() {
  return (
    <h2>AboutChild</h2>
  );
}
...
复制代码

Routes 和 Route

每一个 Route 必须放在 Routes 这个 container 中,我们看下 Routes 的源码

/**
 * @description <Route> elements 的容器
 */
export function Routes({
  children,
  location
}: RoutesProps): React.ReactElement | null {
  return useRoutes(createRoutesFromChildren(children), location);
}
复制代码

发现其接收的 children,即下面

// 这里加下<>,不然代码不高亮
<>
<Route index element={<Home />} />
<Route path="about" element={<About />}/>
<Route path="dashboard" element={<Dashboard />} />
<Route path="*" element={<NoMatch />} />
</>
复制代码

而当我们看 Route 的函数,发现其根本就不渲染

/**
 * @description 实质上不渲染,只是用于收集Route的props
 */
export function Route(
  _props: PathRouteProps | LayoutRouteProps | IndexRouteProps
): React.ReactElement | null {
  // Route实际上没有render,只是作为Routes的child
  // Route必须放Routes里面,不然一进来就会报错
  // 以下是正确的使用方式
  // <Route element={<Layout />}>
  //     <Route index element={<Home />} />
  //     <Route path="about" element={<About />} />
  //   </Route>
  // </Routes>
  invariant(
    false,
    `A <Route> is only ever to be used as the child of <Routes> element, ` +
      `never rendered directly. Please wrap your <Route> in a <Routes>.`
  );
}
复制代码

可能绝大部分人都以为使用了组件那么肯定会进入 render,但其实不是,Route 的目的只是提供 props 供createRoutesFromChildren使用罢了。这也给我们提供了一种全新的思路,打破了以往的认知。

createRoutesFromChildren

createRoutesFromChildren函数的作用是为了递归收集 Route 上的 props,最终返回一个嵌套数组(如果 Route 有多层的话)。

/**
 * @description 创建一个route配置:
 * @example
 * RouteObject {
 *  caseSensitive?: boolean;
 *  children?: RouteObject[];
 *  element?: React.ReactNode;
 *  index?: boolean;
 *  path?: string;
 * }[]
 */
export function createRoutesFromChildren(
  children: React.ReactNode
): RouteObject[] {
  const routes: RouteObject[] = [];
  React.Children.forEach(children, element => {
    if (!React.isValidElement(element)) {
      // 忽悠掉 non-elements
      // Ignore non-elements. This allows people to more easily inline
      // conditionals in their route config.
      return;
    }

    if (element.type === React.Fragment) {
      // element为<></>
      // Transparently support React.Fragment and its children.
      routes.push.apply(
        routes,
        createRoutesFromChildren(element.props.children)
      );
      return;
    }

    const route: RouteObject = {
      caseSensitive: element.props.caseSensitive,
      element: element.props.element,
      index: element.props.index,
      path: element.props.path
    };

    if (element.props.children) {
      /**
       * 如果有children
       * @example
       * <Route path="/" element={<Layout />}>
       *  <Route path='user/*'/>
       *  <Route path='dashboard/*'/>
       * </Route>
       */
      route.children = createRoutesFromChildren(element.props.children);
    }

    routes.push(route);
  });
  return routes;
}
复制代码

我们 log 下 routes,这样看起来比较清晰

也就是说,我们实际上不一定需要通过 Routes 的形式,也可以直接使用 useRoutes,传入嵌套路由数组,同样能生成相同的 routes element,例子如下

import { useRoutes } from 'react-router-dom'
function App() {
  const routelements = useRoutes([
    {
      element: <BasicLayout />,
      children: [
        {
          index: true,
          element: <Home />
        },
        {
          path: 'about/*',
          element: <Home />
        },
        {
          path: 'dashboard',
          element: <Dashboard />,
        },
        {
          path: '*',
         element: <NoMatch />
        }
      ]
    }
  ])
  return (
    routelements
  );
}
复制代码

useRoutes

上面已经处理好了routes: RouteObject[],那么接下来 useRoutes 会通过 routes 匹配到对应的 route element。

函数可以分为三段:

  • 获取 parentMatches 最后一项 routeMatch
  • 通过 matchRoutes 匹配到对应的 matches
  • 通过_renderMatches渲染上面得到的 matches
export function useRoutes(
  routes: RouteObject[],
  // Routes没传入locationArg,这个我们忽略掉
  locationArg?: Partial<Location> | string
): React.ReactElement | null {
  // 第一段:获取parentMatches最后一项routeMatch
  const { matches: parentMatches } = React.useContext(RouteContext);

  const routeMatch = parentMatches[parentMatches.length - 1];
  const parentParams = routeMatch ? routeMatch.params : {};
  const parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";



  // 从 LocationContext 获取location
  const locationFromContext = useLocation();

  let location;
  if (locationArg) {
    const parsedLocationArg =
      typeof locationArg === "string" ? parsePath(locationArg) : locationArg;

    location = parsedLocationArg;
  } else {
    location = locationFromContext;
  }
  // 第二段:通过remainingPathname和routes匹配到对应的`matches: RouteMatch<string>[]`
  // 一般来说,对于http://localhost:3000/about/child,location.pathname为/auth/child
  const pathname = location.pathname || "/";
  // parentPathnameBase不为 '/'那么就从pathname中的parentPathnameBase后截取。
  // 因为useRoutes是在<Routes></Routes>中调用的,remainingPathname代表当前Routes的相对路径
  // eg: pathname = `${parentPathnameBase}xxx`,remainingPathname = 'xxx'
  // eg: pathname = `/about/child`,parentPathnameBase = '/about', remainingPathname = '/child'
  const remainingPathname =
    parentPathnameBase === "/"
      ? pathname
      : pathname.slice(parentPathnameBase.length) || "/";

  const matches = matchRoutes(routes, { pathname: remainingPathname });
  // 第三段:通过`_renderMatches`渲染上面得到的matches
  return _renderMatches(
    matches &&
      matches.map(match =>
        Object.assign({}, match, {
          params: Object.assign({}, parentParams, match.params),
          pathname: joinPaths([parentPathnameBase, match.pathname]),
          pathnameBase: joinPaths([parentPathnameBase, match.pathnameBase])
        })
      ),
    parentMatches
  );
}
复制代码

那么我们分别来分析这三段。

获取 parentMatches 最后一项 routeMatch

我们看到一开始就 use 了一个 Context:RouteContext,从其上面获取 matches,类型看下面代码,从下面的注释我们也得知了这个 context 实际上是在 useRoutes 最后的_renderMatches 函数中使用的,这个我们待会会讲到,这里先说明一下,不然可能看起来可能有点懵逼。


typeof RouteContext = {
  outlet: React.ReactElement | null;
  matches: RouteMatch[];
}

/** 在`_renderMatches`中会用到,`react-router-dom`中的`useOutlet`可得到最近Context的`outlet` */
const RouteContext = React.createContext<RouteContextObject>({
  outlet: null,
  matches: []
});

const { matches: parentMatches } = React.useContext(RouteContext);

const routeMatch = parentMatches[parentMatches.length - 1];
// 如果match了,获取params、pathname、pathnameBase
const parentParams = routeMatch ? routeMatch.params : {};
const parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
复制代码

第一次进入useRoutes的时候处理的是第一个 Routes 下的 Route

function App() {
  return (
    <Routes>
      <Route element={<BasicLayout />}>
        <Route index element={<Home />} />
        {* 注意这里的尾缀必须写成 '/*' *}
        <Route path="about/*" element={<About />} />
        <Route path="dashboard" element={<Dashboard />} />
        <Route path="*" element={<NoMatch />} />
      </Route>
    </Routes>
  );
}
复制代码

这个时候的 parentMatches 为空数组,所以拿到的相应参数都是后面的默认值

const parentParams = {};
const parentPathnameBase = "/";
复制代码

第二次进入useRoutes的时候处理的是<About/>中 Routes 下的 Route:

function About() {
  return (
    <div>
      <h2>About</h2>
      <Link to='child'>about-child</Link>
      <Routes>
        <Route path='child' element={<AboutChild/>  }/>
      </Routes>
    </div>
  );
}
复制代码

这个时候的 parentMatches 为数组的 length 为 2,所以拿到的相应参数为


const parentParams = {
  params: {*: 'child'},
  pathname: "/about/child",
  pathnameBase: "/about",
  route: {caseSensitive: undefined, element: {…}, index: undefined, path: 'about/*'}
};
const parentPathnameBase = "/about";
复制代码

如下图,是两次的 parentMatches,第一次为空数组,第二次图中标注了:

即上一次 Routes 中 useRoutes 后得到的 matches 会作为下一层的 parentMatches(这个我们再后面将 _renderMatches 的时候回详细讲到)

通过 matchRoutes 匹配到对应的 matches

获取 matches 的函数是 matchRoutes,其根据当前 location 对应的 routes 获取相应的 matches:RouteMatch<string>[],类型如下

export interface RouteMatch<ParamKey extends string = string> {
  // url中动态参数的key和value
  params: Params<ParamKey>;
  // route的pathname
  pathname: string;
  // 在子路由之前匹配的部分 URL pathname
  pathnameBase: string;

  route: RouteObject;
}
复制代码

整个 matchRoutes 函数如下,下面会再分段详细解析:

/**
 * @description 根据`location`对应的`routes`,获取对应的`RouteMatch<string>[]`
 *
 * Matches the given routes to a location and returns the match data.
 */
export function matchRoutes(
  routes: RouteObject[],
  locationArg: Partial<Location> | string,
  basename = "/"
): RouteMatch[] | null {
  /** 得到pathname、hash、search */
  const location =
    typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
  // pathname是一个取出来basename的相对路径
  const pathname = stripBasename(location.pathname || "/", basename);

  if (pathname == null) {
    return null;
  }
  // routes有可能是多层设置,那么flatten下
  const branches = flattenRoutes(routes);
  /**
   * 通过score或childrenIndex[]排序branch
   * @example
   *  // 排序前
   *  [
   *  {
   *   path: '/', score: 4, routesMeta: [
   *     {relativePath: "",caseSensitive: false,childrenIndex: 0},
   *     {relativePath: "",caseSensitive: false,childrenIndex: 0}
   *   ]
   * },
   *  {
   *   path: '/login', score: 13, routesMeta: [
   *     {relativePath: "",caseSensitive: false,childrenIndex: 0},
   *     {relativePath: "login",caseSensitive: false,childrenIndex: 1}
   * ]
   * },
   *  {
   *   path: '/protected', score: 13, routesMeta: [
   *     {relativePath: "",caseSensitive: false,childrenIndex: 0},
   *     {relativePath: "protected",caseSensitive: true,childrenIndex: 2}
   *   ]
   * }
   * ]
   * // 排序后
   *  [
   *  { path: '/login', score: 13, ...},
   *  { path: '/protected', score: 13, ...}
   *  { path: '/', score: 4, ... },
   * ]
   */
  rankRouteBranches(branches);

  let matches = null;
  // 直到`matches`有值(意味着匹配到,那么自然不用再找了)或遍历完`branches`才跳出循环
  for (let i = 0; matches == null && i < branches.length; ++i) {
    matches = matchRouteBranch(branches[i], pathname);
  }

  return matches;
}
复制代码

flattenRoutes 收集 routeMeta 后通过 rankRouteBranches 排序

因为 routes 可能是多维数组,那么首先会将传入的 routes flatten 为一维数组,在 flatten 的过程中会收集每个 route 的 props 作为 routeMeta,收集过程是一个深度优先遍历:

/**
 * @description 深度优先遍历,如果route有`children`,会先把children处理完push `branches`,然后再push该`route`,
 * 要特别注意三点:
 * - `LayoutRoute`即只有`element`和`children`,不会push进`branches`
 * - `route`不写`path`的话,会给`meta.relativepath`赋值空字符串'' => `relativePath: route.path || ""`
 * - 当前`route`处于第几层,那么得到的`branch.routesMeta.length` 就为多少
 *
 *
 * @example
 * <Route path='/' element={<Layout />}>
 *   <Route path='auth/*' element={<Auth />} />
 *   <Route path='basic/*' element={<Basic />} />
 * </Route>
 * 那么得到的branches为 [{ path: '/auth/*', ...},{ path: '/basic/*', ...}, { path: '/', ...}]
 *
 * // `LayoutRoute`只有`element`和`children`,不会push进`branches`
 * <Route element={<Layout />}>
 *   <Route path='auth/*' element={<Auth />} />
 *   <Route path='basic/*' element={<Basic />} />
 * </Route>
 * 那么得到的branches为 [{ path: '/auth/*', ...},{ path: '/basic/*', ...}]
 */
function flattenRoutes(
  routes: RouteObject[],
  branches: RouteBranch[] = [],
  parentsMeta: RouteMeta[] = [],
  parentPath = ""
): RouteBranch[] {
  routes.forEach((route, index) => {
    const meta: RouteMeta = {
      // 如果path为'',或者不写(LayoutRoute),那么relativePath统一为''
      relativePath: route.path || "",
      caseSensitive: route.caseSensitive === true,
      childrenIndex: index,
      route,
    };

    if (meta.relativePath.startsWith("/")) {
      /**
       * 如果相对路径以"/"开头,说明是绝对路径,那么必须要以parentPath开头,否则这里会报错。
       * 因为这里是嵌套在parentPath下的路由
       * eg from src/examples/basic/index.tsx:
       * <Route path="about" element={<About />}>
       *   // parentPath为'/about', meta.relativePath = '/child',不是以parentPath开头的,会报错
       *   <Route path='/child' element={<AboutChild />} />
       *   // parentPath为'/about', meta.relativePath = '/about/child',是以parentPath开头的,那么不会报错
       *   <Route path='/about/child' element={<AboutChild />} />
       * </Route>
       */
      invariant(
        meta.relativePath.startsWith(parentPath),
        `Absolute route path "${meta.relativePath}" nested under path ` +
          `"${parentPath}" is not valid. An absolute child route path ` +
          `must start with the combined path of all its parent routes.`
      );
      // 到这里就说明是以parentPath开头了,那么相对路径不需要parentPath,取后面的
      // 如上面eg,path="about" 会被下面的joinPath变成'/about',
      //  meta.relativePath = '/about/child'.slice('/about'.length  // 6) = '/child'
      meta.relativePath = meta.relativePath.slice(parentPath.length);
    }
    // 将parentPath, meta.relativePath用 / 连起来成为绝对路径
    // eg: parentPath = '/', meta.relativePath = 'auhth/*', path = '/auth/*'
    // eg: <Route path="" element={<PublicPage />} /> joinPaths(['', '']) => '/'
    const path = joinPaths([parentPath, meta.relativePath]);
    // 这里用concat就不会影响到parentsMeta
    // 而从这里我们也知道了,如果routesMeta.length > 1, 那么除最后一个meta,前面的肯定是本route的parentsMeta
    const routesMeta = parentsMeta.concat(meta);

    // Add the children before adding this route to the array so we traverse the
    // route tree depth-first and child routes appear before their parents in
    // the "flattened" version.
    if (route.children && route.children.length > 0) {
      // 如果route有children,那么不能为index route,即其prop的index不能为true
      invariant(
        route.index !== true,
        `Index routes must not have child routes. Please remove ` +
          `all child routes from route path "${path}".`
      );
      // 有children的先处理children,处理完的children branch放进branches
      flattenRoutes(route.children, branches, routesMeta, path);
    }

    // Routes without a path shouldn't ever match by themselves unless they are
    // index routes, so don't add them to the list of possible branches.
    if (route.path == null && !route.index) {
      // 如果route没有path,且不是index route,那么不放入branches,这里的route一般是LayoutRoute,
      // 看下面example的<Route element={<Layout />}>
      /**
       * @example
       * 对于 examples/auth/index.tsx, http://localhost:3000/auth
       * // 最外层的Route(LayoutRoute)就没有path和index,那么就return
       *
       * <Route element={<Layout />}>
       *   <Route path="" element={<PublicPage />} />
       *   <Route path="login" element={<LoginPage />} />
       *   <Route
       *     path="protected"
       *     caseSensitive
       *     element={
       *       <RequireAuth>
       *         <ProtectedPage />
       *       </RequireAuth>
       *     }
       *   />
       * </Route>
       *
       * 那么最终最下面收集到的branches为:
       * [
       *  {
       *   path: '/', score: 4, routesMeta: [
       *     {relativePath: "",caseSensitive: false,childrenIndex: 0},
       *     {relativePath: "",caseSensitive: false,childrenIndex: 0}
       *   ]
       * },
       *  {
       *   path: '/login', score: 13, routesMeta: [
       *     {relativePath: "",caseSensitive: false,childrenIndex: 0},
       *     {relativePath: "login",caseSensitive: false,childrenIndex: 1}
       * ]
       * },
       *  {
       *   path: '/protected', score: 13, routesMeta: [
       *     {relativePath: "",caseSensitive: false,childrenIndex: 0},
       *     {relativePath: "protected",caseSensitive: true,childrenIndex: 2}
       *   ]
       * }
       * ]
       */
      return;
    }
    // 到了这里满足上面的所有条件了,那么放入branches
    //那么routesMeta.length = route自身所处层数
    branches.push({ path, score: computeScore(path, route.index), routesMeta });
  });

  return branches;
}
复制代码

通过 flattenRoutes 获取到 branches 后,会根据 score 或 childIndex 排序每个 branch,如果 score 相等才去比较 routesMeta 的每个 childIndex:

/** 通过`score`或`childrenIndex[]`排序`branch` */
function rankRouteBranches(branches: RouteBranch[]): void {
  branches.sort((a, b) =>
    a.score !== b.score
    // 不等的话,高的排前面
      ? b.score - a.score // Higher score first
      // score相等的话,那么判断是否是siblings,是的话比较selfIndex(小的排前面),否则相等
      : compareIndexes(
          a.routesMeta.map(meta => meta.childrenIndex),
          b.routesMeta.map(meta => meta.childrenIndex)
        )
  );
}
复制代码

第一次进入 flattenRoutes 和通过 rankRouteBranches 排序后得到的 branches 如下

其代码为:

function App() {
  return (
    <Routes>
      <Route element={<BasicLayout />}>
        <Route index element={<Home />} />
        {* 注意这里的尾缀必须写成 '/*' *}
        <Route path="about/*" element={<About />} />
        <Route path="dashboard" element={<Dashboard />} />
        <Route path="*" element={<NoMatch />} />
      </Route>
    </Routes>
  );
}
复制代码

第二次得到的 branches 如下

其代码为:

function About() {
  return (
    <div>
      ...
      <Routes>
        <Route path='child' element={<AboutChild/>  }/>
      </Routes>
    </div>
  );
}
复制代码

最后根据 pathname 和每项 branches 看看是否能找到对应的匹配项,找到的话就跳出循环

let matches = null;
// 直到`matches`有值(意味着匹配到,那么自然不用再找了)或遍历完`branches`才跳出循环
for (let i = 0; matches == null && i < branches.length; ++i) {
  matches = matchRouteBranch(branches[i], pathname);
}

return matches;
复制代码

matchRouteBranch

matchRouteBranch 会通过每个 branch 的 routesMeta 来看看是否能匹配到相应的 pathname,只要有一个不匹配,就返回 null,而 routesMeta 最后一项是该 route 自己的路由信息,前面项都是 parentMetas,所以只有从头到尾都匹配到,才表示匹配到完整的路由信息。

function matchRouteBranch<ParamKey extends string = string>(
  branch: RouteBranch,
  pathname: string
): RouteMatch<ParamKey>[] | null {
  const { routesMeta } = branch;
  /** 已匹配到的动态参数 */
  const matchedParams = {};
  /** 表示已经匹配到的路径名 */
  let matchedPathname = "/";
  const matches: RouteMatch[] = [];
  for (let i = 0; i < routesMeta.length; ++i) {
    const meta = routesMeta[i];
    // 是否到了最后一个routesMeta,最后一个就是当前branch自己的routeMeta
    const end = i === routesMeta.length - 1;
    // remainingPathname表示剩下还没匹配到的路径,因为下面是用meta.relativePath去正则匹配,所以这里
    // 每遍历一次要去将传入pathname.slice(matchedPathname.length)
    // matchedPathname不为 '/'那么就从pathname中的matchedPathname后截取
    // eg: pathname = `${matchedPathname}xxx`,remainingPathname = 'xxx'
    // eg: matchedPathname = '/', pathname = `/`,remainingPathname = '/'
    // eg: matchedPathname = '/', pathname = `/auth`,remainingPathname = '/auth'
    const remainingPathname =
      matchedPathname === "/"
        ? pathname
        : pathname.slice(matchedPathname.length) || "/";
    /**
     * 会返回{ params, pathname, pathnameBase, pattern } or null
     */
    const match = matchPath(
      { path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
      remainingPathname
    );
      // 只要有一个没match到,就return掉,意味着即使前面的都match了,但如果最后一个没match到,最终matchRouteBranch还是null
    if (!match) return null;

    Object.assign(matchedParams, match.params);

    // const route = routes[meta.childrenIndex];
    const route = meta.route;

    matches.push({
      params: matchedParams,
      pathname: joinPaths([matchedPathname, match.pathname]),
      pathnameBase: joinPaths([matchedPathname, match.pathnameBase]),
      route
    });

    if (match.pathnameBase !== "/") {
      matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
    }
  }
  // matches.length肯定等于routesMeta.length
  return matches;
}
复制代码

matchPath

而每一项 routeMeta 都会通过 matchPath 函数看看是否匹配到,其会根据 routeMeta 的 relativePath(即我们在 Route 中写的 path,如 path = 'about/*',path='child';),caseSensitive(即根据 relativePath 生成的正则是否忽略大小写)以及 end(是否是最后一项 routeMeta,最后一项表示是该 route 自己的路由信息,同时也意味着匹配到最后了)生成对应的正则匹配。

我们看下matchPath

/**
 * @description 对pathname执行对应的正则匹配,看是否能返回match的信息
 */
export function matchPath<ParamKey extends string = string>(
  pattern: PathPattern | string,
  pathname: string
): PathMatch<ParamKey> | null {
  if (typeof pattern === "string") {
    pattern = { path: pattern, caseSensitive: false, end: true };
  }
  // 根据pattern.path生成正则以及获取path中的动态参数
  // compilePath下面有讲,看到这里可先看下面再回来
  const [matcher, paramNames] = compilePath(
    pattern.path,
    pattern.caseSensitive,
    pattern.end
  );
  // pattern.path生成正则是否match传入的pathname
  const match = pathname.match(matcher);
  if (!match) return null;

  const matchedPathname = match[0];
  // eg: 'about/'.replace(/(.)\/+$/, "$1") => 'about' // 即(.),$1表示第一个匹配到的小括号中的值;
  // eg: 'about/*'.replace(/(.)\/+$/, "$1") => 'about/*'; // 不匹配,返回原字符串
  let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
  // eg: pattern = {path: 'about/*', caseSensitive: false, end: true}, pathname = '/about/child';
  //     matcher = /^\/about(?:\/(.+)|\/*)$/i, paramNames = ['*'];
  //     match = ['/about/child', 'child', index: 0, input: '/about/child', groups: undefined]
  // 那么 matchedPathname = '/about/child', captureGroups = ['child'], params = { '*': 'child' }, pathnamebase = '/about'
  // 从第二项就是()中匹配的,所以叫slice从1开始
  const captureGroups = match.slice(1);
  const params: Params = paramNames.reduce<Mutable<Params>>(
    (memo, paramName, index) => {
      // We need to compute the pathnameBase here using the raw splat value
      // instead of using params["*"] later because it will be decoded then
      if (paramName === "*") {
        const splatValue = captureGroups[index] || "";
        // eg:
        // pattern.path = 'about/*', matchedPathname = '/about/child', captureGroups =['child']
        // matchedPathname.slice(0, matchedPathname.length - splatValue.length) => '/basic/'
        // '/about/'.replace(/(.)\/+$/, "$1") = '/about'
        // 即pathnameBase = '/about'
        pathnameBase = matchedPathname
          .slice(0, matchedPathname.length - splatValue.length)
          .replace(/(.)\/+$/, "$1");
      }

      memo[paramName] = safelyDecodeURIComponent(
        captureGroups[index] || "",
        paramName
      );
      return memo;
    },
    {}
  );

  return {
    params,
    pathname: matchedPathname,
    pathnameBase,
    pattern
  };
}
复制代码

compilePath

matchPath 中的 compilePath 才是真正用到了 relativePathcaseSensitiveend,即根据这几个参数编译出对应的正则,同时编译过程同发现 path 有动态参数的话就收集到一个数组了,如 ['*', 'id', 'name']

/**
 * @description: 根据path生成正则以及获取path中的动态参数
 * @param {string} path path不能是:xxx*,如果尾部是*,那么需要以"/*"结尾,正常的"/", "/auth"没问题
 * @param {boolean} caseSensitive 默认false,根据path生成的正则是否忽略大小写
 * @param {boolean} end 默认true,是否到了最后一个routesMeta
 * @return {[RegExp, string[]]} 正则以及获取path中的动态参数
 *
 * @example
 *
 * compilePath('/') => matcher = /^\/\/*$/i
 * compilePath('/', true, false) => matcher = /^\/(?:\b|$)/i
 * compilePath('/about') => matcher = /^\/about\/*$/i
 * compilePath('/about/child', true) => matcher = /^\/about\/child\/*$/
 * compilePath('about/*', true) => matcher = /^\/about(?:\/(.+)|\/*)$/
 */
function compilePath(
  path: string,
  caseSensitive = false,
  end = true
): [RegExp, string[]] {
  warning(
    path === "*" || !path.endsWith("*") || path.endsWith("/*"),
    `Route path "${path}" will be treated as if it were ` +
      `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` +
      `always follow a \`/\` in the pattern. To get rid of this warning, ` +
      `please change the route path to "${path.replace(/\*$/, "/*")}".`
  );
  // 动态参数名数组
  // eg: '/auth/:id/www/:name/ee' => paramNames = ['id', 'name']
  const paramNames: string[] = [];
  let regexpSource =
    "^" +
    path
      .replace(/\/*\*?$/, "") // 去掉尾部的'/'、'//' ..., 或'/*'、'//*', '///*' ..., '*'
      .replace(/^\/*/, "/") //  开头没'/'那么加上;开头有多个'/',那么保留一个;eg: (//about | about) => /about
      .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // 对\.*+^$?{}或()[]都给加上\,eg: `()[]` => '\(\)\[\]';`.*+^$?{}` => '\.\*\+\^\$\?\{\}'
      .replace(/:(\w+)/g, (_: string, paramName: string) => {  // \w ===  [A-Za-z0-9_], /:(\w+)/g表示处理动态参数
        paramNames.push(paramName);
        /** [^\\/]+ 表示不能是出现/
         * @example
         * '/auth/:id/www/:name/ee' => '/auth/([^\/]+)/www/([^\/]+)/ee'
         * const reg = new RegExp('/auth/([^\/]+)/www/([^\/]+)/ee', 'i')
         * reg.test('/auth/33/www/a1_A/ee') // params = ['33', 'a1_A'], true
         * reg.test('/auth/33/www/a1_A//ee')) // params = ['33', 'a1_A/'], false
         */
        return "([^\\/]+)";
      });

  if (path.endsWith("*")) {
    // 如果path以"*"结尾,那么paramNames也push
    paramNames.push("*");
    regexpSource +=
      // 如果path等于*或/*, 那么regexpSource最终为regexpSource = '^/(.*)$',(.*)$ 表示match剩下的
      path === "*" || path === "/*"
        ? "(.*)$" // Already matched the initial /, just match the rest
        /**
         * (?:x),匹配 'x' 但是不记住匹配项。这种括号叫作非捕获括号,使得你能够定义与正则表达式运算符一起使用的子表达式。
         * @example
         * eg1:
         * /(?:foo){1,2}/。如果表达式是 /foo{1,2}/,{1,2} 将只应用于 'foo' 的最后一个字符 'o'。
         * 如果使用非捕获括号,则 {1,2} 会应用于整个 'foo' 单词
         *
         * eg2: 对比下两种exec的结果
         * const reg = new RegExp('w(?:\\d+)e')
         * reg.exec('w12345e')
         * ['w12345e', index: 0, input: 'w12345e', groups: undefined] // 不记住匹配项
         *
         * 而
         * const reg = new RegExp('w(\\d+)e')
         * reg.exec('w12345e')
         * ['w12345e', '12345', index: 0, input: 'w12345e', groups: undefined] // 记住匹配项
         *
         * 本处eg:
         * path = 'xxx/*'
         * const reg = new RegExp("xxx(?:\\/(.+)|\\/*)$", 'i')
         * 下面的abc是(.+)中的
         * reg.exec('xxx/abc') // ['xxx/abc', 'abc', index: 0, input: 'xxx/abc', groups: undefined]
         * 下面两处满足 `|` 后面的\\/*: '/' 出现出现零次或者多次
         * reg.exec('xxx') //  ['xxx', undefined, index: 0, input: 'xxx', groups: undefined]
         * reg.exec('xxx/') //  ['xxx/', undefined, index: 0, input: 'xxx/', groups: undefined]
         * 当>= 2个'/',就又变成满足\\/(.+)了,所以个人感觉这里的\\/*是不是应该改为\\/{0,1} ????
         * reg.exec('xxx//') //  ['xxx//','/', index: 0, input: 'xxx//', groups: undefined]
         */
        : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
  } else {
    // path不以"*"结尾
    regexpSource += end
      ? "\\/*$" // When matching to the end, ignore trailing slashes 如果是end的话,忽略斜杠"/"
      : // Otherwise, at least match a word boundary. This restricts parent
        // routes to matching only their own words and nothing more, e.g. parent
        // route "/home" should not match "/home2".
        /**
         * 否则,至少匹配到一个单词边界,这限制了parent routes只能匹配自己的单词。比如/home不允许匹配为/home2。
         *
         * \b: 匹配这样的位置:它的前一个字符和后一个字符不全是(一个是,一个不是或不存在) \w (\w ===  [A-Za-z0-9_])
         * 通俗的理解,\b 就是“隐式位置”
         * "It"中 'I' 和 't' 就是显示位置,中间是“隐式位置”。 更多可见:https://www.cnblogs.com/litmmp/p/4925374.html
         * 使用"moon"举例:
         * /\bm/匹配“moon”中的‘m’;
         * /oo\b/并不匹配"moon"中的'oo',因为'oo'被一个“字”字符'n'紧跟着
         * /oon\b/匹配"moon"中的'oon',因为'oon'是这个字符串的结束部分。这样他没有被一个“字”字符紧跟着
         *
         * 本例:
         * compilePath('/', true, false) => matcher = /^\/(?:\b|$)/i
         * '/auth'.match(/^\/(?:\b|$)/i) // ['/', index: 0, input: '/auth', groups: undefined]
         * 'auth'.match(/^\/(?:\b|$)/i) // null
         * reg.exec('/xxx2') or reg.exec('/xxxx') // null
         *  */
        "(?:\\b|$)";
  }

  const matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");

  return [matcher, paramNames];
}
复制代码

matchRoutes中调用了很多函数,这里我们小结一下:

  1. 先是通过 flattenRoutes 收集 routesMeta 到每一个 branches 中,之后通过 rankRouteBranches 排序
  2. 然后通过 matchRouteBranch 根据每个 branch 和 pathname 找到对应的匹配项数组matches,只要找到就跳出循环
  3. matchRouteBranch 会遍历每个 branch 的 routesMeta,通过 matchPath 里调用 compilePath 生成正则以及获取 path 中的动态参数 paramNames,只有所有routesMeta 都匹配了才算该 branch 真正匹配

ps: 这段代码会比较复杂,可能看的过程中有些地方比较懵逼,特别是涉及到正则匹配的地方,想要真正搞懂还是需要通过 debugger 来验证,但相信我只要多看几遍,就能很好地理解

通过_renderMatches 渲染上面得到的 matches

ok,我们终于艰难地度过了第二步了,那么到了第三步就是快见到希望的曙光了~~

我们上面历尽千辛万苦终于拿到匹配项 matches了,那么就要根据匹配项来渲染:

return _renderMatches(
  matches &&
    matches.map(match =>
      Object.assign({}, match, {
        params: Object.assign({}, parentParams, match.params),
        pathname: joinPaths([parentPathnameBase, match.pathname]),
        pathnameBase: joinPaths([parentPathnameBase, match.pathnameBase])
      })
    ),
  parentMatches
);
复制代码

_renderMatches 会根据匹配项和父级匹配项 parentMatches,从右到左,即从 child --> parent 渲染 RouteContext.Provider

/** 根据matches渲染出嵌套的 `<RouteContext.Provider></RouteContext.Provider>`*/
function _renderMatches(
  matches: RouteMatch[] | null,
  parentMatches: RouteMatch[] = []
): React.ReactElement | null {
  if (matches == null) return null;
  return matches.reduceRight((outlet, match, index) => {
    // 如果match.route.element为空,那么<Outlet />实际上就是该RouteContext的outlet,就是下面value的outlet
    return (
      <RouteContext.Provider
        children={match.route.element || <Outlet />}
        value={{
          outlet,
          matches: parentMatches.concat(matches.slice(0, index + 1))
        }}
      />
    );
  }, null as React.ReactElement | null);
}
复制代码

其生成结构类似:

// 这里生成的结构如下,即对于matches `index + 1` 生成的Provider作为 `index` Provider value的outlet(出口) :
// matches.length = 2
return (
  <RouteContext.Provider
    value={{
      matches: parentMatches.concat(matches.slice(0, 1)),
      outlet: (
      <RouteContext.Provider
        value={{
          matches: parentMatches.concat(matches.slice(0, 2)),
          outlet: null // 第一次outletnull,
        }}
      >
        {<Layout2 /> || <Outlet />}
      </RouteContext.Provider>
    ),
    }}
  >
    {<Layout1 /> || <Outlet />}
  </RouteContext.Provider>
)
复制代码

上面刚好用到了<Outlet />,我们顺便看下 Outlet 源码:

export function Outlet(_props: OutletProps): React.ReactElement | null {
  return useOutlet();
}

export function useOutlet(): React.ReactElement | null {
  return React.useContext(RouteContext).outlet;
}
复制代码

<Outlet /> 即返回最近一层 RouteContext 的 outlet

_renderMatches 生成的子 RouteContext.Provider 会作为前一父级的 outlet,而又因为当前 RouteContext.Provider 的 children 就是其 match 的 element 或<Outlet />,那么只要有 <Outlet /> 就能拿到最近一层 RouteContext 的 outlet 了,所以我们常常在嵌套路由的 parent route 的 element 写上一个<Outlet />,相当于插槽的作用,如:

function BasicLayout() {
  return (
    <div>
      <h1>Welcome to the app!</h1>
      <li>
        <Link to=".">Home</Link>
      </li>
      <li>
        <Link to="about">About</Link>
      </li>
      ...
      <hr />
      <Outlet />
    </div>
  );
}

复制代码

终于讲完了 Route 的匹配过程了,下面会讲一些上面没讲到,但比较常用的 hook 和组件。

useNavigate

v6用 useNavigate 替代了 useHistory,其返回了一个 navigate (点击查看用法) 的方法,实现比较简单:

  • NavigationContext拿到navigator,也就是 history 实例。
  • 然后根据tomatches的每项 pathnameBase 以及当前 URL pathname 生成最终的路径 path({pathname, search, hash})
  • 根据是否指定replace来判断是调用 replace 还是 push 方法
export function useNavigate(): NavigateFunction {
  const { basename, navigator } = React.useContext(NavigationContext);
  const { matches } = React.useContext(RouteContext);
  const { pathname: locationPathname } = useLocation();
  // stringgify是为了下面的memo??
  const routePathnamesJson = JSON.stringify(
    matches.map(match => match.pathnameBase)
  );
  /** 是否已挂载 */
  const activeRef = React.useRef(false);
  React.useEffect(() => {
    activeRef.current = true;
  });

  const navigate: NavigateFunction = React.useCallback(
    (to: To | number, options: { replace?: boolean; state?: State } = {}) => {
      warning(
        activeRef.current,
        `You should call navigate() in a React.useEffect(), not when ` +
          `your component is first rendered.`
      );

      if (!activeRef.current) return;

      if (typeof to === "number") {
        navigator.go(to);
        return;
      }

      const path = resolveTo(
        to,
        JSON.parse(routePathnamesJson),
        locationPathname
      );

      if (basename !== "/") {
        path.pathname = joinPaths([basename, path.pathname]);
      }
      // replace为true才调用replace方法,否则都是push
      (!!options.replace ? navigator.replace : navigator.push)(
        path,
        options.state
      );
    },
    [basename, navigator, routePathnamesJson, locationPathname]
  );

  return navigate;
}
复制代码

useLocation

useLocation 即从 LocationContext 获取 location。LocationContext在 Router 中调用,没注意到的可以翻到上面看看

export function useLocation(): Location {
  return React.useContext(LocationContext).location;
}
复制代码

useResolvedPath

useResolvedPath根据当前location以及所处RouteContextmatches解析给定topathname

export function useResolvedPath(to: To): Path {
  const { matches } = React.useContext(RouteContext);
  const { pathname: locationPathname } = useLocation();
  // 转为字符串是为了避免memo依赖加上对象导致缓存失效?
  const routePathnamesJson = JSON.stringify(
    matches.map(match => match.pathnameBase)
  );

  return React.useMemo(
    () => resolveTo(to, JSON.parse(routePathnamesJson), locationPathname),
    [to, routePathnamesJson, locationPathname]
  );
}
复制代码

useParams

顾名思义,用于获取 params

export function useParams<Key extends string = string>(): Readonly<
  Params<Key>
> {
  const { matches } = React.useContext(RouteContext);
  const routeMatch = matches[matches.length - 1];
  return routeMatch ? (routeMatch.params as any) : {};
}
复制代码

useLinkClickHandler

useLinkClickHandler 用于处理路由<Link>组件的点击行为, 比较适用于我们要自定义<Link>组件,因为其返回的方法和 <Link>有相同的点击行为

export function useLinkClickHandler<
  E extends Element = HTMLAnchorElement,
  S extends State = State
>(
  to: To,
  {
    target,
    replace: replaceProp,
    state
  }: {
    target?: React.HTMLAttributeAnchorTarget;
    replace?: boolean;
    state?: S;
  } = {}
): (event: React.MouseEvent<E, MouseEvent>) => void {
  const navigate = useNavigate();
  const location = useLocation();
  const path = useResolvedPath(to);

  return React.useCallback(
    (event: React.MouseEvent<E, MouseEvent>) => {
      if (
        event.button === 0 && // Ignore everything but left clicks 忽略除了左键外的所有内容
        (!target || target === "_self") && // Let browser handle "target=_blank" etc. 让浏览器处理"target=_blank"等
        !isModifiedEvent(event) // Ignore clicks with modifier keys 忽略meta、alt、ctrl、shift等修饰符
      ) {
        event.preventDefault();

        // If the URL hasn't changed, a regular <a> will do a replace instead of
        // a push, so do the same here.
        // 如果有传replace为true或当前location和传入path的`pathname + search + hash`相等,那么replace为true,
        // 即URL没有改变的话,<a>会使用replace而不是push
        // 比如当前路径为/basic, 点后点击<Link to='.'>,那上面的useResolvedPath(to)的path还是为{pathname: '/basic', search: '', hash: ''}
        // 那么这里的replace就满足createPath(location) === createPath(path),即为true了,那就是replace,如果不是跳本路由,那么就为false,那就是push
        const replace =
          !!replaceProp || createPath(location) === createPath(path);
        navigate(to, { replace, state });
      }
    },
    [location, navigate, path, replaceProp, state, target, to]
  );
}
复制代码

讲完一下常用 hook,下面讲些常用的组件。

<Link />

Link (点击查看用法) 实质上只是包装了<a>,对 onClick 事件做了处理,如果自定义了 onClick,那么用该 onClick,否则会用内部的函数internalOnClick

export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
  function LinkWithRef(
    { onClick, replace = false, state, target, to, ...rest },
    ref
  ) {
    const href = useHref(to);
    const internalOnClick = useLinkClickHandler(to, { replace, state, target });
    function handleClick(
      event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
    ) {
      // 如果有传onClick,那么调用该onClick
      if (onClick) onClick(event);
      // 否则如果事件的默认行为没有被阻止的话,那么调用internalOnClick,
      // 因为internalOnClick里面会调用event.preventDefault(),使event.defaultPrevented = true
      if (!event.defaultPrevented) {
        internalOnClick(event);
      }
    }

    return (
      // eslint-disable-next-line jsx-a11y/anchor-has-content
      <a
        {...rest}
        href={href}
        onClick={handleClick}
        ref={ref}
        target={target}
      />
    );
  }
);
复制代码

<Navigate />

用于改变当前 location, 比较常用于 class 组件中,会在 useEffect 后 navigate 到对应的 to。函数组件建议用useNavigate

export function Navigate({ to, replace, state }: NavigateProps): null {
  const navigate = useNavigate();
  React.useEffect(() => {
    navigate(to, { replace, state });
  });

  return null;
}
复制代码

结语

我们以BrowserRouter为例解析了 react router 的匹配过程,然后又解析了几个常用的 hook 如useNavigateuseLocation等和组件<Link/><Navigate/>。如果再看的过程中有什么不明白的,可以通过 clone 该分支feature/examples-source-analysis 跟着一起 debugger,多看几遍,相信可以看懂的。

最后

这是我们react router 源码分析的最后一篇文章了,感谢大家看到这里,可能过程中有些讲的不好,还请见谅。

感谢留下足迹,如果您觉得文章不错😄😄,还请动动手指😋😋,点赞+收藏+转发🌹🌹

往期文章

翻译翻译,什么叫ReactDOM.createRoot

翻译翻译,什么叫JSX

什么,React Router已经到V6了 ??

React Router源码分析之history

分类:
前端
标签:
分类:
前端
标签: