Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

history 以及 React-router #1

Open
forl opened this issue Dec 12, 2018 · 0 comments
Open

history 以及 React-router #1

forl opened this issue Dec 12, 2018 · 0 comments

Comments

@forl
Copy link
Owner

forl commented Dec 12, 2018

history 对象

首先,一切的基础是window.history,它是一个只读属性,能够返回一个 History 对象。这个对象提供了接口用于操作浏览器回话历史(即 session history)。这一特性是 HTML5 引入的,叫做 history API。
我们首先需要关注 histroy 的两个属性和 5 个方法,通过这些属性和方法,我们得以用代码获取 history 状态或操作 history:

  • length,整个 session history 记录(entry)数量,包括当前加载的页面;
  • state,history 栈顶的状态,用于直接获取当前 session 的状态值;
  • back(),进入上一页;
  • forward(),进入下一页;
  • go(),进入指定页,go(1) 等效于 forward(),go(-1) 等效于 back();
  • pushState(),添加 history 记录,接受三个参数:state 对象、title(目前浏览器会忽略该参数)、URL(可选);
  • replaceState(),替换当前的 history 记录,参数与 pushState() 一样。

接下来还要关注一个事件:popstate。当 hostory 的当前活动记录发生变化时,popstate 事件会被派发给 window。如果被激活的 history 记录是通过 pushState 创建或者被 replaceSate 方法修改过,则事件有一个 state 字段,其值是对应 history 记录的 state 拷贝。但是需要注意,只有调用back()、forward()、go()方法或者用户点击浏览器的“后退”、“前进”按钮才会产生 popstate 事件,通过 pushState 和 replaceSate 函数调用对 history 的操作并不会产生 popstate 事件。

location

另一的基础是 location,它是一个对象,包含当前页面 URL 的相关信息,并且提供了一些方法来修改 URL。通过 window.loaction 和 document.location 都可以访问该对象(window.loaction === window.loaction)。

location 对象拥有以下属性和方法(TODO:简要说明以下属性和方法):

  • href
  • protocal
  • host
  • hostname
  • port
  • pathname
  • search
  • hash
  • username
  • password
  • origin
  • assign()
  • reload()
  • relace()
  • toString()

history 库

history 是一个用于管理 session history 的 JS 库,目前由 ReactTraining 维护。在继续阅读本文之前,最好先去扫一眼 history 库的文档。

history 库的目标是要让你可以在任何能够运行 JS 的地方轻松地管理 session history。那么这里的“任何能够运行 JS 的地方”一般就是指浏览器、Node.js 以及向 React-native 这样的非 DOM 环境。注意:这些环境中只有现代浏览器才支持 HTML5 history API。

它管理 session history 的方法与 HTML5 的 history API 类似,也是提供一个 history 对象。为了在不同的环境中提供相对统一的操作方式,history 库提供了三种创建 history 对象的方法:

  • createBrowserHistory,用于支持 HTML5 的现代浏览器环境;
  • createMemoryHistory,用于非 DOM 环境;
  • createHashHistory,用于老式浏览器。

history 对象与 HTML5 的 history API 很相似,但功能有所加强,具体区别请看它提供的属性和方法:

属性:

  • history.length,history 栈中的记录数量
  • history.location,当前的 location
  • history.action,当前的导航动作

导航方法(请注意方法名与 HTML5 history API 的区别):

  • history.push(path, [state])
  • history.replace(path, [state])
  • history.go(n)
  • history.goBack()
  • history.goForward()
  • history.canGo(n) (仅用于 createMemoryHistory)

监听(这是 HTML5 history API 不提供的):

通过 history.listen 方法可以监听 history.location 的变化,使用方式如下:

history.listen((location, action) => {
  console.log(
    `The current URL is ${location.pathname}${location.search}${location.hash}`
  );
  console.log(`The last navigation action was ${action}`);
});

