history.js-4.10.1详解

1,104 阅读16分钟

本篇文档解读的是history.js4.10.1版本,虽然现在已经升级到了5.0版本(react-router6版本使用的是history5版本),但是我还是打算两个版本都了解一下,阅读源码之前充分详细的阅读它的官方文档是很重要的,

首先是目录,可以看到大致和官方文档相符,三个关键的api:createBrowserHistory,createHashHistory,createMemoryHistory,以及其他一下工具文件,

点开index.js还是老样子暴露出去了几个api,我们从最常用的createBrowserHistory看起

createBrowserHistory.js

可以看到直接暴露出去了一个function,这个function返回了一个history对象,包含了函数内声明的一些方法(看名字应该就能大致知道作用)

根据在使用时候的用法首先会调用createBrowserHistory,我们先看一下在初始化的时候会做些什么

初始化

	invariant(canUseDOM, 'Browser history needs a DOM');
    // 全局history
    const globalHistory = window.history;
    // 是否可以使用history pushState api
    const canUseHistory = supportsHistory();
    // 是否需要hashChangeListener
    const needsHashChangeListener = !supportsPopStateOnHashChange();

    // 创建管理对象
    const transitionManager = createTransitionManager();
    // 初始化location
    const initialLocation = getDOMLocation(getHistoryState());
    // 所有的key
    let allKeys = [initialLocation.key];

首先使用了canUseDom方法和tiny-invariant这个库,来判断当前环境,如果不是浏览器环境则会抛出错误

canUseDom

  export const canUseDOM = !!(
      typeof window !== 'undefined' &&
    window.document &&
    window.document.createElement
  );

返回当前是否出了浏览器环境

tiny-invariant

var isProduction = process.env.NODE_ENV === 'production';
var prefix = 'Invariant failed';
function invariant(condition, message) {
    if (condition) {
        return;
    }
    if (isProduction) {
        throw new Error(prefix);
    }
    throw new Error(prefix + ": " + (message || ''));
}
export default invariant;

根据是否为true来抛出error

接下里是判断当前浏览器是否支持history.pushState的api

  // 是否可以使用history pushState api
  const canUseHistory = supportsHistory();

  // 是否支持history api
  export function supportsHistory() {
      const ua = window.navigator.userAgent;
      if (
          (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
      ua.indexOf('Mobile Safari') !== -1 &&
      ua.indexOf('Chrome') === -1 &&
      ua.indexOf('Windows Phone') === -1
      ) { return false; }
      return window.history && 'pushState' in window.history;
  }

判断是否需要使用hashChange时间监听浏览器变化

   // 是否需要hashChangeListener
   const needsHashChangeListener = !supportsPopStateOnHashChange();

  /**
  * Returns true if browser fires popstate on hash change.
  * IE10 and IE11 do not.
  * hash更改是会触发popstate
  *      (
  *          直接使用location.hash=就会触发, 但是注意如果hash值相同  使用location.hash= 赋值 不会触发popstate
  *              但是使用a标签href= 做锚点则不论是否相等  一定会触发popstate
  *       )
  * 在ie10 和ie11中不会触发
  */
   export function supportsPopStateOnHashChange() {
       return window.navigator.userAgent.indexOf('Trident') === -1;
   }

处理basename

	const {
        forceRefresh = false, // 是否强制刷新页面
        getUserConfirmation = getConfirmation, // 自定义离开当前页面钩子
        keyLength = 6 // location.key的长度
    } = props;
    const basename = props.basename
         // 添加第一个字符/  和去掉最后一个字符/
        ? stripTrailingSlash(addLeadingSlash(props.basename))
        : '';

addLeadingSlash

  export function addLeadingSlash(path) {
      return path.charAt(0) === '/' ? path : '/' + path;
  }

添加basename开头的'/'

stripTrailingSlash

  export function stripTrailingSlash(path) {
      return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;
  }

去掉尾部多余的'/'

接下里创建了一个管理对象,

  // 创建管理对象
  const transitionManager = createTransitionManager();

初始化location,存储初始化的key

  // 初始化location
  const initialLocation = getDOMLocation(getHistoryState());
  // 所有的key
  let allKeys = [initialLocation.key];

getHistoryState 获取当前window.history的state

  function 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
          return {};
      }
   }

