欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

淺析history 和 react-router 的實現(xiàn)原理

 更新時間:2023年08月17日 09:47:15   作者:袋鼠云數(shù)棧前端  
react-router 版本更新非???但是它的底層實現(xiàn)原理確是萬變不離其中,在本文中會從前端路由出發(fā)到 react-router 原理總結(jié)與分享,本文對history 和 react-router實現(xiàn)原理講解的非常詳細,需要的朋友跟隨小編一起看看吧

前言

在前一篇文章中,我們詳細的說了 react-router@3.x 升級到 @6.x 需要注意的問題以及變更的使用方式。

react-router 版本更新非???,但是它的底層實現(xiàn)原理確是萬變不離其中,在本文中會從前端路由出發(fā)到 react-router 原理總結(jié)與分享。

前端路由

在 Web 前端單頁面應(yīng)用 SPA(Single Page Application)中,路由是描述 URL 和 UI 之間的映射關(guān)系,這種映射是單向的,即 URL 的改變會引起 UI 更新,無需刷新頁面

如何實現(xiàn)前端路由

實現(xiàn)前端路由,需要解決兩個核心問題

  • 如何改變 URL 卻不引起頁面刷新?
  • 如何監(jiān)測 URL 變化?

在前端路由的實現(xiàn)模式有兩種模式,hash 和 history 模式,分別回答上述兩個問題

