react-router v4.x 源码拾遗1

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 (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/topics">Topics</Link>
          </li>
        </ul>

        <hr />

        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/topics" component={Topics} />
      </div>
    </Router>
  );
}

function Home() {
  return (
    <div>
      <h2>Home</h2>
    </div>
  );
}

function About() {
  return (
    <div>
      <h2>About</h2>
    </div>
  );
}

function Topics({ match }) {
  return (
    <div>
      <h2>Topics</h2>
      <ul>
        <li>
          <Link to={`${match.url}/rendering`}>Rendering with React</Link>
        </li>
        <li>
          <Link to={`${match.url}/components`}>Components</Link>
        </li>
        <li>
          <Link to={`${match.url}/props-v-state`}>Props v. State</Link>
        </li>
      </ul>

      <Route path={`${match.path}/:topicId`} component={Topic} />
      <Route
        exact
        path={match.path}
        render={() => <h3>Please select a topic.</h3>}
      />
    </div>
  );
}

function Topic({ match }) {
  return (
    <div>
      <h3>{match.params.topicId}</h3>
    </div>
  );
}

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 导出的一切内容

《react-router v4.x 源码拾遗1》

统计一下,统共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 导出的一切内容

《react-router v4.x 源码拾遗1》

统计一下,统共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-router v4.x 源码拾遗1》

  1. React, 请求版本大于即是15.x
  2. history, react-router的中心依靠库,注入组件操纵路由的api
  3. invariant, 用来抛出异常的东西库
  4. loose-envify, 运用browserify东西举行打包的时刻,会将项目当中的node全局变量替代为对应的字符串
  5. prop-types, react的props范例校验东西库
  6. react-router, 依靠同版本的react-router
  7. 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 <Router> 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,
      "<BrowserRouter> ignores the history prop. To use a custom history, " +
        "use `import { Router }` instead of `import { BrowserRouter as Router }`."
    );
  }
  // 将history和children作为props通报给Router组件 并返回
  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

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 <Router> may have only one child element"
    );

    // Do this here so we can setState when a <Redirect> changes the
    // location in componentWillMount. This happens e.g. when doing
    // server rendering using a <StaticRouter>.
    // 运用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 <Router history>"
    );
  }
  // 组件烧毁时 解绑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这个库做了啥。

《react-router v4.x 源码拾遗1》

  1. createBrowserHistory.js 运用html5 history api封装的路由控制器
  2. createHashHistory.js 运用hash要领封装的路由控制器
  3. createMemoryHistory.js 针对native app这类原生运用封装的路由控制器,即在内存中保护一份路由表
  4. createTransitionManager.js 针对路由切换时的雷同操纵抽离的一个大众要领,路由切换的操纵器,阻拦器和定阅者都存在于此
  5. DOMUtils.js 针对web端dom操纵或推断兼容性的一个东西要领鸠合
  6. LocationUtils.js 针对location url处置惩罚等抽离的一个东西要领的鸠合
  7. 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等其他组件的解说

    原文作者:372563106
    原文地址: https://segmentfault.com/a/1190000019230420
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