React-router实现原理剖析

React-router 实现最需要关注的就以下几方面:

  1. 路由值到视图的映射规则

  2. 对于路由变更的监听

  3. 路由操作方法

  4. 路由记录存储与操作

1. BrowserRouter

浏览器路由实现,最主要的两个概念是变更路由与监听路由变更。

History 是整个浏览器路由历史记录大对象,其中包含长度等属性 Location 是单一页面所对应的资源定位对象,我们可以理解为当页面跳转时,先生成 Location,然后将 Location push 到 History。

首先我们要明确几个概念:

  1. 历史操作。注意:此操作不会触发 popState
  • history.pushState

  • history.replaceState

  1. 监听变更
  • window.addEventListener("popstate", () => {})
  1. 操作。注意:以下操作会触发 popState
  • history.back()

  • history.forward()

  • history.go()

1.1. 创建Router

javascript 复制代码
export function createBrowserRouter(
  routes: RouteObject[],
  opts?: DOMRouterOpts
): RemixRouter {
  return createRouter({
    basename: opts?.basename,
    future: {
      ...opts?.future,
      v7_prependBasename: true,
    },
    history: createBrowserHistory({ window: opts?.window }),
    hydrationData: opts?.hydrationData || parseHydrationData(),
    routes,
    mapRouteProperties,
  }).initialize();
}

1.2. router下承载history

javascript 复制代码
export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
): BrowserHistory {
  function createBrowserLocation(
    window: Window,
    globalHistory: Window["history"]
  ) {
    let { pathname, search, hash } = window.location;
    return createLocation(
      "",
      { pathname, search, hash },
      (globalHistory.state && globalHistory.state.usr) || null,
      (globalHistory.state && globalHistory.state.key) || "default"
    );
  }

  function createBrowserHref(window: Window, to: To) {
    return typeof to === "string" ? to : createPath(to);
  }

  return getUriBasedHistory(
    createBrowserLocation,
    createBrowserHref,
    null,
    options
  );
}

1.3. history 确定 location

javascript 复制代码
function getUrlBasedHistory(
  getLocation: (window: Window, globalHistory: Window["history"]) => Location,
  createHref: (window: Window, to: To) => string,
  validateLocation: ((location: Location, to: To) => void) | null,
  options: UrlHistoryOptions = {}
): UrlHistory {
  let { window = document.defaultView!, v5Compat = false } = options;
  let globalHistory = window.history;
  let action = Action.Pop;
  let listener: Listener | null = null;

  let index = getIndex()!

  if (index == null) {
    index = 0;
    globalHistory.replaceState({ ...globalHistory.state, idx: index }, "");
  }

  function getIndex(): number {
    let state = globalHistory.state || { idx: null };
    return state.idx;
  }

  function handlePop() {
    action = Action.Pop;
    let nextIndex = getIndex();
    let delta = nextIndex == null ? null : nextIndex - index;
    index = nextIndex;
    if (listener) {
      listener({ action, location: history.location, delta });
    }
  }

  function push(to: To, state?: any) {
    action = Action.Push;
    let location = createLocation(history.location, to, state);
    if (validateLocation) validateLocation(location, to);

    index = getIndex() + 1;
    let historyState = getHistoryState(location, index);
    let url = history.createHref(location);

    try {
      globalHistory.pushState(historyState, "", url);
    } catch (error) {

      if (error instanceof DOMException && error.name === "DataCloneError") {
        throw error;
      }


      window.location.assign(url);
    }

    if (v5Compat && listener) {
      listener({ action, location: history.location, delta: 1 });
    }
  }

  function replace(to: To, state?: any) {
    action = Action.Replace;
    let location = createLocation(history.location, to, state);
    if (validateLocation) validateLocation(location, to);

    index = getIndex();
    let historyState = getHistoryState(location, index);
    let url = history.createHref(location);
    globalHistory.replaceState(historyState, "", url);

    if (v5Compat && listener) {
      listener({ action, location: history.location, delta: 0 });
    }
  }

  function createURL(to: To): URL {

    let base =
      window.location.origin !== "null"
        ? window.location.origin
        : window.location.href;

    let href = typeof to === "string" ? to : createPath(to);
    invariant(
      base,
      `No window.location.(origin|href) available to create URL for href: ${href}`
    ),
      return new URL(href, base);
  }

  let history: History = {
    get action() {
      return action;
    },
    get location() {
      return getLocation(window, globalHistory);
    },
    listen(fn: Listener) {
      if (listener) {
        throw new Error("A history only accepts one active listener");
      }
      window.addEventListener(PopStateEventType, handlePop);
      listener = fn;

      return () => {
        window.removeEventListener(PopStateEventType, handlePop);
        listener = null;
      };
    },
    createHref(to) {
      return createHref(window, to);
    },
    createURL,
    encodeLocation(to) {

      let url = createURL(to);
      return {
        pathname: url.pathname,
        search: url.search,
        hash: url.hash,
      };
    },
    push,
    replace,
    go(n) {
      return globalHistory.go(n);
    },
  };

  return history;
}

1.4. 历史记录栈变更监听

事件处理解决后,接下来就是解决监听,我们上面提到监听 popState 以此来处理路由变更。

javascript 复制代码
listen(fn: Listener) {
  if (listener) {
    throw new Error("A history only accepts one active listener");
  }
  window.addEventListener(PopStateEventType, handlePop);
  listener = fn;

  return () => {
    window.removeEventListener(PopStateEventType, handlePop);
    listener = null;
  };
},

1.5. popState 逻辑处理

