History 對象及事件監(jiān)聽詳解

配圖源自 Freepik

一爸业、前言

理論上說,每個有效的 URL 都指向一個唯一的資源亏镰。這個資源可以是一個 HTML 頁面扯旷,一個 CSS 文檔,一幅圖像等索抓。在地址欄鍵入完整的 URL 地址薄霜,瀏覽器就會將對應(yīng)資源展示出來。

為了在多個 URL 之間往返纸兔,瀏覽器廠商定義了一種可存儲瀏覽器會話歷史(下稱“歷史記錄”)的機(jī)制,每訪問新的 URL 就會在歷史記錄中增加一個新的歷史記錄條目否副。當(dāng)前“歷史條目”可通過 History 對象(即 window.history)獲取汉矿,該對象包括了 back()forward()备禀、go() 等方法洲拇。

在很早以前奈揍,不同 URL 之間進(jìn)行切換,都是需要重新加載資源的赋续。直到 Ajax 的出現(xiàn)男翰,打破了這種限制。Ajax 技術(shù)允許通過 JavaScript 腳本向服務(wù)器發(fā)起請求纽乱,服務(wù)器接收到請求蛾绎,將數(shù)據(jù)返回客戶端(瀏覽器),然后根據(jù)響應(yīng)數(shù)據(jù)按需操作 DOM 以實現(xiàn)局部刷新鸦列。這個過程頁面并不會重新加載租冠,只會更新 DOM 的局部,因此 URL 并沒有發(fā)生變化薯嗤。但是 Ajax 局部刷新的能力顽爹,似乎與一個 URL 對應(yīng)一個資源相悖。于是......就出現(xiàn)了一種解決方案骆姐,既可以實現(xiàn)頁面局部刷新镜粤,也會修改 URL。

那就是 URL 中的 # 模式玻褪,例如:

http://www.example.com/index.html#user

# 號表示網(wǎng)頁的一個位置肉渴,跟在 # 號后面的字符串稱為“錨”。當(dāng)錨發(fā)生變化归园,若頁面中存在這樣一個位置(可通過錨點或標(biāo)簽元素 id 屬性設(shè)置)黄虱,瀏覽器會使頁面自動滾動至對應(yīng)位置。這種機(jī)制的好處是庸诱,僅用于指導(dǎo)瀏覽器的動作捻浦,而對服務(wù)器是完全無用的。例如桥爽,請求上述網(wǎng)址朱灿,HTTP 請求的服務(wù)器地址是:http://www.example.com/index.html(不會包含 #user)。

相比 http://www.example.com/index.html/user 這種形式钠四,URL 上帶 # 號除了看著不順眼之外盗扒,對于分享 URL 或 SEO 來說也是一個問題(對此 Google 還提出了一種優(yōu)化 SEO 的方案,即 URL 中帶上 "#!"缀去,詳見)侣灶。后來 HTML5 中提供了另外一種解決方案。它同樣是可以修改 URL 且不觸發(fā)頁面重載缕碎,而且可以修改 URL 中 Origin 后面的任意路徑(即 /index.html/user)褥影,這點 # 模式是做不到的。他們將這種能力內(nèi)置在 History 對象下咏雌,包含 history.pushState()凡怎、history.replaceState() 方法校焦。

上面提到了一些詞語,有必要說明一下:

  • 歷史記錄
    是指在瀏覽器中每個標(biāo)簽(窗口)的會話歷史(下稱“歷史記錄”)统倒。它由瀏覽器某個線程維護(hù)著寨典,而且標(biāo)簽之間的歷史記錄是相互獨立的,且無法通過 JavaScript 腳本讀取房匆。

    當(dāng)標(biāo)簽關(guān)閉或者退出瀏覽器耸成,會話結(jié)束,歷史記錄也隨之被銷毀(沒錯坛缕,這里的“歷史記錄”墓猎,并不是指瀏覽器應(yīng)用的“歷史記錄”功能)。

  • 歷史條目
    瀏覽器每訪問一個新的 URL赚楚,就會產(chǎn)生一條記錄(下稱“記錄”)毙沾,并保存至“歷史記錄”。這條記錄宠页,僅能在當(dāng)前頁面的 window.history 對象讀取到左胞。

    舉個例子,假設(shè)當(dāng)前歷史記錄里有 3 條不同頁面的記錄(假設(shè)用數(shù)組 [A, B, C] 表示举户,真正如何表示不去深究烤宙,非本文討論范圍),若當(dāng)前處于 C 頁面俭嘁,那么通過 window.history 讀取到數(shù)據(jù)躺枕,是指 C 頁面的記錄信息。而 A供填、B 頁面的信息是獲取不對的拐云,除非后退并在對應(yīng)頁面內(nèi)執(zhí)行腳本。

  • 新的 URL
    請注意近她,這個“新”是相對的叉瘩。由于下文經(jīng)常提到,因此有必要說明一下粘捎。

    假設(shè)在 A 頁面跳轉(zhuǎn)到 B 頁面薇缅,這個 B 就是“新的 URL”。若在 B 中也有一個鏈接指向 A 頁面攒磨,點擊的時候泳桦,這個 A 也是“新的 URL”,因為它是相較于當(dāng)前頁面 URL 所得出來的結(jié)論娩缰。因此蓬痒,這個過程會產(chǎn)生 3 條記錄,所以歷史記錄將會是 [A, B, A]

