Анализ исходного кода React-маршрутизатора

React.js

предисловие

Предыдущая статья представилаВнешняя маршрутизацияДва принципа реализации, сегодня я хочу начать сreact-routerАнализ исходного кода, как они управляют интерфейсной маршрутизацией. Поскольку версия V4 использовалась ранее, следующий анализ также основан на реакции-маршрутизаторе.v4.4.0版本(далее V4), добро пожаловать на комментарии и обмен. Давайте начнем.

дошкольные знания

Прежде чем анализировать исходный код, ознакомьтесь со следующими сопутствующими знаниями, которые помогут лучше понять структуру исходного кода.

  1. существуетreactКак реализовать разные маршруты для рендеринга разных компонентов в ?

Как интерфейсный фреймворк, сам по себе react не имеетview(абстракция между данными и интерфейсом) за исключением любой функции, в предыдущей статье мы работали через триггерную функцию обратного вызоваDOM, тогда как в реакции мы не работаем напрямуюDOM, но управление абстрагируетсяVDOMИлиJSX, для реагирования маршрутизация должна управлять компонентами生命周期, который отображает разные компоненты для разных маршрутов.

  1. history(сторонняя библиотека) использовать

Поскольку React-Router основан наhistoryВ этой библиотеке реализован мониторинг изменений роутинга, поэтому сначала проведем простой анализ этой библиотеки. Конечно, мы в основном анализируем его режим мониторингаlistenКак это реализовано, что имеет решающее значение для реализации маршрутизации, если вы хотите узнать больше о других API, пожалуйста, переместитеhistoryУзнать больше.

history

historyОсновное использование выглядит следующим образом:

import { createBrowserHistory } from 'history';

const history = createBrowserHistory();

// 获取当前location。
const location = history.location;

// 监听当前location的更改。
const unlisten = history.listen((location, action) => {
  // location是一个类似window.location的对象
  console.log(action, location.pathname, location.state);
});

// 使用push、replace和go来导航。
history.push('/home', { some: 'state' });

// 若要停止监听,请调用listen()返回的函数.
unlisten();

Давайте посмотрим на модули исходного кода нижеindex.js, видно, что история выставляет семь методов:

export { default as createBrowserHistory } from './createBrowserHistory';
export { default as createHashHistory } from './createHashHistory';
export { default as createMemoryHistory } from './createMemoryHistory';
export { createLocation, locationsAreEqual } from './LocationUtils';
export { parsePath, createPath } from './PathUtils';