hash 模式

  • hash 是 url 中 hash(#) 及后面的部分,常用錨點在頁面內(nèi)做導(dǎo)航,改變 url 中的 hash 部分不會引起頁面的刷新
  • 通過 hashchange 事件監(jiān)聽 URL 的改變。改變 URL 的方式只有以下幾種:通過瀏覽器導(dǎo)航欄的前進后退、通過<a>標簽、通過window.location,這幾種方式都會觸發(fā)hashchange事件

history 模式

  • history 提供了 pushState 和 replaceState 兩個方法,這兩個方法改變 URL 的 path 部分不會引起頁面刷新
  • 通過 popchange 事件監(jiān)聽 URL 的改變。需要注意只在通過瀏覽器導(dǎo)航欄的前進后退改變 URL 時會觸發(fā)popstate事件,通過<a>標簽和pushState/replaceState不會觸發(fā)popstate方法。但我們可以攔截<a>標簽的點擊事件和pushState/replaceState的調(diào)用來檢測 URL 變化,也是可以達到監(jiān)聽 URL 的變化,相對hashchange顯得略微復(fù)雜

JS 實現(xiàn)前端路由

基于 hash 實現(xiàn)

由于三種改變 hash 的方式都會觸發(fā)hashchange方法,所以只需要監(jiān)聽hashchange方法。需要在DOMContentLoaded后,處理一下默認的 hash 值

// 頁面加載完不會觸發(fā) hashchange,這里主動觸發(fā)一次 hashchange 事件,處理默認hash
window.addEventListener('DOMContentLoaded', onLoad);
// 監(jiān)聽路由變化
window.addEventListener('hashchange', onHashChange);
// 路由變化時,根據(jù)路由渲染對應(yīng) UI
function onHashChange() {
  switch (location.hash) {
    case '#/home':
      routerView.innerHTML = 'This is Home';
      return;
    case '#/about':
      routerView.innerHTML = 'This is About';
      return;
    case '#/list':
      routerView.innerHTML = 'This is List';
      return;
    default:
      routerView.innerHTML = 'Not Found';
      return;
  }
}

hash 實現(xiàn) demo

基于 history 實現(xiàn)

因為 history 模式下,<a>標簽和pushState/replaceState不會觸發(fā)popstate方法,我們需要對<a>的跳轉(zhuǎn)和pushState/replaceState做特殊處理。

  • <a>作點擊事件,禁用默認行為,調(diào)用pushState方法并手動觸發(fā)popstate的監(jiān)聽事件
  • pushState/replaceState可以重寫 history 的方法并通過派發(fā)事件能夠監(jiān)聽對應(yīng)事件
var _wr = function (type) {
  var orig = history[type];
  return function () {
    var e = new Event(type);
    e.arguments = arguments;
    var rv = orig.apply(this, arguments);
    window.dispatchEvent(e);
    return rv;
  };
};
// 重寫pushstate事件
history.pushState = _wr('pushstate');
function onLoad() {
  routerView = document.querySelector('#routeView');
  onPopState();
  // 攔截 <a> 標簽點擊事件默認行為
  // 點擊時使用 pushState 修改 URL并更新手動 UI,從而實現(xiàn)點擊鏈接更新 URL 和 UI 的效果。
  var linkList = document.querySelectorAll('a[href]');
  linkList.forEach((el) =>
    el.addEventListener('click', function (e) {
      e.preventDefault();
      history.pushState(null, '', el.getAttribute('href'));
      onPopState();
    }),
  );
}
// 監(jiān)聽pushstate方法
window.addEventListener('pushstate', onPopState());
// 頁面加載完不會觸發(fā) hashchange,這里主動觸發(fā)一次 popstate 事件,處理默認pathname
window.addEventListener('DOMContentLoaded', onLoad);
// 監(jiān)聽路由變化
window.addEventListener('popstate', onPopState);
// 路由變化時,根據(jù)路由渲染對應(yīng) UI
function onPopState() {
  switch (location.pathname) {
    case '/home':
      routerView.innerHTML = 'This is Home';
      return;
    case '/about':
      routerView.innerHTML = 'This is About';
      return;
    case '/list':
      routerView.innerHTML = 'This is List';
      return;
    default:
      routerView.innerHTML = 'Not Found';
      return;
  }
}

history 實現(xiàn) demo

React-Router 的架構(gòu)

  • history 庫給 browser、hash 兩種 history 提供了統(tǒng)一的 API,給到 react-router-dom 使用
  • react-router 實現(xiàn)了路由的最核心能力。提供了<Router>、<Route>等組件,以及配套 hook
  • react-router-dom 是對 react-router 更上一層封裝。把 history 傳入<Router>并初始化成<BrowserRouter><HashRouter>,補充了<Link>這樣給瀏覽器直接用的組件。同時把 react-router 直接導(dǎo)出,減少依賴

History 實現(xiàn)

history

在上文中說到,BrowserRouter使用 history 庫提供的createBrowserHistory創(chuàng)建的history對象改變路由狀態(tài)和監(jiān)聽路由變化。

? 那么 history 對象需要提供哪些功能訥?

  • 監(jiān)聽路由變化的listen方法以及對應(yīng)的清理監(jiān)聽unlisten方法
  • 改變路由的push方法
// 創(chuàng)建和管理listeners的方法
export const EventEmitter = () => {
  const events = [];
  return {
    subscribe(fn) {
      events.push(fn);
      return function () {
        events = events.filter((handler) => handler !== fn);
      };
    },
    emit(arg) {
      events.forEach((fn) => fn && fn(arg));
    },
  };
};

BrowserHistory

const createBrowserHistory = () => {
  const EventBus = EventEmitter();
  // 初始化location
  let location = {
    pathname: '/',
  };
  // 路由變化時的回調(diào)
  const handlePop = function () {
    const currentLocation = {
      pathname: window.location.pathname,
    };
    EventBus.emit(currentLocation); // 路由變化時執(zhí)行回調(diào)
  };
  // 定義history.push方法
  const push = (path) => {
    const history = window.history;
    // 為了保持state棧的一致性
    history.pushState(null, '', path);
    // 由于push并不觸發(fā)popstate,我們需要手動調(diào)用回調(diào)函數(shù)
    location = { pathname: path };
    EventBus.emit(location);
  };
  const listen = (listener) => EventBus.subscribe(listener);
  // 處理瀏覽器的前進后退
  window.addEventListener('popstate', handlePop);
  // 返回history
  const history = {
    location,
    listen,
    push,
  };
  return history;
};

對于 BrowserHistory 來說,我們的處理需要增加一項,當(dāng)我們觸發(fā) push 的時候,需要手動通知所有的監(jiān)聽者,因為 pushState 無法觸發(fā) popState 事件,因此需要手動觸發(fā)

HashHistory

const createHashHistory = () => {
  const EventBus = EventEmitter();
  let location = {
    pathname: '/',
  };
  // 路由變化時的回調(diào)
  const handlePop = function () {
    const currentLocation = {
      pathname: window.location.hash.slice(1),
    };
    EventBus.emit(currentLocation); // 路由變化時執(zhí)行回調(diào)
  };
  // 不用手動執(zhí)行回調(diào),因為hash改變會觸發(fā)hashchange事件
  const push = (path) => (window.location.hash = path);
  const listen = (listener: Function) => EventBus.subscribe(listener);
  // 監(jiān)聽hashchange事件
  window.addEventListener('hashchange', handlePop);
  // 返回的history上有個listen方法
  const history = {
    location,
    listen,
    push,
  };
  return history;
};

在實現(xiàn) hashHistory 的時候,我們只是對hashchange進行了監(jiān)聽,當(dāng)該事件發(fā)生時,我們獲取到最新的 location 對象,在通知所有的監(jiān)聽者 listener 執(zhí)行回調(diào)函數(shù)

React-Router@6 丐版實現(xiàn)

  • 綠色為 history 中的方法
  • 紫色為 react-router-dom 中的方法
  • 橙色為 react-router 中的方法

Router

??? 基于 Context 的全局狀態(tài)下發(fā)。Router 是一個 “Provider-Consumer” 模型

Router 做的事情很簡單,接收navigator 和location,使用 context 將數(shù)據(jù)傳遞下去,能夠讓子組件獲取到相關(guān)的數(shù)據(jù)

function Router(props: IProps) {
  const { navigator, children, location } = props;
  const navigationContext = React.useMemo(() => ({ navigator }), [navigator]);
  const { pathname } = location;
  const locationContext = React.useMemo(
    () => ({ location: { pathname } }),
    [pathname],
  );
  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider value={locationContext} children={children} />
    </NavigationContext.Provider>
  );
}

