React-router源码详细解析

409 阅读4分钟

React-router源码解析

截图的源码加了关键性注释,简版实现github.com/upxin/react…

路由的概述

1.当用户刷新页面时,浏览器会默认根据当前 URL 对资源进行重新定位(发送请求)。这个动作对 SPA 是不必要的,因为 SPA 作为单页面,无论如何也只会有一个资源与之对应。此时若走正常的请求-刷新流程,反而会使用户的前进后退操作无法被记录。单页面应用对服务端来说,就是一个 URL、一套资源,做到用“不同的 URL”来映射不同的视图内容感知 URL 的变化。一旦我们感知到了,我们就根据这些变化、用 JS 去给它生成不同的内容。 2. 拦截用户的刷新操作,避免服务端盲目响应、返回不符合预期的资源内容,把刷新这个动作完全放到前端逻辑里消化掉; 3.实现前端路由的方式hash 与 history我们(以hash为例,两者在源码实现上相差不大)

看一下这张流程图,捋清楚思路

p.png

react router的管理与架构

react router也是monorepo的方式管理的,monorepo管理的好处就是能让同一仓库同时管理多个独立项目,各个项目之间又能互相引用,下图是react-router主要代码的目录,然后router同级别的目录router-dom引用了router

r.png

就以hash为例,然后我们主要就看两个文件就行了 其中主要思想就体现了 一个是Router代表(Provider)传递路由信息,一个是Switch代表(Consumer)匹配合法的组件

/* ***通常我们这样使用的
 
 <Router>
     <Switch>
        <Route></Route>
        <Route></Route>
      </Switch>
 </Router> */

 
import React from "react";
import { Router } from "react-router";
// 这里可以看出 除了用来history库的不同api来监听地址变化,这两种模式在实现上其实没有啥区别了
//import { createBrowserHistory as createHistory } from "history";
import { createHashHistory as createHistory } from "history";

class HashRouter extends React.Component {
  history = createHistory(this.props);
  render() {
  // 传入history,这是主要的监听api
    return <Router history={this.history} children={this.props.children} />;
  }
}
export default HashRouter;

上面用到了Router,去看看router写了什么

class Router extends React.Component {
  constructor(props) {
    //初始化监听url变化,保存了改变后的location赋值给_pendingLocation
      this.unlisten = props.history.listen(location => {
        if (this._isMounted) {
          this.setState({ location });
        } else {
          this._pendingLocation = location;
        }
      });
  }
  componentDidMount() {
    this._isMounted = true;

    if (this._pendingLocation) {
    // 重置了location
      this.setState({ location: this._pendingLocation });
    }
  }
  render() {
    return (
      <RouterContext.Provider
      // 这里其实就是react creatContext的Provider
        value={{
          history: this.props.history,
          // 传入了location
          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>
    );
  }
}

简单这里我们讲一下Switch,它其实就是匹配一个组件就返回

import React from "react";

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

/**
 * The public API for rendering the first <Route> that matches.
 */
class Switch extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Switch> outside a <Router>");
          const location = this.props.location || context.location;
          let element, match;
          // 遍历children,返回一个唯一的route组件,matchPath方法就是匹配路径的逻辑,如果match匹配到了 那么设置match的值不为null,这样就会返回当前对应的elment,大家可以去详细看看matchPath方法,用到了一个插件path-to-regexp,方便我们将(搜集到route传入的path生成一个正则,去匹配location的pathname),是强匹配还是模糊匹配等,都可以通过参数传入用path-to-regexp生成一个正则,所以这里一定是用 child的path生成的正则,去匹配pathname,可以去试一下非精确匹配的时候 如果你是home
          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>
    );
  }
}

##举个例子,看下图

9705641c34b0ec7e354a36e1872eb0ff3b9c4f167010fba9925826fef4a3d283QzpcVXNlcnNcc3RvXEFwcERhdGFcUm9hbWluZ1xEaW5nVGFsa1wyODg2NjUyMzFfdjJcSW1hZ2VGaWxlc1wxNjIzMzA4NjgxMjM1X0U0REE0NzJBLURDQTktNGJiMC04NzhGLUQwMzBCQUVGRTQ1NS5wbmc=.png 这里我们去'/search/robot' 直接重定向到子路由 '/search/robot/identify', 但是'/search/robot'没有加exact,就是因为此时我们搜集的location pathname是 '/search/robot/identify',那么'/search/robot'的Robot组件也会被渲染,因为不是精确匹配。如果是精确的匹配Robot组件不会渲染,子路由组件也就不存在了,此时路由地址pathname为'/search/robot/identify',路由组件生产的正则大致为 /^/search/robot(?:/#?)?(?=[/#?]|[]|$)/i,言下之意就是/search/robot后面跟任何内容都会渲染Robot组件 ,Robot组件渲染了,它的子组件才能渲染。

最后,router源码有个Provider和Consumer的pollify版本

class Provider extends Component<ProviderProps<T>> {
		// 发布订阅中心
		emitter = createEventEmitter(this.props.value);

		static childContextTypes = {
			[contextProp]: PropTypes.object.isRequired
		};
		// 该方法传递一个context给子组件
		getChildContext() {
			return {
				[contextProp]: this.emitter
			};
		}
}
class Consumer extends Component<ConsumerProps<T>, ConsumerState<T>> {
		getValue(): T {
			if (this.context[contextProp]) {
			// getChildContext 返回值就是 this.context 这是react的原生api
				return this.context[contextProp].get();
			} else {
				return defaultValue;
			}
		}
		onUpdate = (newValue: any, changedBits: number) => {
			const observedBits: number = this.observedBits | 0;
			if ((observedBits & changedBits) !== 0) {
				this.setState({ value: this.getValue() });
			}
		};

		render() {
			return onlyChild(this.props.children)(this.state.value);
		}
	}