前言
React Router 中很大程度上地依赖了 history 的功能,如 useNavigate、useHref 、Router 等都直接或间接地用到了 history,所以我们在分析 React Router 源码之前,有必要深入了解下 history 的用法,相信您看完本篇文章之后,能学到很多之前不知道的东西。写下本篇文章时的 history 版本是 latest 的,为 5.0.1,那废话不多说,让我们开始探索之旅吧~
history 分类
history 又分为 browserHistory 和 hashHistory,对应 react-router-dom 中的 BrowserRouter 和 HashRouter, 这两者的绝大部分都相同,我们先分析下 browserHistory,后面再点出 hashHistory 的一些区别点。
createBrowserHistory
顾名思义,createBrowserHistory 自然是用于创建 browserHistory 的工厂函数,我们先看下类型
export interface BrowserHistory<S extends State = State> extends History<S> {}
export interface History<S extends State = State> {
  /**
   * @description 上一个修改当前 location的action,有 `POP`、`PUSH`、`REPLACE`,初始创建为POP
   */
  readonly action: Action;
  /**
   * @description 当前location
   */
  readonly location: Location<S>;
  /**
   * @description 返回一个新的href, to为string则返回to,否则返回 `createPath(to)` => pathname + search + hash
   */
  createHref(to: To): string;
  /**
   * @description push一个新的location到历史堆栈,stack的length会+1
   */
  push(to: To, state?: S): void;
  /**
   * @description 将历史堆栈中当前location替换为新的,被替换的将不再存在
   */
  replace(to: To, state?: S): void;
  /**
   * @description 历史堆栈前进或后退delta(可正负)
   */
  go(delta: number): void;
  /**
   * @description 同go(-1)
   */
  back(): void;
  /**
   * @description 同go(1)
   */
  forward(): void;
  /**
   * @description 设置路由切换的监听器,`listener`为函数
   *
   * @example
   *
   * const browserHistory = createBrowserHistory()
   * browserHistory.push('/user')
   * const unListen = browserHistory.listen(({action, location}) => {
   *  // 切换后新的action和location,上面push后,这里的action为PUSH, location为 { pathname: '/user', ... }
   *  console.log(action, location)
   * })
   */
  listen(listener: Listener<S>): () => void;
  /**
   * @description 改变路由时阻塞路由变化
   */
  block(blocker: Blocker<S>): () => void;
}
即是说,createBrowserHistory 最终肯定要返回一个上面形状的 browserHistory 实例,我们先看下函数总体概览(这里大致看下框架就好,后面会具体分析)。
export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
): BrowserHistory {
  const { window = document.defaultView! } = options;
  const globalHistory = window.history;
  function getIndexAndLocation(): [number, Location] {}
  let blockedPopTx: Transition | null = null;
  function handlePop() {}
  window.addEventListener(PopStateEventType, handlePop);
  let action = Action.Pop;
  let [index, location] = getIndexAndLocation();
  const listeners = createEvents<Listener>();
  const blockers = createEvents<Blocker>();
  if (index == null) {
    index = 0;
    globalHistory.replaceState({ ...globalHistory.state, idx: index }, '');
  }
  function createHref(to: To) {
    return typeof to === 'string' ? to : createPath(to);
  }
  function getNextLocation(to: To, state: State = null): Location {}
  function getHistoryStateAndUrl(
    nextLocation: Location,
    index: number
  ): [HistoryState, string] {}
  function allowTx(action: Action, location: Location, retry: () => void): boolean {}
  function applyTx(nextAction: Action) {}
  function push(to: To, state?: State) {}
  function replace(to: To, state?: State) {}
  function go(delta: number) {
    globalHistory.go(delta);
  }
  const history: BrowserHistory = {
    get action() {
      return action;
    },
    get location() {
      return location;
    },
    createHref,
    push,
    replace,
    go,
    back() {
      go(-1);
    },
    forward() {
      go(1);
    },
    listen(listener) {
      return listeners.push(listener);
    },
    block(blocker) {
      const unblock = blockers.push(blocker);
      if (blockers.length === 1) {
        window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
      }
      return function() {
        unblock();
        if (!blockers.length) {
          window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
        }
      };
    }
  };
  return history;
}
可以看到函数里面又有一些内部函数和函数作用域内的顶层变量,我们先来看下这些顶层变量的作用。
createBrowserHistory 函数作用域内的顶层变量
export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
): BrowserHistory {
  const { window = document.defaultView! } = options;
  const globalHistory = window.history;
  let blockedPopTx: Transition | null = null;
  let action = Action.Pop;
  let [index, location] = getIndexAndLocation();
  const listeners = createEvents<Listener>();
  const blockers = createEvents<Blocker>();
  ...
}
window
createBrowserHistory 接收一个 options,默认为空对象,而其中的window默认为document.defaultView,即是 Window 对象罢了

