React Router 的原理是什么?

1,128 阅读3分钟

改变地址栏 url 到页面更新,react router 做了什么?

React Router 实现原理是什么?

React Router 用到了 History 的哪些 API?

......

面对这些问题,脑袋里面都是 Link、Route、BrowserRouter 等等的用法那当然是不行的~

以下又是一篇学习笔记。

React Router 的基础 —— history 库

history 是一个第三方库,用来兼容在不同浏览器、不同环境下对历史记录的管理,封装了浏览器内置的 History API 和 Location API。

history 针对不同的环境提供了三个API 来创建 history 对象:

createBrowserHistory:

  • 特点:适用高版本浏览器。
  • 技术:利用HTML5里面的history。
  • 前进:使用 pushStatereplaceState 前进。
  • 后退:监听popstate 实现后退。

createHashHistory:

  • 特点:使用老版本浏览器。
  • 技术:通过 hash 来存储在不同状态下的 history 信息。
  • 前进:使用location.hash=*** location.replace() 实现前进。
  • 后退:监听 hashchange 实现后退。

createMemoryHistory:

  • 特点:适用于 node 环境下
  • 技术:在内存中进行历史记录的存储。
  • 前进:在内存中进行历史记录的存储。
  • 后退:因为是在内存中操作,跟浏览器没有关系,不涉及UI层面的事情,所以可以直接进行历史信息的回退

另外 history 库中的 locationwidnows.location API的一部分,用来存储页面 url 各种信息的,比如 GET 请求中的 search, url hash,页面的位置 pathname,以及用于存储额外数据的额 state 等。比如:

{
  pathname: '/here',
  search: '?key=value',
  hash: '#extra-information',
  state: { modal: true },
  key: '3wgzwe'
}

react-router-dom 的核心组件

react-router-dom 的核心就是 react-router,但多了四个组件:BrowserRouterHashRouterLinkNavLink

BrowserRouter

BrowserRouter 组件的实现很简单,就是使用 history 库中的 createBrowserHistory 接口创建了一个 history 然后传递给了 Router 组件。

import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";

/**
 * The public API for a <Router> that uses HTML5 history.
 */
class [BrowserRouter](https://github.com/ReactTraining/react-router/blob/42933fe141819e4662113ab2c320bf86be3490fb/packages/react-router-dom/modules/BrowserRouter.js#L10) extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

Router

Router 组件中,调用 history 的 listen API 监听了 location 的变化。一旦 location 发生变化,就修改 Router 中的 location。

constructor(props) {
    super(props);

    this.state = {
      location: props.history.location
    };

    // This is a bit of a hack. We have to start listening for location
    // changes here in the constructor in case there are any <Redirect>s
    // on the initial render. If there are, they will replace/push when
    // they mount and since cDM fires in children before parents, we may
    // get a new location before the <Router> is mounted.
    this._isMounted = false;
    this._pendingLocation = null;

    if (!props.staticContext) {
      this.unlisten = props.history.listen(location => {
        if (this._isMounted) {
          this.setState({ location });
        } else {
          this._pendingLocation = location;
        }
      });
    }
  }

Route

Route 组件中对 path 属性做了校验,其中 matchPath 函数将 path props 跟当前 url 进行了匹配,如果满足就返回一个 match 对象,不匹配就返回 null。

// 将 path 属性和当前 URL 比对
const location = this.props.location || context.location;
const match = this.props.computedMatch
  ? this.props.computedMatch // <Switch> already computed the match for us
  : this.props.path
  ? matchPath(location.pathname, this.props)
  : context.match;

const props = { ...context, location, match };

let { children, component, render } = this.props;

// Preact uses an empty array as children by
// default, so use null if that's the case.
if (Array.isArray(children) && children.length === 0) {
  children = null;
}

// 如果 path 匹配成功,就渲染对应的组件(包括 class 组件,function 组件以及 HTML 内容)
return (
  <RouterContext.Provider value={props}>
    {props.match
      ? children
        ? typeof children === "function"
          ? __DEV__
            ? evalChildrenDev(children, props, this.props.path)
            : children(props)
          : children
        : component
        ? React.createElement(component, props)
        : render
        ? render(props)
        : null
      : typeof children === "function"
      ? __DEV__
        ? evalChildrenDev(children, props, this.props.path)
        : children(props)
      : null}
  </RouterContext.Provider>
);

Link

Link 组件内部定义了一个 LinkAnchor 组件,用于生成一个 <a> 标签并封装了 onClick 事件。

let props = {
  ...rest,
  onClick: event => {
    try {
      if (onClick) onClick(event);
    } catch (ex) {
      event.preventDefault();
      throw ex;
    }

    if (
      !event.defaultPrevented && // onClick prevented default
      event.button === 0 && // ignore everything but left clicks
      (!target || target === "_self") && // let browser handle "target=_blank" etc.
      !isModifiedEvent(event) // ignore clicks with modifier keys
    ) {
      event.preventDefault();
      navigate();
    }
  }
};

再看看 Link 部分的逻辑。其中主要有以下几步:

  1. 解析 Link 的 to 属性到对应的 location。 resolveToLocation 处理了 to 参数是一个函数的情况。如果 to 参数是一个字符串,就调用 history 的 createLocation API 创建一个 Location 对象。
  2. 根据 location 生成跳转链接
  3. 根据 Link 的 repalce 属性判断是调用 history.replace() 还是 history.push()

const { history } = context;

// 解析 to 参数(可能是 function 也可能是 string)
const location = normalizeToLocation(
  resolveToLocation(to, context.location),
  context.location
);

// 根据 location 生成跳转链接
const href = location ? history.createHref(location) : "";
const props = {
  ...rest,
  href,
  navigate() {
    const location = resolveToLocation(to, context.location);
		// 根据replace 属性判断是使用哪个 API
    const method = replace ? history.replace : history.push;

    method(location);
  }
};

// React 15 compat
if (forwardRefShim !== forwardRef) {
  props.ref = forwardedRef || innerRef;
} else {
  props.innerRef = innerRef;
}

// component 是 LinkAnchor
return React.createElement(component, props);
        

React Router 的原理

综上,分析完核心的组件之后,大概了解了 React Router 的原理如下图所示:

React Router 原理.png

参考阅读

React-router-dom | 原理解析

ReactTraining/react-router