上一篇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)等都是不可调用执行的。