下面將按歷史順序一一介紹...

二梧奢、URL 的 # 號

其實前面剛提到,# 表示頁面中的一個位置演痒。比如:

https://github.com/toFrankie/csscomb-mini#usage

上述 URL 中亲轨,#usage 表示 https://github.com/toFrankie/csscomb-mini 頁面的 usage 位置。

URL 上跟在 # 后面的所有字符串鸟顺,被稱為 Fragment惦蚊,或片段標(biāo)識符),所以此 URL 的錨為 usage讯嫂。

1. location.hash 屬性

打印 window.location 結(jié)果如下:

{
  hash: '#usage'
  host: 'github.com'
  hostname: 'github.com'
  href: 'https://github.com/toFrankie/csscomb-mini#usage'
  origin: 'https://github.com'
  pathname: '/toFrankie/csscomb-mini'
  port: ''
  protocol: 'https:'
  search: ''
}

其中 location.hash 值為 #usage蹦锋,它是由 # + Fragment 組成的字符串。

如果 URL 中不存在 Fragment欧芽,location.hash 會返回一個空字符串('')莉掂。

// 1. https://github.com/toFrankie/csscomb-mini
window.location.hash // ""

// 2. https://github.com/toFrankie/csscomb-mini#
window.location.hash // ""

// 3. https://github.com/toFrankie/csscomb-mini#/
window.location.hash // "#/"

// 4. https://github.com/toFrankie/csscomb-mini#usage
window.location.hash // "#usage"

2. 修改 URL hash 值

修改 hash 值就會直接體現(xiàn)在地址欄上,并且在歷史記錄中會產(chǎn)生一條新記錄千扔。比如憎妙,執(zhí)行 history.length 可以看到 length 的變化。history.length 表示歷史記錄中的記錄個數(shù)曲楚。

可以通過以下幾種方式去修改:

// 1. 直接給該屬性賦值
window.location.hash = '#usage' // # 號可省略

// 2. 給 window.location 賦值厘唾,請注意 # 是不能省略,否則不僅是修改 Fragment 了
window.location = '#usage'
window.location.href = '#usage'

// 3. 請注意龙誊,只修改 Fragment 部分抚垃,否則會重新加載頁面。類似 history.replaceState 作用
window.location.replace('https://github.com/toFrankie/csscomb-mini#/usage')

// 4. 通過 <a> 標(biāo)簽設(shè)置 href 屬性趟大,且不能省略 # 號
<a href="#usage"></a>

請注意鹤树,多次設(shè)置同一個 Fragment 時,僅首次有效护昧,重復(fù)的部分可以理解為是無效的魂迄。

3. location.hash、location.href 與 location.replace()

前面兩個方法都可讀可寫惋耙,其中 location.hash 絕對不會重載頁面捣炬。這跟它的設(shè)計初衷有關(guān),前面提過了绽榛,不再贅述湿酸。而 location.hreflocation.replace() 若只是 URL 的 Fragment 部分發(fā)生,也不會重載頁面灭美,而其他情況總會重載頁面推溃。

通過 location.hreflocation.hash 方式去“修改” URL届腐,歷史記錄都會新增一條新記錄铁坎。由于 history.length 是歷史記錄數(shù)量的體現(xiàn)蜂奸,因此也會隨之改變。而 location.replace() 則是用新記錄覆蓋當(dāng)前記錄硬萍,因此 history.length 不會發(fā)生變化扩所。

