likes
comments
collection
share

一篇不可错过的react-router源码分析

作者站长头像
站长
· 阅读数 46

一、序言

最近新建了一个专栏,专门用于分析并实现一个router库。那么接下来,小编就会一步步带着大家来看懂react-router源码。

这里再次强调,这个专栏以及这篇文章对应的react-router版本是v6。

二、源码调试

小生迂腐,采用的是 clone源码+浏览器断点 的方式来阅读源码的,各位大佬如果有更好的调试方式,可以在评论区指点一下我。

  • 第一步,我们先clone下源码(去react-router官网点击GitHub即可下载),clone成功后,此时的react-router目录应该是下面这样的:

一篇不可错过的react-router源码分析

这个目录是非常清晰的,其他的我们都不用关注,红框里的目录一定是我们想看的。

  • 第二步,使用create-react-app脚手架搭建有一个项目,并且下载 react-router、react-router-dom。
  • 第三步,打开浏览器的source面板,选择“open file”选项,如下图: 一篇不可错过的react-router源码分析

然后直接搜索 react-router 相关目录下的文件,对相应代码打断点即可。

三、渲染流程分析

3.1、路由配置

App.js代码如下:

import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './Home.js'; // 页面1
import About from './About.js'; // 页面2

const root = React.createRoot(document.getElementById('root'));
root.render(
    <BrowserRouter>
        <Routes>
            <Route path="/about" element={<About />}></Route>
            <Route path="/" element={<Home />}></Route>
        </Routes>
    </BrowserRouter>
)

3.2、渲染概览(流程图)

一篇不可错过的react-router源码分析

3.3、BrowserRouter流程讲解

一篇不可错过的react-router源码分析

<BrowserRouter /> 组件的代码如下:

提醒:这个组件里其实就是操作 history 来完成监听等一系列动作,具体解释请看这篇文章:juejin.cn/post/709317…

    import React from 'react';
    export function BrowserRouter({ basename, children, window }) {
        let historyRef = React.useRef();
        if (historyRef.current == null) {
            // 1、创建history对象,这个对象里包含了操作历史栏的一些方法
            historyRef.current = createBrowserHistory({ window });
        }
        let history = historyRef.current;
        let [state, setState] = React.useState({
            action: history.action,
            location: history.location,
        });
        // 2、页面渲染后,监听popstate事件
        React.useLayoutEffect(() => history.listen(setState), [history]);
        return (
            <Router
              basename={basename}
              children={children}
              location={state.location}
              navigationType={state.action}
              navigator={history}
            />
        );
    }
    
    export function Router({
      basename: basenameProp = "/",
      children = null,
      location: locationProp,
      navigationType = NavigationType.Pop,
      navigator,
      static: staticProp = false,
    }){
          let basename = normalizePathname(basenameProp);
          let navigationContext = React.useMemo(
            () => ({ basename, navigator, static: staticProp }),
            [basename, navigator, staticProp]
          );
          // 重点:中间进行了一些性能优化为代表的转换,对于我们来说暂时不关注,也不影响渲染逻辑。
          return (
            <NavigationContext.Provider value={navigationContext}>
              <LocationContext.Provider
                children={children}
                value={{ location, navigationType }}
              />
            </NavigationContext.Provider>
          );
    }

3.4、Routes流程讲解

Routes标签的代码如下:

export function Routes({ children, location }){
    return useRoutes(createRoutesFromChildren(children), location);
}

其实Routes标签就做了3件事,分别如下:

  • 将Route标签集合转换为一个新的数组A。
  • 调用 matchRoutes函数,得到当前路径对应的路由集合B。
  • 调用 _renderMatches函数,渲染路由集合B。

接下来,我就带着大家沉浸式阅读一下它的流程。我们先启动一下我们的代码 npm run start。此时我们的路由配置只有2个一级路由,分别是\\about。打开浏览器访问 \about。因为我在useRoutes函数里打了断点,所以我本地目前的情况是这样的:

一篇不可错过的react-router源码分析

从上图我们可以看到,进入到useRoutes函数里,第一个参数routes是一个数组,数组里每个对象就是Route对象,里面不仅有当前Route标签对应的元素,还有一些其他的属性,比如唯一id,忽略大小写caseSensitive等等。所以我们就可以判定 createRoutesFromChildren 函数的功能其实就是改造Routes标签的children属性。

接着我们再沉浸式的往下走,看看有啥收获:

一篇不可错过的react-router源码分析

此时我们的断点已经打到了 matchRoutes 路由匹配函数的前一步了。走到这里我们会看到它初始化了很多的变量,比如:routeMatchparentParamsparentPathnameparentPathnameBaseparentRoute。当你在历史栏里手动输入url并访问的时候,这些变量是没有用的,routeMatch 此时就是空数组(因为DataRouterStateContext这个上下文在渲染过程中没有被用到)。

说到这里,我有一个阅读源码的心得,也是曾经犯过的错(不怕大家笑话,哈哈哈)。就是把自己当作了库的开发者,把自己的思想强行贴近作者的思想,力求做到一模一样

好了,我们接着沉浸式阅读,我们先来看一下这个 metchRoutes 路由匹配函数的返回值:

一篇不可错过的react-router源码分析

