history
是一個JavaScript庫,可以讓你在JavaScript運行的任何地方輕鬆管理會話歷史記錄javascript
history
是由Facebook維護的,react-router
依賴於history
,區別於瀏覽器的window.history
,history
是包含window.history
的,讓開發者能夠在任何環境都能使用history
的api(例如Node
、React Native
等)。html
本篇讀後感分爲五部分,分別爲前言、使用、解析、demo、總結,五部分互不相連可根據須要分開看。java
前言爲介紹、使用爲庫的使用、解析爲源碼的解析、demo是抽取源碼的核心實現的小demo,總結爲吹水,學以至用。react
建議跟着源碼結合本文閱讀,這樣更加容易理解!git
history
有三種不一樣的方法建立history對象,取決於你的代碼環境:github
createBrowserHistory
:支持HTML5 history api
的現代瀏覽器(例如:/index
);createHashHistory
:傳統瀏覽器(例如:/#/index
);createMemoryHistory
:沒有Dom的環境(例如:Node
、React Native
)。注意:本片文章只解析
createBrowserHistory
,其實三種構造原理都是差很少的api
<!DOCTYPE html>
<html>
<head>
<script src="./umd/history.js"></script>
<script> var createHistory = History.createBrowserHistory // var createHistory = History.createHashHistory var page = 0 // createHistory建立所須要的history對象 var h = createHistory() // h.block觸發在地址欄改變以前,用於告知用戶地址欄即將改變 h.block(function (location, action) { return 'Are you sure you want to go to ' + location.path + '?' }) // h.listen監聽當前地址欄的改變 h.listen(function (location) { console.log(location, 'lis-1') }) </script>
</head>
<body>
<p>Use the two buttons below to test normal transitions.</p>
<p>
<!-- h.push用於跳轉 -->
<button onclick="page++; h.push('/' + page, { page: page })">history.push</button>
<!-- <button onclick="page++; h.push('/#/' + page)">history.push</button> -->
<button onclick="h.goBack()">history.goBack</button>
</p>
</body>
</html>
複製代碼
block
用於地址改變以前的截取,listener
用於監聽地址欄的改變,push
、replace
、go(n)
等用於跳轉,用法簡單明瞭數組
貼出來的源碼我會刪減對理解原理不重要的部分!!!若是想看完整的請下載源碼看哈瀏覽器
從history的源碼庫目錄能夠看到modules文件夾,包含了幾個文件:react-router
入口文件index.js
export { default as createBrowserHistory } from "./createBrowserHistory";
export { default as createHashHistory } from "./createHashHistory";
export { default as createMemoryHistory } from "./createMemoryHistory";
export { createLocation, locationsAreEqual } from "./LocationUtils";
export { parsePath, createPath } from "./PathUtils";
複製代碼
把全部須要暴露的方法根據文件名區分開,咱們先看history
的構造函數createBrowserHistory
。
// createBrowserHistory.js
function createBrowserHistory(props = {}){
// 瀏覽器的history
const globalHistory = window.history;
// 初始化location
const initialLocation = getDOMLocation(window.history.state);
// 建立地址
function createHref(location) {
return basename + createPath(location);
}
...
const history = {
// window.history屬性長度
length: globalHistory.length,
// history 當前行爲(包含PUSH-進入、POP-彈出、REPLACE-替換)
action: "POP",
// location對象(與地址有關)
location: initialLocation,
// 當前地址(包含pathname)
createHref,
// 跳轉的方法
push,
replace,
go,
goBack,
goForward,
// 截取
block,
// 監聽
listen
};
return history;
}
export default createBrowserHistory;
複製代碼
不管是從代碼仍是從用法上咱們也能夠看出,執行了createBrowserHistory
後函數會返回history
對象,history
對象提供了不少屬性和方法,最大的疑問應該是initialLocation
函數,即history.location
。咱們的解析順序以下:
location屬性存儲了與地址欄有關的信息,咱們對比下createBrowserHistory
的返回值history.location
和window.location
// history.location
history.location = {
hash: ""
pathname: "/history/index.html"
search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
state: undefined
}
// window.location
window.location = {
hash: ""
host: "localhost:63342"
hostname: "localhost"
href: "http://localhost:63342/history/index.html?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
origin: "http://localhost:63342"
pathname: "/history/index.html"
port: "63342"
protocol: "http:"
reload: ƒ reload()
replace: ƒ ()
search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
}
複製代碼
結論是history.location是window.location的兒砸!咱們來研究研究做者是怎麼處理的。
const initialLocation = getDOMLocation(window.history.state)
複製代碼
initialLocation
函數等於getDOMLocation
函數的返回值(getDOMLocation
在history
中會常常調用,理解好這個函數比較重要)。
// createBrowserHistory.js
function createBrowserHistory(props = {}){
// 處理basename(相對地址,例如:首頁爲index,假如設置了basename爲/the/base,那麼首頁爲/the/base/index)
const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : "";
const initialLocation = getDOMLocation(window.history.state);
// 處理state參數和window.location
function getDOMLocation(historyState) {
const { key, state } = historyState || {};
const { pathname, search, hash } = window.location;
let path = pathname + search + hash;
// 保證path是不包含basename的
if (basename) path = stripBasename(path, basename);
// 建立history.location對象
return createLocation(path, state, key);
};
const history = {
// location對象(與地址有關)
location: initialLocation,
...
};
return history;
}
複製代碼
通常大型的項目中都會把一個功能拆分紅至少兩個函數,一個專門處理參數的函數和一個接收處理參數實現功能的函數:
getDOMLocation
函數主要處理state
和window.location
這兩參數,返回自定義的history.location
對象,主要構造history.location
對象是createLocation
函數;createLocation
實現具體構造location
的邏輯。接下來咱們看在LocationUtils.js
文件中的createLocation
函數
// LocationUtils.js
import { parsePath } from "./PathUtils";
export function createLocation(path, state, key, currentLocation) {
let location;
if (typeof path === "string") {
// 兩個參數 例如: push(path, state)
// parsePath函數用於拆解地址 例如:parsePath('www.aa.com/aa?b=bb') => {pathname: 'www.aa.com/aa', search: '?b=bb', hash: ''}
location = parsePath(path);
location.state = state;
} else {
// 一個參數 例如: push(location)
location = { ...path };
location.state = state;
}
if (key) location.key = key;
// location = {
// hash: ""
// pathname: "/history/index.html"
// search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
// state: undefined
// }
return location;
}
// PathUtils.js
export function parsePath(path) {
let pathname = path || "/";
let search = "";
let hash = "";
const hashIndex = pathname.indexOf("#");
if (hashIndex !== -1) {
hash = pathname.substr(hashIndex);
pathname = pathname.substr(0, hashIndex);
}
const searchIndex = pathname.indexOf("?");
if (searchIndex !== -1) {
search = pathname.substr(searchIndex);
pathname = pathname.substr(0, searchIndex);
}
return {
pathname,
search: search === "?" ? "" : search,
hash: hash === "#" ? "" : hash
};
}
複製代碼
createLocation
根據傳遞進來的path
或者location
值,返回格式化好的location
,代碼簡單。
createHref
函數的做用是返回當前路徑名,例如地址http://localhost:63342/history/index.html?a=1
,調用h.createHref(location)
後返回/history/index.html?a=1
// createBrowserHistory.js
import {createPath} from "./PathUtils";
function createBrowserHistory(props = {}){
// 處理basename(相對地址,例如:首頁爲index,假如設置了basename爲/the/base,那麼首頁爲/the/base/index)
const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : "";
function createHref(location) {
return basename + createPath(location);
}
const history = {
// 當前地址(包含pathname)
createHref,
...
};
return history;
}
// PathUtils.js
function createPath(location) {
const { pathname, search, hash } = location;
let path = pathname || "/";
if (search && search !== "?") path += search.charAt(0) === "?" ? search : `?${search}`;
if (hash && hash !== "#") path += hash.charAt(0) === "#" ? hash : `#${hash}`;
return path;
}
複製代碼
在這裏咱們能夠想象下大概的 監聽 流程:
在第二章使用代碼中,建立了History
對象後使用了h.listen
函數。
// index.html
h.listen(function (location) {
console.log(location, 'lis-1')
})
h.listen(function (location) {
console.log(location, 'lis-2')
})
複製代碼
可見listen
能夠綁定多個監聽函數,咱們先看做者的createTransitionManager.js
是如何實現綁定多個監聽函數的。
createTransitionManager
是過渡管理(例如:處理block函數中的彈框、處理listener的隊列)。代碼風格跟createBrowserHistory幾乎一致,暴露全局函數,調用後返回對象便可使用。
// createTransitionManager.js
function createTransitionManager() {
let listeners = [];
// 設置監聽函數
function appendListener(fn) {
let isActive = true;
function listener(...args) {
// good
if (isActive) fn(...args);
}
listeners.push(listener);
// 解除
return () => {
isActive = false;
listeners = listeners.filter(item => item !== listener);
};
}
// 執行監聽函數
function notifyListeners(...args) {
listeners.forEach(listener => listener(...args));
}
return {
appendListener,
notifyListeners
};
}
複製代碼
appendListener
:fn
就是用戶設置的監聽函數,把全部的監聽函數存儲在listeners
數組中;notifyListeners
:執行的時候僅僅須要循環依次執行便可。這裏感受有值得借鑑的地方:添加隊列函數時,增長狀態管理(如上面代碼的isActive
),決定是否啓用。
有了上面的理解,下面看listen
源碼。
// createBrowserHistory.js
import createTransitionManager from "./createTransitionManager";
const transitionManager = createTransitionManager();
function createBrowserHistory(props = {}){
function listen(listener) {
// 添加 監聽函數 到 隊列
const unlisten = transitionManager.appendListener(listener);
// 添加 歷史記錄條目 的監聽
checkDOMListeners(1);
// 解除監聽
return () => {
checkDOMListeners(-1);
unlisten();
};
}
const history = {
// 監聽
listen
...
};
return history;
}
複製代碼
history.listen
是當歷史記錄條目改變時,觸發回調監聽函數。因此這裏有兩步:
transitionManager.appendListener(listener)
把回調的監聽函數添加到隊列裏;checkDOMListeners
監聽歷史記錄條目的改變;下面看看如何歷史記錄條目的改變checkDOMListeners(1)
。
// createBrowserHistory.js
function createBrowserHistory(props = {}){
let listenerCount = 0;
function checkDOMListeners(delta) {
listenerCount += delta;
// 是否已經添加
if (listenerCount === 1 && delta === 1) {
// 添加綁定,當歷史記錄條目改變的時候
window.addEventListener('popstate', handlePopState);
} else if (listenerCount === 0) {
// 解除綁定
window.removeEventListener('popstate', handlePopState);
}
}
// getDOMLocation(event.state) = location = {
// hash: ""
// pathname: "/history/index.html"
// search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
// state: undefined
// }
function handlePopState(event) {
handlePop(getDOMLocation(event.state));
}
function handlePop(location) {
const action = "POP";
setState({ action, location })
}
}
複製代碼
雖然做者寫了不少很細的回調函數,可能會致使有些很差理解,但細細看仍是有它道理的:
checkDOMListeners
:全局只能有一個監聽歷史記錄條目的函數(listenerCount
來控制);handlePopState
:必須把監聽函數提取出來,否則不能解綁;handlePop
:監聽歷史記錄條目的核心函數,監聽成功後執行setState
。setState({ action, location })
做用是根據當前地址信息(location
)更新history。
// createBrowserHistory.js
function createBrowserHistory(props = {}){
function setState(nextState) {
// 更新history
Object.assign(history, nextState);
history.length = globalHistory.length;
// 執行監聽函數listen
transitionManager.notifyListeners(history.location, history.action);
}
const history = {
// 監聽
listen
...
};
return history;
}
複製代碼
在這裏,當更改歷史記錄條目成功後:
這就是h.listen
的主要流程了,是否是還挺簡單的。
history.block
的功能是當歷史記錄條目改變時,觸發提示信息。在這裏咱們能夠想象下大概的 截取 流程:
哈哈這裏是否是感受跟listen
函數的套路差很少呢?其實h.listen
和h.block
的監聽歷史記錄條目改變的代碼是公用同一套(固然拉只能綁定一個監聽歷史記錄條目改變的函數),3.1.3爲了方便理解我修改了部分代碼,下面是完整的源碼。
在第二章使用代碼中,建立了History
對象後使用了h.block
函數(只能綁定一個block
函數)。
// index.html
h.block(function (location, action) {
return 'Are you sure you want to go to ' + location.path + '?'
})
複製代碼
一樣的咱們先看看做者的createTransitionManager.js
是如何實現提示的。
createTransitionManager
是過渡管理(例如:處理block函數中的彈框、處理listener的隊列)。代碼風格跟createBrowserHistory幾乎一致,暴露全局函數,調用後返回對象便可使用。
// createTransitionManager.js
function createTransitionManager() {
let prompt = null;
// 設置提示
function setPrompt(nextPrompt) {
prompt = nextPrompt;
// 解除
return () => {
if (prompt === nextPrompt) prompt = null;
};
}
/** * 實現提示 * @param location:地址 * @param action:行爲 * @param getUserConfirmation 設置彈框 * @param callback 回調函數:block函數的返回值做爲參數 */
function confirmTransitionTo(location, action, getUserConfirmation, callback) {
if (prompt != null) {
const result = typeof prompt === "function" ? prompt(location, action) : prompt;
if (typeof result === "string") {
// 方便理解我把源碼getUserConfirmation(result, callback)直接替換成callback(window.confirm(result))
callback(window.confirm(result))
} else {
callback(result !== false);
}
} else {
callback(true);
}
}
return {
setPrompt,
confirmTransitionTo
...
};
}
複製代碼
setPrompt
和confirmTransitionTo
的用意:
下面看h.block
源碼。
// createBrowserHistory.js
import createTransitionManager from "./createTransitionManager";
const transitionManager = createTransitionManager();
function createBrowserHistory(props = {}){
let isBlocked = false;
function block(prompt = false) {
// 設置提示
const unblock = transitionManager.setPrompt(prompt);
// 是否設置了block
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
}
// 解除block函數
return () => {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
}
// 消除提示
return unblock();
};
}
const history = {
// 截取
block,
...
};
return history;
}
複製代碼
history.block
的功能是當歷史記錄條目改變時,觸發提示信息。因此這裏有兩步:
transitionManager.setPrompt(prompt)
設置提示;checkDOMListeners
監聽歷史記錄條目改變的改變。這裏感受有值得借鑑的地方:調用history.block
,它會返回一個解除監聽方法,只要調用一下返回函數便可解除監聽或者復原(有趣)。
咱們看看監聽歷史記錄條目改變函數checkDOMListeners(1)
(注意:transitionManager.confirmTransitionTo
)。
// createBrowserHistory.js
function createBrowserHistory(props = {}){
function block(prompt = false) {
// 設置提示
const unblock = transitionManager.setPrompt(prompt);
// 是否設置了block
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
}
// 解除block函數
return () => {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
}
// 消除提示
return unblock();
};
}
let listenerCount = 0;
function checkDOMListeners(delta) {
listenerCount += delta;
// 是否已經添加
if (listenerCount === 1 && delta === 1) {
// 添加綁定,當地址欄改變的時候
window.addEventListener('popstate', handlePopState);
} else if (listenerCount === 0) {
// 解除綁定
window.removeEventListener('popstate', handlePopState);
}
}
// getDOMLocation(event.state) = location = {
// hash: ""
// pathname: "/history/index.html"
// search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
// state: undefined
// }
function handlePopState(event) {
handlePop(getDOMLocation(event.state));
}
function handlePop(location) {
// 不須要刷新頁面
const action = "POP";
// 實現提示
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (ok) {
// 肯定
setState({ action, location });
} else {
// 取消
revertPop(location);
}
}
);
}
const history = {
// 截取
block
...
};
return history;
}
複製代碼
就是在handlePop
函數觸發transitionManager.confirmTransitionTo
的(3.1.3我對這裏作了修改成了方便理解)。
transitionManager.confirmTransitionTo
的回調函數callback有兩條分支,用戶點擊提示框的肯定按鈕或者取消按鈕:
setState({ action, location })
;revertPop(location)
(忽略)。到這裏已經瞭解完h.block
函數、h.listen
和createTransitionManager.js
。接下來咱們繼續看另外一個重要的函數h.push
。
function createBrowserHistory(props = {}){
function push(path, state) {
const action = "PUSH";
// 構造location
const location = createLocation(path, state, createKey(), history.location);
// 執行block函數,彈出框
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
// 獲取當前路徑名
const href = createHref(location);
const { key, state } = location;
// 添加歷史條目
globalHistory.pushState({ key, state }, null, href);
if (forceRefresh) {
// 強制刷新
window.location.href = href;
} else {
// 更新history
setState({ action, location });
}
}
);
}
const history = {
// 跳轉
push,
...
};
return history;
}
複製代碼
這裏最重要的是globalHistory.pushState
函數,它直接添加新的歷史條目。
function createBrowserHistory(props = {}){
function replace(path, state) {
const action = "REPLACE";
// 構造location
const location = createLocation(path, state, createKey(), history.location);
// 執行block函數,彈出框
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
// 獲取當前路徑名
const href = createHref(location);
const { key, state } = location;
globalHistory.replaceState({ key, state }, null, href);
if (forceRefresh) {
window.location.replace(href);
} else {
setState({ action, location });
}
}
);
}
const history = {
// 跳轉
replace,
...
};
return history;
}
複製代碼
其實push
和replace
的區別就是history.pushState
和history.replaceState
的區別。
function createBrowserHistory(props = {}){
function go(n) {
globalHistory.go(n);
}
function goBack() {
go(-1);
}
function goForward() {
go(1);
}
const history = {
// 跳轉
go,
goBack,
goForward,
...
};
return history;
}
複製代碼
其實就是history.go
的運用。
手把手帶你上react-router的history車(掘金)
總的來講,若是不須要block
的話,原生方法能夠知足。最主要仍是對history.pushState
、history.replaceState
、history.go(n)
、popstate
方法的運用。公司加班嚴重,利用僅剩的時間擴充下本身的知識面,最好的方法那就是閱讀源碼了哈哈。開始總會有點困難,第一次讀一臉懵逼,第二次讀二臉懵逼,第三次讀有點懵逼,第四次讀這b牛逼~。只要堅持下多寫點測試用例慢慢理解就行了,加油!