React-Router源码小问答

React-router是我们开发过程中,经常使用的库。在不接触源码的基础上,我们可能会对默认写法,props接收到的值产生疑惑。这片文章就是为你解惑的。

问:HashRouterBrowserRouter异同?

答:相同点是底层都依赖于react-router中的Router组件,都是通过history库创建history对象作为参数传给Router组件。

import { createHashHistory as createHistory } from "history";
import { Router } from "react-router";

class HashRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}
复制代码
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";

class BrowserRouter extends React.Component {
  history = createHistory(this.props);

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

差别如上可见,调用了history库中不同的方法创建的history对象,而且不仅仅如此,暴露出去的props也不太一样:

HashRouter.propTypes = {
  basename: PropTypes.string,
  children: PropTypes.node,
  getUserConfirmation: PropTypes.func,
  hashType: PropTypes.oneOf(["hashbang", "noslash", "slash"])
};
复制代码
BrowserRouter.propTypes = {
  basename: PropTypes.string,
  children: PropTypes.node,
  forceRefresh: PropTypes.bool,
  getUserConfirmation: PropTypes.func,
  keyLength: PropTypes.number
};
复制代码

问:Route组件的props为什么能拿到history,location和match对象?

答:Router使用了context,创建了Provider,将locationhistorymatchstaticContext作为内容传给Consumer。而Route作为contextConsumer,就可以接受到参数了。

// Router.js
import React from "react";
import HistoryContext from './HistoryContext'
import RouterContext from './RouterContext'

class Router extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      location: props.history.location
    };
    
    // 监听路由变化。处于mount过程中,有<Redirect>组件导致路由发生变化,
    // 为了防止location丢失,以及页面抖动(setState)
    // 这时需要把变化的location放到内存中,等页面mount后,再setState
    this._isMounted = false;
    this._pendingLocation = null;

    if (!props.staticContext) {
      this.unlisten = props.history.listen(location => {
        // 如果页面mount成功,直接setState;
        // 如果页面还在mounting,则等待页面mount后再setState
        if (this._isMounted) {
          this.setState({ location });
        } else {
          this._pendingLocation = location;
        }
      });
    }
  }

  componentDidMount() {
    this._isMounted = true;

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

  componentWillUnmount() {
    if (this.unlisten) {
      this.unlisten();
      this._isMounted = false;
      this._pendingLocation = null;
    }
  }

  render() {
    return (
      <RouterContext.Provider
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext
        }}
      >
        <HistoryContext.Provider
          children={this.props.children || null}
          value={this.props.history}
        />
      </RouterContext.Provider>
    );
  }
}

// Route.js
import React from "react";

class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          
          const location = this.props.location || context.location;

          // <Switch>组件会传入computedMatch
          // 其他方式是不会传入computedMatch字段的
          const match = this.props.computedMatch
            ? this.props.computedMatch
            : (
              // matchPath:如果是完全匹配的路由,返回{path, url, isExact, params}
              // 如果不匹配,返回null。这行是控制路由组件展示的关键
              this.props.path ? matchPath(location.pathname, this.props) : context.match
            );

          const props = { ...context, location, match };

          let { children, component, render } = this.props;

          if (Array.isArray(children) && React.Children.count(children) === 0) {
            children = null;
          }
          
          return (
            <RouterContext.Provider value={props}>
              {
                props.match ? (
                  children ? (
                    typeof children === "function" ? children(props) : children
                  ) : (
                    component ? React.createElement(component, props) : (render ? render(props) : null)
                  )
                ) : (
                  typeof children === "function" ? children(props) : null
                )
              }
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

// createNamedContext.js
const createNamedContext = name => {
  const context = React.createContext();
  context.displayName = name;

  return context;
}

// HistoryContext.js
const HistoryContext = createNamedContext("Router-History")
export default HistoryContext

// RouterContext.js
const RouterContext = createNamedContext("Router")
export default RouterContext
复制代码

因此,如下问题的发生,就不难解释了。未匹配到/c路由,但是执行了/c路由下的方法。

问题.png
可通过Switch包裹Route组件避免此种情况的发生。

问:Switch组件是如何保证渲染的是第一个路由组件

答:通过遍历子组件,取出第一个满足pathname的路由组件,clone后展示出来。

import React from "react";

class Switch extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          const location = this.props.location || context.location;

          let element, match;

          React.Children.forEach(this.props.children, child => {
            if (match == null && React.isValidElement(child)) {
              element = child;

              const path = child.props.path || child.props.from;
              match = path ? matchPath(location.pathname, { ...child.props, path }) : context.match;
            }
          });

          return match
            ? React.cloneElement(element, { location, computedMatch: match })
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}
复制代码

问:高级组件withRouter如何将location等传入普通组件内

答:withRouter内部订阅了context的consumer,将context的包含的值作为props传入原组件中。

import React from "react";
import hoistStatics from "hoist-non-react-statics";

function withRouter(Component) {
  const C = props => {
    const { wrappedComponentRef, ...remainingProps } = props;

    return (
      <RouterContext.Consumer>
        {context => {
          return (
            <Component
              {...remainingProps}
              {...context}
              ref={wrappedComponentRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  };

  C.WrappedComponent = Component;

  // 拷贝Component上非React的静态方法,防止静态方法丢失
  return hoistStatics(C, Component);
}
复制代码

问:router自带的hook有哪些,原理是啥?

答:Hook底层均是对useContext钩子的封装,通过useContext钩子获取context的值。Hook有如下4种:useHistoryuseLocationuseParamsuseRouteMatch

import { useContext } from 'react'
import RouterContext from "./RouterContext"
import HistoryContext from "./HistoryContext"

export function useHistory() {
  return useContext(HistoryContext)
}

export function useLocation() {
  return useContext(RouterContext).location
}

export function useParams() {
  const match = useContext(RouterContext).match
  return match ? match.params : {}
}

export function useRouteMatch(path) {
  const location = useLocation();
  const match = useContext(RouterContext).match
  return path ? matchPath(location.pathname, path) : match
}
复制代码

相关链接:

hoist-non-react-statics:zhuanlan.zhihu.com/p/36178509
React.Children: segmentfault.com/a/119000001…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享