路由匹配函数的返回值是一个数组match,match里的每一项都符合当前url的匹配机制。为什么match是一个数组呢?因为实际生活中,应用程序并不是只有一级路由,也可能有二级路由,三级路由等等。

我们现在改变一下我们的路由配置:

    <Routes>
        <Route path="/two" element={<About></About>}>
          <Route path="home" element={<About></About>}></Route>
        </Route>
        <Route path="/about" element={<About></About>}/>
        <Route path="/" element={<Home></Home>}/>
    </Routes>

我们现在访问一下/two/about,此时我们就会发现match是一个长度为2的数组。

一篇不可错过的react-router源码分析

接下来,我们就来讲讲matches究竟是怎么得来的。

3.4.1、匹配逻辑讲解

源代码如下:

    export function matchRoutes(routes, locationArg, basename){
          let pathname = stripBasename(location.pathname || "/", basename);
          // 注意flattenRoutes函数才是匹配算法返回正确值的基础
          // 1、将routes数组里面的元素都拍平,拍平的效果请对比数组拍平。
          // 2、拍平后,每个元素里都存在一个 routesMeta属性,这个属性对于子级路由来说非常重要,这个属性会记录子级路由的父集信息。
          // 3、为每个元素增加一波属性,比如积分规则score。
          let branches = flattenRoutes(routes);
          // 对上一步拍平的数组进行排序,积分score越高,index就越靠前
          rankRouteBranches(branches);
          let matches = null;
          for (let i = 0; matches == null && i < branches.length; ++i) {
              // 如果branch[i].path === pathname,那么就将branch[i]的值赋给matches
              matches = matchRouteBranch(branches[i], pathname);
          }
          return matches;
    }

现在我们来跟一下代码,来验证一下我的注释说明。

这里需要说明一点,我们此时的routes配置是这样的(注意顺序),并且历史栏里访问的路径是 \about

<Routes>
    <Route path="/about" element={<About></About>}/>
    <Route path="/" element={<Home></Home>}/>
    <Route path="/two" element={<About></About>}>
      <Route path="home" element={<About></About>}></Route>
    </Route>
</Routes>
flattenRoutes函数

flattenRoutes函数 特别重要,它是整个匹配算法返回值正确的基础。它主要做了以下3件事: 1、将routes数组里面的元素都拍平,拍平的效果请对比数组拍平。 2、拍平后,每个元素里都存在一个 routesMeta属性,这个属性对于子级路由来说非常重要,这个属性会记录子级路由的父集信息。 3、为每个元素增加一波属性,比如积分规则score。

一篇不可错过的react-router源码分析

rankRouteBranches函数

执行 rankRouteBranches 函数后,branches是这样的:

一篇不可错过的react-router源码分析

matchRouteBranch函数

最后,遍历最新的branches数组,依次调用 matchRouteBranch 函数直到matches不为null。

matchRouteBranch 返回的是待渲染的路由,也就意味着如果你访问的是子路由,那么它会将返回一个数组,数组里元素按照 祖父0,父1,子2 ... 的方式排列。

3.4.2、渲染逻辑讲解

源代码如下:

export function _renderMatches(matches, parentMatches){
    if (matches == null) return null;
    // 这块的reduceRight需要注意
    return matches.reduceRight((outlet, match, index) => {
    return (
      <RouteContext.Provider
        children={
          match.route.element !== undefined ? match.route.element : outlet
        }
        value={{
          outlet,
          matches: parentMatches.concat(matches.slice(0, index + 1)),
        }}
      />
    );
  }, null)
}

这里使用了 matches.reduceRight 来渲染组件结构,到这里有的小伙伴估计已经看出来了,正因为matches数组里的元素是按照“祖父、父、子”的方式排序,所以渲染的函数才敢使用reduceRight来返回组件结构

可能有的小伙伴还是没有反应过来,现在我们来做一下数据推演。假设matches的数据是这样的:

    let matches = [
        { path: '/xx',  route: {element: <A />} },
        { path: '/xx/xxx',  route: {element: <B />} },
    ];

那么调用reduceRight方法返回的结构如下:

    <RouteContext.Provider
        children={ <A /> }
        value={{
          outlet: (
              <RouteContext.Provider
                    children={ <B /> }
                    value={{
                      outlet: null,
                      matches: [ { path: '/xx',  route: {element: <A />} }, { path: '/xx/xxx',  route: {element: <B />} ],
                    }}
              />
          ),
          
          matches: [{ path: '/xx',  route: {element: <A />}],
        }}
    />

在A组件中,如果想要渲染B组件,A组件的代码需要这么写:

import { useContext } from 'react';
function A(){
    let child = useContext(RouteContext).outlet;
    return <div className = 'layout'>
        <div className = 'title'></div>
        <div className = 'content'>
            { child }
        </div>
    </div>
}

这也是Outlet组件的伪代码,等价于下面这样:

function A(){
    return <div className = 'layout'>
        <div className = 'title'></div>
        <div className = 'content'>
            <Outlet />
        </div>
    </div>
}

四、最后

好啦,这次的react-router@v6的源码分析到这里就结束啦,文中如果有讲述不对的地方欢迎指正,那么下次再见啦。

转载自:https://juejin.cn/post/7211801284709892157
评论
请登录