注意點:

  • 以上三種方式(包括 <a> 標(biāo)簽形式)去修改 URL,只有在新舊 URL 不相同的情況下朴乖,才會新增一條記錄祖屏。

  • 其中 location.hreflocation.replace() 方法,若 URL 中包含 Fragment 部分买羞,且新舊 URL 之間僅 Fragment 部分發(fā)生變化袁勺,也不會重載頁面。

  • 不管新舊 URL 是否一致(URL 不含 Fragment 時)畜普,location.href 總會重載頁面期丰。

  • 當(dāng)新舊 URL 相同時,location.href 作用等同于 location.reload()漠嵌、history.go(0)咐汞。雖說是重新加載頁面,但多數(shù)是從瀏覽器緩存中加載儒鹿,除非頁面緩存失效或過期了化撕。

  • 對于 location.href 我們通常會賦予一個完整的 URL 地址,但它是支持“相對路徑”形式的 URL 的约炎。(詳見:絕對 URL 和相對 URL

  • 上面是指寫操作植阴,并不是讀操作哈。

一句話總結(jié):若新舊 URL 之間僅僅 Fragment 部分發(fā)生改變圾浅,以上幾種方法都會在歷史記錄新增一條記錄掠手,且不會重載頁面。

4. Fragment 的位置

前面提到狸捕,# + Fragment 表示網(wǎng)頁的一個位置喷鸽,用于指導(dǎo)瀏覽器的行為。當(dāng) Fragment 的值發(fā)生改變灸拍,頁面會滾動至對應(yīng)位置做祝。當(dāng)然,前提是這個位置存在于頁面中鸡岗,否則也是不會發(fā)生滾動的混槐。

那么這個“位置”,如何設(shè)置呢轩性?

講真的声登,天天用框架寫頁面,最原始的反而忘了。有兩種方式:

  • 使用錨點悯嗓,即利用 <a> 標(biāo)簽的 name 屬性(不推薦)
  • 使用標(biāo)簽 id 屬性(推薦)

請注意件舵,<a> 標(biāo)簽的 name 屬性在 HTML5 中已廢棄,請使用 HTML 全局屬性 id 來代替绅作。后者在整個 DOM 中必須是唯一的芦圾。常用于查詢節(jié)點、樣式選擇器俄认、作為頁面 Fragment 的位置。

<!-- 1. 錨點 -->
<a name="usage"></a>

<!-- 2. 設(shè)置 id 屬性 -->
<div id="usage"></div>

<!-- 這種也是可以的洪乍,但這種不稱為錨點 -->
<a id="usage"></a>

再看一例子:

<!-- 1. 在點擊 a 標(biāo)簽時眯杏,會修改 hash 屬性為 #usage,但不會滾動至 a 標(biāo)簽 -->
<a href="#usage"></a>

<!-- 2. 以下情況壳澳,除了修改 hash 值岂贩,頁面也會隨之滾動至 a 標(biāo)簽 -->
<a name="usage" href="#usage"></a>
<a id="usage" href="#usage"></a>

上述示例,作者本人會經(jīng)诚锊ǎ混淆(希望你們不會)萎津,順道提一下。簡單來說抹镊,href="usage" 是為了修改 URL锉屈,當(dāng) URL 的 hash 變成 #usage,瀏覽器就會滾動至對應(yīng)位置(即錨點為 usageid 屬性為 usage 的元素所在位置)垮耳。

5. hashchange 事件

若在全局注冊了 hashchange 事件監(jiān)聽器颈渊,只要 URL 的 Fragment 發(fā)生變化,將會被事件處理程序捕獲到终佛,事件對象包含了 newURLoldURL 等該事件特有的屬性俊嗽。其余的,在下文對比 popstate 事件時再詳細(xì)介紹铃彰。

三绍豁、History 對象

前面提到,每個標(biāo)簽都有一個獨立的歷史記錄牙捉,里面維護(hù)著一條或多條記錄竹揍。每條記錄保存了對應(yīng) URL 的一些狀態(tài),僅能在當(dāng)前頁面的 window.history 對象讀取到鹃共。(這里不再贅述鬼佣,若概念有混淆的,請回到開頭再看一遍)

在 HTML5 之前霜浴,History 對象主要包含以下屬性和方法:

  • history.length
  • history.back()
  • history.forward()
  • history.go()

1. history.length

只讀晶衷,該屬性返回當(dāng)前會話的歷史記錄個數(shù)。由于 history.length 是歷史記錄數(shù)量的體現(xiàn),那么當(dāng)歷史記錄發(fā)生變化時晌纫,它才會隨之改變税迷。

注意以下幾點:

  • 若“主動”打開瀏覽器的新標(biāo)簽,就會產(chǎn)生一條記錄锹漱,盡管它可能是一個空標(biāo)簽頁箭养,即 history.length1。當(dāng)鍵入新 URL 并回車哥牍,此時 history.length 就會變?yōu)?2毕泌。

  • 若瀏覽器的標(biāo)簽是通過類似 <a target="_blank"> 形式自動創(chuàng)建的話,新標(biāo)簽的 history.length1(不是 2 哦)嗅辣。此時原標(biāo)簽的歷史記錄不會受到影響撼泛,它們是相互獨立的。這種情況就類似于在微信里打開一個鏈接澡谭,進(jìn)入頁面的 history.length1愿题。

  • 不管以任何方式刷新頁面,歷史記錄和 history.length 都不會改變蛙奖。

  • 在地址欄鍵入新的 URL潘酗,歷史記錄會增加 1

  • 一般情況下雁仲,若新舊 URL 相同仔夺,此時歷史記錄不會發(fā)生變化,history.length 也不會伯顶。特殊情況是囚灼,history.pushState()history.replaceState() 方法總會產(chǎn)生一條新記錄,即使新舊 URL 相同也會祭衩。

  • 點擊瀏覽器前進(jìn)/后退/刷新按鈕灶体,或者調(diào)用 history.back()history.forward()掐暮、history.go() 方法蝎抽,不會使歷史記錄和 history.length 值發(fā)生變化。這些操作只會退回/前往歷史記錄中某個具體的頁面路克。但會觸發(fā) popstatehashchange 事件(若有注冊的話)樟结。

