亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

react-router v4.x 源碼拾遺1

Joyven / 2551人閱讀

摘要:還是先來一段官方的基礎(chǔ)使用案例,熟悉一下整體的代碼流程中使用了端常用到的等一些常用組件,作為的頂層組件來獲取的和設(shè)置回調(diào)函數(shù)來更新。

react-router是react官方推薦并參與維護(hù)的一個路由庫,支持瀏覽器端、app端、服務(wù)端等常見場景下的路由切換功能,react-router本身不具備切換和跳轉(zhuǎn)路由的功能,這些功能全部由react-router依賴的history庫完成,history庫通過對url的監(jiān)聽來觸發(fā) Router 組件注冊的回調(diào),回調(diào)函數(shù)中會獲取最新的url地址和其他參數(shù)然后通過setState更新,從而使整個應(yīng)用進(jìn)行rerender。所以react-router本身只是封裝了業(yè)務(wù)上的眾多功能性組件,比如Route、Link、Redirect 等等,這些組件通過context api可以獲取到Router傳遞history api,比如push、replace等,從而完成頁面的跳轉(zhuǎn)。
還是先來一段react-router官方的基礎(chǔ)使用案例,熟悉一下整體的代碼流程

import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";

function BasicExample() {
  return (
    
      
  • Home
  • About
  • Topics

); } function Home() { return (

Home

); } function About() { return (

About

); } function Topics({ match }) { return (

Topics

  • Rendering with React
  • Components
  • Props v. State

Please select a topic.

} />
); } function Topic({ match }) { return (

{match.params.topicId}

); } export default BasicExample;

Demo中使用了web端常用到的BrowserRouter、Route、Link等一些常用組件,Router作為react-router的頂層組件來獲取 history 的api 和 設(shè)置回調(diào)函數(shù)來更新state。這里引用的組件都是來自react-router-dom 這個庫,那么react-router 和 react-router-dom 是什么關(guān)系呢。
說的簡單一點(diǎn),react-router-dom 是對react-router所有組件或方法的一個二次導(dǎo)出,并且在react-router組件的基礎(chǔ)上添加了新的組件,更加方便開發(fā)者處理復(fù)雜的應(yīng)用業(yè)務(wù)。

1.react-router 導(dǎo)出的所有內(nèi)容

統(tǒng)計一下,總共10個方法
1.MemoryRouter.js、2.Prompt.js、3.Redirect.js、4.Route.js、5.Router.js、6.StaticRouter.js、7.Switch.js、8.generatePath.js、9.matchPath.js、10.withRouter.js

2.react-router-dom 導(dǎo)出的所有內(nèi)容

統(tǒng)計一下,總共14個方法
1.BrowserRouter.js、2.HashRouter.js、3.Link.js、4.MemoryRouter.js、5.NavLink.js、6.Prompt.js、7.Redirect.js、8.Route.js、9.Router.js、10.StaticRouter.js、11.Switch.js、12.generatePath.js、13.matchPath.js、14.withRouter.js
react-router-dom在react-router的10個方法上,又添加了4個方法,分別是BrowserRouter、HashRouter、Link、以及NavLink。
所以,react-router-dom將react-router的10個方法引入后,又加入了4個方法,再重新導(dǎo)出,在開發(fā)中我們只需要引入react-router-dom這個依賴即可。

下面進(jìn)入react-router-dom的源碼分析階段,首先來看一下react-router-dom的依賴庫

React, 要求版本大于等于15.x

history, react-router的核心依賴庫,注入組件操作路由的api

invariant, 用來拋出異常的工具庫

loose-envify, 使用browserify工具進(jìn)行打包的時候,會將項(xiàng)目當(dāng)中的node全局變量替換為對應(yīng)的字符串

prop-types, react的props類型校驗(yàn)工具庫

react-router, 依賴同版本的react-router

warning, 控制臺打印警告信息的工具庫

①.BrowserRouter.js, 提供了HTML5的history api 如pushState、replaceState等來切換地址,源碼如下

import warning from "warning";
import React from "react";
import PropTypes from "prop-types";
import { createBrowserHistory as createHistory } from "history";
import Router from "./Router";

/**
 * The public API for a  that uses HTML5 history.
 */
class BrowserRouter extends React.Component {
  static propTypes = {
    basename: PropTypes.string, // 當(dāng)應(yīng)用為某個子應(yīng)用時,添加的地址欄前綴
    forceRefresh: PropTypes.bool, // 切換路由時,是否強(qiáng)制刷新
    getUserConfirmation: PropTypes.func, // 使用Prompt組件時 提示用戶的confirm確認(rèn)方法,默認(rèn)使用window.confirm
    keyLength: PropTypes.number, // 為了實(shí)現(xiàn)block功能,react-router維護(hù)創(chuàng)建了一個訪問過的路由表,每個key代表一個曾經(jīng)訪問過的路由地址
    children: PropTypes.node // 子節(jié)點(diǎn)
  };
  // 核心api, 提供了push replace go等路由跳轉(zhuǎn)方法
  history = createHistory(this.props); 
  // 提示用戶 BrowserRouter不接受用戶自定義的history方法,
  // 如果傳遞了history會被忽略,如果用戶使用自定義的history api,
  // 需要使用 Router 組件進(jìn)行替代
  componentWillMount() {
    warning(
      !this.props.history,
      " ignores the history prop. To use a custom history, " +
        "use `import { Router }` instead of `import { BrowserRouter as Router }`."
    );
  }
  // 將history和children作為props傳遞給Router組件 并返回
  render() {
    return ;
  }
}

export default BrowserRouter;

