react-router原理之幕后history

3,415 阅读3分钟

上一篇react-router原理之Link跳转中提到了Link在onClick的处理函数中会调用history的push(或replace)方法。接下来我们就以push方法为例来看一下history具体都做了些什么。Link中的history是通过context传入进来的,需要向外层进行查找,继续以官网为例,最外层是BrowserRouter。

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

const BasicExample = () => (
  <Router>
    <div>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        ...
      </ul>
      <Route exact path="/" component={Home} />
      ...
    </div>
  </Router>
);

打开BrowserRouter文件,可以看到声明了实例属性history对象,history对象的创建来自history包的createBrowserHistory方法。

import { createBrowserHistory as createHistory } from "history";

class BrowserRouter extends React.Component {
  
  history = createHistory(this.props);

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

createBrowserHistory(存储在modules/createBrowserHistory.js)最后返回一个history对象,history对象上拥有许多的属性和方法,其中就有push、replace、listen等。

关于push方法核心代码就两行

globalHistory.pushState({ key, state }, null, href);
setState({ action, location });

globalHistory对应的浏览器环境中的window.history对象,虽然window可以监听popstate事件,但是执行pushState或者replaceState是不会触发该事件的,只有点击浏览器的前进后退按钮时才会触发,因此调用pushState方法只是改变了地址栏的url,其他的没有任何变化。

为了达到url变化即重新渲染页面的目的,就需要用到setState方法了(这里的setState方法只是一个普通的函数)

setState方法中最关键的就是下面这一行代码,执行notifyListeners方法遍历listeners数组中的每个listener并调用执行。

transitionManager.notifyListeners(history.location, history.action);

// notifyListeners方法定义
let listeners = [];
const notifyListeners = (...args) => {
    listeners.forEach(listener => listener(...args));
  };

如果把重新渲染页面的逻辑加入到listeners数组中,那么当点击Link的时候就可以实现页面更新的目的了。接下来就需要回到history生成的地方也就是BrowserHistory去找一找添加listener的逻辑,BrowserRouter在创建好history对象之后,通过props的形式把history传递给了Router。

Router针对history做了两件事

  • 添加到context上,使得Link通过context即可获得history对象
  • 在componentWillMount中调用history.listen方法增加对url变更的监听,当url变化的时候调用setState触发Router的重新渲染
componentWillMount() {
    const { children, history } = this.props;
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }

Router组件是Route的父组件,所以当Router重新render的时候,那么Route自然也可以触发render,这样就可以响应最新的url状态了。

history包与html5 history的关系

html5也提供了history方法,为什么react-router要用history包呢?

虽然history包的createBrowserHistory其实底层依赖的就是html5的history,不过history除了支持createBrowserHistory之外,还提供createHashHistory和createMemoryHistory,这三种方式底层依赖的基础技术各不相同,但是对外暴露的接口都是一致的。这其实就是history包的意义所在

history包对环境的差异进行抽象,提供统一的一致性接口,轻松实现了会话的管理

StaticRouter与BrowserRouter的区别

react-router是支持服务器端渲染的,由于在服务器环境中不存在html5的history对象,因此无法使用history包,所以也不能使用BrowserRouter。

针对服务器端环境,react-router提供了StaticRouter,StaticRouter与BrowserRouter最大的区别就体现在创建的history对象上面,两者的history对象拥有几乎完全一致的属性方法。由于服务器环境没有history,因此也不会有history的改变,因此StaticRouter的history的方法(push、replace、go)等都是不可调用执行的。