对 react-router
的分析,目前准备主要集中在三点:
a. `history` 的分析。
b. `history` 与 `react-router` 的联系。
c. `react-router` 内部匹配及显示原理。
这篇文章准备着重理解 history
.
推荐:★★★☆
索引
引子
-
一段显而易见出现在各大 react v16+ 项目中的代码是这样的:
import 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源码
/** * Creates a history object that uses the HTML5 history API including * pushState, replaceState, and the popstate event. */
-
在该方法的注释里,它说明了是它基于 H5 的
history
创建的对象,对象内包括了一些常用的方法譬如pushState
,replaceState
,popstate
等等。
history
对象
-
那么它具体返回了什么内容呢,下面就是它目前所有的方法和属性:
const 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
方法介绍
-
首先看下该对象返回了哪些方法:
const transitionManager = { setPrompt, confirmTransitionTo, appendListener, notifyListeners }
-
在后续
popstate
相关的方法中,它就应用了appendListener
和与之有关的notifyListeners
方法,我们就先从这些方法看起。 -
它们的设计体现了常见的订阅-发布模式,前者负责实现订阅事件逻辑,后者负责最终发布逻辑。
let 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
.const 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
. -
它们就是上述订阅事件的具体调用方。
// 首先自然是初始化 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
中均有调用。/** * 在该方法中发布 * @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);
const 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
会被触发。 - 让我们在代码中更直观的看吧。
// 定义一个 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
中是怎样实现它的。const 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
的生命周期中。例如:class 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