實現一個 react-router

本文將用盡量容易理解的方式,實現最小可用的 react-router v4history,目的爲了瞭解 react-router 實現原理。html

本文首發於 daweilv.comreact

1、開始以前

在開始閱讀本文以前,但願你至少使用過一次 react-router,知道 react-router 的基本使用方法。git

2、已實現的功能

  • 根據當前頁面的 location.pathname,渲染對應 Route 中的 component
  • 點擊 Link,頁面無刷新,pathname 更新,渲染對應 Route 中的 component
  • 瀏覽器後退/前進,頁面無刷新,渲染對應 Route 中的 component

3、Github 地址與在線預覽

4、原理分析

1. Route 的實現

先來看一段代碼,咱們須要實現的邏輯是:當 location.pathname = '/' 時,頁面渲染 Index 組件,當 location.path = '/about/' 時,頁面渲染 About 組件。github

import React from 'react';

import { BrowserRouter as Router, Route, Link } from "./react-router-dom";

function Index(props) {
  console.log('Index props', props);
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Users() {
  return <h2>Users</h2>;
}

function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about/">About</Link>
            </li>
            <li>
              <Link to="/users/">Users</Link>
            </li>
          </ul>
        </nav>

        <Route path="/" exact component={Index} />
        <Route path="/about/" component={About} />
        <Route path="/users/" component={Users} />
      </div>
    </Router>
  );
}

export default App;
複製代碼

其實,Route 組件內部的核心邏輯就是判斷當前 pathname 是否與自身 props 上的 path 相等,若是相等,則渲染自身 props 上的 component,不等的時候不渲染,返回 null。瀏覽器

好,來看下 Route 的實現:react-router

import React from 'react';
import { RouterContext } from './BrowserRouter';

export default class Route extends React.Component {
    render() {
        const { path, component } = this.props;
        if (this.context.location.pathname !== path) return null;
        return React.createElement(component, { ...this.context })
    }
}

Route.contextType = RouterContext
複製代碼

Route 主要就是一個 render() 函數,內部經過 context 得到當前 pathname。那麼這個 context 是哪來的呢?dom

2. BrowserRouter 的實現

import React from 'react';
import { createBrowserHistory } from '../history';

const history = createBrowserHistory()

export const RouterContext = React.createContext(history)

export default class BrowserRouter extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            location: {
                pathname: window.location.pathname
            }
        }
    }

    render() {
        const { location } = this.state;
        return (
            <RouterContext.Provider value={{ history, location }}> {this.props.children} </RouterContext.Provider> ) } }; 複製代碼

這裏僅貼出了首次渲染的邏輯代碼。BrowserRouter 在 constructor 中根據 window.location 初始化 location,而後將 location 傳入 RouterContext.Provider 組件,子組件 Route 接收到含有 location 的 context,根據 1. Route 的實現 完成首次渲染。ide

注意到傳入 RouterContext.Provider 組件的對象不光有 location,還有 history 對象。這個 history 是作什麼用的呢?實際上是暴露 history.push 和 history.listen 方法,提供給外部作跳轉和監聽跳轉事件使用的。Link 組件的實現也是用到了 history,咱們接着往下看。函數

3. Link 的實現

import React from 'react';
import { RouterContext } from './BrowserRouter';

export default class Link extends React.Component {
    constructor(props) {
        super(props)
        this.clickHandler = this.clickHandler.bind(this)
    }

    clickHandler(e) {
        console.log('click', this.props.to);
        e.preventDefault()
        this.context.history.push(this.props.to)
    }

    render() {
        const { to, children } = this.props;
        return <a href={to} onClick={this.clickHandler}>{children}</a>
    }
}

Link.contextType = RouterContext
複製代碼

Link 組件其實就是一個 a 標籤,與普通 a 標籤不一樣,點擊 Link 組件並不會刷新整個頁面。組件內部把 a 標籤的默認行爲 preventDefault 了,Link 組件從 context 上拿到 history,將須要跳轉的動做告訴 history,即 history.push(to)ui

以下面代碼所示,BrowserRouter 在 componentDidMount 中,經過 history.listen 監聽 location 的變化。當 location 變化的時候,setState 一個新的 location 對象,觸發 render,進而觸發子組件 Route 的從新渲染,渲染出對應 Route。

// BrowserRouter
componentDidMount() {
    history.listen((pathname) => {
        console.log('history change', pathname);
        this.setState({ location: { pathname } })
    })
}
複製代碼

4. history 的實現

history 的內部實現是怎麼樣的呢?請看下面的代碼:

let globalHistory = window.history;

export default function createBrowserHistory() {
    let listeners = []

    const push = function (pathname) {
        globalHistory.pushState({}, '', pathname)
        notifyListeners(pathname)
    }

    const listen = function (listener) {
        listeners.push(listener)
    }

    const notifyListeners = (...args) => {
        listeners.forEach(listener => listener(...args))
    }

    window.onpopstate = function () {
        notifyListeners(window.location.pathname)
    }

    return {
        listeners,
        listen,
        push
    }
};
複製代碼

history 經過 listen 方法收集外部的監聽事件。當外部調用 history.push 方法時,使用 window.history.pushState 修改當前 location,執行 notifyListeners 方法,依次回調全部的監聽事件。注:這裏爲了讓代碼更加容易理解,簡化了 listener this 上下文的處理。

另外,history 內部增長了 window.onpopstate 用來監聽瀏覽器的前進後退事件,執行 notifyListeners 方法。

5、總結

咱們使用了 100 多行代碼,實現了 react-router 的基本功能,對 react-router 有了更深刻的認識。想更加深刻的瞭解 react-router,建議看一下 react-router 的源碼,快速走讀一遍,再對比下本文的實現細節,相信你會有一個更清晰的理解。

以爲本文幫助到你的話,請給個人 build-your-own-react-router 項目點個⭐️吧!

相關文章
相關標籤/搜索