国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

react-router v4.x 源碼拾遺1

itvincent / 1954人閱讀

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

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

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 和 設置回調函數來更新state。這里引用的組件都是來自react-router-dom 這個庫,那么react-router 和 react-router-dom 是什么關系呢。
說的簡單一點,react-router-dom 是對react-router所有組件或方法的一個二次導出,并且在react-router組件的基礎上添加了新的組件,更加方便開發者處理復雜的應用業務。

1.react-router 導出的所有內容

統計一下,總共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 導出的所有內容

統計一下,總共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個方法,再重新導出,在開發中我們只需要引入react-router-dom這個依賴即可。

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

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

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

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

loose-envify, 使用browserify工具進行打包的時候,會將項目當中的node全局變量替換為對應的字符串

prop-types, react的props類型校驗工具庫

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, // 當應用為某個子應用時,添加的地址欄前綴
    forceRefresh: PropTypes.bool, // 切換路由時,是否強制刷新
    getUserConfirmation: PropTypes.func, // 使用Prompt組件時 提示用戶的confirm確認方法,默認使用window.confirm
    keyLength: PropTypes.number, // 為了實現block功能,react-router維護創建了一個訪問過的路由表,每個key代表一個曾經訪問過的路由地址
    children: PropTypes.node // 子節點
  };
  // 核心api, 提供了push replace go等路由跳轉方法
  history = createHistory(this.props); 
  // 提示用戶 BrowserRouter不接受用戶自定義的history方法,
  // 如果傳遞了history會被忽略,如果用戶使用自定義的history api,
  // 需要使用 Router 組件進行替代
  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;

**總結:BrowserRouter組件非常簡單,它本身其實就是對Router組件的一個包裝,將HTML5的history api封裝好再賦予 Router 組件。BrowserRouter就好比一個容器組件,由它來決定Router的最終api,這樣一個Router組件就可以完成多種api的實現,比如HashRouter、StaticRouter 等,減少了代碼的耦合度
②. Router.js, 如果說BrowserRouter是Router的容器組件,為Router提供了html5的history api的數據源,那么Router.js 亦可以看作是子節點的容器組件,它除了接收BrowserRouter提供的history api,最主要的功能就是組件本身會響應地址欄的變化進行setState進而完成react本身的rerender,使應用進行相應的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庫進行講解
        route: {
          location: this.props.history.location, // 也是history庫中的內容
          match: this.state.match // 對當前地址進行匹配的結果
        }
      }
    };
  }
  // Router組件的state,作為一個頂層容器組件維護的state,存在兩個目的
  // 1.主要目的為了實現自上而下的rerender,url改變的時候match對象會被更新
  // 2.Router組件是始終會被渲染的組件,match對象會隨時得到更新,并經過context api
  // 傳遞給下游子組件route等
  state = {
    match: this.computeMatch(this.props.history.location.pathname)
  };
  // match 的4個參數
  // 1.path: 是要進行匹配的路徑可以是 "/user/:id" 這種動態路由的模式
  // 2.url: 地址欄實際的匹配結果
  // 3.parmas: 動態路由所匹配到的參數,如果path是 "/user/:id"匹配到了,那么
  // params的內容就是 {id: 某個值}
  // 4.isExact: 精準匹配即 地址欄的pathname 和 正則匹配到url是否完全相等
  computeMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  }

  componentWillMount() {
    const { children, history } = this.props;
    // 當 子節點并非由一個根節點包裹時 拋出錯誤提示開發者
    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被實例化時注冊一個回調事件,
    // 即location地址發生改變的時候,會重新setState,進而rerender
    // 這里使用willMount而不使用didMount的原因時是因為,服務端渲染時不存在dom,
    // 故不會調用didMount的鉤子,react將在17版本移除此鉤子,那么到時候router應該如何實現此功能?
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }
   // history參數不允許被更改
  componentWillReceiveProps(nextProps) {
    warning(
      this.props.history === nextProps.history,
      "You cannot change "
    );
  }
  // 組件銷毀時 解綁history對象中的監聽事件
  componentWillUnmount() {
    this.unlisten();
  }
  // render的時候使用React.Children.only方法再驗證一次
  // children 必須是一個由根節點包裹的組件或dom
  render() {
    const { children } = this.props;
    return children ? React.Children.only(children) : null;
  }
}