history
上面获取到 window,然后会从 window 上获取到 history

blockedPopTx
用于存储下面的 blockers.call 方法的参数,即每一个 blocker 回调函数的参数,类型如下
export interface Transition<S extends State = State> extends Update<S> {
  /**
   * 被阻塞了后调用retry可以尝试继续跳转到要跳转的路由
   */
  retry(): void;
}
export interface Update<S extends State = State> {
  /**
   * 改变location的action,有POP、PUSH、REPLACE
   */
  action: Action;
  /**
   * 新location
   */
  location: Location<S>;
}
index 与 location
index 为当前 location 的索引,即是说,history 会为每个 location 创建一个idx,放在state中, 会用于在 handlePop中计算 delta,这里稍微提下,后面分析 handlePop 如何阻止路由变化会讲到
// 初始调用
const [index, location] = getIndexAndLocation();
// handlePop中
const [nextIndex, nextLocation] = getIndexAndLocation();
const delta = index - nextIndex;
go(delta)

action
blockers、listeners 的回调会用到 action,其是通过 handlePop、 push、replace 三个函数修改其状态,分别为 POP、PUSH和REPLACE,这样我们就可以通过判断 action 的值来做出不同的判断了。
listeners 与 blokers
我们先看下 创建 listeners 与 blokers 的工厂函数 createEvents
function createEvents<F extends Function>(): Events<F> {
  let handlers: F[] = [];
  return {
    get length() {
      return handlers.length;
    },
    push(fn: F) {
      // 其实就是一个观察者模式,push后返回unsubscribe
      handlers.push(fn);
      return function() {
        handlers = handlers.filter(handler => handler !== fn);
      };
    },
    call(arg) {
      // 消费所有handle
      handlers.forEach(fn => fn && fn(arg));
    }
  };
}
其返回了一个对象,通过 push 添加每个 listener,通过 call通知每个 listener,代码中叫做 handler。
listeners 通过 call 传入 { action, location }, 这样每个 listener 在路由变化时就能接收到,从而做出对应的判断。
listener 类型如下
export interface Update<S extends State = State> {
  action: Action;
  location: Location<S>;
}
export interface Listener<S extends State = State> {
  (update: Update<S>): void;
}
blockers 通过 call 传入 { action, location, retry },比listeners多了一个 retry,从而判断是否要阻塞路由,不阻塞的话需要调用函数 retry。
blocker 类型如下
export interface Transition<S extends State = State> extends Update<S> {
  retry(): void;
}
export interface Blocker<S extends State = State> {
  (tx: Transition<S>): void;
}
知道了顶层变量的作用,那我们接下来一一分析下返回 history 实例对象的每个属性。
action 与 location
  const history: BrowserHistory = {
    get action() {
      return action;
    },
    get location() {
      return location;
    },
    ...
  }
这两个属性都通过 修饰符 get,那么我们每次要获取最新的 action 或 location,就可以通过 history.action 或 history.location 。
避免了只能拿到第一次创建的值,如
const history: BrowserHistory = {
  action,
  location,
}
或需要每次调用函数才能拿到:
const history: BrowserHistory = {
  action: () => action,
  location: () => location,
}
action 我们上面已经分析了,这里我们看下获取 location 的函数。
getIndexAndLocation
即获取当前索引和 location
function getIndexAndLocation(): [number, Location] {
    const { pathname, search, hash } = window.location;
    const state = globalHistory.state || {};
    return [
      state.idx,
      readOnly<Location>({
        pathname,
        search,
        hash,
        state: state.usr || null,
        key: state.key || 'default'
      })
    ];
 }
  ...
// createBrowserHistory创建的时候获取初始当前路径index和location
let [index, location] = getIndexAndLocation();
createBrowserHistory 调用的时候会获取初始当前路径 index 和 location,这个时候的 index 肯定是 undefined(请注意要打开新页面才会,否则刷新后还是有历史堆栈,导致 state.idx 有值,即 index 不为空)

所以下面会通过判断 index 是否为空,空的话会给个默认值 0
if (index == null) {
    // 初始index为空,那么给个0
    index = 0;
    // 这里replaceState后,history.state.idx就为0了
    globalHistory.replaceState({ ...globalHistory.state, idx: index }, '');
  }
通过 replaceState初始重置了历史堆栈,从而就能获取到 state 中的 idx 了。
这个时候我们再通过 history.state.idx 就能获取到

