歡迎關注個人公衆號睿Talk
,獲取我最新的文章:
javascript
使用 React 開發稍微複雜一點的應用,React Router 幾乎是路由管理的惟一選擇。雖然 React Router 經歷了 4 個大版本的更新,功能也愈來愈豐富,但不管怎麼變,它的核心依賴 history 庫卻一直沒變。下面咱們來了解下這個在 github 上有 5k+ 星的庫到底提供了什麼功能。html
聊到history庫,是否是以爲這個單詞有點熟悉?不錯,HTML5規範裏面,也新增了一個同名的history對象。下面咱們來看下這個history對象用來解決什麼問題。前端
在jQuery統治前端的年代,經過ajax請求無刷新更新頁面是當時至關流行的頁面處理方式,SPA的雛形就是那時候演化出來的。爲了標示頁面發生的變化,方便刷新後依然能顯示正確的頁面元素,通常會經過改變url的hash值來惟必定位頁面。但這會帶來另外一個問題:用戶沒法使用前進/後退來切換頁面。java
爲了解決這個問題,history對象應運而生。當頁面的url或者hash發生變化的時候,瀏覽器會自動將新的url push到history對象中。history對象內部會維護一個state數組,記錄url的變化。在瀏覽器進行前進/後退操做的時候,實際上就是調用history對象的對應方法(forward
/back
),取出對應的state,從而進行頁面的切換。git
除了操做url,history對象還提供2個不用經過操做url也能更新內部state的方法,分別是pushState
和replaceState
。還能將額外的數據存到state中,而後在onpopstate
事件中再經過event.state
取出來。若是但願對history對象做更深刻的理解,能夠參考 這裏,和這裏。github
咱們再回過頭來看history庫。它本質上作了如下4件事情:ajax
再對比一些二者api的異同。如下是history庫的:segmentfault
const history = { length, // 屬性,history中記錄的state的數量 action, // 屬性,當前導航的action類型 location, // 屬性,location對象,封裝了pathname、search和hash等屬性 push, // 方法,導航到新的路由,並記錄在history中 replace, // 方法,替換掉當前記錄在history中的路由信息 go, // 方法,前進或後退n個記錄 goBack, // 方法,後退 goForward, // 方法,前進 canGo, // 方法,是否能前進或後退n個記錄 block, // 方法,跳轉前讓用戶肯定是否要跳轉 listen // 方法,訂閱history變動事件 };
如下是HTML5 history對象的:api
const history = { length, // 屬性,history中記錄的state的數量 state, // 屬性,pushState和replaceState時傳入的對象 back, // 方法,後退 forward, // 方法,前進 go, // 方法,前進或後退n個記錄 pushState, // 方法,導航到新的路由,並記錄在history中 replaceState // 方法,替換掉當前記錄在history中的路由信息 } // 訂閱history變動事件 window.onpopstate = function (event) { ... }
從對比中能夠看出,二者的關係是很是密切的,history庫能夠說是history對象的超集,是功能更強大的history對象。數組
下面,咱們以三種history類型中的一種,hashHistory爲例,來分析下history的源碼,看看它都幹了些什麼。先看下它是怎麼處理hash變動的。
// 構造hashHistory對象 const createHashHistory = (props = {}) => { ... const globalHistory = window.history; // 引用HTML5 history對象 ... // transitionManager負責控制是否進行跳轉,以及跳轉後要通知到的訂閱者,後面會詳細討論 const transitionManager = createTransitionManager(); ... // 註冊history變動回調的訂閱者 const listen = listener => { const unlisten = transitionManager.appendListener(listener); checkDOMListeners(1); return () => { checkDOMListeners(-1); unlisten(); }; }; // 監聽hashchange事件 const checkDOMListeners = delta => { listenerCount += delta; if (listenerCount === 1) { window.addEventListener(HashChangeEvent, handleHashChange); } else if (listenerCount === 0) { window.removeEventListener(HashChangeEvent, handleHashChange); } }; // hashchange事件回調 const handleHashChange = () => { ... // 構造內部使用的location對象,包含pathname、search和hash等屬性 const location = getDOMLocation(); ... handlePop(location); }; // 處理hash變動邏輯 const handlePop = location => { ... const action = "POP"; // 給用戶展現確認跳轉的信息(若是有的話),確認後通知訂閱者。若是用戶取消跳轉,則回退到以前狀態 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => { if (ok) { setState({action, location}); // 確認後通知訂閱者 } else { revertPop(location); // 取消則回退到以前狀態 } }); }; // 更新action,location和length屬性,並通知訂閱者 const setState = nextState => { Object.assign(history, nextState); history.length = globalHistory.length; transitionManager.notifyListeners(history.location, history.action); }; ... }
以上就是處理被動的hash變動的邏輯,一句話歸納就是:訂閱hash變動事件,判斷是否確實要變動,如需變動則更新本身的屬性,通知訂閱者,不需變動則回退到以前的狀態。
下面再看下transitionManager作了什麼,重點看發佈/訂閱相關內容,忽略用戶確認跳轉相關內容。
const createTransitionManager = () => { ... // 內部維護的訂閱者列表 let listeners = []; // 註冊訂閱者 const appendListener = fn => { let isActive = true; const listener = (...args) => { if (isActive) fn(...args); }; listeners.push(listener); return () => { isActive = false; listeners = listeners.filter(item => item !== listener); }; }; //通知訂閱者 const notifyListeners = (...args) => { listeners.forEach(listener => listener(...args)); }; ... }
這裏的代碼一目瞭然,就是維護一個訂閱者列表,當hash變動的時候通知到相關的函數。
以上是hash改變的時候被動更新相關的內容,下面再看下主動更新相關的代碼,以push
爲例,replace
大同小異。
const push = (path, state) => { ... const action = "PUSH"; const location = createLocation(path, undefined, undefined, history.location); transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => { if (!ok) // 若是取消,則不跳轉 return; ... pushHashPath(encodedPath); // 用新的hash替換到url當中 ... setState({action, location}); // 更新action,location和length屬性,並通知訂閱者 }); }; // 用新的hash替換到url當中 const pushHashPath = path => (window.location.hash = path);
在瀏覽器進行前進後退操做時,history庫其實是經過操做HTML5 history對象實現的。
const globalHistory = window.history; const go = n => { ... globalHistory.go(n); }; const goBack = () => go(-1); const goForward = () => go(1);
當調用window.history.go
的時候,hash會發生變化,進而觸發hashchange事件,而後history庫再將變動通知到相關的訂閱者。
本文對React Router核心依賴history庫進行了比較深刻的介紹。從HTML5新增的history對象講起,對比了它跟history庫千絲萬縷的關係,並以hashHistory爲例子詳細分析了其代碼的實現細節。
最後,咱們再來回顧一下history庫作了哪些事情:
雖然history庫是React Router的核心依賴,但它跟React自己並無依賴關係。若是你的項目中有操做history的場景,也能夠將其引入到項目中來。