**總結(jié):BrowserRouter組件非常簡單,它本身其實(shí)就是對Router組件的一個包裝,將HTML5的history api封裝好再賦予 Router 組件。BrowserRouter就好比一個容器組件,由它來決定Router的最終api,這樣一個Router組件就可以完成多種api的實(shí)現(xiàn),比如HashRouter、StaticRouter 等,減少了代碼的耦合度
②. Router.js, 如果說BrowserRouter是Router的容器組件,為Router提供了html5的history api的數(shù)據(jù)源,那么Router.js 亦可以看作是子節(jié)點(diǎn)的容器組件,它除了接收BrowserRouter提供的history api,最主要的功能就是組件本身會響應(yīng)地址欄的變化進(jìn)行setState進(jìn)而完成react本身的rerender,使應(yīng)用進(jìn)行相應(yīng)的UI切換,源碼如下**

import warning from "warning";
import invariant from "invariant";
import React from "react";
import PropTypes from "prop-types";

/**
 * The public API for putting history on context.
 */
class Router extends React.Component {
    // react-router 4.x依然使用的使react舊版的context API
    // react-router 5.x將會作出升級
  static propTypes = {
    history: PropTypes.object.isRequired,
    children: PropTypes.node
  };
  // 此處是為了能夠接收父級容器傳遞的context router,不過父級很少有傳遞router的
  // 存在的目的是為了方便用戶使用這種潛在的方式,來傳遞自定義的router對象
  static contextTypes = {
    router: PropTypes.object
  };
  // 傳遞給子組件的context api router, 可以通過context上下文來獲得
  static childContextTypes = {
    router: PropTypes.object.isRequired
  };
  // router 對象的具體值
  getChildContext() {
    return {
      router: {
        ...this.context.router,
        history: this.props.history, // 路由api等,會在history庫進(jìn)行講解
        route: {
          location: this.props.history.location, // 也是history庫中的內(nèi)容
          match: this.state.match // 對當(dāng)前地址進(jìn)行匹配的結(jié)果
        }
      }
    };
  }
  // Router組件的state,作為一個頂層容器組件維護(hù)的state,存在兩個目的
  // 1.主要目的為了實(shí)現(xiàn)自上而下的rerender,url改變的時候match對象會被更新
  // 2.Router組件是始終會被渲染的組件,match對象會隨時得到更新,并經(jīng)過context api
  // 傳遞給下游子組件route等
  state = {
    match: this.computeMatch(this.props.history.location.pathname)
  };
  // match 的4個參數(shù)
  // 1.path: 是要進(jìn)行匹配的路徑可以是 "/user/:id" 這種動態(tài)路由的模式
  // 2.url: 地址欄實(shí)際的匹配結(jié)果
  // 3.parmas: 動態(tài)路由所匹配到的參數(shù),如果path是 "/user/:id"匹配到了,那么
  // params的內(nèi)容就是 {id: 某個值}
  // 4.isExact: 精準(zhǔn)匹配即 地址欄的pathname 和 正則匹配到url是否完全相等
  computeMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  }

  componentWillMount() {
    const { children, history } = this.props;
    // 當(dāng) 子節(jié)點(diǎn)并非由一個根節(jié)點(diǎn)包裹時 拋出錯誤提示開發(fā)者
    invariant(
      children == null || React.Children.count(children) === 1,
      "A  may have only one child element"
    );

    // Do this here so we can setState when a  changes the
    // location in componentWillMount. This happens e.g. when doing
    // server rendering using a .
    // 使用history.listen方法,在Router被實(shí)例化時注冊一個回調(diào)事件,
    // 即location地址發(fā)生改變的時候,會重新setState,進(jìn)而rerender
    // 這里使用willMount而不使用didMount的原因時是因?yàn)?,服?wù)端渲染時不存在dom,
    // 故不會調(diào)用didMount的鉤子,react將在17版本移除此鉤子,那么到時候router應(yīng)該如何實(shí)現(xiàn)此功能?
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }
   // history參數(shù)不允許被更改
  componentWillReceiveProps(nextProps) {
    warning(
      this.props.history === nextProps.history,
      "You cannot change "
    );
  }
  // 組件銷毀時 解綁history對象中的監(jiān)聽事件
  componentWillUnmount() {
    this.unlisten();
  }
  // render的時候使用React.Children.only方法再驗(yàn)證一次
  // children 必須是一個由根節(jié)點(diǎn)包裹的組件或dom
  render() {
    const { children } = this.props;
    return children ? React.Children.only(children) : null;
  }
}

export default Router;

總結(jié):Router組件職責(zé)很清晰就是作為容器組件,將上層組件的api進(jìn)行向下的傳遞,同時組件本身注冊了回調(diào)方法,來滿足瀏覽器環(huán)境下或者服務(wù)端環(huán)境下location發(fā)生變化時,重新setState,達(dá)到組件的rerender。那么history對象到底是怎么實(shí)現(xiàn)對地址欄進(jìn)行監(jiān)聽的,又是如何對location進(jìn)行push 或者 replace的,這就要看history這個庫做了啥。

createBrowserHistory.js 使用html5 history api封裝的路由控制器

createHashHistory.js 使用hash方法封裝的路由控制器

createMemoryHistory.js 針對native app這種原生應(yīng)用封裝的路由控制器,即在內(nèi)存中維護(hù)一份路由表

createTransitionManager.js 針對路由切換時的相同操作抽離的一個公共方法,路由切換的操作器,攔截器和訂閱者都存在于此

DOMUtils.js 針對web端dom操作或判斷兼容性的一個工具方法集合

LocationUtils.js 針對location url處理等抽離的一個工具方法的集合

PathUtils.js 用來處理url路徑的工具方法集合

這里主要分析createBrowserHistory.js文件