這里描述的場景很多,原因是此前對某些場景沒有完全弄清楚(如果你沒有這個困擾精算,簡單略過即可)瓢宦。

既然 history.length 是只讀的,換句話說灰羽,就是我們無法“直接”操作歷史記錄(比如刪除某個歷史記錄)驮履,事實上我們也訪問不到鱼辙。

2. history.back()

它的作用同瀏覽器的后退按鈕,通俗地講就是后退至上一頁玫镐。等價于 history.go(-1)倒戏。

若當(dāng)前頁面是歷史記錄的第一個頁時,調(diào)用此方法不執(zhí)行任何操作恐似。此時瀏覽器后退按鈕也是置灰的杜跷,是不可操作的。換句話說矫夷,此方法僅在 history.length > 1 時有效葛闷。

3. history.forward()

它的作用同瀏覽器的前進(jìn)按鈕,通俗地講就是前往下一頁双藕。等價于 history.go(1)孵运。

若當(dāng)前頁面是歷史記錄里最頂端的頁面時,調(diào)用此方法不執(zhí)行任何操作蔓彩。此時瀏覽器前進(jìn)按鈕也是置灰的,是不可操作的驳概。

4. history.go()

該方法接受一個 delta 參數(shù)(可選)赤嚼,通過當(dāng)前頁面的相對位置加載某個頁面。

window.history.go(delta)

一般來說顺又,參數(shù)可缺省更卒、為 0、為負(fù)整數(shù)(表示后退)稚照、正整數(shù)(表示前進(jìn))蹂空。

  • 比如說,history.go(-2) 會歷史記錄里后退兩個頁面果录。相應(yīng)地上枕,history.go(2) 會前進(jìn)兩個頁面。

  • 其中 history.go(1) 作用同 history.forward()弱恒,history.go(-1) 作用同 history.back()辨萍。

  • 若缺省 delta 或者 delta0,會重新加載當(dāng)前頁面返弹。此時作用同 location.reload() 或者瀏覽器的刷新按鈕锈玉。

  • delta 數(shù)值部分超出了歷史記錄的范圍,不會執(zhí)行任何操作义起。既不會后退至歷史記錄的第一個頁面拉背,也不會前往歷史記錄里最頂端的頁面。它會默默地失敗默终,且不會報錯椅棺。假設(shè)歷史記錄只有 5 條犁罩,然后你試圖后退/前進(jìn) 10 個頁面,這就屬于超出范圍土陪。

  • delta 不是 Number 類型炉爆,內(nèi)部先進(jìn)行隱式類型轉(zhuǎn)換成對應(yīng)的 Number 值,再執(zhí)行 go() 方法甥材。比如常挚,history.go(true) 相當(dāng)于 history.go(1)history.go(NaN) 相當(dāng)于 history.go(0)源哩。

5. 小結(jié)

back()鞋吉、forward()go() 三個方法励烦,簡單總結(jié)一下:

  • 僅調(diào)用以上三個方法谓着,不會使得歷史記錄或 history.length 發(fā)生改變。

  • 調(diào)用以上三個方法坛掠,通常是從瀏覽器緩存中加載頁面赊锚。在 Network 選項卡中往往可以看到類似 from disk cache 的字樣。

  • 當(dāng)超出了當(dāng)前標(biāo)簽的歷史記錄范圍屉栓,調(diào)用以上三個方法都不會執(zhí)行任何操作舷蒲,默默地失敗且不報錯。

  • 請注意友多,若后退/前進(jìn)時牲平,只是錨點發(fā)生變化,是不會重新加載頁面域滥。

