对 react-router
的分析,目前准备主要集中在三点:
a. history
的分析。
b. history
与 react-router
的联系。
c. react-router
内部匹配及显示原理。
这篇文章准备着重理解 history
.
推荐:★★★☆
索引
引子
一段显而易见出现在各大 react v16+ 项目中的代码是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import React, {Component} from 'react'
import { render } from 'react-dom'
import { Router, Route } from 'react-router'
import { createBrowserHistory } from 'history'
const history = createBrowserHistory()
const App = () => (
<Router history={history}>
<div id="app">
{/* something */}
</div>
</Router>
)
render(<App/>, document.body.querySelector('#app'))在
react
v16+ 版本里,通常react-router
也升级到了 4 以上。而
react-router
v4+ 通常是配合history
v4.6+ 使用的。下面就先从
history
开始,让我们一步一步走近react-router
的神秘世界。
history核心
- history源码
history
在内部主要导出了三个方法:createBrowserHistory
,createHashHistory
,createMemoryHistory
.它们分别有着自己的作用:
createBrowserHistory
是为现代主流且支持 HTML5 history 浏览器提供的 API.createHashHistory
是为不支持history
功能的浏览器提供的 API.createMemoryHistory
则是为没有 DOM 环境例如node
或React-Native
或测试提供的 API.
我们就先从最接地气的
createBrowserHistory
也就是我们上文中使用的方法开始看起。
走进createBrowserHistory
话不多说,直接走进 createBrowserHistory源码
1
2
3
4/**
* Creates a history object that uses the HTML5 history API including
* pushState, replaceState, and the popstate event.
*/在该方法的注释里,它说明了是它基于 H5 的
history
创建的对象,对象内包括了一些常用的方法譬如pushState
,replaceState
,popstate
等等。
history
对象
那么它具体返回了什么内容呢,下面就是它目前所有的方法和属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14const globalHistory = window.history;
const history = {
length: globalHistory.length, // (number) The number of entries in the history stack
action: "POP", // (string) The current action (`PUSH`, `REPLACE`, or `POP`)
location: initialLocation, // (object) The current location. May have the following properties.
createHref,
push, // (function) Pushes a new entry onto the history stack
replace, // (function) Replaces the current entry on the history stack
go, // (function) Moves the pointer in the history stack by `n` entries
goBack, // (function) Equivalent to `go(-1)`
goForward, // (function) Equivalent to `go(1)`
block, // (function) Prevents navigation
listen
}globalHistory.length
显而易见是当前存的历史栈的数量。createHref
根据根路径创建新路径,在根路径上添加原地址所带的search
,pathname
,path
参数, 推测作用是将路径简化。location
当前的location
, 可能含有以下几个属性。path
- (string) 当前url
的路径path
.search
- (string) 当前url
的查询参数query string
.hash
- (string) 当前url
的哈希值hash
.state
- - (object) 存储栈的内容。仅存在浏览器历史和内存历史中。
block
阻止浏览器的默认导航。用于在用户离开页面前弹窗提示用户相应内容。the history docs其中,
go
/goBack
/goForward
是对原生history.go
的简单封装。剩下的方法相对复杂些,因此在介绍
push
,replace
等方法之前,先来了解下transitionManager
. 因为下面的很多实现,都用到了这个对象所提供的方法。
transitionManager
方法介绍
首先看下该对象返回了哪些方法:
1
2
3
4
5
6const transitionManager = {
setPrompt,
confirmTransitionTo,
appendListener,
notifyListeners
}在后续
popstate
相关的方法中,它就应用了appendListener
和与之有关的notifyListeners
方法,我们就先从这些方法看起。它们的设计体现了常见的订阅-发布模式,前者负责实现订阅事件逻辑,后者负责最终发布逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26let listeners = [];
/**
* [description 订阅事件]
* @param {Function} fn [description]
* @return {Function} [description]
*/
const appendListener = fn => {
let isActive = true;
// 订阅事件,做了函数柯里化处理,它实际上相当于运行了 `fn.apply(this, ...args)`
const listener = (...args) => {
if (isActive) fn(...args);
};
// 将监听函数一一保存
listeners.push(listener);
return () => {
isActive = false;
listeners = listeners.filter(item => item !== listener);
};
};
/**
* [发布逻辑]
* @param {[type]} ..args [description]
*/
const notifyListeners = (..args) => {
listeners.forEach(listener => listener(..args))
}介绍了上面两个方法的定义,先别急。后续再介绍它们的具体应用。
然后来看看另一个使用的较多的方法
confirmTransitionTo
.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24const confirmTransitionTo = (
location,
action,
getUserConfirmation,
callback
) => {
if (prompt != null) {
const result =
typeof prompt === "function" ? prompt(location, action) : prompt;
if (typeof result === "string") {
if (typeof getUserConfirmation === "function") {
getUserConfirmation(result, callback);
} else {
callback(true);
}
} else {
// Return false from a transition hook to cancel the transition.
// 如果已经在执行,则暂时停止执行
callback(result !== false);
}
} else {
callback(true);
}
};实际上执行的就是从外部传进来的
callback
方法,只是多了几层判断来做校验,而且传入了布尔值来控制是否需要真的执行回调函数。
transitionManager
调用
再然后我们来看看上述方法
appendListener
,notifyListeners
的具体应用。前者体现在了popstate
事件的订阅中。那么就先简单谈谈
popstate
事件。- 当做出浏览器动作时,会触发
popstate
事件, 例如点击浏览器的回退。也就是说,popstate
本身并不是像pushState
或replaceState
一样是history
的方法。 - 不能使用
history.popState
这样的方式来调用。 - 而且,直接调用
history.pushState
或history.replaceState
不会触发popstate
事件。
- 当做出浏览器动作时,会触发
在事件监听方法
listen
中涉及了popstate
的使用,在源码中可以看到以下两个方法listen
和checkDOMListeners
.它们就是上述订阅事件的具体调用方。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// 首先自然是初始化
const transitionManager = createTransitionManager();
const PopStateEvent = "popstate";
const HashChangeEvent = "hashchange";
// 当 URL 的片段标识符更改时,将触发 hashchange 事件(跟在 # 后面的部分,包括 # 符号)
// https://developer.mozilla.org/zh-CN/docs/Web/Events/hashchange
// https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onhashchange
const checkDOMListeners = delta => {
listenerCount += delta;
if (listenerCount === 1) {
// 其实也是最常见最简单的订阅事件, handlePopState 对应的内容在下文有说明
window.addEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
window.addEventListener(HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
window.removeEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
window.removeEventListener(HashChangeEvent, handleHashChange);
}
};
/**
* [订阅事件的具体调用方]
* @param {Function} listener [description]
* @return {Function} [description]
*/
const listen = listener => {
// 返回一个解绑函数
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
// 返回的函数负责取消
return () => {
checkDOMListeners(-1);
unlisten();
};
};简言之,调用
listen
就是给window
绑定了相应方法,再次调用之前listen
返回的函数则是取消。然后来看看发布事件的具体调用方
setState
。它在createBrowserHistory.js
中定义,在popstate
、push
与replace
中均有调用。1
2
3
4
5
6
7
8
9
10/**
* 在该方法中发布
* @param {*} nextState [入参合并到 history]
*/
const setState = nextState => {
Object.assign(history, nextState);
history.length = globalHistory.length;
// 执行所有的监听函数
transitionManager.notifyListeners(history.location, history.action);
};以上是
setState
的定义。我们来看看它在popstate
中的使用。- 上文有许多代码,以此关键代码为例:
window.addEventListener(PopStateEvent, handlePopState);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const handlePopState = (event) => {
handlePop(getDOMLocation(event.state))
}
let forceNextPop = false
const handlePop = (location) => {
if (forceNextPop) {
forceNextPop = false
setState()
} else {
const action = 'POP'
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({ action, location })
} else {
revertPop(location)
}
})
}
}浏览器注册了
popstate
事件,对应的handlePopState
的方法则最终调用了setState
方法。翻译成白话就是浏览器回退操作的时候,会触发
setState
方法。它将在下文以及后一篇博文里起到重要作用。
下面的方法则应用了 confirmTransitionTo
.
push
,replace
这两个上文提到的重要方法,是原生方法的扩展。它们都用到了上述分析过的方法,都负责实现跳转,因此内部有较多逻辑相同。后面会以
push
为例, 它其实就是对原生的history.pushState
的强化。那么这里就先从原生的
history.pushState
开始熟悉了解。history.pushState
接收三个参数,第一个为状态对象,第二个为标题,第三个为 Url.- 状态对象:一个可序列化的对象,且序列化后小于 640k. 否则该方法会抛出异常。(暂时不知这对象可以拿来做什么用,或许
react-router
用来标识页面的变化,以此渲染组件) - 标题(目前被忽略):给页面添加标题。目前使用空字符串作为参数是安全的,未来则是不安全的。Firefox 目前还未实现它。
- **URL(可选)**:新的历史 URL 记录。直接调用并不会加载它,但在其他情况下,重新打开浏览器或者刷新时会加载新页面。
- 一个常见的调用是
history.pushState({ foo: 'bar'}, 'page1', 'bar.html')
. - 调用后浏览器的 url 会立即更新,但页面并不会重新加载。例如 www.google.com 变更为 www.google.com/bar.html. 但页面不会刷新。
- 注意,此时并不会调用
popstate
事件。只有在上述操作后,访问了其他页面,然后点击返回,或者调用history.go(-1)/history.back()
时,popstate
会被触发。 - 让我们在代码中更直观的看吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 定义一个 popstate 事件
window.onpopstate = function(event) {
console.info(event.state)
}
const page1 = { page: 'page1' }
const page2 = { page: 'page2' }
history.pushState(page1, 'page1', 'page1.html')
// 页面地址由 www.google.com => www.google.com/page1.html
// 但不会刷新或重新渲染
history.pushState(page2, 'page2', 'page2.html')
// 页面地址由 www.google.com/page2.html => www.google.com/page2.html
// 但不会刷新或重新渲染
// 此时执行
history.back() // history.go(-1)
// 会触发 popstate 事件, 打印出 page1 对象
// { page: 'page1' }- 状态对象:一个可序列化的对象,且序列化后小于 640k. 否则该方法会抛出异常。(暂时不知这对象可以拿来做什么用,或许
介绍完
pushState
后,看看history
中是怎样实现它的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29const push = (path, state) => {
const action = "PUSH";
const location = createLocation(path, state, createKey(), history.location);
// 过渡方法的应用
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
// 布尔值,用于判断是否需要执行
if (!ok) return;
const href = createHref(location);
const { key, state } = location;
// 在支持 history 的地方则使用 history.pushState 方法实现
if (canUseHistory) {
globalHistory.pushState({ key, state }, null, href);
if (forceRefresh) {
window.location.href = href
} else {
// 如果是非强制刷新时,会更新状态,后续在 react-router 中起到重要作用
// 上文提到过的发布事件调用处
setState({ action, location })
}
} else {
window.location.href = href;
}
}
);
};关键代码:**
globalHistory.pushState({ key, state }, null, href);
** 和上文分析的一致。pushState
和push
方法讲完,replaceState
和replace
也就很好理解了。replaceState
只是把推进栈的方式改为替换栈的行为。它接收的参数与pushState
完全相同。只是方法调用后执行的效果不同。补:本来如果仅仅是介绍当前的
history
. 我之前以为找到pushState
这个核心就已经足够了。但当我继续深入,探究react-router
原理的时候,才发现这里遗漏了重要的一点。那就是setState
方法。那么这个方法具体做了什么呢。在上文中已经做了简单介绍,这里再重申一遍:就是将当前
state
存入history
, 同时发布事件,也就是调用之前订阅时的保存的所有方法。参数则是[history.location, history.action]
. 或许现在,我们可能对该方法的重要性没有那么深的理解,当你再结合后一篇分析react-router
的文章,就知道它起的作用了。
history在react-router中
- 这篇文章快完成的时候,我才发现
react-router
仓库里是有history
的介绍的。此时我一脸茫然。这里面内容虽然不多,却非常值得参考。这里做部分翻译和理解,当作对上文的补充。 - 原地址
history is mutable
在原文档中,说明了
history
对象是可变的。因此建议在react-router
中获取location
时可以使用Route
的props
的方式来替代history.location
的方式。这样的方式会确保你的流程处于React
的生命周期中。例如:1
2
3
4
5
6
7
8
9
10
11class Comp extends React.Component {
componentWillReceiveProps(nextProps) {
// 正确的打开方式
const locationChanged = nextProps.location !== this.props.location
// 错误的打开方式,因为 history 是可变的,所以这里总是不等的 // will *always* be false because history is mutable.
const locationChanged = nextProps.history.location !== this.props.history.location
}
}
<Route component={Comp}/>更多内容请查看the history documentation.
小结
- 一句话形容
history
这个库。它是一个对 HTML5 原生history
的拓展,它对外输出三个方法,用以在支持原生 api 的环境和不兼容的环境,还有 node 环境中调用。而该方法返回的就是一个增强的history
api. - 写这篇文章的时候,第一次有感受到技术栈拓展的无穷魅力。从最初试图分析
react-router
,到发现它依赖的主要的库history
. 再进行细化,到history
主要提供的对象方法。里面涉及的发布订阅设计模式、思路、以及具体的实现使用了柯里化方式。一步一步探究下去可以发现很多有趣的地方。似乎又唤起往日的技术热情。 - 下一篇文章将会继续介绍
react-router
.
占位坑
- 下面两个方法返回的内容和
createBrowserHistory
基本一致,只是具体的实现有部分差别。有时间补上。 createHashHistory
createMemoryHistory
参考
react-router 的实现原理
react-router 源代码学习笔记
Javascript设计模式之发布-订阅模式
评论