import warning from "warning"
import invariant from "invariant"
import { createLocation } from "./LocationUtils"
import {
  addLeadingSlash,
  stripTrailingSlash,
  hasBasename,
  stripBasename,
  createPath
} from "./PathUtils"
import createTransitionManager from "./createTransitionManager"
import {
  canUseDOM,
  addEventListener,
  removeEventListener,
  getConfirmation,
  supportsHistory,
  supportsPopStateOnHashChange,
  isExtraneousPopstateEvent
} from "./DOMUtils"

const PopStateEvent = "popstate"
const HashChangeEvent = "hashchange"

const getHistoryState = () => {
  // ...
}

/**
 * Creates a history object that uses the HTML5 history API including
 * pushState, replaceState, and the popstate event.
 */
const createBrowserHistory = (props = {}) => {
  invariant(
    canUseDOM,
    "Browser history needs a DOM"
  )

  const globalHistory = window.history
  const canUseHistory = supportsHistory()
  const needsHashChangeListener = !supportsPopStateOnHashChange()

  const {
    forceRefresh = false,
    getUserConfirmation = getConfirmation,
    keyLength = 6
  } = props
  const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : ""

  const getDOMLocation = (historyState) => {
     // ...
  }

  const createKey = () =>
    Math.random().toString(36).substr(2, keyLength)

  const transitionManager = createTransitionManager()

  const setState = (nextState) => {
     // ...
  }

  const handlePopState = (event) => {
    // ...
  }

  const handleHashChange = () => {
    // ...
  }

  let forceNextPop = false

  const handlePop = (location) => {
     // ...
  }

  const revertPop = (fromLocation) => {
    // ...
  }

  const initialLocation = getDOMLocation(getHistoryState())
  let allKeys = [ initialLocation.key ]

  // Public interface

  const createHref = (location) =>
    basename + createPath(location)

  const push = (path, state) => {
    // ...
  }

  const replace = (path, state) => {
    // ...
  }

  const go = (n) => {
    globalHistory.go(n)
  }

  const goBack = () =>
    go(-1)

  const goForward = () =>
    go(1)

  let listenerCount = 0

  const checkDOMListeners = (delta) => {
    // ...
  }

  let isBlocked = false

  const block = (prompt = false) => {
    // ...
  }

  const listen = (listener) => {
    // ...
  }

  const history = {
    length: globalHistory.length,
    action: "POP",
    location: initialLocation,
    createHref,
    push,
    replace,
    go,
    goBack,
    goForward,
    block,
    listen
  }

  return history
}

export default createBrowserHistory

createBrowserHistory.js 總共300+行代碼,其原理就是封裝了原生的html5 的history api,如pushState,replaceState,當(dāng)這些事件被觸發(fā)時會激活subscribe的回調(diào)來進(jìn)行響應(yīng)。同時也會對地址欄進(jìn)行監(jiān)聽,當(dāng)history.go等事件觸發(fā)history popstate事件時,也會激活subscribe的回調(diào)。

由于代碼量較多,而且依賴的方法較多,這里將方法分成幾個小節(jié)來進(jìn)行梳理,對于依賴的方法先進(jìn)行簡短闡述,當(dāng)實(shí)際調(diào)用時在深入源碼內(nèi)部去探究實(shí)現(xiàn)細(xì)節(jié)

1. 依賴的工具方法

import warning from "warning"  // 控制臺的console.warn警告
import invariant from "invariant" // 用來拋出異常錯誤信息
// 對地址參數(shù)處理,最終返回一個對象包含 pathname,search,hash,state,key 等參數(shù)
import { createLocation } from "./LocationUtils" 
import { 
  addLeadingSlash,  // 對傳遞的pathname添加首部`/`,即 "home" 處理為 "/home",存在首部`/`的不做處理
  stripTrailingSlash,  // 對傳遞的pathname去掉尾部的 `/`
  hasBasename, // 判斷是否傳遞了basename參數(shù)
  stripBasename, // 如果傳遞了basename參數(shù),那么每次需要將pathname中的basename統(tǒng)一去除
  createPath // 將location對象的參數(shù)生成最終的地址欄路徑
} from "./PathUtils"
import createTransitionManager from "./createTransitionManager" // 抽離的路由切換的公共方法
import {
  canUseDOM,  // 當(dāng)前是否可使用dom, 即window對象是否存在,是否是瀏覽器環(huán)境下
  addEventListener, // 兼容ie 監(jiān)聽事件
  removeEventListener, // 解綁事件
  getConfirmation,   // 路由跳轉(zhuǎn)的comfirm 回調(diào),默認(rèn)使用window.confirm
  supportsHistory, // 當(dāng)前環(huán)境是否支持history的pushState方法
  supportsPopStateOnHashChange, // hashChange是否會觸發(fā)h5的popState方法,ie10、11并不會
  isExtraneousPopstateEvent // 判斷popState是否時真正有效的
} from "./DOMUtils"

const PopStateEvent = "popstate"  // 針對popstate事件的監(jiān)聽
const HashChangeEvent = "hashchange" // 針對不支持history api的瀏覽器 啟動hashchange監(jiān)聽事件

// 返回history的state
const getHistoryState = () => {
  try {
    return window.history.state || {}
  } catch (e) {
    // IE 11 sometimes throws when accessing window.history.state
    // See https://github.com/ReactTraining/history/pull/289
    // IE11 下有時會拋出異常,此處保證state一定返回一個對象
    return {} 
  }
}

creareBrowserHistory的具體實(shí)現(xiàn)