四纵柿、HTML5 History API

History API 作為 HTML5 的新特性之一,解決了 Fragment 的一些痛點启绰,包括 URL 分享昂儒,SEO 優(yōu)化等都得到了很好的解決。這些新特性都內(nèi)置于 History 對象之中:

  • history.state
  • history.scrollRestoration
  • history.pushState()
  • history.replaceState()

1. history.state

只讀酬土,該屬性返回當(dāng)前頁面的狀態(tài)值荆忍。

const currentState = history.state

只有通過 pushState()replaceState() 方法產(chǎn)生的歷史記錄,這個屬性才會有相應(yīng)的值撤缴,否則為 null刹枉。

請注意,history.state 的返回值是一份拷貝值屈呕。

2. history.scrollRestoration

可讀寫微宝,該屬性允許 Web 應(yīng)用程序在歷史導(dǎo)航上顯式地設(shè)置默認(rèn)滾動恢復(fù)行為。此屬性可以是自動的(auto)或者手動的(manual)虎眨。

3. history.pushState()

在當(dāng)前位置蟋软,總會產(chǎn)生一條新的記錄镶摘,并保存在歷史記錄里面,而且 history.length 也會增加岳守。若新舊 URL 不相同的情況下凄敢,也伴隨著 URL 的變化。

請注意湿痢,它并不會重載頁面涝缝。同樣的還有 history.pushState() 方法。

偽代碼:

// 假設(shè)歷史記錄(稱為 histories)有 5 個頁面譬重,當(dāng)前處于最后一個頁面拒逮,即 5 位置。
const histories = [1, 2, 3, 4, 5]

// 若后退 2 頁
history.go(-2) // 此時臀规,我們的頁面處于歷史記錄中的 3 位置滩援。

// 插入一個新記錄,假設(shè)新記錄稱為 6
history.pushState(
  { state: 'new' },      // 通常是對象塔嬉,可通過 history.state 獲取
  'custom title',        // 幾乎所有瀏覽器都會忽略此參數(shù)玩徊,所以是沒用的
  'https://xxx.com'      // 該 URL 必須跟當(dāng)前網(wǎng)頁是同源的,否則會報錯谨究。
)

// 執(zhí)行 pushState() 方法后佣赖,不會加載頁面
window.location.href     // "https://xxx.com"
window.document.URL      // "https://xxx.com"
window.document.title    // 這還是原來的標(biāo)題,而不是 "custom title"
window.history.state     // { state: 'new' }
window.history.length    // 4
histories                // [1, 2, 3, 6]

語法

history.pushState(state, title[, url])
  • state - 可以是任意值记盒,通常為(可序列化)對象。它可以通過 history.state 獲取到外傅,或者在 popstate 事件的事件對象中體現(xiàn)纪吮。

  • title - 請忽視該參數(shù)的作用,它幾乎被所有瀏覽器所忽略萎胰,但不得不傳碾盟。通常,會傳遞 ''技竟、nullundefined冰肴。

  • url - (可選)新 URL,它最終體現(xiàn)在地址欄的 URL 上榔组。請注意熙尉,新 URL 與當(dāng)前頁面 URL 必須是同源的(即 location.origin 相同),否則將會拋出錯誤搓扯。

注意點

參數(shù) state 是可序列化對象检痰,怎么理解?

個人猜測是那些可作用域 JSON.stringify() 方法的原始值或引用值锨推,具體沒去深究铅歼。舉個例子公壤,下面這個將會拋出錯誤:

history.pushState(
  { fn: function () {} }, 
  '', 
  location.href + 'abc'
)
// DOMException: Failed to execute 'pushState' on 'History': 
// function() {} could not be cloned.

較為冷門的東西,參數(shù) url 也支持 絕對 URL 和相對 URL椎椰。舉些例子:

// 假設(shè)當(dāng)前 URL 如下厦幅,它的 Origin 是 https://developer.mozilla.org
// https://developer.mozilla.org/zh-CN/docs/Web/API/History/pushState