通过 listen 方法注册的 listener 函数,无论是用户点击“前进”、“后退”按钮或者代码调用导航方法时,还是代码调用push()、replace()方法时,都会被触发。在前面讲 HTML5 的 history API 时提到,只有调用back()、forward()、go()方法或者用户点击“前进”、“后退”按钮时才会产生 popstate 事件,代码中调用 pushState() 和 replaceState() 方法并不会产生 popstate 事件。所以要实现监听,history 不仅要利用 popstate 事件来监听用户点击“前进”、“后退”按钮的行为,还要自己实现一套监听机制来监听代码中对导航方法的调用。

从 history 源码中发现,它实现了一个 TransitionManager 对象,用于管理监听器以及 prompt(在此可以不用关心 prompt 的管理)。去掉 prompt 管理功能之后的代码如下:

// createTransitionManager.js
const createTransitionManager = () => {
  let listeners = [];

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

  const notifyListeners = (...args) => {
    listeners.forEach(listener => listener(...args));
  };

  return {
    appendListener,
    notifyListeners
  };
};

export default createTransitionManager;

然后再来看 history.listen() 方法的实现:

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

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

对这段代码代码做一个简要解释:

首先将 listener 添加到 transitionManager 中去。另外还有对 checkDOMListeners 的函数的调用,这个函数实现的逻辑有点令人不解,而且做法不太科学,存在潜在的问题,在此就不对它进行分析了,只说它的作用。history 代码中实现了一个名为 handlePopState 的函数,用于监听最开始提到的 popstate 事件,checkDOMListeners(1)是为了确保当 transitionManager 对象中注册的 listener 数量不为 0 时该函数被注册 listener 了,checkDOMListeners(-1) 的作用是确保当 transitionManager 对象中注册的 listener 数量为 0 时该函数从事件监听上移除。

再来看 handlePopState 函数:

const handlePopState = event => {
  // Ignore extraneous popstate events in WebKit.
  if (isExtraneousPopstateEvent(event)) return;

  handlePop(getDOMLocation(event.state));
};

该函数最后调用了名为 handlePop 的函数,再继续看这个函数:

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

先分析这个函数的作用:经过 confirmTransitionTo 抉择(这属于支线剧情,暂时不关心)之后,最终调用 setState 函数,setState 函数的实现如下:

  const setState = nextState => {
    Object.assign(history, nextState);

    history.length = globalHistory.length;

    transitionManager.notifyListeners(history.location, history.action);
  };

在这个函数里,我们通过 history.listen 注册的所有监听器终于被触发了。这就说明,popstate 事件的监听函数,最终会调用我们通过 history.listen 注册的所有监听器,也就是说 history.listen() 达到了监听 popstate 事件的效果。

至此,已经清楚 history 如何利用 popstate 事件来监听用户点击“前进”、“后退”按钮或者代码里的导航动作,接下来继续探究它如何监听用户代码对 push 和 replace 的调用。

通过寻找 setState 函数被调用的地方发现,history 对象的 push 和 replace 函数体中都调用了 setState,那这就很明了了:调用 push 和 replace 也能触发通过 history.listen 注册的所有监听器。
接下来分析其中的细节。

loaction:

// createBrowserHistory.js
// ...
const createBrowserHistory = (props = {}) => {
  const history = {
    length: globalHistory.length,
    action: "POP",
    location: initialLocation,
    createHref,
    push,
    replace,
    go,
    goBack,
    goForward,
    block,
    listen
  };

  return history;
};

从 createBrowserHistory() 函数返回的 history 对象中,location 属性包含了最多的信息,现在来看看 location 对象里有些什么数据。文档中已经有说明:

  • location 对象实现 window.loaction 接口的子集,包括这些属性:
  • location.pathname,URL 的 path 部分
  • location.search,Query string
  • location.hash,URL的 hash 段
  • location.state, 一些无法在 URL 中体现的额外信息(只支持 createBrowserHistory 和
    createMemoryHistory)
  • location.key, 代表该 location 的唯一字符串 (只支持 createBrowserHistory 和 createMemoryHistory)

如果是在支持 HTML5 story API 的现代浏览器环境下,location 中的字段其实是将 window.history.state 和 window.location 这两个对象的数据合并之后的结果。

TODO:待续

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant