最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 你居然不知道React Router用到了history!! - 掘金

    正文概述 掘金(暴走老七)   2021-11-02   1180

    前言

    React Router 中很大程度上地依赖了 history 的功能,如 useNavigateuseHrefRouter 等都直接或间接地用到了 history,所以我们在分析 React Router 源码之前,有必要深入了解下 history 的用法,相信您看完本篇文章之后,能学到很多之前不知道的东西。写下本篇文章时的 history 版本是 latest 的,为 5.0.1,那废话不多说,让我们开始探索之旅吧~

    history 分类

    history 又分为 browserHistoryhashHistory,对应 react-router-dom 中的 BrowserRouterHashRouter, 这两者的绝大部分都相同,我们先分析下 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 对象罢了

    你居然不知道React Router用到了history!! - 掘金

    history

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

    你居然不知道React Router用到了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)
    

    你居然不知道React Router用到了history!! - 掘金

    action

    blockerslisteners 的回调会用到 action,其是通过 handlePoppushreplace 三个函数修改其状态,分别为 POPPUSHREPLACE,这样我们就可以通过判断 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.actionhistory.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 不为空)

    你居然不知道React Router用到了history!! - 掘金

    所以下面会通过判断 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 就能获取到

    你居然不知道React Router用到了history!! - 掘金

    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,根据 tostate 获取到新的 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

    用于历史堆栈的前进后退,backforward 分别是 是 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);pushreplacehandlePop 三个函数中成功切换路由后调用。

    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 都会弹窗提示:

    你居然不知道React Router用到了history!! - 掘金

    你居然不知道React Router用到了history!! - 掘金

    你居然不知道React Router用到了history!! - 掘金

    刷新会询问重新加载此网站?,而关闭 tab 或修改地址栏后 enter 是提示离开此网站?,这两种要注意区别。

    这功能常用在表单提交的页面,避免用户不小心关闭 tab 导致表单数据丢失。

    当然如果 unblock 时发现 blockers 为空就会移除 beforeunload 事件了。

    history 如何阻止路由切换

    说完上面的beforeunload 事件,我们关注下上面跳过的 block 方面的代码

    对于 pushreplace,其中都会有一个 retryallowTx,这里我们再看下

    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,从而无法进入条件,导致无法触发 pushStatereplaceState,所以点击 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])
    

    你居然不知道React Router用到了history!! - 掘金

    我们看下 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 也是)。

    你居然不知道React Router用到了history!! - 掘金

    而由于有 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

    你居然不知道React Router用到了history!! - 掘金

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

    还有需要特别注意一点的是,这里调用 go(delta) 后又会触发 handlePop,那么 if (blockedPopTx)就为 true 了,自然就会调用 blockers.call(blockedPopTx),blocer 可以根据 blockedPopTx 的 retry 看是否允许跳转页面,然后再把blockedPopTx = null

    那么当点击第三次后,由于我们 unblock 后 blockers 为空,且调用了 retry,即 go(-1),这个时候就能成功后退了。

    你居然不知道React Router用到了history!! - 掘金

    你居然不知道React Router用到了history!! - 掘金

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

    你居然不知道React Router用到了history!! - 掘金

    由于 blockers 已经为空,那么 pushreplacehandlePop 中就可以每次都调用 applyTx(nextAction);,从而成功通知到对应的 listeners,这里透露下,BrowserRouterHashRouter 就是通过 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 的不同点

    两个工厂函数的绝大部分代码是一模一样的,以下是稍微不同之处:

    1. 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我们上面已经讲了,这个给个例子

    你居然不知道React Router用到了history!! - 掘金

    即 url 中有多个#,但是会取第一个#后面的来解析对应的 pathname、search 和 hash

    1. createHashHistory 多了监听 hashchange的事件
    window.addEventListener(HashChangeEventType, () => {
      const [, nextLocation] = getIndexAndLocation();
    
      // Ignore extraneous hashchange events.
      // 忽略无关的hashchange事件
      // 检测到hashchange,只有前后pathname + search + hash不一样才执行handlePop
      if (createPath(nextLocation) !== createPath(location)) {
        handlePop();
      }
    });
    
    1. 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));
    }
    

    你居然不知道React Router用到了history!! - 掘金

    你居然不知道React Router用到了history!! - 掘金

    结语

    我们总结一下:

    • historybrowserHistoryhashHistory,两个工厂函数的绝大部分代码相同,只有parsePath的入参不同和 hashHistory 增加了hashchange事件的监听
    • 通过 push、replace 和 go 可以切换路由
    • 可以通过 history.listen 添加路由监听器 listener,每当路由切换可以收到最新的 action 和 location,从而做出不同的判断
    • 可以通过 history.block 添加阻塞器 blocker,会阻塞 push、replace 和浏览器的前进后退。且只要判断有 blockers,那么同时会加上beforeunload阻止浏览器刷新、关闭等默认行为,即弹窗提示。且只要有 blocker,那么上面的 listener 就监听不到
    • 最后我们也透露了BrowserRouter中就是通过 history.listen(setState) 来监听路由的变化,从而管理所有的路由

    最后

    historyreact-router的基础,只有知道了其作用,才能在后续分析 react router 的过程中更加游刃有余,那我们下篇文章就开始真正的 react router 之旅,敬请期待~

    你居然不知道React Router用到了history!! - 掘金

    往期文章

    翻译翻译,什么叫 ReactDOM.createRoot

    翻译翻译,什么叫 JSX

    什么,React Router已经到V6了 ??


    起源地下载网 » 你居然不知道React Router用到了history!! - 掘金

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元