javascript 复制代码
function handlePop() {
  action = Action.Pop;
  let nextIndex = getIndex()
  let delta = nextIndex == null ? null : nextIndex - index
  index = nextIndex
  if (listener) {
    listener({ action, location: history.location, delta })
  }
}

2. MemoryRouter

内存型路由的上层实现与 BrowserRouter 类似,或者说作者在设计之初就考虑了规范的对外统一接口协议,你会发现在使用 React-router 时,不管你用什么类型记录,API 都是一样的,这就是抽象封装的魅力。

2.1. 创建 router

javascript 复制代码
export function createMemoryRouter(
  routes: RouteObject[],
  opts?: {
    basename?: string;
    future?: Partial<Omit<RouterFutureConfig, "v7_prependBasename">>;
    hydrationData?: HydrationState;
    initialEntries?: InitialEntry[];
    initialIndex?: number;
    unstable_dataStrategy?: unstable_DataStrategyFunction;
  }
): RemixRouter {
  return createRouter({
    basename: opts?.basename,
    future: {
      ...opts?.future,
      v7_prependBasename: true,
    },
    history: createMemoryHistory({
      initialEntries: opts?.initialEntries,
      initialIndex: opts?.initialIndex,
    }),
    hydrationData: opts?.hydrationData,
    routes,
    mapRouteProperties,
    unstable_dataStrategy: opts?.unstable_dataStrategy,
  }).initialize();
}

2.2. Router 下承载 history

javascript 复制代码
export function createMemoryHistory(
  options: MemoryHistoryOptions = {}
): MemoryHistory {
  let { initialEntries = ["/"], initialIndex, v5Compat = false } = options;
  let entries: Location[]; // Declare so we can access from createMemoryLocation
  entries = initialEntries.map((entry, index) =>
    createMemoryLocation(
      entry,
      typeof entry === "string" ? null : entry.state,
      index === 0 ? "default" : undefined
    )
  );
  let index = clampIndex(
    initialIndex == null ? entries.length - 1 : initialIndex
  );
  let action = Action.Pop;
  let listener: Listener | null = null;

  function clampIndex(n: number): number {
    return Math.min(Math.max(n, 0), entries.length - 1);
  }

  function getCurrentLocation(): Location {
    return entries[index];
  }

  function createMemoryLocation(
    to: To,
    state: any = null,
    key?: string
  ): Location {
    let location = createLocation(
      entries ? getCurrentLocation().pathname : "/",
      to,
      state,
      key
    );
    warning(
      location.pathname.charAt(0) === "/",
      `relative pathnames are not supported in memory history: ${JSON.stringify(
        to
      )}`
    );
    return location;
  }

  function createHref(to: To) {
    return typeof to === "string" ? to : createPath(to);
  }

  let history: MemoryHistory = {
    get index() {
      return index;
    },
    get action() {
      return action;
    },
    get location() {
      return getCurrentLocation();
    },
    createHref,
    createURL(to) {
      return new URL(createHref(to), "http://localhost");
    },
    encodeLocation(to: To) {
      let path = typeof to === "string" ? parsePath(to) : to;
      return {
        pathname: path.pathname || "",
        search: path.search || "",
        hash: path.hash || "",
      };
    },
    push(to, state) {
      action = Action.Push;
      let nextLocation = createMemoryLocation(to, state);
      index += 1;
      entries.splice(index, entries.length, nextLocation);
      if (v5Compat && listener) {
        listener({ action, location: nextLocation, delta: 1 });
      }
    },
    replace(to, state) {
      action = Action.Replace;
      let nextLocation = createMemoryLocation(to, state);
      entries[index] = nextLocation;
      if (v5Compat && listener) {
        listener({ action, location: nextLocation, delta: 0 });
      }
    },
    go(delta) {
      action = Action.Pop;
      let nextIndex = clampIndex(index + delta);
      let nextLocation = entries[nextIndex];
      index = nextIndex;
      if (listener) {
        listener({ action, location: nextLocation, delta });
      }
    },
    listen(fn: Listener) {
      listener = fn;
      return () => {
        listener = null;
      };
    },
  };

  return history;
}

因为内存型路由跟浏览器历史记录没有关联,因此相较于 BrowserRouter,没有关于浏览器历史记录栈变更的监听,只有单纯的记录压入和跳转。

重点关注:

javascript 复制代码
push(to, state) {
  action = Action.Push;
  let nextLocation = createMemoryLocation(to, state);
  index += 1;
  entries.splice(index, entries.length, nextLocation);
  if (v5Compat && listener) {
    listener({ action, location: nextLocation, delta: 1 });
  }
}
相关推荐
aiguangyuan14 小时前
React 动态路由的使用和实现原理
react·前端开发
aiguangyuan15 小时前
手写简版React-router
react·前端开发
aiguangyuan2 天前
React 19 新特性
react·前端开发
亦世凡华、2 天前
React--》使用vite构建器打造高效的React组件库
经验分享·react·组件库·组件库开发
菜鸡爱上编程2 天前
React16,17,18,19更新对比
前端·javascript·reactjs·react
霸王蟹2 天前
前端项目Excel数据导出同时出现中英文表头错乱情况解决方案。
笔记·学习·typescript·excel·vue3·react·vite
Thanks_ks3 天前
探索现代 Web 开发:从 HTML5 到 Vue.js 的全栈之旅
javascript·vue.js·css3·html5·前端开发·web 开发·全栈实战
aiguangyuan4 天前
浅谈 React Suspense
react·前端开发
恰薯条的屑海鸥4 天前
零基础学前端-传统前端开发(第三期-CSS介绍与应用)
前端·css·学习·css3·前端开发·前端入门·前端教程