В приведенном выше примере давайте сделаем простое сравнениеcreateBrowserHistoryа такжеcreateHashHistory:

  • createHashHistoryИспользование URL-адреса вhash(#)часть, чтобы создать что-то вродеexample.com/#/some/pathмаршрут.

    createHashHistoryАнализ исходного кода:

      function getHashPath() {
        // 我们不能使用window.location.hash,因为它不是跨浏览器一致- Firefox将预解码它!
        const href = window.location.href;
        const hashIndex = href.indexOf('#');
        return hashIndex === -1 ? '' : href.substring(hashIndex + 1);//函数返回hush值
      }
    
  • createBrowserHistoryс помощью браузераHistoryAPI для обработки URL-адресов, созданияexample.com/some/pathТакой реальный URL.

    createBrowserHistory.jsАнализ исходного кода:

    //createBrowserHistory.js
    const PopStateEvent = 'popstate';//变量,下面window监听事件popstate用到
    const HashChangeEvent = 'hashchange';//变量,下面window监听事件hashchange用到
    
    function createBrowserHistory(props = {}) {
        invariant(canUseDOM, 'Browser history needs a DOM');
    
        const globalHistory = window.history; // 创建一个使用HTML5 history API(包括)的history对象
        const canUseHistory = supportsHistory();
        const needsHashChangeListener = !supportsPopStateOnHashChange();
        //...
        //push方法
        function push(path, state) {
            if (canUseHistory) {
                globalHistory.pushState({ key, state }, null, href);//在push方法内使用pushState
              ...
        }
        //replace方法
        function replace(path, state) {
            if (canUseHistory) {
                globalHistory.replaceState({ key, state }, null, href);//在replaceState方法内使用replaceState
            }
        }
        let listenerCount = 0;
        //注册路由监听事件
        function checkDOMListeners(delta) {
            listenerCount += delta;
    
            if (listenerCount === 1 && delta === 1) {
                window.addEventListener(PopStateEvent, handlePopState);//popstate监听前进/后退事件
    
                if (needsHashChangeListener)
                window.addEventListener(HashChangeEvent, handleHashChange);//hashchange监听 URL 的变化
            } else if (listenerCount === 0) {
                window.removeEventListener(PopStateEvent, handlePopState);
    
                if (needsHashChangeListener)
                window.removeEventListener(HashChangeEvent, handleHashChange);
            }
        }
        //路由监听
        function listen(listener) {
            const unlisten = transitionManager.appendListener(listener);
            checkDOMListeners(1);
        
            return () => {
                checkDOMListeners(-1);
                unlisten();
            };
         }
    

Как listen запускает прослушивание:

надcreateBrowserHistory.jsСуществует также введение в регистрацию прослушивателей маршрутизации.Давайте посмотрим, как активировать прослушиватели маршрутизации. Функция обратного вызова прослушивателя событий анализаhandlePopState, который, наконец, проходит черезsetStateдля запуска слушателей маршрута, гдеnotifyListenersпозвоню всемlistenФункция обратного вызова, чтобы уведомить слушателей, прослушивающих изменения маршрутизации.

//createBrowserHistory.js
const transitionManager = createTransitionManager();

  function setState(nextState) {
    Object.assign(history, nextState);
    history.length = globalHistory.length;
   // 调用所有的listen 的回调函数,从而达到通知监听路由变化的监听者
    transitionManager.notifyListeners(history.location, history.action);
  }
  //事件监听的回调函数handlePopState
  function handlePopState(event) {
    // 忽略WebKit中无关的popstate事件。
    if (isExtraneousPopstateEvent(event)) return;
    handlePop(getDOMLocation(event.state));
  }

  let forceNextPop = false;
  //回调执行函数
  function handlePop(location) {
    if (forceNextPop) {
      forceNextPop = false;
      setState();
    } else {
      const action = 'POP';

      transitionManager.confirmTransitionTo(
        location,
        action,
        getUserConfirmation,
        ok => {
          if (ok) {
            setState({ action, location });
          } else {
            revertPop(location);
          }
        }
      );
    }
  }
// createTransitionManager.js
    function notifyListeners(...args) {
        //调用所有的listen 的回调函数
        listeners.forEach(listener => listener(...args));
    }

существуетreact-routerизRouterВызов history.listen вызывается в жизненном цикле компонента componentWillMount, поэтому при изменении маршрута будет вызываться метод setState для отображения соответствующего компонента маршрутизации.

// react-router/Router.js
constructor(props) {
    super(props);

    this.state = {
      location: props.history.location
    };
    //调用history.lesten方法监听,setState渲染组件
    this.unlisten = props.history.listen(location => {
      this.setState({ location });
    });
  }

  componentWillUnmount() {
    //路由监听
    this.unlisten();
  }

резюме: Вышеприведенный анализreact-routerКак использовать сторонние библиотекиhistoryПроцесс мониторинга изменений маршрутизации будет описан ниже.react-routerкак это сочетаетсяhistoryсделай этоSPAИзменения маршрутизации достигают эффекта рендеринга различных компонентов, давайте сначала посмотримreact-routerОсновное использование, разобраться в идеях анализа.

Основное использование реактивного маршрутизатора

//引用官方例子
import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";     

function BasicExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/topics">Topics</Link>
          </li>
        </ul>

        <hr />

        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/topics" component={Topics} />
      </div>
    </Router>
  );
}

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

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

function Topics({ match }) {
  return (
    <div>
      <h2>Topics</h2>
      <ul>
        <li>
          <Link to={`${match.url}/rendering`}>Rendering with React</Link>
        </li>
        <li>
          <Link to={`${match.url}/components`}>Components</Link>
        </li>
        <li>
          <Link to={`${match.url}/props-v-state`}>Props v. State</Link>
        </li>
      </ul>

      <Route path={`${match.path}/:topicId`} component={Topic} />
      <Route
        exact
        path={match.path}
        render={() => <h3>Please select a topic.</h3>}
      />
    </div>
  );
}

function Topic({ match }) {
  return (
    <div>
      <h3>{match.params.topicId}</h3>
    </div>
  );
}

export default BasicExample;

V4Разделите маршрут на следующие пакеты:

  • react-routerОтвечает за общую логику маршрутизации
  • react-router-domОтвечает за управление маршрутизацией браузера.
  • react-router-nativeОтвечает за управление маршрутизацией реактивных

Пользователям нужно только импортироватьreact-router-domилиreact-router-nativeВот и все,react-routerСуществование в качестве зависимости больше не требует отдельного импорта.

React RouterСуществует три типа компонентов:

  • Компоненты маршрутизатора (, )
  • Компоненты сопоставления маршрутов (, )
  • Компоненты навигации (, , ).

В приведенной выше демонстрации мы также используем эти три типа компонентов.Чтобы облегчить анализ исходного кода, мы можем разобраться в основном процессе:

  • использовать<BrowserRouter>Создайте специальный объект истории и зарегистрируйтесь для прослушивания событий.
  • использовать<Route>соответствует пути и отображает соответствующий компонент.
  • использовать<Link>Создайте ссылку для перехода к компоненту, который вы хотите визуализировать.

Далее мы проанализируем шаг за шагом в соответствии с вышеуказанными этапами процесса.react-routerКод.

BrowserRouter

/* react-router-dom/BrowserRouter.js */
//从history引入createBrowserHistory
import { createBrowserHistory as createHistory } from "history";
import warning from "warning";
//导入react-router 的 Router 组件
import Router from "./Router";

class BrowserRouter extends React.Component {
  //创建全局的history对象,这里用的是HTML5 history
  history = createHistory(this.props);

  render() {
  //将 history 作为 props 传递给 react-router 的 Router 组件
    return <Router history={this.history} children={this.props.children} />;
  }
}

BrowserRouterИсходный код находится вreact-router-dom, который является компонентом более высокого порядка, который внутренне создает глобальныйhistoryобъект (который может прослушивать изменения всего маршрута) и будетhistoryпередан в качестве реквизитаreact-routerизRouterкомпонент (компонент маршрутизатора затем передаст это свойство истории в качестве контекста дочерним компонентам). Как показано ниже, компоненты передаются в Route с контекстом, что также объясняет, почему Router должен быть вне всех Routes.

//react-router/Router.js
import React from "react";

import RouterContext from "./RouterContext";

//获取history、location、match...
function getContext(props, state) {
  return {
    history: props.history,
    location: state.location,
    match: Router.computeRootMatch(state.location.pathname),
    staticContext: props.staticContext
  };
}

class Router extends React.Component {
  //定义Router组件的match属性字段
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
    /*
     * path: "/", // 用来匹配的 path
     * url: "/", // 当前的 URL
     * params: {}, // 路径中的参数
     * isExact: pathname === "/" // 是否为严格匹配
     */
  }
  
  constructor(props) {
    super(props);

    this.state = {
      location: props.history.location
    };
    //监听路由的变化并执行回调事件,回调内setState
    this.unlisten = props.history.listen(location => {
      this.setState({ location });
        /*
         *hash: "" // hash
         *key: "nyi4ea" // 一个 uuid
         *pathname: "/explore" // URL 中路径部分
         *search: "" // URL 参数
         *state: undefined // 路由跳转时传递的 state
        */
    });
  }

  componentWillUnmount() {
    //组件卸载时停止监听
    this.unlisten();
  }

  render() {
    const context = getContext(this.props, this.state);

    return (
      <RouterContext.Provider
        children={this.props.children || null}
        value={context}<!--借助 context 向 Route 传递组件-->
      />
    );
  }
}
export default Router;