// 1?? 完整網(wǎng)站,可理解為絕對路徑慨飘,將會變成:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript
history.pushState({}, '', 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript')

// 2?? 含 / 可理解為相對路徑确憨,相對于當(dāng)前 Origin,將會變成 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript
history.pushState({}, '', '/zh-CN/docs/Web/JavaScript')

// 3?? 若為 ../xxx 形式套媚,相對于當(dāng)前 URL缚态,將會變成 https://developer.mozilla.org/zh-CN/docs/Web/API/History/go
history.pushState({}, '', '../History/go')

// 4?? 若為字符串,將會變成 https://developer.mozilla.org/zh-CN/docs/Web/API/History/hhh
history.pushState({}, '', 'hhh')

另外堤瘤,使用 history.pushState() 可以改變 referrer玫芦。

4. history.replaceState()

參數(shù)約定與 pushState() 完全一致,語法如下:

history.replaceState(stateObj, title[, url])

replaceState() 也總會產(chǎn)生一條新記錄本辐,并用新記錄替換掉當(dāng)前頁面對應(yīng)的歷史記錄桥帆。

偽代碼...

const histories = [1, 2, 3, 4, 5] // 當(dāng)前處于 5 位置

history.replaceState({}, '', 'new-url') // 創(chuàng)建一個新記錄,假設(shè)稱為 6

// 新記錄 6 會替換記錄 5
histories // 歷史記錄慎皱,將會變?yōu)?[1, 2, 3, 4, 6]
history.length // 5老虫,未發(fā)生變化

5. pushState 與 replaceState 區(qū)別

還是偽代碼哈:

// 假設(shè)歷史記錄里,有 5 條記錄茫多,并處于歷史記錄的頂端祈匙,即第五個位置
const histories = [1, 2, 3, 4, 5]

// 后退 2 個頁面,即當(dāng)前處于第三個位置
history.go(-2)

// 使用 replaceState 產(chǎn)生一條新記錄(假設(shè)稱為 6)天揖,
// 它的作用是用新記錄替換當(dāng)前記錄夺欲,因此記錄 3 被新記錄 6 所替換
// 但仍處于歷史記錄的第三個位置
history.replace('6', '', 'new-url-6')
histories // [1, 2, 6, 4, 5]
history.length // 5

// 使用 pushState 產(chǎn)生一條新記錄(假設(shè)稱為 7),
// 它的作用是在當(dāng)前記錄后面添加一條新記錄今膊,
// 它會刪除當(dāng)前記錄后面的所有記錄些阅,然后再往后追加一條,
// 同時斑唬,它的位置也會前往至歷史記錄頂端市埋,即第四個位置。
history.replace('7', '', 'new-url-7')
histories // [1, 2, 6, 7]
history.length // 4

如果用 Array.prototype.splice() 來類比的話恕刘,可以這樣:

const arr = [1, 2, 3, 4, 5]

// pushState 類似于
arr.splice(curIndex + 1, 1000, newItem)

// replaceState 類似于
arr.splice(curIndex, 1, newItem)

// 注釋:
// arr 表示歷史記錄
// curIndex 表示當(dāng)前記錄的位置缤谎,
// 1000 只是為了表達(dá)刪除完 curIndex + 1 后面的所有項,可用 arr.length 等替代
// newItem 表示新記錄

簡單來說褐着,pushState()replaceState() 區(qū)別如下:

  • 兩者都會產(chǎn)生新的記錄弓千。
  • 前者會先移除當(dāng)前記錄后面的所有記錄,并將新記錄追加到歷史記錄頂端献起。而后者僅會用新記錄替換當(dāng)前記錄洋访,后面的記錄并不受影響(若有)镣陕。
  • 兩者都會使得歷史記錄發(fā)生變化。后者不會使得 history.length 發(fā)生改變姻政。

另外呆抑,對于歷史記錄及其數(shù)量,history.replaceState()location.replace() 表現(xiàn)是一致的汁展,只是后者有可能會重載頁面鹊碍。

6. popstate 事件

調(diào)用 pushState()replaceState() 方法的話食绿,既不會觸發(fā) popstate 事件監(jiān)聽器侈咕,也不會觸發(fā) hashchange 事件監(jiān)聽器(即使新舊 URL 只是 Fragment 部分不同)。這個也是 History API 的優(yōu)點之一器紧。

其余的下一節(jié)介紹...

五耀销、hashchange 和 popstate 事件

1. hashchange 事件

IE8 及以上瀏覽器都支持 hashchange 事件。注冊事件監(jiān)聽器铲汪,如下:

function listener(e) {
  // 可通過 e.newURL 和 e.oldURL 獲取完整的新舊 URL 值(只讀)
  // do something...
}

// 通過 DOM2 注冊(更推薦)
window.addEventListener('hashchange', listener)

// 通過 DOM0 注冊
window.onhashchange = listener

對于事件監(jiān)聽器的兼容性熊尉,可看:細(xì)讀 JavaScript 事件詳解

除了通過調(diào)用 pushState()掌腰、replaceState() 使 URL 的 Fragment 部分發(fā)生變化狰住,不會觸發(fā) hashchange 事件之外,其他任何方式致使 Fragment 發(fā)生改變齿梁,都會觸發(fā)該事件催植,包括 history.forward()history.back()勺择、location.hash查邢、<a href="#anchor">、操作瀏覽器后退/前進(jìn)按鈕酵幕、修改地址欄 Fragment 值等方式。

本文提到的 Fragment 均指 URL 上跟在 # 后面的所有字符串缓苛。

2. popstate 事件

需要注意的是芳撒,調(diào)用 history.pushState()history.replaceState() 不會觸發(fā) popstate 事件。

只有通過點擊瀏覽器后退/前進(jìn)按鈕未桥,或者通過腳本調(diào)用 history.back()笔刹、history.forward()history.go()go(0) 除外)方法冬耿,popstate 事件才會被觸發(fā)舌菜。

