history是一个JavaScript库,可让你在JavaScript运行的任何地方轻松管理会话历史记录

1.前言

history是由Facebook维护的,react-router依赖于history,区别于浏览器的window.historyhistory是包含window.history的,让开发者可以在任何环境都能使用history的api(例如NodeReact Native等)。

本篇读后感分为五部分,分别为前言、使用、解析、demo、总结,五部分互不相连可根据需要分开看。

前言为介绍、使用为库的使用、解析为源码的解析、demo是抽取源码的核心实现的小demo,总结为吹水,学以致用。

建议跟着源码结合本文阅读,这样更加容易理解!

  1. history
  2. history解析的Github地址
  3. 手把手带你上react-router的history车(掘金)

2.使用

history有三种不同的方法创建history对象,取决于你的代码环境:

  1. createBrowserHistory:支持HTML5 history api的现代浏览器(例如:/index);
  2. createHashHistory:传统浏览器(例如:/#/index);
  3. createMemoryHistory:没有Dom的环境(例如:NodeReact Native)。

注意:本片文章只解析createBrowserHistory,其实三种构造原理都是差不多的

<!DOCTYPE html>
<html><head><script src="./umd/history.js"></script><script>var createHistory = History.createBrowserHistory// var createHistory = History.createHashHistoryvar 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用于监听地址栏的改变,pushreplacego(n)等用于跳转,用法简单明了

3.解析

贴出来的源码我会删减对理解原理不重要的部分!!!如果想看完整的请下载源码看哈

从history的源码库目录可以看到modules文件夹,包含了几个文件:

  1. createBrowserHistory.js 创建createBrowserHistory的history对象;
  2. createHashHistory.js 创建createHashHistory的history对象;
  3. createMemoryHistory.js 创建createMemoryHistory的history对象;
  4. createTransitionManager.js 过渡管理(例如:处理block函数中的弹框、处理listener的队列);
  5. DOMUtils.js Dom工具类(例如弹框、判断浏览器兼容性);
  6. index.js 入口文件;
  7. LocationUtils.js 处理Location工具;
  8. PathUtils.js 处理Path工具。

入口文件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

3.1 createBrowserHistory

// createBrowserHistory.js
function createBrowserHistory(props = {}){// 浏览器的historyconst globalHistory = window.history;// 初始化locationconst 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。我们的解析顺序如下:

  1. location;
  2. createHref;
  3. block;
  4. listen;
  5. push;
  6. replace。

3.2 location

location属性存储了与地址栏有关的信息,我们对比下createBrowserHistory的返回值history.locationwindow.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函数的返回值(getDOMLocationhistory中会经常调用,理解好这个函数比较重要)。

// 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.locationfunction 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;
}
复制代码

一般大型的项目中都会把一个功能拆分成至少两个函数,一个专门处理参数的函数和一个接收处理参数实现功能的函数:

  1. 处理参数:getDOMLocation函数主要处理statewindow.location这两参数,返回自定义的history.location对象,主要构造history.location对象是createLocation函数;
  2. 构造功能: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,代码简单。

3.3 createHref

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;
}
复制代码

3.4 listen

在这里我们可以想象下大概的 监听 流程:

  1. 绑定我们设置的监听函数;
  2. 监听历史记录条目的改变,触发监听函数。

第二章使用代码中,创建了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) {// goodif (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};
}
复制代码
  1. 设置监听函数appendListenerfn就是用户设置的监听函数,把所有的监听函数存储在listeners数组中;
  2. 执行监听函数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是当历史记录条目改变时,触发回调监听函数。所以这里有两步:

  1. transitionManager.appendListener(listener)把回调的监听函数添加到队列里;
  2. 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 })}
}
复制代码

虽然作者写了很多很细的回调函数,可能会导致有些不好理解,但细细看还是有它道理的:

  1. checkDOMListeners:全局只能有一个监听历史记录条目的函数(listenerCount来控制);
  2. handlePopState:必须把监听函数提取出来,不然不能解绑;
  3. handlePop:监听历史记录条目的核心函数,监听成功后执行setState

setState({ action, location })作用是根据当前地址信息(location)更新history。

