【React】React源碼梳理筆記(八)

前言

  • 本篇將react路由組件過一遍。初學時候被路由坑死了,不瞭解原理。現在看這些源碼都不成問題,同時實現出簡易的react路由一套,馬上記錄下。

react-router-dom的HashRouter

  • 源碼:
import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";

/**
 * The public API for a <Router> that uses window.location.hash.
 */
class HashRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}
if (__DEV__) {
  HashRouter.propTypes = {
    basename: PropTypes.string,
    children: PropTypes.node,
    getUserConfirmation: PropTypes.func,
    hashType: PropTypes.oneOf(["hashbang", "noslash", "slash"])
  };

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

export default HashRouter;
  • 可以看見就是借用history和Router組件進行了個屬性控制。

  • BrowserHistory同理,不放了。

react-router-dom的Link

  • 源碼:
import React from "react";
import { __RouterContext as RouterContext } from "react-router";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
import { resolveToLocation, normalizeToLocation } from "./utils/locationUtils";

// React 15 compat
const forwardRefShim = C => C;
let { forwardRef } = React;
if (typeof forwardRef === "undefined") {
  forwardRef = forwardRefShim;
}

function isModifiedEvent(event) {
  return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}

const LinkAnchor = forwardRef(
  (
    {
      innerRef, // TODO: deprecate
      navigate,
      onClick,
      ...rest
    },
    forwardedRef
  ) => {
    const { target } = rest;

    let props = {
      ...rest,
      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();
        }
      }
    };

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

    return <a {...props} />;
  }
);

if (__DEV__) {
  LinkAnchor.displayName = "LinkAnchor";
}

/**
 * The public API for rendering a history-aware <a>.
 */
const Link = forwardRef(
  (
    {
      component = LinkAnchor,
      replace,
      to,
      innerRef, // TODO: deprecate
      ...rest
    },
    forwardedRef
  ) => {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Link> outside a <Router>");

          const { history } = context;

          const location = normalizeToLocation(
            resolveToLocation(to, context.location),
            context.location
          );

          const href = location ? history.createHref(location) : "";
          const props = {
            ...rest,
            href,
            navigate() {
              const location = resolveToLocation(to, context.location);
              const 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);
        }}
      </RouterContext.Consumer>
    );
  }
);
  • 這個有點長,可以看見拿的是react-router中的__RouterContext作爲上下文,上下文中有個history對象,這個就是前面Router組件裏傳入的history對象了,然後本質都是通過history對象上提供的各種路由跳轉方法完成的。
  • 本身是個函數組件,拿forwardref套了下,傳遞ref,兼容老版本react的屬性上innerRef的寫法。
  • 最後的渲染就是渲染傳入component,這個component是LinkAnchor,可以看見是個a標籤,給他裝了屬性。
  • 我將其簡化一下,做成自己的link標籤,也是可以跳轉的:
const LinkAnchor = forwardRef(
  (
    {
      innerRef, // TODO: deprecate
      navigate,
      onClick,
      ...rest
    },ref
  ) => {
    let props = {
      ...rest
    };
    return <a {...props} />;
  }
);
const Mylink = forwardRef(
  (
    {
      component=LinkAnchor,
      replace,
      to,
      innerRef, // TODO: deprecate
      ...rest
    },
    forwardedRef
  ) => {
    return (
      <RouterContext.Consumer>
        {context => {
          const { history } = context;
          const location = createLocation(to, null, null, context.location)
          const href = location ? history.createHref(location) : "";
          const props = {
            ...rest,
            href
          };
          return React.createElement(component, props);
        }}
      </RouterContext.Consumer>
    );
  }
);
  • 這就是個精簡版的。
  • 所以,其實這個link跟我們寫a標籤有多大區別?最大區別就是是browser還是hash路由都可以寫同一個路由,如果你要用hash路由,用a標籤得寫個#號。而history路由的話,a標籤會刷新頁面,而link其實是借用history功能主動觸發。
  • 然後這個路由改變,其實是瀏覽器行爲,瀏覽器把路由改變後,會影響context,從而進行條件渲染。