const createBrowserHistory = (props = {}) => {
  // 當(dāng)不在瀏覽器環(huán)境下直接拋出錯誤
  invariant(
    canUseDOM,
    "Browser history needs a DOM"
  )

  const globalHistory = window.history          // 使用window的history
  // 此處注意android 2. 和 4.0的版本并且ua的信息是 mobile safari 的history api是有bug且無法解決的
  const canUseHistory = supportsHistory()      
  // hashChange的時候是否會進(jìn)行popState操作,ie10、11不會進(jìn)行popState操作 
  const needsHashChangeListener = !supportsPopStateOnHashChange()

  const {
    forceRefresh = false,                     // 默認(rèn)切換路由不刷新
    getUserConfirmation = getConfirmation,    // 使用window.confirm
    keyLength = 6                             // 默認(rèn)6位長度隨機(jī)key
  } = props
  // addLeadingSlash 添加basename頭部的斜杠
  // stripTrailingSlash 去掉 basename 尾部的斜杠
  // 如果basename存在的話,保證其格式為 ‘/xxx’
  const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : ""

  const getDOMLocation = (historyState) => {
       // 獲取history對象的key和state
    const { key, state } = (historyState || {})
     // 獲取當(dāng)前路徑下的pathname,search,hash等參數(shù)
    const { pathname, search, hash } = window.location 
      // 拼接一個完整的路徑
    let path = pathname + search + hash               

    // 當(dāng)傳遞了basename后,所有的pathname必須包含這個basename
    warning(
      (!basename || hasBasename(path, basename)),
      "You are attempting to use a basename on a page whose URL path does not begin " +
      "with the basename. Expected path "" + path + "" to begin with "" + basename + ""."
    )
    
    // 去掉path當(dāng)中的basename
    if (basename)
      path = stripBasename(path, basename)
    
    // 生成一個自定義的location對象
    return createLocation(path, state, key)
  }

  // 使用6位長度的隨機(jī)key
  const createKey = () =>
    Math.random().toString(36).substr(2, keyLength)

  // transitionManager是history中最復(fù)雜的部分,復(fù)雜的原因是因?yàn)?  // 為了實(shí)現(xiàn)block方法,做了對路由攔截的hack,雖然能實(shí)現(xiàn)對路由切時的攔截功能
  // 比如Prompt組件,但同時也帶來了不可解決的bug,后面在討論
  // 這里返回一個對象包含 setPrompt、confirmTransitionTo、appendListener
  // notifyListeners 等四個方法
  const transitionManager = createTransitionManager()
  
  const setState = (nextState) => {
    // nextState包含最新的 action 和 location
    // 并將其更新到導(dǎo)出的 history 對象中,這樣Router組件相應(yīng)的也會得到更新
    // 可以理解為同react內(nèi)部所做的setState時相同的功能
    Object.assign(history, nextState)
    // 更新history的length, 實(shí)實(shí)保持和window.history.length 同步
    history.length = globalHistory.length
    // 通知subscribe進(jìn)行回調(diào)
    transitionManager.notifyListeners(
      history.location,
      history.action
    )
  }
  // 當(dāng)監(jiān)聽到popState事件時進(jìn)行的處理
  const handlePopState = (event) => {
    // Ignore extraneous popstate events in WebKit.
    if (isExtraneousPopstateEvent(event))
      return 
    // 獲取當(dāng)前地址欄的history state并傳遞給getDOMLocation
    // 返回一個新的location對象
    handlePop(getDOMLocation(event.state))
  }

  const handleHashChange = () => {
      // 監(jiān)聽到hashchange時進(jìn)行的處理,由于hashchange不會更改state
      // 故此處不需要更新location的state
    handlePop(getDOMLocation(getHistoryState()))
  }
   // 用來判斷路由是否需要強(qiáng)制
  let forceNextPop = false
   // handlePop是對使用go方法來回退或者前進(jìn)時,對頁面進(jìn)行的更新,正常情況下來說沒有問題
   // 但是如果頁面使用Prompt,即路由攔截器。當(dāng)點(diǎn)擊回退或者前進(jìn)就會觸發(fā)histrory的api,改變了地址欄的路徑
   // 然后彈出需要用戶進(jìn)行確認(rèn)的提示框,如果用戶點(diǎn)擊確定,那么沒問題因?yàn)榈刂窓诟淖兊牡刂肪褪菍⒁D(zhuǎn)到地址
   // 但是如果用戶選擇了取消,那么地址欄的路徑已經(jīng)變成了新的地址,但是頁面實(shí)際還停留再之前,這就產(chǎn)生了bug
   // 這也就是 revertPop 這個hack的由來。因?yàn)轫撁娴奶D(zhuǎn)可以由程序控制,但是如果操作的本身是瀏覽器的前進(jìn)后退
   // 按鈕,那么是無法做到真正攔截的。
  const handlePop = (location) => {
    if (forceNextPop) {
      forceNextPop = false
      setState()
    } else {
      const action = "POP"

      transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
        if (ok) {
          setState({ action, location })
        } else {
            // 當(dāng)攔截器返回了false的時候,需要把地址欄的路徑重置為當(dāng)前頁面顯示的地址
          revertPop(location)
        }
      })
    }
  }
   // 這里是react-router的作者最頭疼的一個地方,因?yàn)殡m然用hack實(shí)現(xiàn)了表面上的路由攔截
   // ,但也會引起一些特殊情況下的bug。這里先說一下如何做到的假裝攔截,因?yàn)楸旧韍tml5 history
   // api的特性,pushState 這些操作不會引起頁面的reload,所有做到攔截只需要不手懂調(diào)用setState頁面不進(jìn)行render即可
   // 當(dāng)用戶選擇了取消后,再將地址欄中的路徑變?yōu)楫?dāng)前頁面的顯示路徑即可,這也是revertPop實(shí)現(xiàn)的方式
   // 這里貼出一下對這個bug的討論:https://github.com/ReactTraining/history/issues/690
  const revertPop = (fromLocation) => {
      // fromLocation 當(dāng)前地址欄真正的路徑,而且這個路徑一定是存在于history歷史
      // 記錄當(dāng)中某個被訪問過的路徑,因?yàn)槲覀冃枰獙⒌刂窓诘倪@個路徑重置為頁面正在顯示的路徑地址
      // 頁面顯示的這個路徑地址一定是還再history.location中的那個地址
      // fromLoaction 用戶原本想去但是后來又不去的那個地址,需要把他換位history.location當(dāng)中的那個地址      
    const toLocation = history.location

    // TODO: We could probably make this more reliable by
    // keeping a list of keys we"ve seen in sessionStorage.
    // Instead, we just default to 0 for keys we don"t know.
     // 取出toLocation地址再allKeys中的下標(biāo)位置
    let toIndex = allKeys.indexOf(toLocation.key)

    if (toIndex === -1)
      toIndex = 0
     // 取出formLoaction地址在allKeys中的下標(biāo)位置
    let fromIndex = allKeys.indexOf(fromLocation.key)

    if (fromIndex === -1)
      fromIndex = 0
     // 兩者進(jìn)行相減的值就是go操作需要回退或者前進(jìn)的次數(shù)
    const delta = toIndex - fromIndex
     // 如果delta不為0,則進(jìn)行地址欄的變更 將歷史記錄重定向到當(dāng)前頁面的路徑   
    if (delta) {
      forceNextPop = true // 將forceNextPop設(shè)置為true
      // 更改地址欄的路徑,又會觸發(fā)handlePop 方法,此時由于forceNextPop已經(jīng)為true則會執(zhí)行后面的
      // setState方法,對當(dāng)前頁面進(jìn)行rerender,注意setState是沒有傳遞參數(shù)的,這樣history當(dāng)中的
      // location對象依然是之前頁面存在的那個loaction,不會改變history的location數(shù)據(jù)
      go(delta) 
    }
  }

  // 返回一個location初始對象包含
  // pathname,search,hash,state,key key有可能是undefined
  const initialLocation = getDOMLocation(getHistoryState())
  let allKeys = [ initialLocation.key ]

  // Public interface

  // 拼接上basename
  const createHref = (location) =>
    basename + createPath(location)

  const push = (path, state) => {
    warning(
      !(typeof path === "object" && path.state !== undefined && state !== undefined),
      "You should avoid providing a 2nd state argument to push when the 1st " +
      "argument is a location-like object that already has state; it is ignored"
    )

    const action = "PUSH"
    const location = createLocation(path, state, createKey(), history.location)

    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
      if (!ok)
        return

      const href = createHref(location)  // 拼接basename
      const { key, state } = location

      if (canUseHistory) {
        globalHistory.pushState({ key, state }, null, href) // 只是改變地址欄路徑 此時頁面不會改變

        if (forceRefresh) {
          window.location.href = href // 強(qiáng)制刷新
        } else {
          const prevIndex = allKeys.indexOf(history.location.key) // 上次訪問的路徑的key
          const nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1)

          nextKeys.push(location.key) // 維護(hù)一個訪問過的路徑的key的列表
          allKeys = nextKeys

          setState({ action, location }) // render頁面
        }
      } else {
        warning(
          state === undefined,
          "Browser history cannot push state in browsers that do not support HTML5 history"
        )

        window.location.href = href
      }
    })
  }

  const replace = (path, state) => {
    warning(
      !(typeof path === "object" && path.state !== undefined && state !== undefined),
      "You should avoid providing a 2nd state argument to replace when the 1st " +
      "argument is a location-like object that already has state; it is ignored"
    )

    const action = "REPLACE"
    const location = createLocation(path, state, createKey(), history.location)

    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
      if (!ok)
        return

      const href = createHref(location)
      const { key, state } = location

      if (canUseHistory) {
        globalHistory.replaceState({ key, state }, null, href)

        if (forceRefresh) {
          window.location.replace(href)
        } else {
          const prevIndex = allKeys.indexOf(history.location.key)

          if (prevIndex !== -1)
            allKeys[prevIndex] = location.key

          setState({ action, location })
        }
      } else {
        warning(
          state === undefined,
          "Browser history cannot replace state in browsers that do not support HTML5 history"
        )

        window.location.replace(href)
      }
    })
  }

  const go = (n) => {
    globalHistory.go(n)
  }

  const goBack = () =>
    go(-1)

  const goForward = () =>
    go(1)

  let listenerCount = 0
   // 防止重復(fù)注冊監(jiān)聽,只有l(wèi)istenerCount == 1的時候才會進(jìn)行監(jiān)聽事件
  const checkDOMListeners = (delta) => {
    listenerCount += delta

    if (listenerCount === 1) {
      addEventListener(window, PopStateEvent, handlePopState)

      if (needsHashChangeListener)
        addEventListener(window, HashChangeEvent, handleHashChange)
    } else if (listenerCount === 0) {
      removeEventListener(window, PopStateEvent, handlePopState)

      if (needsHashChangeListener)
        removeEventListener(window, HashChangeEvent, handleHashChange)
    }
  }
  // 默認(rèn)情況下不會阻止路由的跳轉(zhuǎn)
  let isBlocked = false
  // 這里的block方法專門為Prompt組件設(shè)計,開發(fā)者可以模擬對路由的攔截
  const block = (prompt = false) => {
      // prompt 默認(rèn)為false, prompt可以為string或者func
      // 將攔截器的開關(guān)打開,并返回可關(guān)閉攔截器的方法
    const unblock = transitionManager.setPrompt(prompt)
      // 監(jiān)聽事件只會當(dāng)攔截器開啟時被注冊,同時設(shè)置isBlock為true,防止多次注冊
    if (!isBlocked) {
      checkDOMListeners(1)
      isBlocked = true
    }
     // 返回關(guān)閉攔截器的方法
    return () => {
      if (isBlocked) {
        isBlocked = false
        checkDOMListeners(-1)
      }

      return unblock()
    }
  }

  const listen = (listener) => {
    const unlisten = transitionManager.appendListener(listener) // 添加訂閱者
    checkDOMListeners(1) // 監(jiān)聽popState pushState 等事件

    return () => {
      checkDOMListeners(-1)
      unlisten()
    }
  }

  const history = {
    length: globalHistory.length,
    action: "POP",
    location: initialLocation,
    createHref,
    push,
    replace,
    go,
    goBack,
    goForward,
    block,
    listen
  }

  return history
}