HashRouter

基于不同的 history 調(diào)用 Router 組件。并且在 history 發(fā)生改變的時候,監(jiān)聽 history,能夠在 location 發(fā)生改變的時候,執(zhí)行回調(diào)改變 location。

在下面的代碼中,能夠發(fā)現(xiàn)監(jiān)聽者為 setState 函數(shù),在上述 hashHistory 中,如果我們的 location 發(fā)生了改變,會通知到所有的監(jiān)聽者執(zhí)行回調(diào),也就是我們這里的 setState 函數(shù),即我們能夠拿到最新的 location 信息通過 LocationContext 傳遞給子組件,再去做對應(yīng)的路由匹配

function HashRouter({ children }) {
  let historyRef = React.useRef();
  if (historyRef.current == null) {
    historyRef.current = createHashHistory();
  }
  let history = historyRef.current;
  let [state, setState] = React.useState({
    location: history.location,
  });
  React.useEffect(() => {
    const unListen = history.listen(setState);
    return unListen;
  }, [history]);
  return (
    <Router children={children} location={state.location} navigator={history} />
  );
}

Routes/Route

我們能夠發(fā)現(xiàn)在 v6.0 的版本 Route 組件只是一個工具人,并沒有做任何事情。

function Route(_props: RouteProps): React.ReactElement | null {
  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>.`,
  );
}

實際上處理一切邏輯的組件是 Routes,它內(nèi)部實現(xiàn)了根據(jù)路由的變化,匹配出一個正確的組件。

const Routes = ({ children }) => {
  return useRoutes(createRoutesFromChildren(children));
};

useRoutes 為整個 v6 版本的核心,分為路由上下文解析、路由匹配、路由渲染三個步驟

<Routes>
  <Route path="/home" element={<Home />}>
    <Route path="1" element={<Home1 />}>
      <Route path="2" element={<Home2 />}></Route>
    </Route>
  </Route>
  <Route path="/about" element={<About />}></Route>
  <Route path="/list" element={<List />}></Route>
  <Route path="/notFound" element={<NotFound />} />
  <Route path="/navigate" element={<Navigate to="/notFound" />} />
</Routes>

上述 Routes 代碼中,通過 createRoutesFromChildren 函數(shù)將 Route 組件結(jié)構(gòu)化??梢园?nbsp;<Route> 類型的 react element 對象,變成了普通的 route 對象結(jié)構(gòu),如下圖

useRoutes

useRoutes 才是真正處理渲染關(guān)系的,其代碼如下:

// 第一步:獲取相關(guān)的 pathname
let location = useLocation();
let { matches: parentMatches } = React.useContext(RouteContext);
// 第二步:找到匹配的路由分支,將 pathname 和 Route 的 path 做匹配
const matches = matchRoutes(routes, location);
// 第三步:渲染真正的路由組件
const renderedMatches = _renderMatches(matches, parentMatches);
return renderedMatches;

matchRoutes

matchRoutes 中通過 pathname 和路由的 path 進行匹配

因為我們在 Route 中定義的 path 都是相對路徑,所以我們在 matchRoutes 方法中,需要對 routes 對象遍歷,對于 children 里面的 path 需要變成完整的路徑,并且需要將 routes 扁平化,不在使用嵌套結(jié)構(gòu)

const flattenRoutes = (
  routes,
  branches = [],
  parentsMeta = [],
  parentPath = '',
) => {
  const flattenRoute = (route) => {
    const meta = {
      relativePath: route.path || '',
      route,
    };
    const path = joinPaths([parentPath, meta.relativePath]);
    const routesMeta = parentsMeta.concat(meta);
    if (route.children?.length > 0) {
      flattenRoutes(route.children, branches, routesMeta, path);
    }
    if (route.path == null) {
      return;
    }
    branches.push({ path, routesMeta });
  };
  routes.forEach((route) => {
    flattenRoute(route);
  });
  return branches;
};

當(dāng)我們訪問/#/home/1/2的時候,獲得的 matches 如下

我們得到的 match 順序是從 Home → Home1 → Home2

_renderMatches

_renderMatches 才會渲染所有的 matches 對象

const _renderMatches = (matches, parentMatches = []) => {
  let renderedMatches = matches;
  return renderedMatches.reduceRight((outlet, match, index) => {
    let matches = parentMatches.concat(renderedMatches.slice(0, index + 1));
    const getChildren = () => {
      let children;
      if (match.route.Component) {
        children = <match.route.Component />;
      } else if (match.route.element) {
        children = match.route.element;
      } else {
        children = outlet;
      }
      return (
        <RouteContext.Provider
          value={{
            outlet,
            matches,
          }}
        >
          {children}
        </RouteContext.Provider>
      );
    };
    return getChildren();
  }, null);
};

_renderMatches 這段代碼我們能夠明白 outlet 作為子路由是如何傳遞給父路由渲染的。matches 采用從右往左的遍歷順序,將上一項的返回值作為后一項的 outlet,那么子路由就作為 outlet 傳遞給了父路由

Outlet

實際上就是內(nèi)部渲染 RouteContext 的 outlet 屬性

function Outlet(props) {
  return useOutlet(props.context);
}
function useOutlet(context?: unknown) {
  let outlet = useContext(RouteContext).outlet; // 獲取上一級 RouteContext 上面的 outlet
  if (outlet) {
    return (
      <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
    );
  }
  return outlet;
}

Link

在 Link 中,我們使用<a>標簽來做跳轉(zhuǎn),但是 a 標簽會使頁面重新刷新,所以需要阻止 a 標簽的默認行為,調(diào)用 useNavigate 方法進行跳轉(zhuǎn)

function Link({ to, children, onClick }) {
  const navigate = useNavigate();
  const handleClick = onClick
    ? onClick
    : (event) => {
        event.preventDefault();
        navigate(to);
      };
  return (
    <a href={to} onClick={handleClick}>
      {children}
    </a>
  );
}

Hooks

function useLocation() {
  return useContext(LocationContext).location;
}
function useNavigate() {
  const { navigator } = useContext(NavigationContext);
  const navigate = useCallback(
    (to: string) => {
      navigator.push(to);
    },
    [navigator],
  );
  return navigate;
}

本文所有的代碼鏈接可點擊查看

參考鏈接

到此這篇關(guān)于一文了解 history 和 react-router 的實現(xiàn)原理的文章就介紹到這了,更多相關(guān)history 和 react-router實現(xiàn)原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • React實現(xiàn)表格選取

    React實現(xiàn)表格選取

    這篇文章主要為大家詳細介紹了React實現(xiàn)表格選取,類似于Excel選中一片區(qū)域并獲得選中區(qū)域的所有數(shù)據(jù),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-08-08
  • React?函數(shù)式組件和類式組件詳情

    React?函數(shù)式組件和類式組件詳情

    這篇文章主要介紹了React函數(shù)式組件和類式組件詳情,React是組件化的的JS庫,組件化也是React的核心思想,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下
    2022-08-08
  • React?Server?Component混合式渲染問題詳解

    React?Server?Component混合式渲染問題詳解

    React?官方對?Server?Comopnent?是這樣介紹的:?zero-bundle-size?React?Server?Components,這篇文章主要介紹了React?Server?Component:?混合式渲染,需要的朋友可以參考下
    2022-12-12
  • 詳解如何用webpack4從零開始構(gòu)建react開發(fā)環(huán)境

    詳解如何用webpack4從零開始構(gòu)建react開發(fā)環(huán)境

    這篇文章主要介紹了詳解如何用webpack4從零開始構(gòu)建react開發(fā)環(huán)境,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-01-01
  • react實現(xiàn)組件狀態(tài)緩存的示例代碼

    react實現(xiàn)組件狀態(tài)緩存的示例代碼

    本文主要介紹了react實現(xiàn)組件狀態(tài)緩存的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-02-02
  • React useReducer終極使用教程

    React useReducer終極使用教程

    useReducer是在react V16.8推出的鉤子函數(shù),從用法層面來說是可以代替useState。相信前期使用過 React 的前端同學(xué),大都會經(jīng)歷從class語法向hooks用法的轉(zhuǎn)變,react的hooks編程給我們帶來了絲滑的函數(shù)式編程體驗
    2022-10-10
  • 詳解如何構(gòu)建自己的react hooks

    詳解如何構(gòu)建自己的react hooks

    我們組的前端妹子在組內(nèi)分享時談到了 react 的鉤子,趁此機會我也對我所理解的內(nèi)容進行下總結(jié),方便更多的同學(xué)了解。在 React 的 v16.8.0 版本里添加了 hooks 的這種新的 API,我們非常有必要了解下他的使用方法,并能夠結(jié)合我們的業(yè)務(wù)編寫幾個自定義的 hooks。
    2021-05-05
  • 使用react-virtualized實現(xiàn)圖片動態(tài)高度長列表的問題

    使用react-virtualized實現(xiàn)圖片動態(tài)高度長列表的問題

    一般我們在寫react項目中,同時渲染很多dom節(jié)點,會造成頁面卡頓, 空白的情況。為了解決這個問題,今天小編給大家分享一篇教程關(guān)于react-virtualized實現(xiàn)圖片動態(tài)高度長列表的問題,感興趣的朋友跟隨小編一起看看吧
    2021-05-05
  • 詳解react native頁面間傳遞數(shù)據(jù)的幾種方式

    詳解react native頁面間傳遞數(shù)據(jù)的幾種方式

    這篇文章主要介紹了詳解react native頁面間傳遞數(shù)據(jù)的幾種方式,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-11-11
  • react進階教程之異常處理機制error?Boundaries

    react進階教程之異常處理機制error?Boundaries

    在react中一旦出錯,如果每個組件去處理出錯情況則比較麻煩,下面這篇文章主要給大家介紹了關(guān)于react進階教程之異常處理機制error?Boundaries的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下
    2022-08-08

最新評論