探索JavaScript中history源码之hashHistory的实现

摘录自:https://segmentfault.com/a/1190000012656017

history简介

通常有2种history,分别是hashHistorybrowserHistory,本文带领大家从零开始实现一个hashHistory

hashHistory:'#/home'
browserHistory: '/home'

下面的实现方案是根据官方history源码来分析的,以hashHistory源码结合本文学习。

实现方案

1、创建createHashHistory函数

const createHashHistory = () => {
  const history = {};
  return history;
};
export default createHashHistory;

2、先要了解history对象长什么样,接着,我们一个个去实现它

history = {
  length: 1,  //  Number
  action: "POP",  //  String
  loaction: {}, //  Object
  createHref, // 函数
  push, //  函数
  replace,  //  函数
  go, //  函数
  goBack, //  函数
  goForward,  //  函数
  listen  //  函数
}

3、实现length
在window下面有一个history对象,可以用来获取length。

const globalHistory = window.history;
history = {
  length: globalHistory.length
};

4、action默认为POP,它还可能是PUSH或者REPLACE。我们不在这一步实现它,等下面实现push和replace的时候再来实现。
5、实现location
location对象包含下面几个key,这里能用到的是pathname。history.location和window.location是不一样的,history.location是window.location的精简版。你可以在浏览器控制台打印window.location看一下完整的location对象。

location = {
  hash: "",
  pathname: "/",
  search: "",
  state: undefined
}

定义一个getDOMLocation函数,用来获取封装后的location。

const decodePath = path => 
  path.charAt(0) === "/" ? path : "/" + path

const getHashPath = () => {
  //  如果url存在#,则去掉#,返回路径
  //  比如:"http://localhost:8080/#/",返回'/'
  const href = window.location.href;
  const hashIndex = href.indexOf("#");
  return hashIndex === -1 ? "" : href.substring(hashIndex + 1);
}

const getDOMLocation = () => {
  //  getHashPath获取url的路由,如果存在#,则去掉#
  let path = decodePath(getHashPath());
  //  创建location
  return createLocation(path);
}

这一步的核心就是createLocation()的实现。但是,它不复杂,只是代码有点长,如果要了解,请看源码如下:

import resolvePathname from "resolve-pathname"
import valueEqual from "value-equal"
import { parsePath } from "./PathUtils"

export const createLocation = (path, state, key, currentLocation) => {
  let location
  if (typeof path === "string") {
    // Two-arg form: push(path, state)
    location = parsePath(path)
    location.state = state
  } else {
    // One-arg form: push(location)
    location = { ...path }

    if (location.pathname === undefined) location.pathname = ""

    if (location.search) {
      if (location.search.charAt(0) !== "?")
        location.search = "?" + location.search
    } else {
      location.search = ""
    }

    if (location.hash) {
      if (location.hash.charAt(0) !== "#") location.hash = "#" + location.hash
    } else {
      location.hash = ""
    }

    if (state !== undefined && location.state === undefined)
      location.state = state
  }

  try {
    location.pathname = decodeURI(location.pathname)
  } catch (e) {
    if (e instanceof URIError) {
      throw new URIError(
        'Pathname "' +
          location.pathname +
          '" could not be decoded. ' +
          "This is likely caused by an invalid percent-encoding."
      )
    } else {
      throw e
    }
  }

  if (key) location.key = key

  if (currentLocation) {
    // Resolve incomplete/relative pathname relative to current location.
    if (!location.pathname) {
      location.pathname = currentLocation.pathname
    } else if (location.pathname.charAt(0) !== "/") {
      location.pathname = resolvePathname(
        location.pathname,
        currentLocation.pathname
      )
    }
  } else {
    // When there is no prior location and pathname is empty, set it to /
    if (!location.pathname) {
      location.pathname = "/"
    }
  }

  return location
}

export const locationsAreEqual = (a, b) =>
  a.pathname === b.pathname &&
  a.search === b.search &&
  a.hash === b.hash &&
  a.key === b.key &&
  valueEqual(a.state, b.state)

6、实现createHref
你可能没有用过history.createHref(),它用来创建一个hash路由,也就是'#/'或者'#/home'这类的。

const 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;
}

const createHref = location =>
  "#" + encodePath(createPath(location));

7、实现push方法
我们在使用push的时候,通常是history.push('/home')这种形式,不需要自己加#。
push实现的原理:判断push传入的路由和当前url的路由是否一样,如果一样,则不更新路由,否则就更新路由。