react-router的Router

  • 源碼:
import React from "react";
import PropTypes from "prop-types";
import warning from "tiny-warning";

import RouterContext from "./RouterContext";

/**
 * The public API for putting history on context.
 */
class Router extends React.Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);

    this.state = {
      location: props.history.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.
    this._isMounted = false;
    this._pendingLocation = null;

    if (!props.staticContext) {
      this.unlisten = props.history.listen(location => {
        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();
  }

  render() {
    return (
      <RouterContext.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
        }}
      />
    );
  }
}

if (__DEV__) {
  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>"
    );
  };
}

export default Router;
  • 我們知道react-router-dom實際就是給router傳了個history和children,那麼Router到底是咋回事?
  • 這玩意其實就是必須要傳入個history,這個history就是hash或者browser,然後放入這個組件的state中,
  • 我們可以精簡下寫成這樣:
class MyRouter extends React.Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }
  constructor(props) {
    super(props);
    this.state = {
      location: props.history.location
    };
    this._isMounted = false;
    this._pendingLocation = null;
    if (!props.staticContext) {
      this.unlisten = props.history.listen(location => {
        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();
  }
  render() {
    return (
      <RouterContext.Provider
        children={this.props.children || null}
        value={{
          history: this.props.history,
          location: this.state.location,
          match: MyRouter.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext
        }}
      />
    );
  }
}
  • 這樣就很清楚了,主要就是調history的listen監聽瀏覽器路由改變,從而改變組件內state,組件卸載就卸載監聽。
  • 組件可能會加載慢,那麼就等組件加載完改變組件內state。
  • 最後通過context傳遞。

條件渲染的Route

  • react-router-dom源碼有這樣句:
export {
  MemoryRouter,
  Prompt,
  Redirect,
  Route,
  Router,
  StaticRouter,
  Switch,
  generatePath,
  matchPath,
  withRouter,
  useHistory,
  useLocation,
  useParams,
  useRouteMatch
} from "react-router";

export { default as BrowserRouter } from "./BrowserRouter.js";
export { default as HashRouter } from "./HashRouter.js";
export { default as Link } from "./Link.js";
export { default as NavLink } from "./NavLink.js";
  • 所以react-router-dom的route就是react-router的route。
  • 看一下route源碼:
import React from "react";
import { isValidElementType } from "react-is";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
import warning from "tiny-warning";

import RouterContext from "./RouterContext";
import matchPath from "./matchPath";

function isEmptyChildren(children) {
  return React.Children.count(children) === 0;
}

function evalChildrenDev(children, props, path) {
  const value = children(props);

  warning(
    value !== undefined,
    "You returned `undefined` from the `children` function of " +
      `<Route${path ? ` path="${path}"` : ""}>, but you ` +
      "should have returned a React element or `null`"
  );

  return value || null;
}


 
class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Route> outside a <Router>");

          const location = this.props.location || context.location;
          const match = this.props.computedMatch
            ? this.props.computedMatch // <Switch> already computed the match for us
            : this.props.path
            ? matchPath(location.pathname, this.props)
            : context.match;

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

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

          // Preact uses an empty array as children by
          // default, so use null if that's the case.
          if (Array.isArray(children) && children.length === 0) {
            children = null;
          }

          return (
            <RouterContext.Provider value={props}>
              {props.match
                ? children
                  ? typeof children === "function"
                    ? __DEV__
                      ? evalChildrenDev(children, props, this.props.path)
                      : children(props)
                    : children
                  : component
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? __DEV__
                  ? evalChildrenDev(children, props, this.props.path)
                  : children(props)
                : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

if (__DEV__) {
  Route.propTypes = {
    children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
    component: (props, propName) => {
      if (props[propName] && !isValidElementType(props[propName])) {
        return new Error(
          `Invalid prop 'component' supplied to 'Route': the prop is not a valid React component`
        );
      }
    },
    exact: PropTypes.bool,
    location: PropTypes.object,
    path: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.arrayOf(PropTypes.string)
    ]),
    render: PropTypes.func,
    sensitive: PropTypes.bool,
    strict: PropTypes.bool
  };

  Route.prototype.componentDidMount = function() {
    warning(
      !(
        this.props.children &&
        !isEmptyChildren(this.props.children) &&
        this.props.component
      ),
      "You should not use <Route component> and <Route children> in the same route; <Route component> will be ignored"
    );

    warning(
      !(
        this.props.children &&
        !isEmptyChildren(this.props.children) &&
        this.props.render
      ),
      "You should not use <Route render> and <Route children> in the same route; <Route render> will be ignored"
    );

    warning(
      !(this.props.component && this.props.render),
      "You should not use <Route component> and <Route render> in the same route; <Route render> will be ignored"
    );
  };

  Route.prototype.componentDidUpdate = function(prevProps) {
    warning(
      !(this.props.location && !prevProps.location),
      '<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
    );

    warning(
      !(!this.props.location && prevProps.location),
      '<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
    );
  };
}

export default Route;
  • 這個比較長,把一些警告無用的給刪了後會發現它的構造很神奇。它既是消費者也是生產者,所以這就是多級路由必須寫全纔可以匹配的原因!多級路由下,它將從它開始,往下傳遞的context給改掉了。
  • 精簡後:
class MyRoute extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          const location = this.props.location || context.location;
          const match = this.props.computedMatch
            ? this.props.computedMatch // <Switch> already computed the match for us
            : 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) && children.length === 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>
    );
  }
}
  • 我們要在路由組件裏傳path和component,有時傳exact,這個是在這個組件的props裏。
  • 在處理匹配時,會交給matchPath來進行計算。這個matchPatch是別人做的一個匹配url的正則搞得:
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;
function compilePath(path, options) {
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  const pathCache = cache[cacheKey] || (cache[cacheKey] = {});
  if (pathCache[path]) return pathCache[path];
  const keys = [];
  const regexp = pathToRegexp(path, keys, options);
  const result = { regexp, keys };
  if (cacheCount < cacheLimit) {
    pathCache[path] = result;
    cacheCount++;
  }
  return result;
}
function matchPath(pathname, options = {}) {
  if (typeof options === "string" || Array.isArray(options)) {
    options = { path: options };
  }
  const { path, exact = false, strict = false, sensitive = false } = options;
  const paths = [].concat(path);
  return paths.reduce((matched, path) => {
    if (!path && path !== "") return null;
    if (matched) return matched;
    const { regexp, keys } = compilePath(path, {
      end: exact,
      strict,
      sensitive
    });
    const match = regexp.exec(pathname);
    if (!match) return null;
    const [url, ...values] = match;
    const isExact = pathname === url;
    if (exact && !isExact) return null;
    return {
      path, // the path used to match
      url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
      isExact, // whether or not we matched exactly
      params: keys.reduce((memo, key, index) => {
        memo[key.name] = values[index];
        return memo;
      }, {})
    };
  }, null);
}
  • 可以看見爲了防止重複運算,還做了個緩存對象。
  • 其中,我們寫的exact就會傳給這個配置項end。然後搞出個匹配url的正則函數,通過正則匹配。
  • 緩存那個計數寫的有點神奇,下面那個判斷是針對動態路由的,有點意思。同時防止緩存過多,一個路由上限緩存1w個。
  • 然後它的判斷是這樣:正則不匹配就提前return 返回個null,匹配的話就返回個match對象。
  • 最後渲染時候,不匹配渲染個null,匹配了才渲染。
  • 渲染那個三目運算寫的有點噁心,這裏面有優先級問題,可以看見,children是最先匹配的,所以如果Route裏面傳children,那麼不會渲染component render之類的。

