react-router与history 源码解读

History源码解读笔记

这个库输出的模块有:node

exports.createBrowserHistory = createBrowserHistory;
exports.createHashHistory = createHashHistory;
exports.createMemoryHistory = createMemoryHistory;
exports.createLocation = createLocation;
exports.locationsAreEqual = locationsAreEqual;
exports.parsePath = parsePath;
exports.createPath = createPath;复制代码

这里重点分析 createBrowserHistory createMemoryHistoryreact

1.createBrowserHistory

首先看createBrowserHistory的返回是:web

var history = {
    length: globalHistory.length,
    action: 'POP',
    location: initialLocation,
    createHref: createHref,
    push: push,
    replace: replace,
    go: go,
    goBack: goBack,
    goForward: goForward,
    block: block,
    listen: listen
  };
  return history;复制代码

createBrowserHistory主要是基于h5 的history中的pushstate以及replacestate来变动浏览器地址栏。当浏览器不支持h5时,会用location.href直接赋值的办法直接跳转页面,检测是否支持h5 的函数supportsHistory,以及 needsHashChangeListener 是判断是否支持hashchange检测的,以下代码:npm

/**
 * Returns true if browser fires popstate on hash change.
 * IE10 and IE11 do not.
 */

function supportsPopStateOnHashChange() {
  return window.navigator.userAgent.indexOf('Trident') === -1;
}

var needsHashChangeListener = !supportsPopStateOnHashChange();复制代码

逻辑图:

createBrowserHistory 的History其实使用的就是window.history的api,会利用window.addEventListener监听popstate和hashchange事件,而且会将listener的回调函数加到队列中,react-router的回调函数是:api

function (location) {
        if (_this._isMounted) {
          _this.setState({
            location: location
          });
        } else {
          _this._pendingLocation = location;
        }
      }复制代码

其中location就是逻辑图最后listener.apply的参数args,即history 主要是维护一个history栈,监听浏览器变化,控制history的栈记录,而且返回当前location信息给react-router,react-router会根据相应的location render出对应的components。数组



因此当历史记录条目变动时,就会触发popState事件。

1)checkDOMListeners : 监听历史记录条目的改变,而且开启或关闭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);
    }
  }复制代码

如上,listenerCount是现有的监听事件函数,checkDOMListeners只在listen函数以及block函数中被调用,参数只为1 与 -1.因此以此来控制add监听或remove监听。浏览器

2)handlePop函数

handlePop函数是在popstate事件触发时的回调函数,主要就是setState action以及location。而setState方法首先利用_extends方法把action和history覆盖到history同属性值上,也就值替换掉当前的state和location。缓存

其次调用notifyListeners方法,这个方法就是调用在listeners队列中的listener.apply()回调方法也就是上面提到的react-router中的listen方法里的函数,而history里的location会传给react-router里的listen回调函数来使用。bash

react-router拿到了当前应该展示那个location页面组件,接下来就是react-router的舞台了。react-router

function handlePop(location) {
    if (forceNextPop) {
      forceNextPop = false;
      setState();
    } else {
      var action = 'POP';
      transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
        if (ok) {
          setState({
            action: action,
            location: location
          });
        } else {
          revertPop(location);
        }
      });
    }
  }复制代码
//setState代码
function setState(nextState) {
    _extends(history, nextState);

    history.length = globalHistory.length;
    transitionManager.notifyListeners(history.location, history.action);
  }复制代码
//notifyListeners代码
function notifyListeners() {
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }

    listeners.forEach(function (listener) {
      return listener.apply(void 0, args);
    });
  }复制代码

3)replace方法、push方法、go方法、back方法等

以上方法其实都是调用的window.history的原生api,以下