function listener(e) {
  // 通過 e.state 可以獲取當(dāng)前記錄的狀態(tài)對象對應(yīng)的拷貝值。
  // 非 pushState亦镶、replaceState 產(chǎn)生的記錄日月,該屬性值都為 null袱瓮。
}

// 通過 DOM2 注冊(更推薦)
window.addEventListener('popstate', listener)

// 通過 DOM0 注冊
window.onpopstate = listener

另外,不同瀏覽器在加載頁面時處理 popstate 事件的形式可能存在差異爱咬。

3. 小結(jié)

下面總結(jié)了很多條尺借,很大可能會記不住,沒關(guān)系:

  • 通過 back()精拟、forward()燎斩、go() 或瀏覽器后退/前進(jìn)按鈕切換的過程,一定會觸發(fā) popstate 事件蜂绎。若伴隨著 Fragment 的變化栅表,也會觸發(fā) hashchange 事件。(與記錄產(chǎn)生的方式無關(guān))

  • 在調(diào)用 pushState()师枣、replaceState() 時怪瓶,既不會觸發(fā) popstate 事件,也不會觸發(fā) hashchange 事件(即使包括 Fragment 發(fā)生改變)坛吁。

  • 除了 pushState()劳殖、replaceState(),其他任何方式致使 Fragment 發(fā)生改變拨脉,都會觸發(fā) hashchange 事件哆姻。

  • 通過 location.hash = 'foo' 方式致使 Fragment 發(fā)生改變,會觸發(fā) hashchange 事件玫膀,而不會觸發(fā) popstate 事件矛缨。

  • 而通過 window.location = '#foo'<a href="#foo"> 形式致使 Fragment 發(fā)生改變,同時觸發(fā) hashchangepopstate 事件帖旨。

簡化記憶:

其實常用的方法只有三個:history.pushState()箕昭、history.replaceState()location.hash解阅。最重要的是落竹,通常一個項目不會兩者混用,不然得多亂啊货抄。例如 React 述召、Vue 提供的路由系統(tǒng)只能二選一:

  • History 模式:使用 HTML5 History API,更符合未來發(fā)展的方向
  • Hash 模式:利用 location.hashhashchange 事件實現(xiàn)蟹地,兼容性較好积暖,且服務(wù)端無需額外的配置。

所以怪与,就簡化成兩條:

  • 調(diào)用 pushState()夺刑、replaceState() 時,不會觸發(fā) popstate 事件。其他 URL 的變化都會觸發(fā)此事件遍愿。
  • 當(dāng) URL 的 Fragment 部分發(fā)生改變存淫,都會觸發(fā) hashchange 事件。

六错览、比較

History 模式和 Hash 模式纫雁,在不重載頁面的前提下,實現(xiàn)了局部刷新的能力倾哺。

從某種程度來說, 調(diào)用 pushState()window.location= "#foo" 基本上一樣, 他們都會在當(dāng)前的歷史記錄中創(chuàng)建和激活一個新的歷史條目轧邪。但是 pushState() 有以下優(yōu)勢:

  • 新的 URL 可以是任何和當(dāng)前 URL 同源的 URL。但是設(shè)置 window.location 只會在你只設(shè)置 Fragment 的時候才會使當(dāng)前的 URL羞海。

  • 非強制修改 URL忌愚。相反,設(shè)置 window.location = '#foo' 僅僅會在錨的值不是 #foo 情況下創(chuàng)建一條新的歷史記錄却邓。

  • 可以在新的歷史記錄中關(guān)聯(lián)任何數(shù)據(jù)硕糊。window.location = "#foo" 形式的操作,你只可以將所需數(shù)據(jù)寫入錨的字符串中腊徙。