由于篇幅過長,所以這里抽取push方法來梳理整套流程

  const push = (path, state) => {
      // push可接收兩個參數(shù),第一個參數(shù)path可以是字符串,或者對象,第二個參數(shù)是state對象
      // 里面是可以被瀏覽器緩存的數(shù)據(jù),當(dāng)path是一個對象并且path中的state存在,同時也傳遞了
      // 第二個參數(shù)state,那么這里就會給出警告,表示path中的state參數(shù)將會被忽略
      
    warning(
      !(typeof path === "object" && path.state !== undefined && state !== undefined),
      "You should avoid providing a 2nd state argument to push when the 1st " +
      "argument is a location-like object that already has state; it is ignored"
    )

     const action = "PUSH" // 動作為push操作
     //將即將訪問的路徑path, 被緩存的state,將要訪問的路徑的隨機(jī)生成的6位隨機(jī)字符串,
     // 上次訪問過的location對象也可以理解為當(dāng)前地址欄里路徑對象,  
     // 返回一個對象包含 pathname,search,hash,state,key
    const location = createLocation(path, state, createKey(), history.location)
     // 路由的切換,最后一個參數(shù)為回調(diào)函數(shù),只有返回true的時候才會進(jìn)行路由的切換
    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
      if (!ok)
        return
      
      const href = createHref(location)  // 拼接basename
      const { key, state } = location  // 獲取新的key和state

      if (canUseHistory) {
          // 當(dāng)可以使用history api時候,調(diào)用原生的pushState方法更改地址欄路徑
          // 此時只是改變地址欄路徑 頁面并不會發(fā)生變化 需要手動setState從而rerender
        // pushState的三個參數(shù)分別為,1.可以被緩存的state對象,即刷新瀏覽器依然會保留
        // 2.頁面的title,可直接忽略 3.href即新的地址欄路徑,這是一個完整的路徑地址
        globalHistory.pushState({ key, state }, null, href) 
        
        if (forceRefresh) { 
          window.location.href = href // 強(qiáng)制刷新
        } else {
          // 獲取上次訪問的路徑的key在記錄列表里的下標(biāo)
          const prevIndex = allKeys.indexOf(history.location.key)
          // 當(dāng)下標(biāo)存在時,返回截取到當(dāng)前下標(biāo)的數(shù)組key列表的一個新引用,不存在則返回一個新的空數(shù)組
          // 這樣做的原因是什么?為什么不每次訪問直接向allKeys列表中直接push要訪問的key
          // 比如這樣的一種場景, 1-2-3-4 的頁面訪問順序,這時候使用go(-2) 回退到2的頁面,假如在2
          // 的頁面我們選擇了push進(jìn)行跳轉(zhuǎn)到4頁面,如果只是簡單的對allKeys進(jìn)行push操作那么順序就變成了
          // 1-2-3-4-4,這時候就會產(chǎn)生一悖論,從4頁面跳轉(zhuǎn)4頁面,這種邏輯是不通的,所以每當(dāng)push或者replace
          // 發(fā)生的時候,一定是用當(dāng)前地址欄中path的key去截取allKeys中對應(yīng)的訪問記錄,來保證不會push
          // 連續(xù)相同的頁面
          const nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1)

          nextKeys.push(location.key) // 將新的key添加到allKeys中
          allKeys = nextKeys // 替換

          setState({ action, location }) // render頁面
        }
      } else {
        warning(
          state === undefined,
          "Browser history cannot push state in browsers that do not support HTML5 history"
        )

        window.location.href = href
      }
    })
  }