測試用例

  • 我簡化的路由代碼可以拿去試一下,跟原版可以同時使用:
import React, { Component, forwardRef } from 'react';
import { HashRouter as Router, Link, Route } from 'react-router-dom';
import reactDom from 'react-dom';
import { __RouterContext as RouterContext } from "react-router";
import { createLocation,createHashHistory  } from "history";
import pathToRegexp from "path-to-regexp";


const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
)

const About = () => (
  <div>
    <h2>About</h2>
  </div>
)

const Product = (props) => (
 
  <div>
    <h2>Product</h2>
    <Mylink to={`${props.match.path}/j44`}>j4444</Mylink>
    <MyRoute path={`${props.match.path}/j44`} component={About}></MyRoute>
  </div>
)


const LinkAnchor = forwardRef(
  (
    {
      innerRef, // TODO: deprecate
      navigate,
      onClick,
      ...rest
    },ref
  ) => {
    let props = {
      ...rest
    };
    return <a {...props} />;
  }
);
const Mylink = forwardRef(
  (
    {
      component=LinkAnchor,
      replace,
      to,
      innerRef, // TODO: deprecate
      ...rest
    },
    forwardedRef
  ) => {
    return (
      <RouterContext.Consumer>
        {context => {
          const { history } = context;
          const location = createLocation(to, null, null, context.location)
          const href = location ? history.createHref(location) : "";
          const props = {
            ...rest,
            href
          };
          return React.createElement(component, props);
        }}
      </RouterContext.Consumer>
    );
  }
);



