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…

相關文章
相關標籤/搜索