createLocation的源碼

export const createLocation = (path, state, key, currentLocation) => {
  let location
  if (typeof path === "string") {
    // Two-arg form: push(path, state)
    // 分解pathname,path,hash,search等,parsePath返回一個對象
    location = parsePath(path)
    location.state = state 
  } else {
    // One-arg form: push(location)
    location = { ...path }

    if (location.pathname === undefined)
      location.pathname = ""

    if (location.search) {
      if (location.search.charAt(0) !== "?")
        location.search = "?" + location.search
    } else {
      location.search = ""
    }

    if (location.hash) {
      if (location.hash.charAt(0) !== "#")
        location.hash = "#" + location.hash
    } else {
      location.hash = ""
    }

    if (state !== undefined && location.state === undefined)
      location.state = state
  }

  // 嘗試對pathname進(jìn)行decodeURI解碼操作,失敗時進(jìn)行提示
  try {
    location.pathname = decodeURI(location.pathname)
  } catch (e) {
    if (e instanceof URIError) {
      throw new URIError(
        "Pathname "" + location.pathname + "" could not be decoded. " +
        "This is likely caused by an invalid percent-encoding."
      )
    } else {
      throw e
    }
  }

  if (key)
    location.key = key

  if (currentLocation) {
    // Resolve incomplete/relative pathname relative to current location.
    if (!location.pathname) {
      location.pathname = currentLocation.pathname
    } else if (location.pathname.charAt(0) !== "/") {
      location.pathname = resolvePathname(location.pathname, currentLocation.pathname)
    }
  } else {
    // When there is no prior location and pathname is empty, set it to /
    // pathname 不存在的時候返回當(dāng)前路徑的根節(jié)點(diǎn)
    if (!location.pathname) {
      location.pathname = "/"
    }
  }

  // 返回一個location對象包含
  // pathname,search,hash,state,key
  return location
}