По сравнению с обратным вызовом слушателяsetStateсделать операцию,setStateЭто более значимо само по себе - каждое изменение маршрута -> срабатывание верхнего уровняRouterсобытие обратного вызова ->RouterпровестиsetState-> пройти внизnextContext(contextсодержит последниеlocation) -> нижеRouteполучить новыйnextContextОпределите, нужно ли рендерить.

Route

В исходном коде есть это введение: «Общедоступный API для сопоставления одного пути и рендеринга». просто понимается как нахождениеlocationа также<router>изpathсоответствующие компоненты и рендер.

//react-router/Route.js
//判断Route的子组件是否为空
function isEmptyChildren(children) {
  return React.Children.count(children) === 0;
}
//获取history、location、match...
//父组件没传的话使用context中的
function getContext(props, context) {
  const location = props.location || context.location;
  const match = props.computedMatch
    ? props.computedMatch // <Switch> already computed the match for us
    : props.path
      ? matchPath(location.pathname, props)//在./matchPath.js中匹配location.pathname与path
      : context.match;

  return { ...context, location, match };
}
class Route extends React.Component {

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

          const props = getContext(this.props, context);
          //context 更新 props 和 nextContext会重新匹配
          let { children, component, render } = this.props;
          // 提前使用一个空数组作为children默认值,如果是这样,就使用null。
          if (Array.isArray(children) && children.length === 0) {
            children = null;
          }

          if (typeof children === "function") {
            children = children(props);

            if (children === undefined) {

              children = null;
            }
          }