//  更新history对象的值,length、location和action
const setState = nextState => {
  Object.assign(history, nextState);
  history.length = globalHistory.length;
  transitionManager.notifyListeners(history.loation, history.action);
}
//  notifyListeners函数用来通知history的更新
const notifyListeners = (...args) => {
  listeners.forEach(listener => listener(...args));
}
//  更新路由
const pushHashPath = path => (windows.location.hash = path);
//  push核心代码
const push = (path, state) => {
  //  更新action为'PUSH'
  const action = "PUSH";
  //  更新location对象
  const location = createLocation(
    path,
    undefined,
    undefined,
    history.location
  );
  //  更新路由前的确认操作,confirmTransitionTo函数内部会处理好路由切换的状态判断,如果ok,则执行最后一个参数,它是回调函数。
  transitionManager.confirmTransitionTo(
    location,
    action,
    getUserConfirmation,
    ok => {
      //  如果不服路由切换的条件,就不更新路由  
      if (!ok) return;
      //  获取location中的路径pathname,比如'/home'
      const path = createPath(location);
      const encodePath = encodePath(path);
      //  比较当前的url中的路由和push函数传入的路由是否相同,
      //  不相同则hashChanged为true。
      const hashChanged = getHashPath() !== encodePath;
      if (hashChanged) {
        //  路由允许更新
        ignorePath = path;
        //  更新路由
        pushHashPath(encodePath);
        const prevIndex = allPaths.lastIndexOf(createPath(history.location));
        const nextPaths = allPaths.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
        nextPaths.push(path);
        allPaths = nextPaths;
        //  setState更新history对象。
        setState({ action, location });
      } else {
        //  push的路由和当前路由一样,会发出一个警告
        //  "Hash history cannot PUSH the same path; a new entry will not be added to the history stack"
        setState();
      }
    }
  )
}

8、实现replace
replace和push都能更新路由,但是replace是更新当前路由,而push是增加一个历史记录。

//  更新路由
const replaceHashPath = path => {
  const hashIndex = window.location.href.indexOf("#");
  window.location.replace(
    window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + "#" + path;
  );
}
//  replace核心代码
const replace = (path, state) => {
  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 encodePath = encodePath(path);
      const hashChanged = getHashPath() !== encodePath;
      //  到这里为止,前面的代码和push函数的实现是一样的

      if (hashChanged) {
        ignorePath = path;
        //  更新路由
        replaceHashPath(encodePath);
      }
      const prevIndex = allPaths.indexof(createPath(history.location));
      if (prevIndex !== -1) allPaths[prevIndex] = path;
      setState({ action, location });
    }
  )
}

9、实现go

go方法的使用时history.go(-1)这种形式:

//  globalHistory是window.history
const go = n => globalHistory.go(n);

10、实现goBack

这个应该能一眼看懂了

const goBack = () => go(-1);

11、实现goForward

这个应该也能一眼看懂了

const goForward = () => go(1);

12、实现listen

const listen = listener => {
  const unlisten = transitionManager.appendListener(listener);
  checkDOMListeners(1);
  return () => {
    checkDOMListeners(-1);
    unlisten();
  }
}

//  监听hashchange的改变,handleHashChange函数用来判断是哪种类型的路由更新,replace、push等各种hash改变都实现了一个函数,具体看源码。
const checkDOMListeners = delta => {
  listenerCount += delta;
  if (listenerCount === 1) {
    //  注册监听函数
    window.addEventListener('hashchange', handleHashChange);
  } else if (listenerCount === 0) {
    //  转移监听函数
    window.removeEvenListener('hashchange', handleHashChange);
  }
}

//  appendListener函数实现
ley listener = [];
const appendListener = fn => {
  let isActive = true;
  const listener = (...args) => {
    if (isActive) fn(...args);
  }
  listeners.push(listener);
  return () => {
    isActive = false;
    listeners = listeners.filter(item => item !== listener);
  }
};

总结

history对象的所有属性和方法都实现了一遍,在react-router中,将history对象封装进了Router、Route等组件中,使得你可以在react组件中通过this.props.history读取。
看完源码,你会发现history的实现真的不复杂,找准思路,一个个函数去实现,再考虑兼容性,就非常完美了,以后你在其他博客上看到有人宣传自己搞了个自己的路由插件,不要觉得很牛逼,重构history换个新姿势就是一个插件了。

你可能感兴趣的:(探索JavaScript中history源码之hashHistory的实现)