「history」浅析BrowserHistory中的push、replace

521 阅读3分钟

History

History分类

  1. browserHistory
  2. hashHistory

CreateBrowserHistory

react-router-dom中的<BrowserHistory>组件中有这么几行代码:

let historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
  historyRef.current = createBrowserHistory({ window });
}

那么这个createBrowserHistory函数究竟干了些什么事情呢?

简单来说,它返回了一个类型为BrowserHistory的对象,上面封装了方法和携带了属性,就如下图所示(省略了back和forward方法)

browser-router.png

那我还是比较好奇,这里面到底干了些什么事情,想一探究竟。

前情提要:这里不去展开讲block相关的事情,因为我自己也没弄懂,主要focus在push replace这两个方法上。

History.push

不管三七二十一,先把代码贴出来。

function push(to: To, state?: any) {
  let nextAction = Action.Push;
  let nextLocation = getNextLocation(to, state);
  function retry() {
    push(to, state);
  }
  if (allowTx(nextAction, nextLocation, retry)) {
    let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1); // index表示当前的页面的idx
​
    try {
      globalHistory.pushState(historyState, "", url);
    } catch (error) {
      window.location.assign(url);
    }
    applyTx(nextAction);
  }
}

第一行没啥好讲的,相当于给 nextAction 赋值为 PUSH

第二行这里我们来看下 getNextLocation这个函数

其实也没什么骚操作,仅仅就是返回了一个Location对象,这个对象是经过Object.freeze处理过的。

function getNextLocation(to: To, state: any = null): Location {
  return readOnly<Location>({
    pathname: location.pathname,
    hash: "",
    search: "",
    ...(typeof to === "string" ? parsePath(to) : to),
    state,
    key: createKey(),
  });
}

继续往下看,定义了一个retry函数,这里我们忽略即可,涉及到了block

然后做了一个判断allowTx,这里我们也是默认为true,这个主要是用来判断页面跳转是否被阻塞了

接着看getHistoryStateAndUrl这个函数

到这里,感觉都很好理解,仅仅就是返回了最新的一个HistoryState和一个URL

function getHistoryStateAndUrl(nextLocation: Location, index: number): [HistoryState, string] {
  return [
    {
      usr: nextLocation.state,
      key: nextLocation.key,
      idx: index,
    },
    createHref(nextLocation), // 返回一个url
  ];
}
​
function createHref(to: To) {
  return typeof to === "string" ? to : createPath(to);
}

然后通过globalHistory上的api,来实现路由压栈的操作

最后的关键是applyTx(nextAction)这个操作,废话不多说先来看代码。

function applyTx(nextAction: Action) {
  action = nextAction;                          // 更新history对象内部的action属性
  [index, location] = getIndexAndLocation();    // 更新history对象内部的index和location属性
  listeners.call({ action, location });         // 调用listensers.call方法
}

这个listeners是啥东东捏?先简单看下代码:

interface Update {
  action: Action;
  location: Location;
}
​
interface Listener {
  (update: Update): void;
}
​
let listeners = createEvents<Listener>();
​
function createEvents<F extends Function>(): Events<F> {
  let handlers: F[] = [];
​
  return {
    get length() {
      return handlers.length;
    },
    push(fn: F) {
      handlers.push(fn);
      return function () {
        handlers = handlers.filter((handler) => handler !== fn);
      };
    },
    call(arg) {
      handlers.forEach((fn) => fn && fn(arg));
    },
  };
}

简单来理解:

  1. createEvents通过闭包的方式声明了一个队列(数组)
  2. createEvents返回了一个对象,上面有get、push、call三种方法,而这个对象实际上就是listeners
  3. 通过调用listeners.call({ action, location }),实际上就是把闭包内的那个队列中的函数给执行了一遍。

那么问题来了,我们是怎么把函数注册进这个listensers中的队列的呢?

可以看history上的listen方法:

function createBrowserHistory(options: BrowserHistoryOptions = {}): BrowserHistory {
 // some codes 
  
  let history: BrowserHistory = {
    ... other methods,
    listen(listener) {
      return listeners.push(listener);
    }
  };
​
  return history;
}

History.replace

其实replace和push大同小异,就不加赘述。

function replace(to: To, state?: any) {
  let nextAction = Action.Replace;
  let nextLocation = getNextLocation(to, state);
  function retry() {
    replace(to, state);
  }
  if (allowTx(nextAction, nextLocation, retry)) {
    let [historyState, url] = getHistoryStateAndUrl(nextLocation, index);
    // TODO: Support forced reloading
    globalHistory.replaceState(historyState, "", url);
    applyTx(nextAction);
  }
}

\