实现一个 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 项目点个⭐️吧!

相关文章
相关标签/搜索