createTransitionManager.js的源碼

import warning from "warning"

const createTransitionManager = () => {
  // 這里使一個閉包環(huán)境,每次進(jìn)行路由切換的時候,都會先進(jìn)行對prompt的判斷
  // 當(dāng)prompt != null 的時候,表示路由的上次切換被阻止了,那么當(dāng)用戶confirm返回true
  // 的時候會直接進(jìn)行地址欄的更新和subscribe的回調(diào)
  let prompt = null // 提示符
  
  const setPrompt = (nextPrompt) => {
      // 提示prompt只能存在一個
    warning(
      prompt == null,
      "A history supports only one prompt at a time"
    )

    prompt = nextPrompt
     // 同時將解除block的方法返回
    return () => {
      if (prompt === nextPrompt)
        prompt = null
    }
  }
  // 
  const confirmTransitionTo = (location, action, getUserConfirmation, callback) => {
    // TODO: If another transition starts while we"re still confirming
    // the previous one, we may end up in a weird state. Figure out the
    // best way to handle this.
    if (prompt != null) {
      // prompt 可以是一個函數(shù),如果是一個函數(shù)返回執(zhí)行的結(jié)果
      const result = typeof prompt === "function" ? prompt(location, action) : prompt
       // 當(dāng)prompt為string類型時 基本上就是為了提示用戶即將要跳轉(zhuǎn)路由了,prompt就是提示信息
      if (typeof result === "string") {
          // 調(diào)用window.confirm來顯示提示信息
        if (typeof getUserConfirmation === "function") {
            // callback接收用戶 選擇了true或者false
          getUserConfirmation(result, callback)
        } else {
            // 提示開發(fā)者 getUserConfirmatio應(yīng)該是一個function來展示阻止路由跳轉(zhuǎn)的提示
          warning(
            false,
            "A history needs a getUserConfirmation function in order to use a prompt message"
          )
          // 相當(dāng)于用戶選擇true 不進(jìn)行攔截
          callback(true)
        }
      } else {
        // Return false from a transition hook to cancel the transition.
        callback(result !== false)
      }
    } else {
        // 當(dāng)不存在prompt時,直接執(zhí)行回調(diào)函數(shù),進(jìn)行路由的切換和rerender
      callback(true)
    }
  }
   // 被subscribe的列表,即在Router組件添加的setState方法,每次push replace 或者 go等操作都會觸發(fā)
  let listeners = []
  // 將回調(diào)函數(shù)添加到listeners,一個發(fā)布訂閱模式
  const appendListener = (fn) => {
    let isActive = true
     // 這里有個奇怪的地方既然訂閱事件可以被解綁就直接被從數(shù)組中刪除掉了,為什么這里還需要這個isActive
     // 再加一次判斷呢,其實(shí)是為了避免一種情況,比如注冊了多個listeners: a,b,c 但是在a函數(shù)中注銷了b函數(shù)
     // 理論上來說b函數(shù)應(yīng)該不能在執(zhí)行了,但是注銷方法里使用的是數(shù)組的filter,每次返回的是一個新的listeners引用,
     // 故每次解綁如果不添加isActive這個開關(guān),那么當(dāng)前循環(huán)還是會執(zhí)行b的事件。加上isActive后,原始的liteners中
     // 的閉包b函數(shù)的isActive會變?yōu)閒alse,從而阻止事件的執(zhí)行,當(dāng)循環(huán)結(jié)束后,原始的listeners也會被gc回收
    const listener = (...args) => {
      if (isActive)
        fn(...args)
    }

    listeners.push(listener)
     
    return () => {
      isActive = false
      listeners = listeners.filter(item => item !== listener)
    }
  }
  // 通知被訂閱的事件開始執(zhí)行
  const notifyListeners = (...args) => {
    listeners.forEach(listener => listener(...args))
  }

  return {
    setPrompt,
    confirmTransitionTo,
    appendListener,
    notifyListeners
  }
}

export default createTransitionManager