function push(path, state) {
    
    var location = createLocation(path, state, createKey(), history.location);
    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
      if (!ok) return;
      var href = createHref(location);
      var key = location.key,
          state = location.state;

      if (canUseHistory) {
        globalHistory.pushState({
          key: key,
          state: state
        }, null, href);

        if (forceRefresh) {
          window.location.href = href;
        } else {
          //重点代码:
          /**************************/
          // 更新存储的allKeys
          // allKeys 缓存历史堆栈中的数据标识
          // 当location处于history队尾时,实际为push
          // 当location处于history中间时,会删除以后的keys,并添加新key ??????????TODO:::::::::
          var prevIndex = allKeys.indexOf(history.location.key);
          var nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
          nextKeys.push(location.key);
          allKeys = nextKeys;
          setState({
            action: action,
            location: location
          });
          /*****************************/
          
        }
      } else {
        warning(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history');
        window.location.href = href;
      }
    });
  }复制代码
//replace方法点睛之笔:
var prevIndex = allKeys.indexOf(history.location.key);
  if (prevIndex !== -1) allKeys[prevIndex] = location.key;
  setState({
    action: action,
    location: location
  });复制代码
function go(n) {
    globalHistory.go(n);
  }

  function goBack() {
    go(-1);
  }

  function goForward() {
    go(1);
  }复制代码

疑问:revertPop部分,有点不是很理解:1.是在跳转后弹出confirm弹窗吗?2.fromlocation和tolocation什么时候取得,为啥以为二者同样?TODO:::::::::::::::::::

2.createMemoryHistory

createMemoryHistory的逻辑同createBrowserHistory,可是不是直接使用的window.history的api。

createMemoryHistory 用于在内存中建立彻底虚拟的历史堆栈,只缓存历史记录,但与真实的地址栏无关(不会引发地址栏变动,不会和原生的 history 对象保持同步),也与 popstate, hashchange 事件无关。

createMemoryHistory 的参数 props 接受 getUserConfirmation, initialEntries, initialIndex, keyLength 属性。其中,props.initialEntries 指定最初的历史堆栈内容 history.entries;props.initialIndex 指定最初的索引值 history.index。push, replace 方法均将改变 history.entries 历史堆栈内容;go, goBack, goForward 均基于 history.entries 历史堆栈内容,以改变 history.index 及 history.location。实现参见源码。

React-Router源码解读笔记

这个库输出的模块有:

exports.MemoryRouter = MemoryRouter;
exports.Prompt = Prompt;
exports.Redirect = Redirect;
exports.Route = Route;
exports.Router = Router;
exports.StaticRouter = StaticRouter;
exports.Switch = Switch;
exports.generatePath = generatePath;
exports.matchPath = matchPath;
exports.withRouter = withRouter;
exports.__RouterContext = context;复制代码

重点解析route router

1 Router

用法实例


源码

Router里会传入history,history通常是利用history npm包实例化的实例。

var context =
/*#__PURE__*/
createNamedContext("Router");//(1)

var Router = 
    function (_React$Componet) {
    	_inheritsLoose(Router,_React$Component);//(2)
      
      Router.computeRootMatch = function computeRootMatch(pathname) { //(3)
        return {
          path: "/",
          url: "/",
          params: {},
          isExact: pathname === "/"
        };
      };
      
      function Router(props) {
      	、、、
        、、、
        _this._isMounted = false;
   			 _this._pendingLocation = null;
        
        if (!props.staticContext) {
          _this.unlisten = props.history.listen(function (location) { //(4) 这里location是props的location
            if (_this._isMounted) {
              _this.setState({
                location: location
              });
            } else {
              _this._pendingLocation = location;
            }
          });
        }
        
        return _this;
      }
      
      var _proto = Router.prototype;

      _proto.componentDidMount = function componentDidMount() {//(6)
        this._isMounted = true;

        if (this._pendingLocation) {
          this.setState({
            location: this._pendingLocation
          });
        }
      };

      _proto.componentWillUnmount = function componentWillUnmount() {//(7)
        if (this.unlisten) this.unlisten();
      };

      _proto.render = function render() { //(5)
        return React.createElement(context.Provider, {
          children: this.props.children || null,
          value: {
            history: this.props.history,
            location: this.state.location,
            match: Router.computeRootMatch(this.state.location.pathname),
            staticContext: this.props.staticContext
          }
        });
      };
 	
      return Router;
}(React.Component);

