React-router源码解读

我们常用的有React-router的监测路由发生变化的两种方法 BrowserRouter HashRouter ,这两个区别在于一个监测的是url 的hash 变化做出处理,而 一个是通过操作history的会话记录实现

下面分别分析两种方式的实现以期能实现一个简易的路由器

我们先来看下官网提供的示例:

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about">About</Link>
            </li>
            <li>
              <Link to="/users">Users</Link>
            </li>
          </ul>
        </nav>

        {/* A <Switch> looks through its children <Route>s and
            renders the first one that matches the current URL. */}
        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/users">
            <Users />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Users() {
  return <h2>Users</h2>;
}

上面代码在浏览器中渲染出来demo如下:

对比看到Router里面的元素成功被渲染出来,且可以随着location的pathname变化而 改变内容,我们继续看react-router-dom.js文件中的对应代码,我们用的是BrowserRouter

BrowserRouter.js 函数中返回一个class组件,下面dev模式下会给出了提示信息等

import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";

/**
 * The public API for a <Router> that uses HTML5 history.
 */
class BrowserRouter extends React.Component {
  history = createHistory(this.props); // 新建history

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

if (__DEV__) {
  BrowserRouter.propTypes = {...};

  BrowserRouter.prototype.componentDidMount = function() {
    warning(
      !this.props.history,
      "<BrowserRouter> ignores the history prop. To use a custom history, " +
        "use `import { Router }` instead of `import { BrowserRouter as Router }`."
    );
  };
}

export default BrowserRouter;

history = createHistory(this.props); ` createHistory 是引用了history包里面的函数,它基于html5 的history API扩展对象,下面精简列出createHistory函数的 内容:

function createBrowserHistory(props) {
  if (props === void 0) {
    props = {};
  }
  ...
  
  var globalHistory = window.history; // 新建history对象
  var canUseHistory = supportsHistory(); // 判断是否支持history用法,部分api需要兼容
  
  ...
  
  function go(n) {
    globalHistory.go(n);
  }
  
  function push(path, state) {
  	...
    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
        ...
      if (canUseHistory) {
        globalHistory.pushState({
          key: key,
          state: state
        }, null, href);

        if (forceRefresh) {
          window.location.href = href;
        } else {
          var prevIndex = allKeys.indexOf(history.location.key);
          var nextKeys = allKeys.slice(0, prevIndex + 1);
          nextKeys.push(location.key);
          allKeys = nextKeys;
          setState({
            action: action,
            location: location
          });
        }
      } else {
        process.env.NODE_ENV !== "production" ? warning(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history') : void 0;
        window.location.href = href;
      }
    });
  }
  
  ...
  
  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;
}
  

下面继续看BrowserRouter

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }

Router这个元素对应下面的函数

var Router = function (_React$Component) {
  _inheritsLoose(Router, _React$Component);
    
    ...

  function Router(props) {
    var _this;
    ...

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

  return Router;
}(React.Component);

常用组件

Switch

var Switch = function (_React$Component) {
  _inheritsLoose(Switch, _React$Component);

  function Switch() {
    return _React$Component.apply(this, arguments) || this;
  }

  var _proto = Switch.prototype;

  _proto.render = function render() {
    var _this = this;

    return React.createElement(context.Consumer, null, function (context) {
        ...
        // 优先取得Switch的 location 这个  props
      var location = _this.props.location || context.location;
      var element, match; 

        
        // 遍历Switch组件的子组件进行比对 location.pathname 和 每个子组件的path 是否一致
      React.Children.forEach(_this.props.children, function (child) {
        if (match == null && React.isValidElement(child)) {
          element = child;
          var path = child.props.path || child.props.from;
          match = path ? matchPath(location.pathname, _extends({}, child.props, {
            path: path
          })) : context.match;
        }
      });
        
       // 返回最终符合path和location 一致的组件
      return match ? React.cloneElement(element, {
        location: location,
        computedMatch: match
      }) : null;
    });
  };

  return Switch;
}(React.Component);

Switch如果传递props {pathname: 'xxx'},则会使用此 pathname进行匹配无论url是否改变,而如果没有传递此props则会使用Router中history监测得到的path

// Router 部分代码,详细见上面
_proto.render = function render() {
        return React.createElement(context.Provider, {
            value: {
                history: this.props.history,
                location: this.state.location,  // 通过Content传递location给子组件
                match: Router.computeRootMatch(this.state.location.pathname),
                staticContext: this.props.staticContext
            }
        }, React.createElement(historyContext.Provider, {
            children: this.props.children || null,
            value: this.props.history
        }));
    };

Router将监测到的location作为Context传递给所有子组件,所以Switch 必须爆包含在 Router中才能使用到

遍历Switch的所有Children,调用matchPath函数对location.pathname和当前child的path这个props进行比对如果一致则返回对应的element,不是则返回null

var Link = forwardRef(function (_ref2, forwardedRef) {
 	var _ref2$component = _ref2.component, 
        component = _ref2$component === void 0 ? LinkAnchor : _ref2$component, // 默认为LinkAnchor LinkAnchor 默认返回a标签
	... 

  return React.createElement(__RouterContext.Consumer, null, function (context) {
  	...
    var history = context.history; // 通过context传递下来的history ,这个也是在Router创建,如果是hashRouter或者BrowserRouter 时候赋值的
    var location = normalizeToLocation(resolveToLocation(to, context.location), context.location); // 处理location的写法
    var href = location ? history.createHref(location) : ""; // 

    var props = _extends({}, rest, {
      href: href,
      navigate: function navigate() {
        var location = resolveToLocation(to, context.location);
        var method = replace ? history.replace : history.push;
        method(location);
      }
    }); // React 15 compat


    if (forwardRefShim !== forwardRef) {
      props.ref = forwardedRef || innerRef;
    } else {
      props.innerRef = innerRef;
    }

    return React.createElement(component, props);  //最后返回包装后的LinkAnchor
  });
});
 function createHref(location) {
    return basename + createPath(location);
  }
  
  
  function createPath(location) {
      var pathname = location.pathname,
          search = location.search,
          hash = location.hash;
      var path = pathname || '/';
      if (search && search !== '?') path += search.charAt(0) === '?' ? search : "?" + search;
      if (hash && hash !== '#') path += hash.charAt(0) === '#' ? hash : "#" + hash; // 如果有hash 则判断path是否添加 了# 没有添加
      return path;
}
var LinkAnchor = forwardRef(function (_ref, forwardedRef) {
  var innerRef = _ref.innerRef,
      navigate = _ref.navigate,
      _onClick = _ref.onClick,
      rest = _objectWithoutPropertiesLoose(_ref, ["innerRef", "navigate", "onClick"]);

  var target = rest.target;

    // 合并Link传递过来的props
  var props = _extends({}, rest, {
    onClick: function onClick(event) {
      try {
        if (_onClick) _onClick(event);
      } catch (ex) {
        event.preventDefault();
        throw ex;
      }

      if (!event.defaultPrevented && // onClick prevented default
      event.button === 0 && ( // ignore everything but left clicks
      !target || target === "_self") && // let browser handle "target=_blank" etc.
      !isModifiedEvent(event) // ignore clicks with modifier keys
      ) {
          event.preventDefault();
          navigate(); // Link传递的navigate函数
        }
    }
  }); 
    
    ... 

  return React.createElement("a", props);
});

当我们在HashRouter或者BrowserRouter 里面使用Link组件,会经历 Link , return React.createElement(component, props); 最后返回了一个React元素,createElement 里面的第一个元素是LinkAnchor,而 LinkAnchor 组件则会返回一个把Link里面href作为a标签href。 传递给a标签的href 是通过 var href = location ? history.createHref(location) : ""; 创建的

当hash改变的时候如何通知组件?

  function setState(nextState) {
    _extends(history, nextState);

    history.length = globalHistory.length;
    transitionManager.notifyListeners(history.location, history.action);
  }
posted @ 2021-01-21 20:50  O噗寺的小和尚  阅读(184)  评论(0)    收藏  举报