手写react-router,理解react-router原理

一. react-router介绍

react-routerReact框架配套的路由库,支持配置路由与组件的映射关系。

代码示例如下

javascript 复制代码
function Home() {
  return <h1>Home</h1>
}

function Account() {
  return <h1>Account</h1>
}

function App() {
  return (
    <BrowserRouter>
      <div>
        <ul>
          <li>
            <Link to='/'>home</Link>
          </li>
          <li>
            <Link to='/account'>account</Link>
          </li>
        </ul>
        <Routes>
          <Route path='/' element={<Home />} />
          <Route path='/account' element={<Account />} />d 
        </Routes>
      </div>
    </BrowserRouter>
  )
}

二. 实现react-router

react-router核心原理是采用路由劫持,即监听windowpopstate事件,当页面路由变更时会触发popstate事件,执行路由组件渲染逻辑。

2.1 BrowserRouter

BrowserRouter组件的主要逻辑是负责初始化路由劫持逻辑,当路由变更时触发组件更新渲染。触发组件更新渲染主要通过ReactuseSyncExternalStore方法实现,在往期文章手写React useSyncExternalStore,理解useSyncExternalStore原理有介绍其实现原理,本文不再赘述。

javascript 复制代码
function BrowserRouter({ children }) {
  const historyRef = useRef()

  if (historyRef.current === null) historyRef.current = createBrowserHistory()

  const history = historyRef.current

  const { action, location } = useSyncExternalStore(
    history.subscribe,
    history.getSnapshot,
  )

  return (
    <Router location={location} navigator={history} navigationType={action}>
      {children}
    </Router>
  )
}

2.1.1 createBrowserHistory

创建自定义history对象实例。listener变量用于记录触发组件更新渲染的方法,当路由变更时,会调用该方法触发更新渲染。

javascript 复制代码
const Action = {
  Pop: 'Pop',
  Push: 'Push',
  Replace: 'Replace',
}

function createBrowserHistory() {
  let action = Action.Pop
  let listener = null

  const handlePop = () => {}

  const history = {
    get action() {
      return action
    },
    get location() {
      const { pathname, search, hash } = window.location
      return {
        pathname,
        search,
        hash,
      }
    },
    push: to => {
      action = Action.Push
      window.history.pushState(null, '', to)
      listener()
    },
    subscribe: callback => {
      listener = callback
      window.addEventListener('popstate', handlePop)
      return () => {
        window.removeEventListener('popstate', handlePop)
        listener = null
      }
    },
    getSnapshot: () => {
      return {
        action: history.action,
        location: history.location,
      }
    },
  }

  return history
}

2.1.2 Router

Router组件逻辑比较简单,主要是添加两个Context组件包括子组件,便于子组件通过useContext拿到传递的属性值。

javascript 复制代码
const NavigationContext = createContext(null)
const LocationContext = createContext(null)

function Router({ location, navigator, navigationType, children }) {
  return (
    <NavigationContext.Provider value={{ navigator }}>
      <LocationContext.Provider value={{ location, navigationType }}>
        {children}
      </LocationContext.Provider>
    </NavigationContext.Provider>
  )
}

Link组件负责渲染a标签,当触发点击事件时会调用自定义history.push方法变更页面路由,然后触发组件更新渲染。

javascript 复制代码
function Link({ to, children }) {
  const { navigator } = useContext(NavigationContext)

  return (
    <a
      onClick={() => {
        navigator.push(to)
      }}
    >
      {children}
    </a>
  )
}

2.3 Routes

Routes组件负责解析route配置,并根据当前页面路由匹配需要渲染的route配置。

javascript 复制代码
function useRoutes(routes) {
  const location = useLocation()
  // 获取匹配的路由
  const matches = matchRoutes(routes, location)
  return matches.map((match, index) => (
    <RouteContext.Provider key={index} value={{ match }}>
      {match.route.element}
    </RouteContext.Provider>
  ))
}

// 遍历Route组件创建其对应的route配置
function createRoutesFromChildren(children) {
  return children.map(element => ({
    path: element.props.path,
    element: element.props.element,
  }))
}

function Routes({ children }) {
  return useRoutes(createRoutesFromChildren(children))
}

2.4 Route

Route组件没什么业务逻辑,其作用类似React.Fragment组件,主要是为Routes组件服务的,便于转换成对应的route配置。

javascript 复制代码
function Route({ path, element }) {
  return <></>
}

三. 总结

react-router核心原理是采用路由劫持,当页面路由变更时触发组件更新渲染,然后获取当前页面路由对应的组件进行渲染。代码仓库

创作不易,如果文章对你有帮助,那点个小小的赞吧,你的支持是我持续更新的动力!

相关推荐
神明不懂浪漫几秒前
【第三章】CSS(一)——基础选择器、CSS的属性
前端·css·html·css3
IVEN_9 分钟前
本地正常,Docker 怎么就空白:Next.js SSR 的 Alpine musl DNS 陷阱
前端·docker·next.js
凡人叶枫9 分钟前
Effective C++ 条款24:若所有参数皆须要类型转换,请为此采用 non-member 函数
linux·前端·c++·算法·嵌入式开发
用户8876654266311 分钟前
Web3 前端实时通信如何落地:从 SSE 订阅到行情、订单与账户状态更新
前端·react.js·web3
an3174214 分钟前
使用 LangGraph + DeepSeek 构建 AI 面试官:状态图设计与实践
前端·ai编程
代码不加糖15 分钟前
MessageChannel是什么,有什么使用场景?
前端·javascript
小小龙学IT19 分钟前
HTMX:让 HTML 重新成为前端核心的超轻量动态交互库
前端·html·交互
星栈20 分钟前
写 Makepad Demo 不难,难的是把它写成项目
前端·rust
用户0595401744621 分钟前
localStorage清除策略踩坑实录:一个过期的token让我排查了3小时
前端·css
Nanachi23 分钟前
跨框架的前端源码定位,再加上点LLM
前端