history.createHref
history.createHref 来自 createBrowserHistory 的内部函数,接收一个 To 类型的参数,返回值为字符串 href
type To = string | Partial<Path>;
interface Path {
  pathname: string;
  search: string;
  hash: string;
}
function createHref(to: To) {
    return typeof to === 'string' ? to : createPath(to);
 }
如果 to 不为字符串,会通过 createPath 函数转为字符串,即是把 pathname、search 和 hash 拼接起来罢了
export function createPath({
  pathname = '/',
  search = '',
  hash = ''
}: PartialPath) {
  return pathname + search + hash;
}
history.push
function push(to: To, state?: State) {
  const nextAction = Action.Push;
  const nextLocation = getNextLocation(to, state);
  // 跳过,后面blockers会讲到
  function retry() {
    push(to, state);
  }
  // 跳过,后面blockers会讲到,这里我们先默认为true
  if (allowTx(nextAction, nextLocation, retry)) {
    const [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
    // try...catch because iOS limits us to 100 pushState calls :/
    // 用try  catch的原因是因为ios限制了100次pushState的调用
    try {
      globalHistory.pushState(historyState, '', url);
    } catch (error) {
      // They are going to lose state here, but there is no real
      // way to warn them about it since the page will refresh...
      window.location.assign(url);
    }
    // 跳过,后面listeners会讲到
    applyTx(nextAction);
  }
}
首先会通过 getNextLocation,根据 to 和 state 获取到新的 location,注意这时候路由还没切换
const nextLocation = getNextLocation(to, state);
/**
 * @description 获取新的Location
 * @param to 新的path
 * @param state 状态
 */
function getNextLocation(to: To, state: State = null): Location {
  return readOnly<Location>({
    ...location,
    ...(typeof to === 'string' ? parsePath(to) : to),
    state,
    key: createKey()
  });
}
如果 to 是字符串的话,会通过parsePath解析对应的 pathname、search、hash(三者都是可选的,不一定会出现在返回的对象中)
/**
 * @example
 * parsePath('https://juejin.cn/post/7005725282363506701?utm_source=gold_browser_extension#heading-2')
 * {
 *   "hash": "#heading-2",
 *   "search": "?utm_source=gold_browser_extension",
 *   "pathname": "https://juejin.cn/post/7005725282363506701"
 * }
 * 从结果可看到,去掉 `hash` 、 `search` 就是 `pathname` 了
 *
 * parsePath('?utm_source=gold_browser_extension#heading-2')
 * {
 *   "hash": "#heading-2",
 *   "search": "?utm_source=gold_browser_extension",
 * }
 * parsePath('') => {}
 * 而如果只有search和hash,那么parse完也没有pathname,这里要特别注意
 *
 * @see https://github.com/ReactTraining/history/tree/master/docs/api-reference.md#parsepath
 */
export function parsePath(path: string) {
  const partialPath: PartialPath = {};
  if (path) {
    const hashIndex = path.indexOf('#');
    if (hashIndex >= 0) {
      partialPath.hash = path.substr(hashIndex);
      path = path.substr(0, hashIndex);
    }
    const searchIndex = path.indexOf('?');
    if (searchIndex >= 0) {
      partialPath.search = path.substr(searchIndex);
      path = path.substr(0, searchIndex);
    }
    if (path) {
      partialPath.pathname = path;
    }
  }
  return partialPath;
}
再根据新的 location 获取新的 state 和 url,而因为是 push,所以这里的 index 自然是加一
const [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
/** 获取state和url */
  function getHistoryStateAndUrl(
    nextLocation: Location,
    index: number
  ): [HistoryState, string] {
    return [
      {
        usr: nextLocation.state,
        key: nextLocation.key,
        idx: index
      },
      createHref(nextLocation)
    ];
  }
最后调用history.pushState成功跳转页面,这个时候路由也就切换了
globalHistory.pushState(historyState, '', url);
history.replace
replace和 push 类似,区别只是 index 不变以及调用 replaceState
function replace(to: To, state?: State) {
  const nextAction = Action.Replace;
  const nextLocation = getNextLocation(to, state);
  // 跳过,后面blockers会讲到
  function retry() {
    replace(to, state);
  }
  if (allowTx(nextAction, nextLocation, retry)) {
    const [historyState, url] = getHistoryStateAndUrl(nextLocation, index);
    globalHistory.replaceState(historyState, '', url);
    // 跳过,后面listeners会讲到
    applyTx(nextAction);
  }
}
history.go、history.back、history.forward
用于历史堆栈的前进后退,back 和 forward 分别是 是 go(-1) 和 go(1) ,delta 可正负,代表前进后退
function go(delta: number) {
  globalHistory.go(delta);
}
history.listen
const history: HashHistory = {
  ...
  listen(listener) {
    return listeners.push(listener);
  },
  ...
}
history.listen 可以往 history 中添加 listener,返回值是 unListen,即取消监听。这样每当成功切换路由,就会调用 applyTx(nextAction); 来通知每个 listener,applyTx(nextAction);在 push、 replace 和 handlePop 三个函数中成功切换路由后调用。
function push(to: To, state?: State) {
   ...
  // 跳过,后面blockers会讲到,这里我们先默认为true
  if (allowTx(nextAction, nextLocation, retry)) {
    ...
    // 下面会讲到
    applyTx(nextAction);
  }
}
function replace(to: To, state?: State) {
   ...
  if (allowTx(nextAction, nextLocation, retry)) {
    ...
    // 下面会讲到
    applyTx(nextAction);
  }
}
function handlePop() {
  if (blockedPopTx) {
    ...
  } else {
    ...
    if (blockers.length) {
    ...
    } else {
      // // 下面会讲到
      applyTx(nextAction);
    }
  }
}
function applyTx(nextAction: Action) {
  action = nextAction;
//  获取当前index和location
  [index, location] = getIndexAndLocation();
  listeners.call({ action, location });
}
即只要满足 allowTx 返回 true(push 和 replace 函数中) 或没有 blocker(handlePop 函数中) 就能通知每个 listener。那我们看下 allowTx
function allowTx(action: Action, location: Location, retry: () => void): boolean {
  return (
    !blockers.length || (blockers.call({ action, location, retry }), false)
  );
}
allowTx的作用是判断是否允许路由切换,有 blockers 就不允许,逻辑如下:
- blockers 不为空,那么通知每个 blocker,然后返回 false
- blockers 为空,返回 true
那么要返回 true 的话就必须满足 blockers 为空,也即是说,listener 能否监听到路由变化,取决于当前页面是否被阻塞了(block)。
history.block
上面我们说有 blocker 就会导致 listener 收不到监听,且无法成功切换路由,那我们看下 block 函数:
const history: BrowserHistory = {
  ...
  block(blocker) {
    // push后返回unblock,即把该blocker从blockers去掉
    const unblock = blockers.push(blocker);
    if (blockers.length === 1) {
      // beforeunload
      // 只在第一次block加上beforeunload事件
      window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
    }
    return function() {
      unblock();
      // 移除beforeunload事件监听器以便document在pagehide事件中仍可以使用
      // Remove the beforeunload listener so the document may
      // still be salvageable in the pagehide event.
      // See https://html.spec.whatwg.org/#unloading-documents
      if (!blockers.length) {
        // 移除的时候发现blockers空了那么就移除`beforeunload`事件
        window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
      }
    };
  }
};
beforeunload
我们发现添加第一个 blocker 时会添加 beforeunload 事件,也就是说只要 block 了,那么我们刷新、关闭页面,通过修改地址栏 url 后 enter 都会弹窗提示:



刷新会询问重新加载此网站?,而关闭 tab 或修改地址栏后 enter 是提示离开此网站?,这两种要注意区别。
这功能常用在表单提交的页面,避免用户不小心关闭 tab 导致表单数据丢失。
当然如果 unblock 时发现 blockers 为空就会移除 beforeunload 事件了。
history 如何阻止路由切换
说完上面的beforeunload 事件,我们关注下上面跳过的 block 方面的代码
对于 push 和 replace,其中都会有一个 retry 和 allowTx,这里我们再看下
function retry() {
  push(to, state);
}
if (allowTx(nextAction, nextLocation, retry)) {
  globalHistory.pushState(historyState, '', url);
  // or
  globalHistory.replaceState(historyState, '', url);
}
function allowTx(action: Action, location: Location, retry: () => void): boolean {
  return (
    !blockers.length || (blockers.call({ action, location, retry }), false)
  );
}
如果我们通过 block 添加了一个 blocker,那么每次 push 或 replace 都会判断到 blocker.length 不为 0,那么就会传入对应的参数通知每个 blocker,之后会返回 false,从而无法进入条件,导致无法触发 pushState 或 replaceState,所以点击 Link 或调用 navigate 无法切换路由。
还有另外一个是在 handlePop 中,其在一开始调用 createBrowserHistory 的时候就往 window 上添加监听事件:
// PopStateEventType = 'popstate'
window.addEventListener(PopStateEventType, handlePop);
只要添加了该事件,那我们只要点击浏览器的前进、后退按钮、在 js 代码中调用 history.back()、history.forward()、history.go 方法,点击 a 标签都会触发该事件。
比如我们在 useEffect 中添加一个 blocker(详细代码可查看blocker) ,这段代码的意思是只要触发了上面的行为,那么第一和第二次都会弹窗提示,等到第三次才会调用 retry 成功切换路由
const countRef = useRef(0)
const { navigator } = useContext(UNSAFE_NavigationContext)
useEffect(() => {
  const unblock  = navigator.block((tx) => {
    // block两次后调用retry和取消block
    if (countRef.current < 2) {
      countRef.current  = countRef.current + 1
      alert(`再点 ${3 - countRef.current}次就可以切换路由`)
    } else {
      unblock();
      tx.retry()
    }
  })
}, [navigator])

我们看下 popstate的回调函数 handlePop:
function handlePop() {
  if (blockedPopTx) {
    blockers.call(blockedPopTx);
    blockedPopTx = null;
  } else {
    const nextAction = Action.Pop;
    const [nextIndex, nextLocation] = getIndexAndLocation();
    if (blockers.length) {
      if (nextIndex != null) {
        const delta = index - nextIndex;
        if (delta) {
          // Revert the POP
          blockedPopTx = {
            action: nextAction,
            location: nextLocation,
            retry() {
              go(delta * -1);
            }
          };
          go(delta);
        }
      } else {
        // Trying to POP to a location with no index. We did not create
        // this location, so we can't effectively block the navigation.
        warning(
          false,
          // TODO: Write up a doc that explains our blocking strategy in
          // detail and link to it here so people can understand better what
          // is going on and how to avoid it.
          `You are trying to block a POP navigation to a location that was not ` +
            `created by the history library. The block will fail silently in ` +
            `production, but in general you should do all navigation with the ` +
            `history library (instead of using window.history.pushState directly) ` +
            `to avoid this situation.`
        );
      }
    } else {
      applyTx(nextAction);
    }
  }
}
这里我们举个?比较容易理解:比如当前 url 为 http://localhost:3000/blocker,其 index 为 2,我们点击后退(其他能触发popstate的事件都可以),这个时候就立即触发了 handlePop,而此时地址栏的 url 实际上已经变化为http://localhost:3000 了,其获取到的 nextIndex 为 1(注意,这里的 index 只是我们举例用到,实际上不一定是上面的值,下面的 delta 也是)。

而由于有 blocker,所以会进行 blockedPopTx的赋值,从上面的 index 和 nextIndex 能获取到对应的 delta 为 1,那么 retry 中的 delta * -1 即为-1 了
const delta = index - nextIndex;
retry() {
  go(delta * -1)
}
然后继续走到下面的 go(delta),由于 delta是 1,那么又重新回到 http://localhost:3000/blocker了

注意!!注意!!集中精神了!!此处是真正触发前进后退时保持当前 location 不变的关键所在,也就是说其实 url 已经切换了,但是这里又通过go(delta)把 url 给切换回去。

还有需要特别注意一点的是,这里调用 go(delta) 后又会触发 handlePop,那么 if (blockedPopTx)就为 true 了,自然就会调用 blockers.call(blockedPopTx),blocer 可以根据 blockedPopTx 的 retry 看是否允许跳转页面,然后再把blockedPopTx = null。
那么当点击第三次后,由于我们 unblock 后 blockers 为空,且调用了 retry,即 go(-1),这个时候就能成功后退了。


也就是说,我点击后退,此时 url 为/,触发了handlePop,第一次给blockedPopTx赋值,然后go(delta)又返回了/blocker,随即又触发了handlePop,再次进入发现blockedPopTx有值,将 blockedPopTx 回调给每个 blocker,blocker 函数中 unblock 后调用 retry,即go(delta * -1)又切回了/,真是左右横跳啊~

由于 blockers 已经为空,那么 push、 replace 和 handlePop 中就可以每次都调用 applyTx(nextAction);,从而成功通知到对应的 listeners,这里透露下,BrowserRouter、HashRouter 就是通过 history.listen(setState)收听到每次 location 变化从而 setState 触发 render 的。
export function BrowserRouter({
  basename,
  children,
  window
}: BrowserRouterProps) {
  const historyRef = React.useRef<BrowserHistory>();
  if (historyRef.current == null) {
    // 如果为空,则创建
    historyRef.current = createBrowserHistory({ window });
  }
  const history = historyRef.current;
  const [state, setState] = React.useState({
    action: history.action,
    location: history.location
  });
  React.useLayoutEffect(() => {
    /**
     * popstate、push、replace时如果没有blokcers的话,会调用applyTx(nextAction)触发这里的setState
     * function applyTx(nextAction: Action) {
     *   action = nextAction;
     * //  获取当前index和location
     *   [index, location] = getIndexAndLocation();
     *   listeners.call({ action, location });
     * }
     */
    history.listen(setState)
  }, [history]);
  // 一般变化的就是action和location
  return (
    <Router
      basename={basename}
      children={children}
      action={state.action}
      location={state.location}
      navigator={history}
    />
  );
}
这也解释了为何block后路由虽然有切换,但是当前页面没有卸载,就是因为 applyTx(nextAction) 没有执行,导致 BrowserRouter 中没有收到通知。
重新看整个createBrowserHistory
我们上面一一解析了每个函数的作用,下面我们全部合起来再看一下,相信经过上面的分析,再看这整个函数就比较容易理解了
export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
): BrowserHistory {
  // 默认值是document.defaultView,即浏览器的window
  const { window = document.defaultView! } = options;
  const globalHistory = window.history;
  /** 获取索引和当前location */
  function getIndexAndLocation(): [number, Location] {
    const { pathname, search, hash } = window.location;
    const state = globalHistory.state || {};
    return [
      state.idx,
      readOnly<Location>({
        pathname,
        search,
        hash,
        state: state.usr || null,
        key: state.key || 'default'
      })
    ];
  }
  /** 用于存储下面的 blockers.call方法的参数,有 { action,location,retry }  */
  let blockedPopTx: Transition | null = null;
  /** popstate的回调, 点击浏览器 ← 或 → 会触发 */
  function handlePop() {
    // 第一次进来`blockedPopTx`没有值,然后下面的else判断到有blockers.length就会给`blockedPopTx`赋值,之后判断到if (delta)就会调用go(delta),
    // 从而再次触发handlePop,然后这里满足条件进入blockers.call(blockedPopTx)
    if (blockedPopTx) {
      // 如果参数有值,那么将参数传给blockers中的handlers
      blockers.call(blockedPopTx);
      // 然后参数置空
      blockedPopTx = null;
    } else {
      // 为空的话,给blockPopTx赋值
      // 因为是popstate,那么这里的nextAction就是pop了
      const nextAction = Action.Pop;
      // 点击浏览器前进或后退后的state.idx和location
      // 比如/basic/about的index = 2, 点击后退后就会触发handlePop,后退后的nextLocation.pathname = /basic, nextIndex = 1
      const [nextIndex, nextLocation] = getIndexAndLocation();
      if (blockers.length) {
        if (nextIndex != null) {
          // 这里的index是上一次的getIndexAndLocation得到了,下面有
          // 从上面例子 delta = index - nextIndex = 2 - 1 = 1
          const delta = index - nextIndex;
          if (delta) {
            // Revert the POP
            blockedPopTx = {
              action: nextAction,
              location: nextLocation,
              retry() {
                // 由于下面的go(delta)阻塞了当前页面的变化,那么retry就可以让页面真正符合浏览器行为的变化了
                // 这个在blocker回调中可以调用,但下面的go(delta)会触发handlePop,可是go(delta * -1)不会,为何????
                go(delta * -1);
              }
            };
            // 上面/basic/about => /basic,delta为1,那么go(1)就又到了/basic/about
            // 此处是真正触发前进后退时保持当前location不变的关键所在
            // 还有需要特别注意一点的是,这里调用go后又会触发handleProp,那么if (blockedPopTx)就为true了,那么
            // 就会调用blockers.call(blockedPopTx),blocer可以根据blockedPopTx的retry看是否允许跳转页面,然后再把blockedPopTx = null
            go(delta);
          }
        } else {
          // Trying to POP to a location with no index. We did not create
          // this location, so we can't effectively block the navigation.
          warning(
            false,
            // TODO: Write up a doc that explains our blocking strategy in
            // detail and link to it here so people can understand better what
            // is going on and how to avoid it.
            `You are trying to block a POP navigation to a location that was not ` +
              `created by the history library. The block will fail silently in ` +
              `production, but in general you should do all navigation with the ` +
              `history library (instead of using window.history.pushState directly) ` +
              `to avoid this situation.`
          );
        }
      } else {
        // blockers为空,那么赋值新的action,然后获取新的index和location,然后
        // 将action, location作为参数消费listeners
        applyTx(nextAction);
      }
    }
  }
  /**
   * 监听popstate
   * 调用history.pushState()或history.replaceState()不会触发popstate事件。
   * 只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的前进、后退按钮、在Javascript代码中调用history.back()
   * 、history.forward()、history.go方法,此外,a 标签的锚点也会触发该事件
   *
   * @see https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event
   */
  window.addEventListener(PopStateEventType, handlePop);
  let action = Action.Pop;
  // createBrowserHistory创建的时候获取初始当前路径index和location
  let [index, location] = getIndexAndLocation();
  // blockers不为空的话listeners不会触发
  const listeners = createEvents<Listener>();
  const blockers = createEvents<Blocker>();
  if (index == null) {
    // 初始index为空,那么给个0
    index = 0;
    // 这里replaceState后,history.state.idx就为0了
    globalHistory.replaceState({ ...globalHistory.state, idx: index }, '');
  }
  /** 返回一个新的href, to为string则返回to,否则返回 `createPath(to)` => pathname + search + hash */
  function createHref(to: To) {
    return typeof to === 'string' ? to : createPath(to);
  }
  /**
   * @description 获取新的Location
   * @param to 新的path
   * @param state 状态
   */
  function getNextLocation(to: To, state: State = null): Location {
    return readOnly<Location>({
      ...location,
      ...(typeof to === 'string' ? parsePath(to) : to),
      state,
      key: createKey()
    });
  }
  /** 获取state和url */
  function getHistoryStateAndUrl(
    nextLocation: Location,
    index: number
  ): [HistoryState, string] {
    return [
      {
        usr: nextLocation.state,
        key: nextLocation.key,
        idx: index
      },
      createHref(nextLocation)
    ];
  }
  /**
   * @description 判断是否允许路由切换,有blockers就不允许
   *
   * - blockers有handlers,那么消费handlers,然后返回false
   * - blockers没有handlers,返回true
   *  */
  function allowTx(action: Action, location: Location, retry: () => void): boolean {
    return (
      !blockers.length || (blockers.call({ action, location, retry }), false)
    );
  }
  /** blocker为空才执行所有的listener, handlePop、push、replace都会调用 */
  function applyTx(nextAction: Action) {
    debugger
    action = nextAction;
  //  获取当前index和location
    [index, location] = getIndexAndLocation();
    listeners.call({ action, location });
  }
  /** history.push,跳到哪个页面 */
  function push(to: To, state?: State) {
    debugger
    const nextAction = Action.Push;
    const nextLocation = getNextLocation(to, state);
    /**
     * retry的目的是为了如果有blockers可以在回调中调用
     * @example
     * const { navigator } = useContext(UNSAFE_NavigationContext)
     * const countRef = useRef(0)
     * useEffect(() => {
     *   const unblock  = navigator.block((tx) => {
     *     // block两次后调用retry和取消block
     *     if (countRef.current < 2) {
     *       countRef.current  = countRef.current + 1
     *     } else {
     *       unblock();
     *       tx.retry()
     *     }
     *   })
     * }, [navigator])
     *
     * 当前路径为/blocker
     * 点击<Link to="about">About({`<Link to="about">`})</Link>
     * 第三次(countRef.current >= 2)因为unblock了,随后调用rety也就是push(to, state)判断到下面的allowTx返回true,
     * 就成功pushState了,push到/blocker/about了
     */
    function retry() {
      push(to, state);
    }
    // 只要blockers不为空下面就进不去
    // 但是blockers回调里可以unblock(致使blockers.length = 0),然后再调用retry,那么又会重新进入这里,
    // 就可以调用下面的globalHistory改变路由了
    if (allowTx(nextAction, nextLocation, retry)) {
      const [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
      // TODO: Support forced reloading
      // try...catch because iOS limits us to 100 pushState calls :/
      // 用try  catch的原因是因为ios限制了100次pushState的调用
      try {
        globalHistory.pushState(historyState, '', url);
      } catch (error) {
        // They are going to lose state here, but there is no real
        // way to warn them about it since the page will refresh...
        window.location.assign(url);
      }
      applyTx(nextAction);
    }
  }
  function replace(to: To, state?: State) {
    const nextAction = Action.Replace;
    const nextLocation = getNextLocation(to, state);
    /**
     * retry的目的是为了如果有blockers可以在回调中调用
     * @example
     * const { navigator } = useContext(UNSAFE_NavigationContext)
     * const countRef = useRef(0)
     * useEffect(() => {
     *   const unblock  = navigator.block((tx) => {
     *     // block两次后调用retry和取消block
     *     if (countRef.current < 2) {
     *       countRef.current  = countRef.current + 1
     *     } else {
     *       unblock();
     *       tx.retry()
     *     }
     *   })
     * }, [navigator])
     *
     * 当前路径为/blocker
     * 点击<Link to="about">About({`<Link to="about">`})</Link>
     * 第三次(countRef.current >= 2)因为unblock了,随后调用rety也就是push(to, state)判断到下面的allowTx返回true,
     * 就成功pushState了,push到/blocker/about了
     */
    function retry() {
      replace(to, state);
    }
    // 只要blockers不为空下面就进不去
    // 但是blockers回调里可以unblock(致使blockers.length = 0),然后再调用retry,那么又会重新进入这里,
    // 就可以调用下面的globalHistory改变路由了
    if (allowTx(nextAction, nextLocation, retry)) {
      const [historyState, url] = getHistoryStateAndUrl(nextLocation, index);
      // TODO: Support forced reloading
      globalHistory.replaceState(historyState, '', url);
      applyTx(nextAction);
    }
  }
  /** eg: go(-1),返回上一个路由,go(1),进入下一个路由 */
  function go(delta: number) {
    globalHistory.go(delta);
  }
  // 这里创建一个新的history
  const history: BrowserHistory = {
    get action() {
      return action;
    },
    get location() {
      return location;
    },
    createHref,
    push,
    replace,
    go,
    back() {
      go(-1);
    },
    forward() {
      go(1);
    },
    listen(listener) {
      return listeners.push(listener);
    },
    block(blocker) {
      // push后返回unblock,即把该blocker从blockers去掉
      const unblock = blockers.push(blocker);
      if (blockers.length === 1) {
        // beforeunload
        // 只在第一次block加上beforeunload事件
        window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
      }
      return function() {
        unblock();
        // 移除beforeunload事件监听器以便document在pagehide事件中仍可以使用
        // Remove the beforeunload listener so the document may
        // still be salvageable in the pagehide event.
        // See https://html.spec.whatwg.org/#unloading-documents
        if (!blockers.length) {
          // 移除的时候发现blockers空了那么就移除`beforeunload`事件
          window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
        }
      };
    }
  };
  return history;
}
createHashHistory 与 createBrowserHistory 的不同点
两个工厂函数的绝大部分代码是一模一样的,以下是稍微不同之处:
- getIndexAndLocation。createBrowserHistory 是直接获取 window.location,而 createHashHistory 是- parsePath(window.location.hash.substr(1))
function getIndexAndLocation(): [number, Location] {
  const { pathname = '/', search = '', hash = '' } = parsePath(
    window.location.hash.substr(1)
  );
  const state = globalHistory.state || {};
  return [
    state.idx,
    readOnly<Location>({
      pathname,
      search,
      hash,
      state: state.usr || null,
      key: state.key || 'default'
    })
  ];
}
parsePath我们上面已经讲了,这个给个例子

即 url 中有多个#,但是会取第一个#后面的来解析对应的 pathname、search 和 hash
- createHashHistory 多了监听 hashchange的事件
window.addEventListener(HashChangeEventType, () => {
  const [, nextLocation] = getIndexAndLocation();
  // Ignore extraneous hashchange events.
  // 忽略无关的hashchange事件
  // 检测到hashchange,只有前后pathname + search + hash不一样才执行handlePop
  if (createPath(nextLocation) !== createPath(location)) {
    handlePop();
  }
});
- createHref 会在前面拼接 getBaseHref() + '#'
function getBaseHref() {
  // base一般为空,所以下面的href一般返回空字符串
  // 如果有 类似<base href="http://www.google.com"/>,那么获取到的href就为 "http://www.google.com/",可看下面示意图
  const base = document.querySelector('base');
  let href = '';
  if (base && base.getAttribute('href')) {
    const url = window.location.href;
    const hashIndex = url.indexOf('#');
    // 有hash的话去掉#及其之后的
    href = hashIndex === -1 ? url : url.slice(0, hashIndex);
  }
  return href;
}
// 后面部分和createBrowserHistory的createHref相同
function createHref(to: To) {
  return getBaseHref() + '#' + (typeof to === 'string' ? to : createPath(to));
}


结语
我们总结一下:
- history有- browserHistory和- hashHistory,两个工厂函数的绝大部分代码相同,只有- parsePath的入参不同和 hashHistory 增加了- hashchange事件的监听
- 通过 push、replace 和 go 可以切换路由
- 可以通过 history.listen 添加路由监听器 listener,每当路由切换可以收到最新的 action 和 location,从而做出不同的判断
- 可以通过 history.block 添加阻塞器 blocker,会阻塞 push、replace 和浏览器的前进后退。且只要判断有 blockers,那么同时会加上beforeunload阻止浏览器刷新、关闭等默认行为,即弹窗提示。且只要有 blocker,那么上面的 listener 就监听不到
- 最后我们也透露了BrowserRouter中就是通过history.listen(setState)来监听路由的变化,从而管理所有的路由
最后
history是react-router的基础,只有知道了其作用,才能在后续分析 react router 的过程中更加游刃有余,那我们下篇文章就开始真正的 react router 之旅,敬请期待~

往期文章
翻译翻译,什么叫 ReactDOM.createRoot
翻译翻译,什么叫 JSX
什么,React Router已经到V6了 ??
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
 
                     
     
        
       
        
       
        
       
        
       
    
发表评论
还没有评论,快来抢沙发吧!