最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 面试官,别再问我React-Router了!每一行源码我都看过了!

    正文概述 掘金(DerrickTel)   2021-03-06   554

    前言

    此处介绍一下React-Router的核心原理。特别细致的标点符号的不予讨论。

    学前小知识

    React-Router其实最核心的东西是Route组件和由统一作者开发的History库来建立的。接下来跟着镜头一起走进神秘的ßReact-Router世界吧。

    已经知道怎么使用的直接跳过,到下面的源码分析去┗|`O′|┛ 嗷~~

    简单示例

    一起建一个简单的示例吧。先用react官网的create-react-app脚手架弄个react出来。

    npx create-react-app my-app
    cd my-app
    npm start
    

    安装react-router-dom

    npm install react-router-dom
    

    在大家安装之余,我简答的介绍一下react-routerreact-router-dom的区别。

    react-router和react-router-dom的区别。

    先看提供的API

    import { Switch, Route, Router } from 'react-router';
    
    import { Swtich, Route, BrowserRouter, HashHistory, Link } from 'react-router-dom';
    

    React-router

    提供了路由的核心api。如Router、Route、Switch等,但没有提供有关dom操作进行路由跳转的api;

    React-router-dom

    提供了BrowserRouterRouteLink等api,可以通过dom操作触发事件控制路由。

    Link组件,会渲染一个a标签;BrowserRouterHashRouter组件,前者使用pushStatepopState事件构建路由,后者使用hashhashchange 事件构建路由。

    react-router-domreact-router的基础上扩展了可操作domapi

    SwtichRoute 都是从react-router中导入了相应的组件并重新导出,没做什么特殊处理。

    react-router-dompackage.json依赖中存在对react-router的依赖,故此,不需要npm安装react-router

    简单修改

    ?,大家?应该已经安装完了吧?接下来简单的修改一下脚手架里面的内容。主要是为了熟悉对手,知己知彼百战百胜。

    面试官,别再问我React-Router了!每一行源码我都看过了!

    修改一下src/app.js

    import React from 'react';
    import {
      BrowserRouter as Router,
      Switch,
      Route,
      Link,
    } from "react-router-dom";
    
    function Home() {
      return (
        <>
          <h1>首页</h1>
          <Link to="/login">登录</Link>
        </>
      )
    }
    
    function Login() {
      return (
        <>
          <h1>登录页</h1>
          <Link to="/">回首页</Link>
        </>
      );
    }
    
    function App() {
      return (
        <Router>
          <Switch>
            <Route path="/login" component={Login}/>
            <Route path="/" component={Home}/>
          </Switch>
        </Router>
      );
    }
    
    export default App;
    

    这样就完成了一个最简单的示例了。

    SPA的核心思想

    • 监听URL的变化
    • 改变某些context的值
    • 获取对应的页面组件
    • render新的页面

    源码攻读

    BrowserRouter

    从前面的简单示例中我们,发现有一个最外层的伙计,叫BrowserRouter。我们直接干到他的github源码去瞅瞅。

    链接:https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/BrowserRouter.js

    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);
    
      render() {
        return <Router history={this.history} children={this.props.children} />;
      }
    }
    
    if (__DEV__) {
      BrowserRouter.propTypes = {
        basename: PropTypes.string,
        children: PropTypes.node,
        forceRefresh: PropTypes.bool,
        getUserConfirmation: PropTypes.func,
        keyLength: PropTypes.number
      };
    
      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;
    
    

    抛开一些七七八八的判断。重新看。

    import React from "react";
    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} />;
      }
    }
    

    写着几个大字:《瓜子二手车》他只是一个中间商,他在赚差价。(打钱!

    BrowserRouter是依赖于两个库:分别为historyreact-router。ok,我们一探究竟。

    react-router

    源码链接:https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Router.js

    import HistoryContext from "./HistoryContext.js";
    import RouterContext from "./RouterContext.js";
    

    这两个东西其实很简单,都是引用了一个叫做createContext,目的也很简单,这里其实就是创建的普通context,只不过拥有特定的名称而已。源码如下。就几行。

    // TODO: Replace with React.createContext once we can assume React 16+
    import createContext from "mini-create-react-context";
    
    const createNamedContext = name => {
      const context = createContext();
      context.displayName = name;
    
      return context;
    };
    
    export default createNamedContext;
    

    OK,重点是接下来,抛开context之后,我们需要关注的东西。我整理一下。

    先看构造函数

    构造函数

    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.
      
      // _isMounted 表示组件是否加载完成
      this._isMounted = false;
      // 组件未加载完毕,但是 location 发生的变化,暂存在 _pendingLocation 字段中
      this._pendingLocation = null;
    
      // 没有 staticContext 属性,表示是 HashRouter 或是 BrowserRouter
      if (!props.staticContext) {
        this.unlisten = props.history.listen(location => {
          if (this._isMounted) {
            // 组件加载完毕,将变化的 location 方法 state 中
            this.setState({ location });
          } else {
            this._pendingLocation = location;
          }
        });
      }
    }
    

    有两个值☞_isMounted_pendingLocation

    分别是 是否挂载 待定

    内部维护了一个location,默认值是由外面传入的history,我找到传入的地方。

    其实就是之前看到的BrowserRouter

    import React from "react";
    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这个库来提供的,这个后面会提到,我们继续看。

    先简单看一下注释的翻译(谷歌翻译)

    什么意思呢,我简单描述一下:

    收回来,进入if继续看。

    this.unlisten = props.history.listen(location => {
      if (this._isMounted) {
      	this.setState({ location });
      } else {
      	this._pendingLocation = location;
      }
    });
    

    就是调用了historylisten方法。从代码中可以大致了解到就是对history进行监听,然后进行一些操作。

    componentWillUnmount

    那么这个unlisten什么时候执行呢?

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

    Router这个组件卸载的时候就执行啦。也就是说,取消了对history的监听。

    componentDidUnmount

    然后看一下conponentDidMount

    componentDidMount() {
        this._isMounted = true;
    
        if (this._pendingLocation) {
          this.setState({ location: this._pendingLocation });
        }
      }
    

    也很简单,就是修改一下是否挂载的值,以及继续之前在构造函数里面的判断。如果暂存数据有的话就把他存下来。

    render

    render() {
        return (
          <RouterContext.Provider
            value={{
              // 根据 HashRouter 还是 BrowserRouter,可判断 history 类型
              history: this.props.history,
              // 这个 location 就是监听 history 变化得到的 location
              location: this.state.location,
              // path url params isExact 四个属性
              match: Router.computeRootMatch(this.state.location.pathname),
              // 只有 StaticRouter 会传 staticContext
              // HashRouter 和 BrowserRouter 都是 null
              staticContext: this.props.staticContext
            }}
          >
            <HistoryContext.Provider
              children={this.props.children || null}
              value={this.props.history}
            />
          </RouterContext.Provider>
        );
      }
    

    总结

    Router这个组件主要就是将一些数据进行存储。存到Context,之间不乏一些特殊情况的判断,比如子组件渲染比父组件早,以及Redirect的情况的处理。在卸载的时候要移除对history的监听。

    子组件作为消费者,就可以对页面进行修改,跳转,获取这些数值。

    history

    createBrowserHistory

    返回的内容

    之前一直有不断提到的history,我们一起来看看它是谁

    源码连接:https://github.com/ReactTraining/history/blob/master/packages/history/index.ts

    我们之前用到的createBrowserHistory,他其实是返回的一个对象,这个对象里面有我们常用的一些方法。

    let history: BrowserHistory = {
        get action() {
          return action;
        },
        get location() {
          return location;
        },
        createHref,
        push,
        replace,
        go,
        back() {
          go(-1);
        },
        forward() {
          go(1);
        },
        listen(listener) {
          return listeners.push(listener);
        },
        block(blocker) {
          let unblock = blockers.push(blocker);
    
          if (blockers.length === 1) {
            window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
          }
    
          return function() {
            unblock();
    
            // Remove the beforeunload listener so the document may
            // still be salvageable in the pagehide event.
            // See https://html.spec.whatwg.org/#unloading-documents
            if (!blockers.length) {
              window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
            }
          };
        }
      };
    
      return history;
    }
    

    大部分精髓就是这里。发现有多熟悉的伙伴! => pushreplacego等等。有些其实都是window提供的。

    有些直接看代码就可以明白的就不解释了,比如forwardback

    go

    比如上面提到的go方法,截取部分源码。

    let globalHistory = window.history;
    function go(delta: number) {
        globalHistory.go(delta);
      }
    
    createHref
    
    // 返回一个完整的url
    export function createPath({
      pathname = '/',
      search = '',
      hash = ''
    }: PartialPath) {
      return pathname + search + hash;
    }
    
    // 返回一个url
    function createHref(to: To) {
      	// 看上面
        return typeof to === 'string' ? to : createPath(to);
      }
    
    push

    push的源码,里面附带了一些会用到的函数。

    // 就是简单处理一下返回值。
    function getNextLocation(to: To, state: State = null): Location {
      return readOnly<Location>({
        ...location,
        ...(typeof to === 'string' ? parsePath(to) : to),
        state,
        key: createKey()
      });
    }
    
    // 进行判断
    function allowTx(action: Action, location: Location, retry: () => void) {
        return (
          // 长度为0就返回true,长度大于0就调用函数,并传入参数。这个blockers等一下仔细探讨一下
          !blockers.length || (blockers.call({ action, location, retry }), false)
        );
      }
    
    
    // 顾名思义获取state和url
    function getHistoryStateAndUrl(
        nextLocation: Location,
        index: number
      ): [HistoryState, string] {
        return [
          {
            usr: nextLocation.state,
            key: nextLocation.key,
            idx: index
          },
          // 看上面 有专门介绍
          createHref(nextLocation)
        ];
      }
    
    // 返回一些关于location的信息
    function getIndexAndLocation(): [number, Location] {
        let { pathname, search, hash } = window.location;
        let state = globalHistory.state || {};
        return [
          state.idx,
          readOnly<Location>({
            pathname,
            search,
            hash,
            state: state.usr || null,
            key: state.key || 'default'
          })
        ];
      }
    
    // 执行listeners内部的一些函数(也就是跳转),后面也会详细解读
    function applyTx(nextAction: Action) {
        action = nextAction;
        [index, location] = getIndexAndLocation();
        listeners.call({ action, location });
      }
    
    
    function push(to: To, state?: State) {
      	// 这里是一个枚举值
        let nextAction = Action.Push;
      	
        let nextLocation = getNextLocation(to, state);
      	// 顾名思义,就是再来一次
        function retry() {
          push(to, state);
        }
    
        if (allowTx(nextAction, nextLocation, retry)) {
          let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
    
          // TODO: Support forced reloading
          // try...catch because iOS limits us to 100 pushState calls :/
          try {
            // MDN的地址: https://developer.mozilla.org/zh-CN/docs/Web/API/History/pushState
            globalHistory.pushState(historyState, '', url);
          } catch (error) {
            // They are going to lose state here, but there is no real
            // way to warn them about it since the page will refresh...
            // MDN的地址: https://developer.mozilla.org/zh-CN/docs/Web/API/Location/assign
            window.location.assign(url);
          }
    
          applyTx(nextAction);
        }
      }
    

    我在注释里面已经进行非常详细的解读了,用到的每个函数都有解释或者官方权威的url。

    总结一下:history.push的一个完整流程

    • 调用history.pushState
      • 错误由window.location.assign来处理
    • 执行一下listeners里面的函数

    是的,你没有看错,就这么简单,只是里面有很多调用的函数,我都截取出来一一解释,做到每行代码都理解,所以显得比较长,概括来说就是这么简单。

    重点来看一下listen和被调用的createBrowserHistory

    replace

    这里面用的函数,在前面的push都有解析,可以往上面去找找,就不多赘述了。

    function replace(to: To, state?: State) {
        let nextAction = Action.Replace;
        let nextLocation = getNextLocation(to, state);
        function retry() {
          replace(to, state);
        }
    
        if (allowTx(nextAction, nextLocation, retry)) {
          let [historyState, url] = getHistoryStateAndUrl(nextLocation, index);
    
          // TODO: Support forced reloading
          globalHistory.replaceState(historyState, '', url);
    
          applyTx(nextAction);
        }
      }
    
    listen

    history返回的listen是一个函数。这个函数我们之前在react-router的源码中发现,他是在构造函数和卸载的时候会用到。

    listen(listener) {
          return listeners.push(listener);
        },
    

    仔细看看,这个listeners是在做些什么。

    createEvents

    这个函数后面的blockers也会用到

    let listeners = createEvents<Listener>();
    
    function createEvents<F extends Function>(): Events<F> {
      let handlers: F[] = [];
    
      return {
        get length() {
          return handlers.length;
        },
        push(fn: F) {
          handlers.push(fn);
          return function() {
            handlers = handlers.filter(handler => handler !== fn);
          };
        },
        call(arg) {
          handlers.forEach(fn => fn && fn(arg));
        }
      };
    }
    

    这个方法,顾名思义,就是创建事件。定义了一个变量 handlers 数组,用于存放要处理的回调函数事件。

    然后返回了一个对象。

    push 方法就是往 handlers 中添加要执行的函数。

    这块主要在 history.listen() 中使用,可以翻到开头看下 history 中返回了 listen() 方法,就是调用了listeners.push(listener)

    最后 call() 方法就比较容易理解,就是取出 handlers 里面的回调函数并逐个执行。

    总结一下,就是存储一下push进来的函数,并进行过滤。之后调用的时候会依次执行。length就是当前拥有的函数数量。

    再切回去,就会发现,每次调用这个listen就相当于push一个函数到内部的一个变量handlers中。

    block

    listeners一样,也是用createEvents创建的,就不多说啦。说一下哪里会用到这个吧。

    let blockers = createEvents<Blocker>();
    
    • block(prompt) - (function) Prevents navigation (see the history docs)

    这个是react-router官网的解释。

    我这里简单概括一下,就是用于关闭或者回退浏览器的误操作会用到的。详细的可以看点击进去查看。

    总结

    至此,我们调用的createBrowserHistory所返回的一些属性的源码都已经了如指掌了。但是具体是怎么工作还是一知半解。

    具体核心原理

    先秀一下源码。history的核心原理就是这个。先别被这么多行代码唬到了,很多都是我们在之前的push里面有解释的

    const PopStateEventType = 'popstate';
    let blockedPopTx: Transition | null = null;
    function handlePop() {
      // 如果有
      if (blockedPopTx) {
        blockers.call(blockedPopTx);
        blockedPopTx = null;
      } else {
        let nextAction = Action.Pop;
        let [nextIndex, nextLocation] = getIndexAndLocation();
    
        if (blockers.length) {
          if (nextIndex != null) {
            let delta = index - nextIndex;
            if (delta) {
              // Revert the POP
              blockedPopTx = {
                action: nextAction,
                location: nextLocation,
                retry() {
                  go(delta * -1);
                }
              };
    
              go(delta);
            }
          } else {
            // Trying to POP to a location with no index. We did not create
            // this location, so we can't effectively block the navigation.
            warning(
              false,
              // TODO: Write up a doc that explains our blocking strategy in
              // detail and link to it here so people can understand better what
              // is going on and how to avoid it.
              `You are trying to block a POP navigation to a location that was not ` +
              `created by the history library. The block will fail silently in ` +
              `production, but in general you should do all navigation with the ` +
              `history library (instead of using window.history.pushState directly) ` +
              `to avoid this situation.`
            );
          }
        } else {
          applyTx(nextAction);
        }
      }
    }
    
    window.addEventListener(PopStateEventType, handlePop);
    

    重点说一下window.addEventListener(PopStateEventType, handlePop)

    MDN地址:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event

    其实就是监听路由的变化然后,执行回调函数

    createHashHistory

    这个大体上与普通路由一致。这里我想强调一些问题。

    感兴趣的自己查询一下,这个不在本次的讨论范围内。

    具体核心原理

    之前说的到,history路由的核心是window.addEventListener(PopStateEventType, handlePop);

    而哈希路由的核心也是监听路由的变化,只是参数不同。

    window.addEventListener('hashchange',function(e){
        /* 监听改变 */
    })
    

    而对路由的改变。history路由是:history.pushStatehistory.replaceState

    哈希路由是:

    window.location.hash

    通过window.location.hash 属性获取和设置 hash 值。

    具体的话很差不多,关心细节的伙伴可以去看其源码哦。

    核心API

    之前我们看了BrowserRouter。我们回过头来看看,demo中的实例代码还有多少没有解决:

    import React from 'react';
    import {
      BrowserRouter as Router,
      Switch,
      Route,
      Link,
    } from "react-router-dom";
    
    function Home() {
      return (
        <>
          <h1>首页</h1>
          <Link to="/login">登录</Link>
        </>
      )
    }
    
    function Login() {
      return (
        <>
          <h1>登录页</h1>
          <Link to="/">回首页</Link>
        </>
      );
    }
    
    function App() {
      return (
        <Router>
          <Switch>
            <Route path="/login" component={Login}/>
            <Route path="/" component={Home}/>
          </Switch>
        </Router>
      );
    }
    
    export default App;
    

    我们已经看完了BrowserRouterRouter。下面还有SwitchRoute

    Switch

    先上源码。我摘取最核心的一部分。

    class Switch extends React.Component {
      render() {
        return (
          <RouterContext.Consumer>
            {context => {
              invariant(context, "You should not use <Switch> outside a <Router>");
    
              // 默认使用context.location,如果有特殊定制的location才会使用。
              // 下面有介绍
              const location = this.props.location || context.location;
    
              let element, match;
    
              // We use React.Children.forEach instead of React.Children.toArray().find()
              // here because toArray adds keys to all child elements and we do not want
              // to trigger an unmount/remount for two <Route>s that render the same
              // component at different URLs.
              // React.Children.forEach 对子元素做遍历
              React.Children.forEach(this.props.children, child => {
                // 只要找到一个 match,那么就不会再进来了
                if (match == null && React.isValidElement(child)) {
                  element = child;
    
                  // child.props.path 就不多讲了, Route 的标准写法
                  // 需要注意的是,使用 from 也会被匹配到
                  // 任何组件,只要在 Switch 下,有 from 属性,并且和当前路径匹配,就会被渲染
                  // from具体是给<Redirect>使用的,后面会说到
                  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>
        );
      }
    }
    
    

    Switch中的location这个props怎么用?什么用?

    matchPath

    function matchPath(pathname, options = {}) {
      // 规范结构体
      // 如果 options 传的是个 string,那默认这个 string 代表 path
      // 如果 options 传的是个 数组,那只要有一个匹配,就认为匹配
      if (typeof options === "string" || Array.isArray(options)) {
        options = { path: options };
      }
    
      const { path, exact = false, strict = false, sensitive = false } = options;
    
      // 转化成数组进行判断
      const paths = [].concat(path);
    
      // 都是很简单的内容,难点就在这个reduce,这个很有意思,感兴趣或者不了解的赶紧去MDN了解一下!!!
      return paths.reduce((matched, path) => {
        if (!path && path !== "") return null;
        // 只要有一个 match,直接返回,认为是 match
        if (matched) return matched;
    
        // regexp 是正则表达式
        // keys 是切割出来的得 key 的值
        const { regexp, keys } = compilePath(path, {
          end: exact,
          strict,
          sensitive
        });
        // exec() 该方法如果找到了匹配的文本的话,则会返回一个结果数组,否则的话,会返回一个
        const match = regexp.exec(pathname);
        /* 匹配不成功,返回null */
        if (!match) return null;
    
        // url 表示匹配到的部分
        const [url, ...values] = match;
        // pathname === url 表示完全匹配
        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);
    }
    

    matchPath 函数也是由 react-router export 出去的函数,我们可以用来获得某个 url 中的指定的参数。

    Route

    Route 是用于声明路由映射到应用程序的组件层。

    Route 有三种渲染的方法,当然,都配置的话只有一个会生效,优先级是 children > component > render

    \1. \2. \3.

    每个在不同的情况下都有用,大多数情况下,会使用 component。

    component

    component 表示只有当位置匹配时才会渲染的 React 组件。使用 component(而不是 render 或 children )Route 使用从给定组件 React.createElement(element, props) 创建新的 React element。这意味着,使用 component 创建的组件能获得 router 中的 props。

    children

    从源码中可以看出,children 的优先级是高于 component,而且可以是一个组件,也可以是一个函数,children 没有获得 router 的 props。

    children 有一个非常特殊的地方在于,当路由不匹配且 children 是一个函数的时候,会执行 children 方法,这就给了设计很大的灵活性。

    render

    render 必须是一个函数,优先级是最低的,当匹配成功的时候,执行这个函数。

    exact & strict & sensitive

    这三者都是使用 path-to-regexp 做路径匹配需要的三个参数。

    1. exact: 如果为 true,则只有在路径完全匹配 location.pathname 时才匹配。
    2. strict: 在确定为位置是否与当前 URL 匹配时,将考虑位置 pathname 后的斜线。
    3. sensitive: 如果路径区分大小写,则为 true ,则匹配。

    location

    Route 元素尝试其匹配 path 到当前浏览器 URL,但是,也可以通过 location 实现与当前浏览器位置以外的位置相匹配。

    下面列出 Route 源码,并且删去了 dev 部分。

    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.js";
    import matchPath from "./matchPath.js";
    
    /**
     * The public API for matching a single path and rendering.
     */
    class Route extends React.Component {
      render() {
        return (
          <RouterContext.Consumer>
            {context => {
              invariant(context, "You should not use <Route> outside a <Router>");
    
              // 可以看出,用户传的 location 覆盖掉了 context 中的 location
              const location = this.props.location || context.location;
    
              // 如果有 computedMatch 就用 computedMatch 作为结果
              // 如果没有,则判断是否有 path 传参
              // matchPath 是调用 path-to-regexp 判断是否匹配
              // path-to-regexp 需要三个参数
              // exact: 如果为 true,则只有在路径完全匹配 location.pathname 时才匹配
              // strict: 如果为 true 当真实的路径具有一个斜线将只匹配一个斜线location.pathname
              // sensitive: 如果路径区分大小写,则为 true ,则匹配
              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;
    
              // props 就是更新后的 context
              // location 做了更新(有可能是用户传入的location)
              // 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.
              // children 默认是个空数组,如果是默认情况,置为 null
              if (Array.isArray(children) && children.length === 0) {
                children = null;
              }
    
              return (
                // RouterContext 中更新了 location, match
                <RouterContext.Provider value={props}>
                  {props.match
                  // 首先判断的是有无 children
                    ? children
                      // 如果 children 是个函数,执行,否则直接返回 children
                      ? typeof children === "function"
                      : children(props)
                      : children
                      // 如果没有 children,判断有无 component
                    : component
                      // 有 component,重新新建一个 component
                      ? React.createElement(component, props)
                      // 没有 component,判断有无 render
                      : render
                      // 有 render,执行 render 方法
                      ? render(props)
                      // 没有返回 null
                      : null
    
                    // 这里是不 match 的情况,判断 children 是否函数
                    : typeof children === "function"
                    // 是的话执行
                    ? children(props)
                    : null}
                </RouterContext.Provider>
              );
            }}
          </RouterContext.Consumer>
        );
      }
    }
    
    export default Route;
    

    Route 组件根据自身的传参,对上层 RouterContext 中的部分属性(location 和 match)进行了更新,并且如果当前路径和配置的 path 路径 match,则渲染该组件,渲染的方式有 children,component,render 三种方式,我们最常用的就是 component 方式,注意每种方式的区别。

    Prompt

    Prompt 用于路由切换提示。这在某些场景下是非常有用的,比如用户在某个页面修改数据,离开时,提示用户是否保存,Prompt 组件有俩个属性:

    1. message:用于显示提示的文本信息。
    2. when:传递布尔值,相当于标签的开关,默认是 true,设置成 false 时,失效。

    Prompt 的本质是在 when 为 true 的时候,调用 context.history.block 方法,为全局注册路由监听,block 的原理看之前的 history 相关文章。路有变化的时候,默认使用 window.confirm 进行确认,我们也可以自定义 confirm 的形式,就是在 BrowserRouter 或者 HashRouter 传入 getUserConfirmation 这个参数,会替换掉 window.confirm。

    import React from "react";
    import PropTypes from "prop-types";
    import invariant from "tiny-invariant";
    
    import Lifecycle from "./Lifecycle.js";
    import RouterContext from "./RouterContext.js";
    
    /**
     * The public API for prompting the user before navigating away from a screen.
     */
    function Prompt({ message, when = true }) {
      return (
        <RouterContext.Consumer>
          {context => {
            invariant(context, "You should not use <Prompt> outside a <Router>");
    
            if (!when || context.staticContext) return null;
    
            // 调用了 history.block 方法
            const method = context.history.block;
    
            return (
              <Lifecycle
                onMount={self => {
                  self.release = method(message);
                }}
                onUpdate={(self, prevProps) => {
                  if (prevProps.message !== message) {
                    self.release();
                    self.release = method(message);
                  }
                }}
                onUnmount={self => {
                  self.release();
                }}
                message={message}
              />
            );
          }}
        </RouterContext.Consumer>
      );
    }
    
    export default Prompt;
    

    Redirect

    Redirect 与其说是一个组件,不如说是有组件封装的一组方法,该组件在 componentDidMount 生命周期内,通过调用 history API 跳转到到新位置,默认情况下,新位置将覆盖历史堆栈中的当前位置。

    to 表示要重定向到的网址。to 也可以是一个 location 对象

    push 为 true 时,重定向会将新条目推入历史记录,而不是替换当前条目。

    结合 Switch 和 Redirect 源码看,如果 Redirect 中有 from 属性,会被 Switch 获得,当 from 和当前路径匹配的时候,就会渲染 Redirect 组件,执行跳转。

    import React from "react";
    import PropTypes from "prop-types";
    import { createLocation, locationsAreEqual } from "history";
    import invariant from "tiny-invariant";
    
    import Lifecycle from "./Lifecycle.js";
    import RouterContext from "./RouterContext.js";
    import generatePath from "./generatePath.js";
    
    /**
     * The public API for navigating programmatically with a component.
     */
    function Redirect({ computedMatch, to, push = false }) {
      return (
        // 啥都有的大哥 RouterContext
        <RouterContext.Consumer>
          {context => {
            invariant(context, "You should not use <Redirect> outside a <Router>");
    
            const { history, staticContext } = context;
    
            // 一般来说,Redirect 操作都不需要留有 history,所以选择选择 history.replace
            const method = push ? history.push : history.replace;
    
    
            const location = createLocation(
              // computedMatch 就是看看 switch 有没有多管闲事
              computedMatch
                ? typeof to === "string"
                  ? generatePath(to, computedMatch.params)
                  : {
                      ...to,
                      pathname: generatePath(to.pathname, computedMatch.params)
                    }
                : to
            );
    
            // When rendering in a static context,
            // set the new location immediately.
            // staticRouter 专用
            if (staticContext) {
              method(location);
              return null;
            }
    
            return (
              <Lifecycle
                onMount={() => {
                  // componentDidMount 的时候执行 method(location),也就是 history.replace 操作
                  method(location);
                }}
                onUpdate={(self, prevProps) => {
                  // componentDidUpdate 时候判断当前 location 和上一个 location 是否发生变化
                  // 只要发生变化,调用 method(location)
                  // 一般来讲,在 componentDidMount 的时候就跳走了,不会等到 componentDidUpdate
                  const prevLocation = createLocation(prevProps.to);
                  if (
                    !locationsAreEqual(prevLocation, {
                      ...location,
                      key: prevLocation.key
                    })
                  ) {
                    method(location);
                  }
                }}
    
                // 无效
                to={to}
              />
            );
          }}
        </RouterContext.Consumer>
      );
    }
    
    export default Redirect;
    

    Lifecycle 不 render 任何页面,只有生命周期函数,Lifecycle 提供了 onMount, onUpdate, onUnmount 三个生命周期函数。

    import React from "react";
    
    class Lifecycle extends React.Component {
      componentDidMount() {
        if (this.props.onMount) this.props.onMount.call(this, this);
      }
    
      componentDidUpdate(prevProps) {
        if (this.props.onUpdate) this.props.onUpdate.call(this, this, prevProps);
      }
    
      componentWillUnmount() {
        if (this.props.onUnmount) this.props.onUnmount.call(this, this);
      }
    
      render() {
        return null;
      }
    }
    
    export default Lifecycle;
    

    总结

    整个react-router是由createBrowserHistory或者createHashHistory来牵头,与我们的React组件绑定在一起,然后传递了一些属于history这个库的方法以及数值。当然,还有路由的匹配和渲染。

    history这个库里面又有对于路由的监听,改变等等。

    流程

    history模式做参考(也是我们重点阅读的。

    修改url

    url改变的时候,会触发写在window上面的监听window.addEventListener('popstate', handlePop)

    调用了我们的函数handlePop

    函数内部我们setState,修改了location,方便传递正确的值下去,并通过了Switch找出匹配的Route组件。

    触发了组件的渲染。


    起源地下载网 » 面试官,别再问我React-Router了!每一行源码我都看过了!

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元