注意: pushState() 不會造成 hashchange 事件調(diào)用简十,即使新舊 URL 只是 Fragment 不同。

更多...

七撬腾、React Router

在 React 的路由系統(tǒng)中螟蝙,修改路由、監(jiān)聽路由實際上是由 history 庫中 createBrowserHistory()createHashHistory() 方法所構(gòu)造的 history 對象(有別于 window.history 對象)去操作的民傻。

在 React 中胰默,路由操作有這幾種方法。

  • props.history.push() - 新增一條歷史記錄

  • props.history.replace() - 新增一條記錄漓踢,并替換當(dāng)前記錄

  • props.history.go() - 后退/前進(jìn)

  • props.history.goBack() - 即 props.history.go(-1)

  • props.history.goForward() - 即 props.history.go(1)

其中牵署,props.history.go() 實際上就是調(diào)用了 window.history.go() 方法。前面兩個方法喧半,在不同路由模式下奴迅,調(diào)用的能力是不一樣的。

BrowserRouter 模式下挺据,對應(yīng) window.history.pushState()window.history.replaceState() 方法取具。

HashRouter 模式下,對應(yīng) window.location.hashwindow.location.replace() 方法吴菠。

在 React Router 中,路由更新以加載不同的組件浩村,是通過 React Context 實現(xiàn)的做葵,即 Provider/Consumer 的模式。當(dāng)路由更新時心墅,Providervalue 屬性會發(fā)生變化酿矢,使得對應(yīng)消費 Consumer 的組件得以更新榨乎。

前面我們提到過,調(diào)用 history.pushState()history.replaceState() 并不會觸發(fā) popstate 事件監(jiān)聽函數(shù)瘫筐。那么 React Router 是怎么知道 URL 發(fā)生變化的呢蜜暑?

首先在選擇使用 <BrowserRouter><HashHistory> 組件時,它內(nèi)部設(shè)置了一個監(jiān)聽器策肝,這個監(jiān)聽器的回調(diào)函數(shù)里面有一個 setState() 方法肛捍。當(dāng)我們在 React 組件中使用 props.history.push() 方法去跳轉(zhuǎn)頁面時,它除了會執(zhí)行 window.history.pushState() 使得 URL 發(fā)生改變之外之众,還會執(zhí)行前面提到的監(jiān)聽器拙毫,那么監(jiān)聽器的回調(diào)函數(shù)也會被執(zhí)行,既然里面有 setState() 操作棺禾,就會使得 <BrowserRouter><HashHistory> 組件執(zhí)行一次更新缀蹄,那么該組件的 Provider 就會更新,React Router 的 Consumer 們根據(jù) URL 來匹配對應(yīng)的路由膘婶,以加載相應(yīng)的組件缺前。因此,我們就能在瀏覽器中看到 URL 的變化以及頁面的跳轉(zhuǎn)悬襟。

未完待續(xù)...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末衅码,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子古胆,更是在濱河造成了極大的恐慌肆良,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逸绎,死亡現(xiàn)場離奇詭異惹恃,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)棺牧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門巫糙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人颊乘,你說我怎么就攤上這事参淹。” “怎么了乏悄?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵浙值,是天一觀的道長。 經(jīng)常有香客問我檩小,道長开呐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮筐付,結(jié)果婚禮上卵惦,老公的妹妹穿的比我還像新娘。我一直安慰自己瓦戚,他們只是感情好沮尿,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著较解,像睡著了一般畜疾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上哨坪,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天庸疾,我揣著相機(jī)與錄音,去河邊找鬼当编。 笑死届慈,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的忿偷。 我是一名探鬼主播金顿,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鲤桥!你這毒婦竟也來了揍拆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤茶凳,失蹤者是張志新(化名)和其女友劉穎嫂拴,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贮喧,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡筒狠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了箱沦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辩恼。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖谓形,靈堂內(nèi)的尸體忽然破棺而出灶伊,到底是詐尸還是另有隱情,我是刑警寧澤寒跳,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布聘萨,位于F島的核電站,受9級特大地震影響童太,放射性物質(zhì)發(fā)生泄漏米辐。R本人自食惡果不足惜碾牌,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望儡循。 院中可真熱鬧,春花似錦征冷、人聲如沸择膝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肴捉。三九已至,卻和暖如春叔收,著一層夾襖步出監(jiān)牢的瞬間齿穗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工饺律, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留窃页,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓复濒,卻偏偏與公主長得像脖卖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子巧颈,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容