由于篇幅太長,自己都看的蒙圈了,現(xiàn)在就簡單做一下總結(jié),描述router工作的原理。
1.首先BrowserRouter通過history庫使用createBrowserHistory方法創(chuàng)建了一個history對象,并將此對象作為props傳遞給了Router組件
2.Router組件使用history對的的listen方法,注冊了組件自身的setState事件,這樣一樣來,只要觸發(fā)了html5的popstate事件,組件就會執(zhí)行setState事件,完成整個應(yīng)用的rerender
3.history是一個對象,里面包含了操作頁面跳轉(zhuǎn)的方法,以及當(dāng)前地址欄對象的location的信息。首先當(dāng)創(chuàng)建一個history對象時候,會使用props當(dāng)中的四個參數(shù)信息,forceRefresh、basename、getUserComfirmation、keyLength 來生成一個初始化的history對象,四個參數(shù)均不是必傳項(xiàng)。首先會使用window.location對象獲取當(dāng)前路徑下的pathname、search、hash等參數(shù),同時如果頁面是經(jīng)過rerolad刷新過的頁面,那么也會保存之前向state添加過數(shù)據(jù),這里除了我們自己添加的state,還有history這個庫自己每次做push或者repalce操作的時候隨機(jī)生成的六位長度的字符串key
拿到這個初始化的location對象后,history開始封裝push、replace、go等這些api。
以push為例,可以接收兩個參數(shù)push(path, state)----我們常用的寫法是push("/user/list"),只需要傳遞一個路徑不帶參數(shù),或者push({pathname: "/user", state: {id: "xxx"}, search: "?name=xxx", hash: "#list"})傳遞一個對象。任何對地址欄的更新都會經(jīng)過confirmTransitionTo 這個方法進(jìn)行驗(yàn)證,這個方法是為了支持prompt攔截器的功能。正常在攔截器關(guān)閉的情況下,每次調(diào)用push或者replace都會隨機(jī)生成一個key,代表這個路徑的唯一hash值,并將用戶傳遞的state和key作為state,注意這部分state會被保存到 瀏覽器 中是一個長效的緩存,將拼接好的path作為傳遞給history的第三個參數(shù),調(diào)用history.pushState(state, null, path),這樣地址欄的地址就得到了更新。
地址欄地址得到更新后,頁面在不使用foreceRefrsh的情況下是不會自動更新的。此時需要循環(huán)執(zhí)行在創(chuàng)建history對象時,在內(nèi)存中的一個listeners監(jiān)聽隊(duì)列,即在步驟2中在Router組件內(nèi)部注冊的回調(diào),來手動完成頁面的setState,至此一個完整的更新流程就算走完了。
在history里有一個block的方法,這個方法的初衷是為了實(shí)現(xiàn)對路由跳轉(zhuǎn)的攔截。我們知道瀏覽器的回退和前進(jìn)操作按鈕是無法進(jìn)行攔截的,只能做hack,這也是history庫的做法。抽離出了一個路徑控制器,方法名稱叫做createTransitionManager,可以理解為路由操作器。這個方法在內(nèi)部維護(hù)了一個prompt的攔截器開關(guān),每當(dāng)這個開關(guān)打開的時候,所有的路由在跳轉(zhuǎn)前都會被window.confirm所攔截。注意此攔截并非真正的攔截,雖然頁面沒有改變,但是地址欄的路徑已經(jīng)改變了。如果用戶沒有取消攔截,那么頁面依然會停留在當(dāng)前頁面,這樣和地址欄的路徑就產(chǎn)生了悖論,所以需要將地址欄的路徑再重置為當(dāng)前頁面真正渲染的頁面。為了實(shí)現(xiàn)這一功能,不得不創(chuàng)建了一個用隨機(jī)key值的來表示的訪問過的路徑表allKeys。每次頁面被攔截后,都需要在allKeys的列表中找到當(dāng)前路徑下的key的下標(biāo),以及實(shí)際頁面顯示的location的key的下標(biāo),后者減前者的值就是頁面要被回退或者前進(jìn)的次數(shù),調(diào)用go方法后會再次觸發(fā)popstate事件,造成頁面的rerender。
正式因?yàn)橛辛薖rompt組件才會使history不得不增加了key列表,prompt開關(guān),導(dǎo)致代碼的復(fù)雜度成倍增加,同時很多開發(fā)者在開發(fā)中對此組件的濫用也導(dǎo)致了一些特殊的bug,并且這些bug都是無法解決的,這也是作者為什么想要在下個版本中移除此api的緣由。討論地址在鏈接描述

。下篇將會進(jìn)行對Route Switch Link等其他組件的講解

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/53901.html

相關(guān)文章

  • react-router v4.x 源碼拾遺1

    摘要:還是先來一段官方的基礎(chǔ)使用案例,熟悉一下整體的代碼流程中使用了端常用到的等一些常用組件,作為的頂層組件來獲取的和設(shè)置回調(diào)函數(shù)來更新。 react-router是react官方推薦并參與維護(hù)的一個路由庫,支持瀏覽器端、app端、服務(wù)端等常見場景下的路由切換功能,react-router本身不具備切換和跳轉(zhuǎn)路由的功能,這些功能全部由react-router依賴的history庫完成,his...

    itvincent 評論0 收藏0
  • react-router v4.x 源碼拾遺2

    摘要:如果將添加到當(dāng)前組件,并且當(dāng)前組件由包裹,那么將引用最外層包裝組件的實(shí)例而并非我們期望的當(dāng)前組件,這也是在實(shí)際開發(fā)中為什么不推薦使用的原因,使用一個回調(diào)函數(shù)是一個不錯的選擇,也同樣的使用的是回調(diào)函數(shù)來實(shí)現(xiàn)的。 回顧:上一篇講了BrowserRouter 和 Router之前的關(guān)系,以及Router實(shí)現(xiàn)路由跳轉(zhuǎn)切換的原理。這一篇來簡短介紹react-router剩余組件的源碼,結(jié)合官方文...

    luoyibu 評論0 收藏0
  • react-router v4.x 源碼拾遺2

    摘要:如果將添加到當(dāng)前組件,并且當(dāng)前組件由包裹,那么將引用最外層包裝組件的實(shí)例而并非我們期望的當(dāng)前組件,這也是在實(shí)際開發(fā)中為什么不推薦使用的原因,使用一個回調(diào)函數(shù)是一個不錯的選擇,也同樣的使用的是回調(diào)函數(shù)來實(shí)現(xiàn)的。 回顧:上一篇講了BrowserRouter 和 Router之前的關(guān)系,以及Router實(shí)現(xiàn)路由跳轉(zhuǎn)切換的原理。這一篇來簡短介紹react-router剩余組件的源碼,結(jié)合官方文...

    CoorChice 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<