{
  Router.propTypes = {
    children: PropTypes.node,
    history: PropTypes.object.isRequired,
    staticContext: PropTypes.object
  };

  Router.prototype.componentDidUpdate = function (prevProps) {
    warning(prevProps.history === this.props.history, "You cannot change <Router history>");
  };
}复制代码

(1)createNamedContext会调用createContext方法,createContext是require的mini-create-react-context,代码大致以下:

//provider
var Provider = {
	function Provider() {
      var _this;

      _this = _Component.apply(this, arguments) || this;
      _this.emitter = createEventEmitter(_this.props.value);//createEventEmitter定义了handler=[],实现了on(handler)、off(handler)、get(handler)、set(handler)方法来不一样的将函数放到handler数组后删除;
      return _this;
    }

		var _proto = Provider.prototype;

    _proto.getChildContext = function getChildContext() { //getChildContext方法:向react context中绑定全局变量。
      var _ref;

      return _ref = {}, _ref[contextProp] = this.emitter, _ref;
    };

	_proto.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {

      if (this.props.value !== nextProps.value) {
        var oldValue = this.props.value;
        var newValue = nextProps.value;
        
        、、、
    
      	 this.emitter.set(nextProps.value, changedBits);//当createContext有第二个参数时才起做用,因此这里暂时留个疑问,没有立刻看懂。TODO::::::::::::::
       
        、、、
          
        }
  }
}

//consumer
`````
````复制代码

(2) _inheritsLoose(Router,_React$Component);是Router继承_React$Component,而_React$Component就是Router包裹的组件。

(3)computeRootMatch Router会有一个默认值

(4)Router会有一个history.listen也就是讲history时的listen函数,当前location也就是经过这里传到了react-router的。

可是这里没有很明白一点:在listen这部分有一块注释,意思是当组件mount以前,若是发生了popstate、hashchange事件,那我能够及时的在mount以前就利用_this._pendingLocation = location;那样在componentDidmount的时候就能够用最新的location进行渲染。我是这么理解的,不敢确认必定正确。??????

// This is a bit of a hack. We have to start listening for location
    // changes here in the constructor in case there are any <Redirect>s
    // on the initial render. If there are, they will replace/push when
    // they mount and since cDM fires in children before parents, we may
    // get a new location before the <Router> is mounted.复制代码

(5)Router render:

其实就是将children组件render出来,不过react16里加了provider,因此这里会将以前context.provider里的属性对象等也会包裹进来。

(6)componentDidMount 结合上满(4)

(7)componentWillUnmount 组件卸载时,会调用this.unlisten解除Router监听事件。

Route 待完善TODO:::::::::

从app.js 中,发现 Route 使用方式是<Route exact path="/" component={Home}/>

var Route = function(_React$component) {
	function Route() {
    return _React$Component.apply(this, arguments) || this;
  }

  var _proto = Route.prototype;
  _proto.render = function render() {
    return React.createElement(context.Consumer, null, function (context$$1) {
     var location = _this.props.location || context$$1.location;
      var match = _this.props.computedMatch ? _this.props.computedMatch // <Switch> already computed the match for us
      : _this.props.path ? matchPath(location.pathname, _this.props) : context$$1.match;

      var props = _extends({}, context$$1, {
        location: location,
        match: match
      });
      
     return React.createElement(context.Provider, {
        value: props
      }, children && !isEmptyChildren(children) ? children : props.match ? component ? React.createElement(component, props) : render ? render(props) : null : null);
    });
  }
 }
}复制代码

从render 方法能够知道,会经过match、location、path来匹配是否和当前location符合,而后决定render到哪一个具体的children组件。

props = {match, location, history, staticContext} 这些属性在组件中会有很大的用途

参考文献:

1 history:zhuanlan.zhihu.com/p/55837818

2 react-router:juejin.im/post/5b8251…

3 react-router4官方文档:reacttraining.com/react-route…

相关文章
相关标签/搜索