若是要你實現一個前端路由,應該如何實現瀏覽器的前進與後退 ?前端
首先瀏覽器中主要有這幾個限制,讓前端不能隨意的操做瀏覽器的瀏覽紀錄:程序員
•沒有提供監聽前進後退的事件。•不容許開發者讀取瀏覽紀錄,也就是 js 讀取不了瀏覽紀錄。•用戶能夠手動輸入地址,或使用瀏覽器提供的前進後退來改變 url。web
因此要實現一個自定義路由,解決方案是本身維護一份路由歷史的記錄,從而區分 前進、刷新、回退。算法
下面介紹具體的方法。數組
目前筆者知道的方法有兩種,一種是 在數組後面進行增長與刪除,另一種是 利用棧的後進先出原理。瀏覽器
我本身是一名從事了多年開發的web前端老程序員,目前辭職在作本身的web前端私人定製課程,今年年初我花了一個月整理了一份最適合2019年學習的web前端學習乾貨,各類框架都有整理,送給每一位前端小夥伴,想要獲取的能夠關注我並添加個人web前端交流裙【六零零】+【六一零】+【一五一】,便可免費獲取。緩存
經過監聽路由的變化事件 hashchange,與路由的第一次加載事件 load ,判斷以下狀況:session
•url 存在於瀏覽記錄中即爲後退,後退時,把當前路由後面的瀏覽記錄刪除。•url 不存在於瀏覽記錄中即爲前進,前進時,往數組裏面 push 當前的路由。•url 在瀏覽記錄的末端即爲刷新,刷新時,不對路由數組作任何操做。數據結構
另外,應用的路由路徑中可能容許相同的路由出現屢次(例如 A -> B -> A),因此給每一個路由添加一個 key 值來區分相同路由的不一樣實例。框架
注意:這個瀏覽記錄須要存儲在 sessionStorage 中,這樣用戶刷新後瀏覽記錄也能夠恢復。
筆者以前實現的 用原生 js 實現的輕量級路由 ,就是用這種方法實現的,具體代碼以下:
// 路由構造函數function Router() { this.routes = {}; //保存註冊的全部路由 this.routerViewId = "#routerView"; // 路由掛載點 this.stackPages = true; // 多級頁面緩存 this.history = []; // 路由歷史} Router.prototype = { init: function(config) { var self = this; //頁面首次加載 匹配路由 window.addEventListener('load', function(event) { // console.log('load', event); self.historyChange(event) }, false) //路由切換 window.addEventListener('hashchange', function(event) { // console.log('hashchange', event); self.historyChange(event) }, false) }, // 路由歷史紀錄變化 historyChange: function(event) { var currentHash = util.getParamsUrl(); var nameStr = "router-history" this.history = window.sessionStorage[nameStr] ? JSON.parse(window.sessionStorage[nameStr]) : [] var back = false, // 後退 refresh = false, // 刷新 forward = false, // 前進 index = 0, len = this.history.length; // 比較當前路由的狀態,得出是後退、前進、刷新的狀態。 for (var i = 0; i < len; i++) { var h = this.history[i]; if (h.hash === currentHash.path && h.key === currentHash.query.key) { index = i if (i === len - 1) { refresh = true } else { back = true } break; } else { forward = true } } if (back) { // 後退,把歷史紀錄的最後一項刪除 this.historyFlag = 'back' this.history.length = index + 1 } else if (refresh) { // 刷新,不作其餘操做 this.historyFlag = 'refresh' } else { // 前進,添加一條歷史紀錄 this.historyFlag = 'forward' var item = { key: currentHash.query.key, hash: currentHash.path, query: currentHash.query } this.history.push(item) } // 若是不須要頁面緩存功能,每次都是刷新操做 if (!this.stackPages) { this.historyFlag = 'forward' } window.sessionStorage[nameStr] = JSON.stringify(this.history) }, }
在說第二個方法以前,先來弄明白棧的定義與後進者先出,先進者後出原理。
棧的特色:後進者先出,先進者後出。
舉一個生活中的例子說明:就是一摞疊在一塊兒的盤子。咱們平時放盤子的時候,都是從下往上一個一個放;取的時候,咱們也是從上往下一個一個地依次取,不能從中間任意抽出。
由於棧的後進者先出,先進者後出的特色,因此只能棧一端進行插入和刪除操做。這也和第一個方法的原理有殊途同歸之妙。
下面用 JavaScript 來實現一個順序棧:
// 基於數組實現的順序棧class ArrayStack { constructor(n) { this.items = []; // 數組 this.count = 0; // 棧中元素個數 this.n = n; // 棧的大小 } // 入棧操做 push(item) { // 數組空間不夠了,直接返回 false,入棧失敗。 if (this.count === this.n) return false; // 將 item 放到下標爲 count 的位置,而且 count 加一 this.items[this.count] = item; ++this.count; return true; } // 出棧操做 pop() { // 棧爲空,則直接返回 null if (this.count == 0) return null; // 返回下標爲 count-1 的數組元素,而且棧中元素個數 count 減一 let tmp = items[this.count-1]; --this.count; return tmp; }}
其實 JavaScript 中,數組是自動擴容的,並不須要指定數組的大小,也就是棧的大小 n 能夠不指定的。
棧的經典應用: 函數調用棧
操做系統給每一個線程分配了一塊獨立的內存空間,這塊內存被組織成「棧」這種結構, 用來存儲函數調用時的臨時變量。每進入一個函數,就會將臨時變量做爲一個棧幀入棧,當被調用函數執行完成,返回以後,將這個函數對應的棧幀出棧。爲了讓你更好地理解,咱們一塊來看下這段代碼的執行過程。
function add(x, y) { let sum = 0; sum = x + y; return sum;} function main() { let a = 1; let ret = 0; let res = 0; ret = add(3, 5); res = a + ret; console.log("res: ", res); reuturn 0;}
上面代碼也很簡單,就是執行 main 函數求和,main 函數裏面又調用了 add 函數,先調用的先進入棧。
執行過程以下:
第二個方法就是:用兩個棧實現瀏覽器的前進、後退功能。
咱們使用兩個棧,X 和 Y,咱們把首次瀏覽的頁面依次壓入棧 X,當點擊後退按鈕時,再依次從棧 X 中出棧,並將出棧的數據依次放入棧 Y。當咱們點擊前進按鈕時,咱們依次從棧 Y 中取出數據,放入棧 X 中。當棧 X 中沒有數據時,那就說明沒有頁面能夠繼續後退瀏覽了。當棧 Y 中沒有數據,那就說明沒有頁面能夠點擊前進按鈕瀏覽了。
好比你順序查看了 a,b,c 三個頁面,咱們就依次把 a,b,c 壓入棧,這個時候,兩個棧的數據以下:
當你經過瀏覽器的後退按鈕,從頁面 c 後退到頁面 a 以後,咱們就依次把 c 和 b 從棧 X 中彈出,而且依次放入到棧 Y。這個時候,兩個棧的數據就是這個樣子:
這個時候你又想看頁面 b,因而你又點擊前進按鈕回到 b 頁面,咱們就把 b 再從棧 Y 中出棧,放入棧 X 中。此時兩個棧的數據是這個樣子:
這個時候,你經過頁面 b 又跳轉到新的頁面 d 了,頁面 c 就沒法再經過前進、後退按鈕重複查看了,因此須要清空棧 Y。此時兩個棧的數據這個樣子:
若是用代碼來實現,會是怎樣的呢 ?各位能夠想一下。
其實就是在第一個方法的代碼裏面, 添加多一份路由歷史紀錄的數組便可,對這兩份歷史紀錄的操做如上面示例圖所示便可,也就是對數組的增長和刪除操做而已, 這裏就不展開了。
其中第二個方法與參考了 王爭老師的 數據結構與算法之美。