getDOMLocation

	 // 获取当前url 生成location
    function getDOMLocation(historyState) {
        const { key, state } = historyState || {};
        const { pathname, search, hash } = window.location;

        let path = pathname + search + hash;
        // 如果传入basename 并且当前路径不包含basename 则抛出warning
        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 +
        '".'
        );
        // 如果有basename 截取path除了basename的部分
        if (basename) { path = stripBasename(path, basename); }

        return createLocation(path, state, key);
    }

获取当前historyState存储的key和state,获取当前浏览器location的pathname search hash

拼接pathname search hash 为path

hasBasename

使用hasBasename判断当前path是否包含basename(开通,且basename后为 / 或 ? 或 #),如果没有包含则抛出warn

  export function hasBasename(path, prefix) {
      return (
          // 判断pat是否一个prefix 开头
          path.toLowerCase().indexOf(prefix.toLowerCase()) === 0 &&
          // 判断path的prefix之后是否为 / ? #
      '/?#'.indexOf(path.charAt(prefix.length)) !== -1
      );
  }

stripBasename 把path的basename截去

   // 截取path除了prefix的部分
  export function stripBasename(path, prefix) {
      return hasBasename(path, prefix) ? path.substr(prefix.length) : path;
  }

使用createLocation创建location

createLocation

// 根据path state生成location
export function createLocation(path, state, key, currentLocation) {
    let location;
    // 处理两个参数push的情况
    if (typeof path === 'string') {
        location = parsePath(path);
        location.state = state;
    // 处理一个参数push的情况
    } else {
        location = { ...path };
        if (location.pathname === undefined) { location.pathname = ''; }
        // 补全search ?
        if (location.search) {
            if (location.search.charAt(0) !== '?') { location.search = '?' + location.search; }
        } else {
            location.search = '';
        }
        // 补全hash #
        if (location.hash) {
            if (location.hash.charAt(0) !== '#') { location.hash = '#' + location.hash; }
        } else {
            location.hash = '';
        }
        // 如果state不为undefined 且location的state为undefined 则赋值
        if (state !== undefined && location.state === undefined) { location.state = state; }
    }
    // 转码pathname
    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;
        }
    }
    // 如果key存在设置location的key
    if (key) { location.key = key; }
    if (currentLocation) {
        if (!location.pathname) {
            location.pathname = currentLocation.pathname;
        // 解析不完整的相对路径
        } else if (location.pathname.charAt(0) !== '/') {
            location.pathname = resolvePathname(
                location.pathname,
                currentLocation.pathname
            );
        }
    } else {
    // 且path为空的情况下设置为/
        if (!location.pathname) {
            location.pathname = '/';
        }
    }
    return location;
}

这个方法会判断传入的path类型,如果为string类型,则使用parseaPath解析

parsePath

// 解析path, 返回pathname search hash
export function parsePath(path) {
    let pathname = path || '/';
    let search = '';
    let hash = '';
    const hashIndex = pathname.indexOf('#');
    if (hashIndex !== -1) {
        hash = pathname.substr(hashIndex);
        pathname = pathname.substr(0, hashIndex);
    }
    const searchIndex = pathname.indexOf('?');
    if (searchIndex !== -1) {
        search = pathname.substr(searchIndex);
        pathname = pathname.substr(0, searchIndex);
    }
    return {
        pathname,
        search: search === '?' ? '' : search,
        hash: hash === '#' ? '' : hash
    };
}

这个方法解析path并返回pathname search hash

解析完path之后,赋值传入的state给location

转码当前的pathname

设置当前location的key

如果传入当前的currentLocation,并且location的pathname不存在,则使用current的pathname

如果location的pathname为相对路径,则拼接当前location的pathname

如果path为不为string,则拷贝当前的path为location,分别补全当前的search,hash,如果传入state,且当前location的state为undefined则赋值state

使用push方法

