不到 100 行代碼,實現 React Router 核心邏輯

造輪子就是應用核心原理 + 周邊功能的堆砌,所以學習成熟庫的源碼往往會受到非核心代碼干擾,Router 這個 repo 用不到 100 行源碼實現了 React Router 核心機制,很適合用來學習。

精讀

Router 快速實現了 React Router 3 個核心 API:RouternavigateLink,下面列出基本用法,配合理解源碼實現會更方便:

const App = () => (
  <Router
    routes={[
      { path: '/home', component: <Home /> },
      { path: '/articles', component: <Articles /> }
    ]}
  />
)

const Home = () => (
  <div>
    home, <Link href="/articles">go articles</Link>,
    <span onClick={() => navigate('/details')}>or jump to details</span>
  </div>
)

首先看 Router 的實現,在看代碼之前,思考下 Router 要做哪些事情?

所以 Router 是一個路由渲染分配器與 url 監聽器:

export default function Router ({ routes }) {
  // 存儲當前 url path,方便其變化時引發自身重渲染,以返回新的 url 對應的組件
  const [currentPath, setCurrentPath] = useState(window.location.pathname);

  useEffect(() => {
    const onLocationChange = () => {
      // 將 url path 更新到當前數據流中,觸發自身重渲染
      setCurrentPath(window.location.pathname);
    }

    // 監聽 popstate 事件,該事件由用戶點擊瀏覽器前進/後退時觸發
    window.addEventListener('popstate', onLocationChange);

    return () => window.removeEventListener('popstate', onLocationChange)
  }, [])

  // 找到匹配當前 url 路徑的組件並渲染
  return routes.find(({ path, component }) => path === currentPath)?.component
}

最後一段代碼看似每次都執行 find 有一定性能損耗,但其實根據 Router 一般在最根節點的特性,該函數很少因父組件重渲染而觸發渲染,所以性能不用太擔心。

但如果考慮做一個完整的 React Router 組件庫,考慮了更復雜的嵌套 API,即 RouterRouter 後,不僅監聽方式要變化,還需要將命中的組件緩存下來,需要考慮的點會逐漸變多。

下面該實現 navigate Link 了,他倆做的事情都是跳轉,有如下區別:

  1. API 調用方式不同,navigate 是調用式函數,而 Link 是一個內置 navigate 能力的 a 標籤。

  2. Link 其實還有一種按住 ctrl 後打開新 tab 的跳轉模式,該模式由瀏覽器對 a 標籤默認行爲完成。

所以 Link 更復雜一些,我們先實現 navigate,再實現 Link 時就可以複用它了。

既然 Router 已經監聽 popstate 事件,我們顯然想到的是觸發 url 變化後,讓 popstate 捕獲,自動觸發後續跳轉邏輯。但可惜的是,我們要做的 React Router 需要實現單頁跳轉邏輯,而單頁跳轉的 API history.pushState 並不會觸發 popstate,爲了讓實現更優雅,我們可以在 pushState 後手動觸發 popstate 事件,如源碼所示:

export function navigate (href) {
  // 用 pushState 直接刷新 url,而不觸發真正的瀏覽器跳轉
  window.history.pushState({}, "", href);

  // 手動觸發一次 popstate,讓 Route 組件監聽並觸發 onLocationChange
  const navEvent = new PopStateEvent('popstate');
  window.dispatchEvent(navEvent);
}

接下來實現 Link 就很簡單了,有幾個考慮點:

  1. 返回一個正常的 <a> 標籤。

  2. 因爲正常 <a> 點擊後就發生網頁刷新而不是單頁跳轉,所以點擊時要阻止默認行爲,換成我們的 navigate(源碼裏沒做這個抽象,筆者稍微優化了下)。

  3. 但按住 ctrl 時又要打開新 tab,此時用默認 <a> 標籤行爲就行,所以此時不要阻止默認行爲,也不要繼續執行 navigate,因爲這個 url 變化不會作用於當前 tab。

export function Link ({ className, href, children }) {
  const onClick = (event) => {
    // mac 的 meta or windows 的 ctrl 都會打開新 tab
    // 所以此時不做定製處理,直接 return 用原生行爲即可
    if (event.metaKey || event.ctrlKey) {
      return;
    }

    // 否則禁用原生跳轉
    event.preventDefault();

    // 做一次單頁跳轉
    navigate(href)
  };

  return (
    <a className={className} href={href} onClick={onClick}>
      {children}
    </a>
  );
};

這樣的設計,既能兼顧 <a> 標籤默認行爲,又能在點擊時優化爲單頁跳轉,裏面對 preventDefaultmetaKey 的判斷值得學習。

總結

從這個小輪子中可以學習到一下幾個經驗:

討論地址是:精讀《react-snippets - Router 源碼》· Issue #418 · dt-fe/weekly

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/QZ49KF-k14NamqlF_hBT9A