react封装函数_React-Router源码解读
目前負(fù)責(zé)的項(xiàng)目中有一個(gè)微信網(wǎng)頁(yè),用的是react技術(shù)棧。在該項(xiàng)目中增加了一個(gè)微信分享功能后,線上ios出現(xiàn)了問(wèn)題,經(jīng)排查,定位到了react的路由系統(tǒng)。
這次線上bug,讓我決定,先從react-router-dom開始,看看它內(nèi)部實(shí)現(xiàn)了什么。
前端目前用到的就是react-router-dom這個(gè)庫(kù),它提供了兩個(gè)高級(jí)路由器,分別是BrowserRouter和HashRouter,它兩的區(qū)別就是一個(gè)用的history API ,一個(gè)是使用URL的hash部分,接下來(lái)我以BrowserRouter為例,做一個(gè)解讀。
簡(jiǎn)易過(guò)程圖解讀(只摘取核心代碼進(jìn)行展示)首先看看整個(gè)react-router-dom提供了點(diǎn)啥?
export {MemoryRouter,Prompt,Redirect,Route,Router,StaticRouter,Switch,generatePath,matchPath,withRouter,useHistory,useLocation,useParams,useRouteMatch } from "react-router";export { default as BrowserRouter } from "./BrowserRouter.js"; export { default as HashRouter } from "./HashRouter.js"; export { default as Link } from "./Link.js"; export { default as NavLink } from "./NavLink.js";除了下面它自己實(shí)現(xiàn)的四個(gè)組件外,其余的都是將react-router提供的組件做了一個(gè)引入再導(dǎo)出,那看來(lái)底層核心的東西還是在react-router上。
1.先從一個(gè)簡(jiǎn)單demo開始
import { BrowserRouter, Route, Switch, Link } from "react-router-dom"function App() {return (<BrowserRouter><div>主菜單</div><Link to="/home">home</Link><br /><Link to="/search">search</Link><hr /><Switch> <Route path="/home" component={Home} /><Route path="/search" component={Search} /></Switch></BrowserRouter>) }ReactDOM.render(<App />, document.getElementById('root'));需要通過(guò)路由跳轉(zhuǎn)來(lái)實(shí)現(xiàn)UI變化的組件,要用BrowserRouter作為一個(gè)根組件來(lái)包裹起來(lái),Route用來(lái)盛放頁(yè)面級(jí)的組件。
那按照這種層級(jí)關(guān)系,我們先來(lái)看下BrowersRouter里實(shí)現(xiàn)了什么功能。
2. BrowersRouter
import { Router } from "react-router"; import { createBrowserHistory as createHistory } from "history";class BrowserRouter extends React.Component {history = createHistory(this.props);render() {return <Router history={this.history} children={this.props.children} />;} }非常少量的幾行代碼,很清晰的看到,核心點(diǎn)是history這個(gè)庫(kù)所提供的函數(shù)。組件在render前執(zhí)行了createHistory這個(gè)函數(shù),然后它會(huì)返回一個(gè)history的對(duì)象實(shí)例,然后通過(guò)props傳給Router這個(gè)路由器,另外其中包裹的所有子組件,統(tǒng)統(tǒng)傳給Router。
這里其實(shí)官網(wǎng)上已經(jīng)說(shuō)的很清楚,大家用的時(shí)候可以多留意下。
那么思路就很清楚,重點(diǎn)放在Router和history庫(kù)上,看看Router是怎么用這個(gè)history對(duì)象的,以及這個(gè)history對(duì)象里又包含了啥,和window.history有什么區(qū)別?讓我們接著往下走。
3. Router
import HistoryContext from "./HistoryContext.js"; import RouterContext from "./RouterContext.js";Router是核心的路由器,上面我們已經(jīng)看到BrowsRouter傳遞給它了一個(gè)history對(duì)象。
首先引入了兩個(gè)context,這里其實(shí)就是創(chuàng)建的普通context,只不過(guò)擁有特定的名稱而已。
它的內(nèi)部實(shí)現(xiàn)是這樣
const createNamedContext = name => {const context = createContext();context.displayName = name;return context; };// 上述的引用就相當(dāng)于 HistoryContext = createNamedContext("Router-History")引入了這兩個(gè)context后,在來(lái)看它的構(gòu)造函數(shù)。
constructor(props) {super(props);this.state = {location: props.history.location};this.unlisten = props.history.listen(location => {this.setState({ location });});}Router組件維護(hù)了一個(gè)內(nèi)部狀態(tài)location對(duì)象,初始值為上面提到的在BrowsRouter中創(chuàng)建的history提供的。
之后,執(zhí)行了history對(duì)象提供的listen函數(shù),這個(gè)函數(shù)需要一個(gè)回調(diào)函數(shù)作為入?yún)?#xff0c;傳入的回調(diào)函數(shù)的功能就是來(lái)更新當(dāng)前Router內(nèi)部狀態(tài)中的location的,關(guān)于什么時(shí)候會(huì)執(zhí)行這個(gè)回調(diào),以及l(fā)isten函數(shù),后面會(huì)詳細(xì)剖析。
componentWillUnmount() {if (this.unlisten) {this.unlisten();}}等這個(gè)Router組件將要卸載時(shí),就取消對(duì)history的監(jiān)聽。
render() {return (<RouterContext.Providervalue={{history: this.props.history,location: this.state.location,match: Router.computeRootMatch(this.state.location.pathname),staticContext: this.props.staticContext}}> <HistoryContext.Providerchildren={this.props.children || null}value={this.props.history}/> </RouterContext.Provider>);}最后生成的react樹,就是由最開始引入的context組成的,然后傳入history、location這些值。
總結(jié)就是整個(gè)Router就是一個(gè)傳入了history、locaiton和其它一些數(shù)據(jù)的context的提供者,然后它的子組件作為消費(fèi)者就可以共享使用這些數(shù)據(jù),來(lái)完成后面的路由跳轉(zhuǎn)、UI更新等動(dòng)作。
3. histroy庫(kù)
在Router組件可以看到已經(jīng)用到了createBrowserHistory函數(shù)返回的history實(shí)例了,如:history.location和history.listen,這個(gè)庫(kù)里的封裝的函數(shù)那是相當(dāng)多了,細(xì)節(jié)也很多,我仍然挑最重要的解讀。
首先是咱們這個(gè)出鏡率較高的history提供了哪些屬性和方法
看起來(lái)都是些熟悉的東西,如push、replace、go這些,都是window對(duì)象屬性history所提供的。但有些屬性其實(shí)是重寫了的,如push、replace,其它的是做了一個(gè)簡(jiǎn)單封裝。
function goBack() {go(-1);}function goForward() {go(1);}Router內(nèi)部狀態(tài)location的初始數(shù)據(jù),是使用window.location與window.history.state做的重組。
路由系統(tǒng)最為重要的兩個(gè)切換頁(yè)面動(dòng)作,一個(gè)是push,一個(gè)是replace,我們平時(shí)只用Link組件的話,并沒有確切的感受,其中Link接受一個(gè)props屬性,to :string 或者to : object
<link to='/course'>跳轉(zhuǎn)</link>
此時(shí)點(diǎn)擊它時(shí),調(diào)用的就是props.history中重寫的push方法。
<Link to='/course' replace>跳轉(zhuǎn)</Link>
如果增加replace屬性,則用的就是replace方法
這兩個(gè)方法主要用的是pushState和replaceState這兩個(gè)API,它們提供的能力就是可以增加新的window.history中的歷史記錄和瀏覽器地址欄上的url,但是又不會(huì)發(fā)起真正的網(wǎng)絡(luò)請(qǐng)求。
這是實(shí)現(xiàn)單頁(yè)面應(yīng)用的關(guān)鍵點(diǎn)。
然后讓我們看一下這兩個(gè)路由跳轉(zhuǎn)方法
精簡(jiǎn)后,代碼還是不少,我解讀下。
push中的入?yún)ath,是接下來(lái)準(zhǔn)備要跳轉(zhuǎn)的路由地址。createLocation方法先將這個(gè)path,與當(dāng)前的location做一個(gè)合并,返回一個(gè)更新的loation。
然后就是重頭戲,transitionManager這個(gè)對(duì)象,讓我們先關(guān)注下成功回調(diào)里面的內(nèi)容。
通過(guò)更新后的location,創(chuàng)建出將要跳轉(zhuǎn)的href,然后調(diào)用pushState方法,來(lái)更新window.history中的歷史記錄。
如果你在BrowserRouter中傳了forceRefresh這個(gè)屬性,那么之后就會(huì)直接修改window.lcoation.href,來(lái)實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn),但這樣就相當(dāng)于要重新刷新來(lái)進(jìn)行網(wǎng)絡(luò)請(qǐng)求你的文件資源了。
如果沒有傳的話,就是調(diào)用setState這個(gè)函數(shù),注意這個(gè)setState并不是react提供的那個(gè),而是history庫(kù)自己實(shí)現(xiàn)的。
function setState(nextState) {history.length = globalHistory.length;transitionManager.notifyListeners(history.location, history.action);}還是用到了transitionManager對(duì)象的一個(gè)方法。
另外當(dāng)我們執(zhí)行了pushState后,接下來(lái)所獲取到的window.history都是已經(jīng)更新的了。
接下來(lái)就剩transitionManager這最后的一個(gè)點(diǎn)了。
transitionManager是通過(guò)createTransitionManager這個(gè)函數(shù)實(shí)例出的一個(gè)對(duì)象
function createTransitionManager() {var listeners = [];function appendListener(fn) {var isActive = true;function listener() {if (isActive) fn.apply(void 0, arguments);}listeners.push(listener);return function () {isActive = false;listeners = listeners.filter(function (item) {return item !== listener;});};}function notifyListeners() {for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {args[_key] = arguments[_key];}listeners.forEach(function (listener) {return listener.apply(void 0, args);});}return { appendListener: appendListener, notifyListeners: notifyListeners };}還記的開始時(shí)我們?cè)赗outer組件中已經(jīng)用過(guò)一個(gè)history.listen方法,其中內(nèi)部實(shí)現(xiàn)就是用的transitionManager.appendListener方法
function listen(listener) {var unlisten = transitionManager.appendListener(listener);checkDOMListeners(1);return function () {checkDOMListeners(-1);unlisten();};}當(dāng)時(shí)我們給listen傳入了一個(gè)回調(diào)函數(shù),這個(gè)回調(diào)函數(shù)是用來(lái)通過(guò)React的setState來(lái)更新組件內(nèi)部狀態(tài)的locaton數(shù)據(jù),然后又因?yàn)檫@個(gè)lcoation傳入了Router-context的value中,所以當(dāng)它發(fā)生變化時(shí),所有的消費(fèi)組件,都會(huì)重新render,以此來(lái)達(dá)到更新UI的目的。
listen的執(zhí)行細(xì)節(jié)是,把它的入?yún)⒑瘮?shù)(這里指更新Rrouter的state.location的函數(shù))會(huì)傳入到appendListener中。
執(zhí)行appendListener后,appendListener將這個(gè)入?yún)⒑瘮?shù)推到listeners這個(gè)數(shù)組中,保存起來(lái)。然后返回一個(gè)函數(shù)用來(lái)刪除掉推進(jìn)該數(shù)組的那個(gè)函數(shù),以此來(lái)實(shí)現(xiàn)取消監(jiān)聽的功能。
所以當(dāng)我們使用push,切換路由時(shí),它會(huì)執(zhí)行notifyListeners并傳入更新的location。
然后就是遍歷listeners,執(zhí)行我們?cè)趌isten傳入的回調(diào),此時(shí)就是最終的去更新Router的location的過(guò)程了。
后面的流程,簡(jiǎn)單說(shuō)下,Router里面的Route組件通過(guò)匹配pathname 和 更新的location ,來(lái)決定是否渲染該頁(yè)面組件,到此整個(gè)的路由跳轉(zhuǎn)的過(guò)程就結(jié)束了。
總結(jié)
第一次閱讀源碼,盡管刪減了很多,但還是寫了不少。
希望大家可以沿著這個(gè)思路,自己也去看看,還是有很多細(xì)節(jié)值得推敲的。
總結(jié)
以上是生活随笔為你收集整理的react封装函数_React-Router源码解读的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 有向图的强连通图——Kosaraju
- 下一篇: 手机远程开电脑6不6手机能远程开电脑吗