          return (
            <RouterContext.Provider value={props}>
              {children && !isEmptyChildren(children)
                ? children    
                : props.match//对应三种渲染方式children、component、render,只能使用一种
                  ? component
                    ? React.createElement(component, props)
                    : render
                      ? render(props)
                      : null
                  : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

Routeпринять верхнийRouterвходящийcontext,RouterИстория в нем отслеживает изменения роутинга всей страницы.При скачках страницы,historyвызвать событие слушателя,Routerпройти внизnextContext, он будет обновлятьсяRouteизpropsа такжеcontextсудить о текущемRouteизpathсоответствоватьlocation, рендеринг, если он совпадает, иначе не делать.

Матч основан наmatchPathЭта функция будет проанализирована в следующем разделе, здесь вам нужно только знать, если сопоставление не удается.matchдляnull, если совпадение прошло успешно,matchрезультат какpropsчасть компонента, переданного для рендеринга в render.

Из метода рендеринга вы можете узнать, что есть три способа рендеринга компонентов (children,component,render) Приоритет отрисовки тоже в порядке.Если предыдущая отрендерилась, то вернётся напрямую.

  • children(еслиchildrenэто метод, выполнить этот метод, если это только дочерний элемент, напрямуюrenderэтот элемент)
  • component(передайте компонент напрямую, затем перейдите кrenderкомпоненты)
  • render(рендеринг - это метод, пройдите методrenderэтот компонент)

Далее мы смотрим наmatchPathкак судитьlocationСоответствует ли он path .

function matchPath(pathname, options = {}) {
  if (typeof options === "string") options = { path: options };

  const { path, exact = false, strict = false, sensitive = false } = options;

  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, // 用来进行匹配的路径,其实是直接导出的传入 matchPath 的 options 中的 path
    url: path === "/" && url === "" ? "/" : url, // URL的匹配部分
    isExact, // url 与 path 是否是 exact 的匹配
    // 返回的是一个键值对的映射
    // 比如你的 path 是 /users/:id,然后匹配的 pathname 是 /user/123
    // 那么 params 的返回值就是 {id: '123'}
    params: keys.reduce((memo, key, index) => {
      memo[key.name] = values[index];
      return memo;
    }, {})
  };
}

Link

class Link extends React.Component {
  static defaultProps = {
    replace: false
  };

  handleClick(event, context) {
    if (this.props.onClick) this.props.onClick(event);

    if (
      !event.defaultPrevented && // 阻止默认事件
      event.button === 0 && // 忽略除左击之外的所有内容
      !this.props.target && // 让浏览器处理“target=_blank”等。
      !isModifiedEvent(event) // 忽略带有修饰符键的单击
    ) {
      event.preventDefault();

      const method = this.props.replace
        ? context.history.replace
        : context.history.push;

      method(this.props.to);
    }
  }

  render() {
    const { innerRef, replace, to, ...props } = this.props;
    // eslint-disable-line no-unused-vars

    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Link> outside a <Router>");

          const location =
            typeof to === "string"
              ? createLocation(to, null, null, context.location)
              : to;
          const href = location ? context.history.createHref(location) : "";

          return (
            <a
              {...props}
              onClick={event => this.handleClick(event, context)}
              href={href}
              ref={innerRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

С точки зрения рендера,LinkНа самом деле это тег , вhandleClick, чтобы не былоpreventDefaultиз && щелчок левой кнопкой мыши && не_пустой переход && щелчок без удержания других функциональных клавишpreventDefault,ПотомpushВойтиhistory, о чем тоже упоминалось ранее - смена маршрута и переход страницы не связаны друг с другом,V4существуетLinkпрошедшийhistoryбиблиотекаpushназываетсяHTML5 historyизpushState, но это только меняет маршрут и ничего больше. Вы еще помните listen в Router, он прослушивает изменения в маршрутах, а потом пропускаетcontextвозобновитьpropsа такжеnextContextпусть нижнийRouteПерейдите на матч-реванш и завершите обновление, которое необходимо отрендерить.

Суммировать

Давайте вспомним процесс, через который мы прошли после прочтения основного использования:

  • использовать<BrowserRouter>Создайте специальный объект истории и зарегистрируйтесь для прослушивания событий.
  • использовать<Route>соответствует пути и отображает соответствующий компонент.
  • использовать<Link>Создайте ссылку для перехода к компоненту, который вы хотите визуализировать.

В сочетании с исходным кодом мы проанализируем конкретную реализацию

  1. использоватьBrowserRouterоказыватьRouterсоздает глобальныйhistoryобъект и черезpropsперешел кRouter, пока вRouterФункция прослушивателя устанавливается в , которая использует прослушивание библиотеки истории, и выполняется инициированный обратный вызов.setStateПередайте следующий контекст.
  2. при нажатии на страницуLinkДа, действительно нажалaтеги, но с использованиемpreventDefaultНе допуститьaПереход на вкладку страницы, даваяaДобавьте событие щелчка к метке для выполненияhitsory.push(to).
  3. Изменение маршрута вызоветRouterизsetStateДа, вRouterВ этой главе написано: каждое изменение маршрута -> активировать верхний уровеньRouterсобытие слушателя ->RouterвызыватьsetState-> передать новыйnextContext(nextContextсодержит последниеlocation).
  4. Маршрут принимает новый nextContext и использует функцию matchPath, чтобы определить, соответствует ли путь местоположению.Если он соответствует, он будет отображаться, а если не соответствует, он не будет отображаться.