React Router核心依赖--history库

欢迎关注个人公众号睿Talk,获取我最新的文章:
clipboard.pngjavascript

1、前言

使用 React 开发稍微复杂一点的应用,React Router 几乎是路由管理的惟一选择。虽然 React Router 经历了 4 个大版本的更新,功能也愈来愈丰富,但不管怎么变,它的核心依赖 history 库却一直没变。下面咱们来了解下这个在 github 上有 5k+ 星的库到底提供了什么功能。html

2、HTML5 history对象

聊到history库,是否是以为这个单词有点熟悉?不错,HTML5规范里面,也新增了一个同名的history对象。下面咱们来看下这个history对象用来解决什么问题。前端

在jQuery统治前端的年代,经过ajax请求无刷新更新页面是当时至关流行的页面处理方式,SPA的雏形就是那时候演化出来的。为了标示页面发生的变化,方便刷新后依然能显示正确的页面元素,通常会经过改变url的hash值来惟必定位页面。但这会带来另外一个问题:用户没法使用前进/后退来切换页面。java

为了解决这个问题,history对象应运而生。当页面的url或者hash发生变化的时候,浏览器会自动将新的url push到history对象中。history对象内部会维护一个state数组,记录url的变化。在浏览器进行前进/后退操做的时候,实际上就是调用history对象的对应方法(forward/back),取出对应的state,从而进行页面的切换。git

除了操做url,history对象还提供2个不用经过操做url也能更新内部state的方法,分别是pushStatereplaceState。还能将额外的数据存到state中,而后在onpopstate事件中再经过event.state取出来。若是但愿对history对象做更深刻的理解,能够参考 这里,和这里github

3、history库与HTML5 history对象的关系

咱们再回过头来看history库。它本质上作了如下4件事情:ajax

  1. 借鉴HTML5 history对象的理念,在其基础上又扩展了一些功能
  2. 提供3种类型的history:browserHistory,hashHistory,memoryHistory,并保持统一的api
  3. 支持发布/订阅功能,当history发生改变的时候,能够自动触发订阅的函数
  4. 提供跳转拦截、跳转确认和basename等实用功能

再对比一些二者api的异同。如下是history库的:segmentfault

const history = {
    length,        // 属性,history中记录的state的数量
    action,        // 属性,当前导航的action类型
    location,      // 属性,location对象,封装了pathname、search和hash等属性
    push,          // 方法,导航到新的路由,并记录在history中
    replace,       // 方法,替换掉当前记录在history中的路由信息
    go,            // 方法,前进或后退n个记录
    goBack,        // 方法,后退
    goForward,     // 方法,前进
    canGo,         // 方法,是否能前进或后退n个记录
    block,         // 方法,跳转前让用户肯定是否要跳转
    listen         // 方法,订阅history变动事件
  };

如下是HTML5 history对象的:api

const history = {
    length,         // 属性,history中记录的state的数量
    state,          // 属性,pushState和replaceState时传入的对象
    back,           // 方法,后退
    forward,        // 方法,前进
    go,             // 方法,前进或后退n个记录
    pushState,      // 方法,导航到新的路由,并记录在history中
    replaceState    // 方法,替换掉当前记录在history中的路由信息
}

// 订阅history变动事件
window.onpopstate = function (event) {
    ...
}

从对比中能够看出,二者的关系是很是密切的,history库能够说是history对象的超集,是功能更强大的history对象。数组

4、createHashHistory源码分析

下面,咱们以三种history类型中的一种,hashHistory为例,来分析下history的源码,看看它都干了些什么。先看下它是怎么处理hash变动的。

// 构造hashHistory对象
const createHashHistory = (props = {}) => {
    ...
    const globalHistory = window.history;    // 引用HTML5 history对象
    ...
    // transitionManager负责控制是否进行跳转,以及跳转后要通知到的订阅者,后面会详细讨论
    const transitionManager = createTransitionManager();
    ...
    // 注册history变动回调的订阅者
    const listen = listener => {
        const unlisten = transitionManager.appendListener(listener);
        checkDOMListeners(1);

        return () => {
            checkDOMListeners(-1);
            unlisten();
        };
    };
    
    // 监听hashchange事件
    const checkDOMListeners = delta => {
        listenerCount += delta;

        if (listenerCount === 1) {
            window.addEventListener(HashChangeEvent, handleHashChange);
        } else if (listenerCount === 0) {
            window.removeEventListener(HashChangeEvent, handleHashChange);
        }
    };
    
    // hashchange事件回调
    const handleHashChange = () => {
        ...
        // 构造内部使用的location对象,包含pathname、search和hash等属性
        const location = getDOMLocation();    
        ...
        handlePop(location);
    };
    
    // 处理hash变动逻辑
    const handlePop = location => {
        ...
        const action = "POP";
        // 给用户展现确认跳转的信息(若是有的话),确认后通知订阅者。若是用户取消跳转,则回退到以前状态
        transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {
            if (ok) {
                setState({action, location});    // 确认后通知订阅者
            } else {
                revertPop(location);             // 取消则回退到以前状态
            }
        });
    };
    
    // 更新action,location和length属性,并通知订阅者
    const setState = nextState => {
        Object.assign(history, nextState);

        history.length = globalHistory.length;

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

以上就是处理被动的hash变动的逻辑,一句话归纳就是:订阅hash变动事件,判断是否确实要变动,如需变动则更新本身的属性,通知订阅者,不需变动则回退到以前的状态。

下面再看下transitionManager作了什么,重点看发布/订阅相关内容,忽略用户确认跳转相关内容。

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

这里的代码一目了然,就是维护一个订阅者列表,当hash变动的时候通知到相关的函数。

以上是hash改变的时候被动更新相关的内容,下面再看下主动更新相关的代码,以push为例,replace大同小异。

const push = (path, state) => {
    ...
    const action = "PUSH";
    const location = createLocation(path, undefined, undefined, history.location);

    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {
        if (!ok)     // 若是取消,则不跳转
            return;
        ...
        pushHashPath(encodedPath);        // 用新的hash替换到url当中
        ...
        setState({action, location});     // 更新action,location和length属性,并通知订阅者

    });
};

// 用新的hash替换到url当中
const pushHashPath = path => (window.location.hash = path);

在浏览器进行前进后退操做时,history库其实是经过操做HTML5 history对象实现的。

const globalHistory = window.history;

const go = n => {
    ...
    globalHistory.go(n);
};

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

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

当调用window.history.go的时候,hash会发生变化,进而触发hashchange事件,而后history库再将变动通知到相关的订阅者。

5、总结

本文对React Router核心依赖history库进行了比较深刻的介绍。从HTML5新增的history对象讲起,对比了它跟history库千丝万缕的关系,并以hashHistory为例子详细分析了其代码的实现细节。

最后,咱们再来回顾一下history库作了哪些事情:

  1. 借鉴HTML5 history对象的理念,在其基础上又扩展了一些功能
  2. 提供3种类型的history:browserHistory,hashHistory,memoryHistory,并保持统一的api
  3. 支持发布/订阅功能,当history发生改变的时候,能够自动触发订阅的函数
  4. 提供跳转拦截、跳转确认和basename等实用功能

虽然history库是React Router的核心依赖,但它跟React自己并无依赖关系。若是你的项目中有操做history的场景,也能够将其引入到项目中来。

相关文章
相关标签/搜索