// createBrowserHistory.js
function createBrowserHistory(props = {}){function setState(nextState) {// 更新historyObject.assign(history, nextState);history.length = globalHistory.length;// 执行监听函数listentransitionManager.notifyListeners(history.location, history.action);}const history = {// 监听listen...};return history;
}
复制代码

在这里,当更改历史记录条目成功后:

  1. 更新history;
  2. 执行监听函数listen;

这就是h.listen的主要流程了,是不是还挺简单的。

3.5 block

history.block的功能是当历史记录条目改变时,触发提示信息。在这里我们可以想象下大概的 截取 流程:

  1. 绑定我们设置的截取函数;
  2. 监听历史记录条目的改变,触发截取函数。

哈哈这里是不是感觉跟listen函数的套路差不多呢?其实h.listenh.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...};
}
复制代码

setPromptconfirmTransitionTo的用意:

  1. 设置提示setPrompt:把用户设置的提示信息函数存储在prompt变量;
  2. 实现提示confirmTransitionTo:
    1. 得到提示信息:执行prompt变量;
    2. 提示信息后的回调:执行callback把提示信息作为结果返回出去。

下面看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);// 是否设置了blockif (!isBlocked) {checkDOMListeners(1);isBlocked = true;}// 解除block函数return () => {if (isBlocked) {isBlocked = false;checkDOMListeners(-1);}// 消除提示return unblock();};}const history = {// 截取block,...};return history;
}
复制代码

history.block的功能是当历史记录条目改变时,触发提示信息。所以这里有两步:

  1. transitionManager.setPrompt(prompt) 设置提示;
  2. checkDOMListeners 监听历史记录条目改变的改变。

这里感觉有值得借鉴的地方:调用history.block,它会返回一个解除监听方法,只要调用一下返回函数即可解除监听或者复原(有趣)。


我们看看监听历史记录条目改变函数checkDOMListeners(1)(注意:transitionManager.confirmTransitionTo)。