调用当前的push方法, 首先是容错的判断,如果path为一个存在state不为undefined的object,且传入第二个参数state,则warning

	// push函数 使用pushState
    function push(path, state) {
        warning(
            !(
                typeof path === 'object' &&
                path.state !== undefined &&
                state !== undefined
            ),
            // 当地一个参数已经是有state的对象时,不应该使用第二个参数,将被忽略
            '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的location
        const location = createLocation(path, state, createKey(), history.location);
        // 调用转换confirm
        transitionManager.confirmTransitionTo(
            location,
            action,
            getUserConfirmation,
            ok => {
                if (!ok) { return; }
                // 创建完整的href
                const href = createHref(location);
                // 获取当前location的state 和key
                const { key, state } = location;
                // 当前环境支持puState api
                if (canUseHistory) {
                    // 使用全局history的pushState  并传入{key, state} 作为state
                    globalHistory.pushState({ key, state }, null, href);
                    // 如果强制刷新 则使用location.href直接刷新
                    if (forceRefresh) {
                        window.location.href = href;
                    } else {
                        // 获取上一个key的index
                        const prevIndex = allKeys.indexOf(history.location.key);
                        // 获取之前的keys
                        const nextKeys = allKeys.slice(0, prevIndex + 1);
                        // 添加当前的key
                        nextKeys.push(location.key);
                        allKeys = nextKeys;
                        // 重新赋值history 调用监听函数
                        setState({ action, location });
                    }
                } else {
                    warning(
                        state === undefined,
                        'Browser history cannot push state in browsers that do not support HTML5 history'
                    );


                    window.location.href = href;
                }
            }
        );
    }

创建当前的action为push,创建location,传入path,state,

使用createKey创建一个key

	 // 创建key
    function createKey() {
        return Math.random()
            .toString(36)
            .substr(2, keyLength);
    }

传入当前的location

调用管理对象的confirmTransitionTo,传入callback

当callback参数为false时,则直接return(用户取消跳转)

当为true时使用createHref创建完整的href

createHref 拼接当前的basename

	function createHref(location) {
        return basename + createPath(location);
    }
	// 创建完整路径
export function createPath(location) {
    const { pathname, search, hash } = location;
    let path = pathname || '/';

    if (search && search !== '?') { path += search.charAt(0) === '?' ? search : `?${search}`; }

    if (hash && hash !== '#') { path += hash.charAt(0) === '#' ? hash : `#${hash}`; }

    return path;
}

如果当前支持history.pushState 则调用window.history.pushState方法,使用key和state最为第一个history的state传入

如果forceRefresh参数为true则直接使用window.location.href刷新页面

获取当前的keys,并加入新生成的key,调用setState方法,传入action和location

setState

    function setState(nextState) {
        // history合并最新的location和action
        Object.assign(history, nextState);
        // 赋值最新的历史记录属性
        history.length = globalHistory.length;
        // 调用监听函数
        transitionManager.notifyListeners(history.location, history.action);
    }

这里同步的把action和location同步给了history,同步了history.length,调用监听函数,传入了location和action

使用replace方法

// replace函数 使用replaceState
    function replace(path, state) {
        // 当path为一个含有state的location对象,并且第二个state参数有值时,第二个参数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';
        // 创建当前location
        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) {
                    // 使用replaceState替换当前href
                    globalHistory.replaceState({ key, state }, null, href);

                    if (forceRefresh) {
                        window.location.replace(href);
                    } else {
                        const prevIndex = allKeys.indexOf(history.location.key);
                        // 使用当前key替换上一个key
                        if (prevIndex !== -1) { allKeys[prevIndex] = location.key; }
                        // 重新赋值history,调用监听函数
                        setState({ action, location });
                    }
                } else {
                    warning(
                        state === undefined,
                        'Browser history cannot replace state in browsers that do not support HTML5 history'
                    );

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

与pushState方法不同的是使用了history.replaceState方法,key从push变为替换,直接使用href=变为replace

block/listen/checkDOMListeners

let listenerCount = 0;
    // 添加 popstate 和hashchange函数
    function checkDOMListeners(delta) {
        // 依据参数判断添加监听或者移除
        listenerCount += delta;

        if (listenerCount === 1 && delta === 1) {
            window.addEventListener(PopStateEvent, handlePopState);

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

    let isBlocked = false;

    // 设置block函数
    function block(prompt = false) {
        // 设置block
        const unblock = transitionManager.setPrompt(prompt);


        if (!isBlocked) {
            // 第一次添加block函数  传入checkDOMListeners 1
            checkDOMListeners(1);
            // 已添加block
            isBlocked = true;
        }
        // 返回取消block函数
        return () => {
            if (isBlocked) {
                // 设置blocked
                isBlocked = false;
                // 更改监听content
                checkDOMListeners(-1);
            }

            return unblock();
        };
    }
    // 监听
    function listen(listener) {
        const unlisten = transitionManager.appendListener(listener);
        checkDOMListeners(1);

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

checkDOMListeners方法用于添加和移除popState和hashChaneg的事件监听器,巧妙的利用了listenerCount的值,来避免重复添加

block方法设置了管理对象的prompt,调用checkDOMListeners传入1,使用isBlock来避免重复调用,返回一个函数,调用checkDOMListerns传入-1,并调用了管理对象的setPrompt返回的方法

listen方法则是直接调用管理对象添加监听函数,调用checkDOMListeners传入1,返回取消监听函数调用checkDOMListeners传入-1,调用管理对象返回的取消监听函数

浏览器后退、前进、使用go方法触发popstate/hashchange

	 window.addEventListener(PopStateEvent, handlePopState);
     if (needsHashChangeListener) { window.addEventListener(HashChangeEvent, handleHashChange); }

这里可以看到在使用block和listen的时候添加了对于popstate和hashchang的事件监听器(在ie10 ie11 hashchange的back和go不会触发popstate事件)

	// popState函数
    function handlePopState(event) {
        // Ignore extraneous popstate events in WebKit.
        if (isExtraneousPopstateEvent(event)) { return; }
        // 获取当前浏览器储存的state  生成location
        handlePop(getDOMLocation(event.state));
    }
    // hashchange 函数
    function handleHashChange() {
        /*
            在ie10 ie11 hash的change不会触发popstate,所以需要使用window.history.state获取当前的history.state
       */
        handlePop(getDOMLocation(getHistoryState()));
    }

两个函数都是使用getDOMLocation生成当前的location传入handlepop函数 不同的是popstate传入的是event的state,hash使用的是window.history.state

isExtraneousPopstateEvent

其中在popstate做了一层容错判断

  // 兼容ios的chrome会触发一次无意义的popstate
  export function isExtraneousPopstateEvent(event) {
      return event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1;
  }

handlePop

    let forceNextPop = false;
    // 处理hashchange 和popState 接收当前的location
    function handlePop(location) {
        // 如果当前为revertPop 直接setState(没有action和新的location是并没有更改,在这里调用是为了同步history.length)
        if (forceNextPop) {
            forceNextPop = false;
            setState();
        } else {
            // 声明action
            const action = 'POP';
            // 调用跳转确认函数
            transitionManager.confirmTransitionTo(
                location,
                action,
                getUserConfirmation,
                ok => {
                    // 如果确认跳转 则更改location
                    if (ok) {
                        setState({ action, location });
                    } else {
                        // 如果不跳转 则revert
                        revertPop(location);
                    }
                }
            );
        }
    }

这里声明了一个forceNextPop变量为fals,先不管,按照正常流程用户点击后退按钮,这为false,

则声明action为pop,调用管理对象的确认跳转函数,如果为true则直接setState,传入action,location重新赋值history,并调用监听函数

如果为false这里调用了一个reverPop函数传入了location

revertPop

     /*
        revert函数 场景:在用户点击返回按钮时触发popstate或者hashchange,获取当前的history.state与url生成fromLocation
            调用confirm,如果用户点击取消,则返回之前的url,
            那么此时的fromLocation是跳转之后根据浏览器url生成的location
            toLocation为当前history变量的location,即上一个location
    */
    function revertPop(fromLocation) {
        const toLocation = history.location;
        let toIndex = allKeys.indexOf(toLocation.key);
        // 找到前往的key
        if (toIndex === -1) { toIndex = 0; }
        // 找到来源key
        let fromIndex = allKeys.indexOf(fromLocation.key);


        if (fromIndex === -1) { fromIndex = 0; }
        // 获得revert的层级
        const delta = toIndex - fromIndex;


        if (delta) {
            forceNextPop = true;
            go(delta);
        }
    }

按照用户点击后退然后又点击取消跳转,那么此时的fromLocation为跳转过之后的location,history.location为当前要前往的location,这里使用了key的位置来确定了revert的层级(如果用户点击后退,那么层级必然相差1),更改了forceNextPop为true,又一次使用了go方法跳转

那么此时又会触发pop时间,调用handlePop函数,此时foreNextPop为true,则直接setState,因为此时的history变量并没有改变所以未传值

createHashHistory.js

使用hash路由模式,与browser模式的区别是使用hashPath,即#后边的路由模式,使用hashchange监听路由更改,由于好多东西和browser重复,一样的就简单略过

可以看到同样是暴露出来了一个history对象,和一些方法,我们还是先从初始化开始

初始化

	 // 是否在浏览器环境
    invariant(canUseDOM, 'Hash history needs a DOM');

    // 全局history
    const globalHistory = window.history;

    // 使用history.go是否会触发页面重载
    const canGoWithoutReload = supportsGoWithoutReloadUsingHash();

    // 跳转确认函数, hash编码方式
    const { getUserConfirmation = getConfirmation, hashType = 'slash' } = props;

    const basename = props.basename
        // 对basename处理,加上开头和结尾的 /
        ? stripTrailingSlash(addLeadingSlash(props.basename))
        : '';
    // 拿到编码方式
    const { encodePath, decodePath } = HashPathCoders[hashType];

    // 创建管理对象
    const transitionManager = createTransitionManager();

    // 获取当前hashPath
    const path = getHashPath();
    // 获取当前编码后的hahsPath
    const encodedPath = encodePath(path);
    // 如果不相等 使用replaceHash替换当前path
    /*
    *  场景使用默认slash编码方式
    *   当前初始化url:/ 则当前hashPath为''  编码后的hashPath为/
    *   则会替换当前hashPath
    */
    if (path !== encodedPath) { replaceHashPath(encodedPath); }


    // 初始化location
    const initialLocation = getDOMLocation();
    let allPaths = [createPath(initialLocation)];

判断当前是否在浏览器环境下

拿到window的history

使用history.go是否会导致页面重载

/* Returns false if using go(n) with hash history causes a full page reload.
 * 使用history.go()是否会触发页面重载(hash的更改)
 */
export function supportsGoWithoutReloadUsingHash() {
    // 应该是在底版的firefox有这个问题
    return window.navigator.userAgent.indexOf('Firefox') === -1;
}

这里特别针对firefox判断了一下,应该是低版本的firefox有这个问题

接下来拿到了跳转确认函数,处理了basename,和brwoser是一样的

HashPathCoders

拿到了hash的编码方式

	const HashPathCoders = {
      hashbang: {
          encodePath: path =>
              path.charAt(0) === '!' ? path : '!/' + stripLeadingSlash(path),
          decodePath: path => (path.charAt(0) === '!' ? path.substr(1) : path)
      },
      noslash: {
          encodePath: stripLeadingSlash,
          decodePath: addLeadingSlash
      },
      slash: {
          encodePath: addLeadingSlash,
          decodePath: addLeadingSlash
      }
   };

有这几种编码方式,无非是加了一个!和 带不带/开头

获得当前的hashPath也就是#后边的内容,然后进行转码,判断hashpath和转码之后的不同的话replace了一下

	// 获取当前hashPath
    const path = getHashPath();
    // 获取当前编码后的hahsPath
    const encodedPath = encodePath(path);
    // 如果不相等 使用replaceHash替换当前path
    /*
    *  场景使用默认slash编码方式
    *   当前初始化url:/ 则当前hashPath为''  编码后的hashPath为/
    *   则会替换当前hashPath
    */
    if (path !== encodedPath) { replaceHashPath(encodedPath); }	

getHashPath

    // 获得当前浏览器的hash 没有使用location.hash 因为在firefox存在编码问题(可以是低版本的)
    function getHashPath() {
        const href = window.location.href;
        const hashIndex = href.indexOf('#');
        return hashIndex === -1 ? '' : href.substring(hashIndex + 1);
    }

replaceHashPath

    // 使用replace方法替换当前hash  直接使用location.hash方法会产生历史记录
    function replaceHashPath(path) {
        window.location.replace(stripHash(window.location.href) + '#' + path);
    }

获取当前location,初始化key

    // 初始化location
    const initialLocation = getDOMLocation();
    let allPaths = [createPath(initialLocation)];

getDOMLocation

getDOMLocation与browser的区别在于获取的是hashPath

    // 获取当前的location
    function getDOMLocation() {
        let path = decodePath(getHashPath());

        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 +
        '".'
        );
        // 截去当前basename
        if (basename) { path = stripBasename(path, basename); }

        return createLocation(path);
    }	

push流程

function push(path, state) {
        warning(
            state === undefined,
            'Hash history cannot push state; it is ignored'
        );
        /*
            这里可以看到如果传入第二个参数state是被忽略的,但是createLocation并没有判断当前使用的hash模式不赋值state
            所以使用path为一个object的情况下还是可以传的,
            官方文档说的hash模式不支持state,应该是把state存入当前的history.state加上key,
            也就是state和当前路径是绑定的,而hash模式是不使用pushState的
        */
        const action = 'PUSH';
        const location = createLocation(
            path,
            undefined,
            undefined,
            history.location
        );
        transitionManager.confirmTransitionTo(
            location,
            action,
            getUserConfirmation,
            ok => {
                if (!ok) { return; }


                const path = createPath(location);
                // 创建编码后的hashPath
                const encodedPath = encodePath(basename + path);
                // 比较此次push的path是否更改
                const hashChanged = getHashPath() !== encodedPath;

                if (hashChanged) {
                    // 储存当前的path,在触发hashChang的时候标识为push更改的
                    ignorePath = path;
                    pushHashPath(encodedPath);


                    // 找到最后一个当前的path,hash模式是没有生成唯一key的,因为没有pushState第一个参数那样的储存方式
                    const prevIndex = allPaths.lastIndexOf(createPath(history.location));
                    const nextPaths = allPaths.slice(0, prevIndex + 1);
                    // 添加当前的path
                    nextPaths.push(path);
                    allPaths = nextPaths;


                    setState({ action, location });
                } else {
                    warning(
                        false,
                        'Hash history cannot PUSH the same path; a new entry will not be added to the history stack'
                    );


                    setState();
                }
            }
        );
    }

这里不同的地方在于hash模式不支持state参数,因为作者是想把state作为每一个记录的状态储存下来,但是hash模式不像browser模式下可以使用pushState的第一个参数那样可以往全局的history记录状态

这里也是使用createLocation创建location,调用路由跳转的确认

在这里比较了当前hash与即将更改的hash是否相同,如果相同的话,报了个warning,直接setState了一下

如果没有更改的话使用ignorePath变量储存当前的path,标识触发hashchange的是为push更改

使用pushHash更改当前hash

	// 使用location.hash更改hash
    function pushHashPath(path) {
        window.location.hash = path;
    }

添加当前的key,与browser不同的是,存储的是当前的hash,并且使用lastIndexOf方法,因为可能存在两个相同的历史

最后使用setState同步history,调用监听函数

replace

    function replace(path, state) {
        warning(
            state === undefined,
            'Hash history cannot replace state; it is ignored'
        );

        const action = 'REPLACE';
        const location = createLocation(
            path,
            undefined,
            undefined,
            history.location
        );

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

                const path = createPath(location);
                const encodedPath = encodePath(basename + path);
                const hashChanged = getHashPath() !== encodedPath;

                if (hashChanged) {
                    ignorePath = path;
                    replaceHashPath(encodedPath);
                }

                const prevIndex = allPaths.indexOf(createPath(history.location));

                if (prevIndex !== -1) { allPaths[prevIndex] = path; }

                setState({ action, location });
            }
        );
    }

这里与push方法相似,不同的是使用了replace方法更新了hash,hash未更改的时候也会传入setState方法,应该是为了更换action,添加key的方法变为了替换

浏览器前进/后退、使用go方法触发hashchange

	/*
    *  在browser模式中很奇怪,hash的更改会触发popstate事件,并且直接使用location.hash=就能触发,
    *   更奇怪的是赋值相同的hash 使用location.hash=不会触发popstate,但是使用a.click()相同的hash也能触发
    *   但是在hash模式下,hashchang事件完全监听不到相同的hash值赋予,无论是location.hash=还是a.click()
    */
    function handleHashChange() {
        const path = getHashPath();
        const encodedPath = encodePath(path);
        // 如果当前path与转码之后不符,则使用replace替换重新触发
        if (path !== encodedPath) {
            // Ensure we always have a properly-encoded hash.
            replaceHashPath(encodedPath);
        } else {
            const location = getDOMLocation();
            const prevLocation = history.location;
            // 如果hash更改但是hashPath search hash没有更改则直return
            if (!forceNextPop && locationsAreEqual(prevLocation, location)) { return; }
            // 如果是push replace触发的hashchange 则直接return
            if (ignorePath === createPath(location)) { return; }


            ignorePath = null;


            handlePop(location);
        }
    }

这里是先获得当前的hash,判断与转码之后是否相同,不同的话就使用replace重新触发

接下来判断hash是否更改,这里的更改指的是pathname search hash是否更改 因为本来就是hash下的path search hash(但是具测试,相同的hash值使用location.= 和a.click不会触发hashchange)

locationsAreEqual

	 function locationsAreEqual(a, b) {
        return (
            a.pathname === b.pathname && a.search === b.search && a.hash === b.hash
        );
    }

接下来是如果是push或者replace触发的直接return

   if (ignorePath === createPath(location)) { return; }

置ignorePath为null

调用handlepop

	 function handlePop(location) {
        if (forceNextPop) {
            forceNextPop = false;
            setState();
        } else {
            const action = 'POP';


            transitionManager.confirmTransitionTo(
                location,
                action,
                getUserConfirmation,
                ok => {
                    if (ok) {
                        setState({ action, location });
                    } else {
                        revertPop(location);
                    }
                }
            );
        }
    }

这里与browser类似,正常流程forceNextPop为false

调用确认跳转函数,如果确认直接setState,同步history,调用订阅函数

如果为false则调用revertPop

revertPop

    function revertPop(fromLocation) {
        const toLocation = history.location;
        let toIndex = allPaths.lastIndexOf(createPath(toLocation));

        if (toIndex === -1) { toIndex = 0; }

        let fromIndex = allPaths.lastIndexOf(createPath(fromLocation));


        if (fromIndex === -1) { fromIndex = 0; }

        const delta = toIndex - fromIndex;


        if (delta) {
            forceNextPop = true;
            go(delta);
        }
    }

这样与browser类似,fromLocation是当前浏览器获得的location,toLocation是当前声明的history的location(还未同步),只不过找到key的方法改为了lastIndexOf,使用key相差的层级来确定go的层级

再次触发hashchange,注意此时的

    // 如果hash更改但是hashPath search hash没有更改则直return
    if (!forceNextPop && locationsAreEqual(prevLocation, location)) { return; }

两个location是相同的,但是forceNextPop为true,

触发handlePop直接setState

	 if (forceNextPop) {
         forceNextPop = false;
         setState();	

other

其他api大致与browser一样,不同的是在使用go的时候会判断是否刷新(firefox)触发警告

	 function go(n) {
        warning(
            canGoWithoutReload,
            'Hash history go(n) causes a full page reload in this browser'
        );
        globalHistory.go(n);
    }

全文在博主个人博客,由于字数限制 这里删减了后续一部分