class MyRouter extends React.Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }
  constructor(props) {
    super(props);
    this.state = {
      location: props.history.location
    };
    this._isMounted = false;
    this._pendingLocation = null;
    if (!props.staticContext) {
      this.unlisten = props.history.listen(location => {
        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();
  }

  render() {
    return (
      <RouterContext.Provider
        children={this.props.children || null}
        value={{
          history: this.props.history,
          location: this.state.location,
          match: MyRouter.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext
        }}
      />
    );
  }
}
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;

function compilePath(path, options) {
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  const pathCache = cache[cacheKey] || (cache[cacheKey] = {});
  if (pathCache[path]) return pathCache[path];
  const keys = [];
  const regexp = pathToRegexp(path, keys, options);
  const result = { regexp, keys };
  
  if (cacheCount < cacheLimit) {
    pathCache[path] = result;
    cacheCount++;
  }
  return result;
}

function matchPath(pathname, options = {}) {
  if (typeof options === "string" || Array.isArray(options)) {
    options = { path: options };
  }
  const { path, exact = false, strict = false, sensitive = false } = options;
  const paths = [].concat(path);
  return paths.reduce((matched, path) => {
    if (!path && path !== "") return null;
    if (matched) return matched;
    const { regexp, keys } = compilePath(path, {
      end: exact,
      strict,
      sensitive
    });
    
    const match = regexp.exec(pathname);
    if (!match) return null;
    const [url, ...values] = match;
    const isExact = pathname === url;
    if (exact && !isExact) return null;
    return {
      path, // the path used to match
      url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
      isExact, // whether or not we matched exactly
      params: keys.reduce((memo, key, index) => {
        memo[key.name] = values[index];
        return memo;
      }, {})
    };
  }, null);
}


class MyRoute extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          const location = this.props.location || context.location;
          const match = this.props.computedMatch
            ? this.props.computedMatch // <Switch> already computed the match for us
            : 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) && children.length === 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>
    );
  }
}


class App extends Component {
  
  render() {
    return (
      <MyRouter history={ createHashHistory (this.props)}>
        <div className="App">
          <Link to="/">Home</Link>
          <Link to="/About">About</Link>
          <Link to="/Product">Product</Link>
          <Mylink to='/j'>333</Mylink>
          <a href='#/about'>dddddd</a>
          <hr/>
          <Route path="/j" exact component={About}></Route>
          <Route path="/" exact component={Home}></Route>
          <Route path="/product" component={Product}></Route>
          <MyRoute path='/product' component={Product}></MyRoute>
          <MyRoute path='/about' component={About}></MyRoute>
        </div>
      </MyRouter>
    );
  }
}
reactDom.render(
  <App></App>
,document.getElementById('root')
)

總結

  • react路由就是通過context傳遞history監聽到的路由變化從而產生的對象。link之類就是通過history來生成跳轉。組件條件渲染其實就是拿到history的當前url,與自己本身的url做正則匹配,匹配成功渲染,同時改變自己組件下的context。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章