// createBrowserHistory.js
function createBrowserHistory(props = {}){function block(prompt = false) {// 设置提示const unblock = transitionManager.setPrompt(prompt);// 是否设置了blockif (!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有两条分支,用户点击提示框的确定按钮或者取消按钮:

  1. 当用户点击提示框的确定后,执行setState({ action, location })
  2. 当用户点击提示框的取消后,执行revertPop(location)(忽略)。

到这里已经了解完h.block函数、h.listencreateTransitionManager.js。接下来我们继续看另一个重要的函数h.push

3.6 push

function createBrowserHistory(props = {}){function push(path, state) {const action = "PUSH";// 构造locationconst 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 {// 更新historysetState({ action, location });}});}const history = {// 跳转push,...};return history;
}
复制代码

这里最重要的是globalHistory.pushState函数,它直接添加新的历史条目。

3.7 replace

function createBrowserHistory(props = {}){function replace(path, state) {const action = "REPLACE";// 构造locationconst 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;
}
复制代码

其实pushreplace的区别就是history.pushStatehistory.replaceState的区别。

3.8 go

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的运用。

4.demo

手把手带你上react-router的history车(掘金)

5.总结

总的来说,如果不需要block的话,原生方法可以满足。最主要还是对history.pushStatehistory.replaceStatehistory.go(n)popstate方法的运用。公司加班严重,利用仅剩的时间扩充下自己的知识面,最好的方法那就是阅读源码了哈哈。开始总会有点困难,第一次读一脸懵逼,第二次读二脸懵逼,第三次读有点懵逼,第四次读这b牛逼~。只要坚持下多写点测试用例慢慢理解就好了,加油!

history源码解析-管理会话历史记录相关推荐

  1. 【kafka】Kafka 源码解析:Group 协调管理机制

    1.概述 转载:Kafka 源码解析:Group 协调管理机制 在 Kafka 的设计中,消费者一般都有一个 group 的概念(当然,也存在不属于任何 group 的消费者),将多个消费者组织成一个 ...

  2. Redis源码解析——内存管理

    在<Redis源码解析--源码工程结构>一文中,我们介绍了Redis可能会根据环境或用户指定选择不同的内存管理库.在linux系统中,Redis默认使用jemalloc库.当然用户可以指定 ...

  3. hox 状态管理库源码解析

    文章目录 hox是什么 hox实现状态共享的方式 基本使用 全局状态共享 局部状态共享 源码解析 index.js 入口文件 coantainer.tsx 管理每个hook 全局状态共享的实现 Hox ...

  4. Netty源码解析之内存管理-PooledByteBufAllocator-PoolArena

      PooledByteBufAllocator是Netty中比较复杂的一种ByteBufAllocator , 因为他涉及到对内存的缓存,分配和释放策略,PooledByteBufAllocator ...

  5. postgres 源码解析25 缓冲池管理器-3

      本文讲解缓冲块的选择策略BufferAlloc,同时该函数也是替换策略的核心函数, 知识回顾: postgres源码解析 缓冲池管理–1 postgres源码解析 缓冲池管理–2 总结<执行 ...

  6. ⭐openGauss数据库源码解析系列文章—— 角色管理⭐

    在前面介绍过"9.1 安全管理整体架构和代码概览.9.2 安全认证",本篇我们介绍第9章 安全管理源码解析中"9.3 角色管理"的相关精彩内容介绍. 9.3 角 ...

  7. ⭐openGauss数据库源码解析系列文章—— 对象权限管理⭐

    在前面文章中介绍过"9.3 角色管理整",本篇我们介绍第9章 安全管理源码解析中"9.4 对象权限管理"的相关精彩内容介绍. 9.4 对象权限管理 权限管理是安 ...

  8. Python源码解析:内存管理(DEBUG模式)的几个理解点

    写了这多贴子,顺带写点自己的感想吧!其实很多贴子在写的时候很踌躇,比如这次打算写的python内存管理,因为内存管理都比较琐碎,在软件架构里,也是很容易出问题的地方,涉及的细节内容非常多,要写好写明白 ...

  9. postgres 源码解析11 CLOG管理器--2

      在本小节中,着重讲解CLOG日志的读写操作,获取事务的状态信息进行可见性判断内容,相关背景知识见回顾通道: 1 postgres CLOG源码解析-1 2 postgres源码分析 Slru缓冲池 ...

最新文章

  1. 有望取代Spark,Michael Jordan和Ion Stoica提出下一代分布式实时机器学习框架Ray牛在哪?...
  2. How to bind multiple properties with formatter on one control
  3. Apache的认证、授权、访问控制
  4. Java Date Nuances的痛苦提醒
  5. JEECG 喜讯[后续推出功能]
  6. 30问提升技术人写作力-第1问作业
  7. linux安装7z到指定目录,linux下安装7zip
  8. CKEditor设置背景图片及宽高
  9. 分享过滤条件中增加一个自定义过滤变量插件代码
  10. 【Caffe代码解析】Blob
  11. linux Vi操作和使用方法详解
  12. c语言写按键程序,单片机按键设定软件c语言 单片机C语言按键开关程序
  13. qq pc9.4协议机器人框架源码
  14. 集线器与交换机的区别
  15. 0ctf Babyheap 2017
  16. 企业运维自动化实战-CSDN公开课-专题视频课程
  17. java project 显示感叹号_项目工程上有感叹号或者差号
  18. 怎么使用7zip进行分批压缩_7z解压软件(7-zip)分卷压缩怎么做?
  19. 转 js控制excel打印完美解决方案
  20. 上板子在线抓波发现app_rdy一直为低

热门文章

  1. GitHub官方代码扫描工具上线,免费查找代码漏洞 !
  2. nginx、lvs、keepalived、f5、DNS轮询(lvs为何不能完全替代DNS轮询)
  3. Electron教程(六)应用菜单设置例子
  4. win和linux下的磁盘测速(读写速度)
  5. linux之SECURITY(安全)一
  6. mac电脑解决Error: command failed: npm install --loglevel error --legacy-peer-deps
  7. Matlab数组操作_实现三维数组的写入与读取
  8. php mysql 上一页 下一页 分页代码片段
  9. linux命令如何删除子目录文件,Linux如何删除目录下所有文件包括子目录
  10. 希望,所有珍惜都不需要靠失去才懂得