export default Router;

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

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

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

createMemoryHistory.js 針對native app這種原生應用封裝的路由控制器,即在內存中維護一份路由表

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,當這些事件被觸發時會激活subscribe的回調來進行響應。同時也會對地址欄進行監聽,當history.go等事件觸發history popstate事件時,也會激活subscribe的回調。

由于代碼量較多,而且依賴的方法較多,這里將方法分成幾個小節來進行梳理,對于依賴的方法先進行簡短闡述,當實際調用時在深入源碼內部去探究實現細節

1. 依賴的工具方法

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

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

// 返回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的具體實現

const createBrowserHistory = (props = {}) => {
  // 當不在瀏覽器環境下直接拋出錯誤
  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的時候是否會進行popState操作,ie10、11不會進行popState操作 
  const needsHashChangeListener = !supportsPopStateOnHashChange()

  const {
    forceRefresh = false,                     // 默認切換路由不刷新
    getUserConfirmation = getConfirmation,    // 使用window.confirm
    keyLength = 6                             // 默認6位長度隨機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 || {})
     // 獲取當前路徑下的pathname,search,hash等參數
    const { pathname, search, hash } = window.location 
      // 拼接一個完整的路徑
    let path = pathname + search + hash               

    // 當傳遞了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當中的basename
    if (basename)
      path = stripBasename(path, basename)
    
    // 生成一個自定義的location對象
    return createLocation(path, state, key)
  }

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

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

  const handleHashChange = () => {
      // 監聽到hashchange時進行的處理,由于hashchange不會更改state
      // 故此處不需要更新location的state
    handlePop(getDOMLocation(getHistoryState()))
  }
   // 用來判斷路由是否需要強制
  let forceNextPop = false
   // handlePop是對使用go方法來回退或者前進時,對頁面進行的更新,正常情況下來說沒有問題
   // 但是如果頁面使用Prompt,即路由攔截器。當點擊回退或者前進就會觸發histrory的api,改變了地址欄的路徑
   // 然后彈出需要用戶進行確認的提示框,如果用戶點擊確定,那么沒問題因為地址欄改變的地址就是將要跳轉到地址
   // 但是如果用戶選擇了取消,那么地址欄的路徑已經變成了新的地址,但是頁面實際還停留再之前,這就產生了bug
   // 這也就是 revertPop 這個hack的由來。因為頁面的跳轉可以由程序控制,但是如果操作的本身是瀏覽器的前進后退
   // 按鈕,那么是無法做到真正攔截的。
  const handlePop = (location) => {
    if (forceNextPop) {
      forceNextPop = false
      setState()
    } else {
      const action = "POP"

      transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
        if (ok) {
          setState({ action, location })
        } else {
            // 當攔截器返回了false的時候,需要把地址欄的路徑重置為當前頁面顯示的地址
          revertPop(location)
        }
      })
    }
  }
   // 這里是react-router的作者最頭疼的一個地方,因為雖然用hack實現了表面上的路由攔截
   // ,但也會引起一些特殊情況下的bug。這里先說一下如何做到的假裝攔截,因為本身html5 history
   // api的特性,pushState 這些操作不會引起頁面的reload,所有做到攔截只需要不手懂調用setState頁面不進行render即可
   // 當用戶選擇了取消后,再將地址欄中的路徑變為當前頁面的顯示路徑即可,這也是revertPop實現的方式
   // 這里貼出一下對這個bug的討論:https://github.com/ReactTraining/history/issues/690
  const revertPop = (fromLocation) => {
      // fromLocation 當前地址欄真正的路徑,而且這個路徑一定是存在于history歷史
      // 記錄當中某個被訪問過的路徑,因為我們需要將地址欄的這個路徑重置為頁面正在顯示的路徑地址
      // 頁面顯示的這個路徑地址一定是還再history.location中的那個地址
      // fromLoaction 用戶原本想去但是后來又不去的那個地址,需要把他換位history.location當中的那個地址      
    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中的下標位置
    let toIndex = allKeys.indexOf(toLocation.key)

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

    if (fromIndex === -1)
      fromIndex = 0
     // 兩者進行相減的值就是go操作需要回退或者前進的次數
    const delta = toIndex - fromIndex
     // 如果delta不為0,則進行地址欄的變更 將歷史記錄重定向到當前頁面的路徑   
    if (delta) {
      forceNextPop = true // 將forceNextPop設置為true
      // 更改地址欄的路徑,又會觸發handlePop 方法,此時由于forceNextPop已經為true則會執行后面的
      // setState方法,對當前頁面進行rerender,注意setState是沒有傳遞參數的,這樣history當中的
      // location對象依然是之前頁面存在的那個loaction,不會改變history的location數據
      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 // 強制刷新
        } else {
          const prevIndex = allKeys.indexOf(history.location.key) // 上次訪問的路徑的key
          const nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1)

          nextKeys.push(location.key) // 維護一個訪問過的路徑的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
   // 防止重復注冊監聽,只有listenerCount == 1的時候才會進行監聽事件
  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)
    }
  }
  // 默認情況下不會阻止路由的跳轉
  let isBlocked = false
  // 這里的block方法專門為Prompt組件設計,開發者可以模擬對路由的攔截
  const block = (prompt = false) => {
      // prompt 默認為false, prompt可以為string或者func
      // 將攔截器的開關打開,并返回可關閉攔截器的方法
    const unblock = transitionManager.setPrompt(prompt)
      // 監聽事件只會當攔截器開啟時被注冊,同時設置isBlock為true,防止多次注冊
    if (!isBlocked) {
      checkDOMListeners(1)
      isBlocked = true
    }
     // 返回關閉攔截器的方法
    return () => {
      if (isBlocked) {
        isBlocked = false
        checkDOMListeners(-1)
      }

      return unblock()
    }
  }

  const listen = (listener) => {
    const unlisten = transitionManager.appendListener(listener) // 添加訂閱者
    checkDOMListeners(1) // 監聽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可接收兩個參數,第一個參數path可以是字符串,或者對象,第二個參數是state對象
      // 里面是可以被瀏覽器緩存的數據,當path是一個對象并且path中的state存在,同時也傳遞了
      // 第二個參數state,那么這里就會給出警告,表示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" // 動作為push操作
     //將即將訪問的路徑path, 被緩存的state,將要訪問的路徑的隨機生成的6位隨機字符串,
     // 上次訪問過的location對象也可以理解為當前地址欄里路徑對象,  
     // 返回一個對象包含 pathname,search,hash,state,key
    const location = createLocation(path, state, createKey(), history.location)
     // 路由的切換,最后一個參數為回調函數,只有返回true的時候才會進行路由的切換
    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
      if (!ok)
        return
      
      const href = createHref(location)  // 拼接basename
      const { key, state } = location  // 獲取新的key和state

      if (canUseHistory) {
          // 當可以使用history api時候,調用原生的pushState方法更改地址欄路徑
          // 此時只是改變地址欄路徑 頁面并不會發生變化 需要手動setState從而rerender
        // pushState的三個參數分別為,1.可以被緩存的state對象,即刷新瀏覽器依然會保留
        // 2.頁面的title,可直接忽略 3.href即新的地址欄路徑,這是一個完整的路徑地址
        globalHistory.pushState({ key, state }, null, href) 
        
        if (forceRefresh) { 
          window.location.href = href // 強制刷新
        } else {
          // 獲取上次訪問的路徑的key在記錄列表里的下標
          const prevIndex = allKeys.indexOf(history.location.key)
          // 當下標存在時,返回截取到當前下標的數組key列表的一個新引用,不存在則返回一個新的空數組
          // 這樣做的原因是什么?為什么不每次訪問直接向allKeys列表中直接push要訪問的key
          // 比如這樣的一種場景, 1-2-3-4 的頁面訪問順序,這時候使用go(-2) 回退到2的頁面,假如在2
          // 的頁面我們選擇了push進行跳轉到4頁面,如果只是簡單的對allKeys進行push操作那么順序就變成了
          // 1-2-3-4-4,這時候就會產生一悖論,從4頁面跳轉4頁面,這種邏輯是不通的,所以每當push或者replace
          // 發生的時候,一定是用當前地址欄中path的key去截取allKeys中對應的訪問記錄,來保證不會push
          // 連續相同的頁面
          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進行decodeURI解碼操作,失敗時進行提示
  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 不存在的時候返回當前路徑的根節點
    if (!location.pathname) {
      location.pathname = "/"
    }
  }

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

createTransitionManager.js的源碼

import warning from "warning"

const createTransitionManager = () => {
  // 這里使一個閉包環境,每次進行路由切換的時候,都會先進行對prompt的判斷
  // 當prompt != null 的時候,表示路由的上次切換被阻止了,那么當用戶confirm返回true
  // 的時候會直接進行地址欄的更新和subscribe的回調
  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 可以是一個函數,如果是一個函數返回執行的結果
      const result = typeof prompt === "function" ? prompt(location, action) : prompt
       // 當prompt為string類型時 基本上就是為了提示用戶即將要跳轉路由了,prompt就是提示信息
      if (typeof result === "string") {
          // 調用window.confirm來顯示提示信息
        if (typeof getUserConfirmation === "function") {
            // callback接收用戶 選擇了true或者false
          getUserConfirmation(result, callback)
        } else {
            // 提示開發者 getUserConfirmatio應該是一個function來展示阻止路由跳轉的提示
          warning(
            false,
            "A history needs a getUserConfirmation function in order to use a prompt message"
          )
          // 相當于用戶選擇true 不進行攔截
          callback(true)
        }
      } else {
        // Return false from a transition hook to cancel the transition.
        callback(result !== false)
      }
    } else {
        // 當不存在prompt時,直接執行回調函數,進行路由的切換和rerender
      callback(true)
    }
  }
   // 被subscribe的列表,即在Router組件添加的setState方法,每次push replace 或者 go等操作都會觸發
  let listeners = []
  // 將回調函數添加到listeners,一個發布訂閱模式
  const appendListener = (fn) => {
    let isActive = true
     // 這里有個奇怪的地方既然訂閱事件可以被解綁就直接被從數組中刪除掉了,為什么這里還需要這個isActive
     // 再加一次判斷呢,其實是為了避免一種情況,比如注冊了多個listeners: a,b,c 但是在a函數中注銷了b函數
     // 理論上來說b函數應該不能在執行了,但是注銷方法里使用的是數組的filter,每次返回的是一個新的listeners引用,
     // 故每次解綁如果不添加isActive這個開關,那么當前循環還是會執行b的事件。加上isActive后,原始的liteners中
     // 的閉包b函數的isActive會變為false,從而阻止事件的執行,當循環結束后,原始的listeners也會被gc回收
    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))
  }

  return {
    setPrompt,
    confirmTransitionTo,
    appendListener,
    notifyListeners
  }
}

export default createTransitionManager

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

。下篇將會進行對Route Switch Link等其他組件的講解

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/109660.html

相關文章

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

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

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

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

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

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

    CoorChice 評論0 收藏0

發表評論

0條評論

itvincent

|高級講師

TA的文章

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