react路由原理

494 阅读5分钟

SPA页面

单页面应用

单页面应用就是一个html,一次性加载js, css等资源,所有页面都在一个容器页面下,页面切换实质是组件的切换。

image.png

react-router-domreact-routerhistory库三者什么关系

react-router和history库都是react-router-dom的核心和集成。

react-router实现了Route、Router、Switch、Redirect等重要路由组件

而react-router-dom在history的基础上实现了BrowserRouter、HashRouter、Link、NavLink等组件。

使用方式(一)

import { Route, Switch, Router } from 'react-router-dom';
import { createBrowserHistory } from 'history'

const history = createBrowserHistory()

<Router history={history}>
    <Switch>
      <Route path='/' component={<A />} />
      <Route path='/b' component={<B />} />
    </Switch>
</Router>

history库做了什么

首先来讨论下,原始的window.history对象做了什么?

history路由

执行history对象上的go、back、forward等方法,会自动触发window的popstate事件,并且向history栈内push一条历史记录。

hash路由

利用锚点实现的,页面的hash内容发生变化时,会触发window的hashchange事件,并且向history栈内push一条历史记录。

以上,我们知道了不管是history路由还是hash路由,切换url的时候都会触发全局事件。

我们要想在单页面应用中,在页面跳转时匹配路由所对应的组件,就需要捕获到全局事件,添加回调,获取新的history对象,传递给Route组件。

history库:

createBrowserHistory方法:

const PopStateEvent = 'popstate' 
const HashChangeEvent = 'hashchange' /* 这里简化了createBrowserHistory,列出了几个核心api及其作用 */ 
function createBrowserHistory(){ 
/* 全局history */ 
const globalHistory = window.history 
/* 处理路由转换,记录了listens信息。 */ 
const transitionManager = createTransitionManager() 
/* 改变location对象,通知组件更新 */ 
const setState = () => { 
/* ... */ 
} 
function go(n) {
    globalHistory.go(n);
}

function goBack() {
    go(-1);
}

function goForward() {
    go(1);
}
/* 当path改变后,处理popstate变化的回调函数 */ 
const handlePopState = () => { 
// 获取当前location,触发setState
/* ... */
} 
/* history.push方法,改变路由,通过全局对象history.pushState改变url, 通知router触发更新,替换组件 */ 
const push=() => { 
/*...*/ 
} 
/* Router订阅location的变化 */ 
const listen=()=>{ 
/*...*/ 
} 
return { 
    push,
    listen, 
    go,
    goBack
    /* .... */ } 
}


createBrowserHistory方法是产生记录路由历史状态的实例,是window.history对象的封装。

  1. history实例的go、goBack、goForward沿用了window.history对象的方法,会触发popstate事件,history库监听了popstate事件,并在回调函数中获取新的location对象,并触发setState。
  2. 对于window.history对象上的pushState和replaceState方法,由于不会触发popstate事件,捕获不到由push和replace引起的url变化,history库封装了push、replace方法,用原生的pushState和replaceState方法实现url跳转,并且获取新的location,触发setState。
  3. setState怎么起作用? Router订阅了history实例上的location对象的变化事件,setState时会发布新的location对象给订阅者(Router)。

以上,就实现了history库的createBrowserHistory的功能。

history库总结:利用原生history对象实现无请求跳转,并通知Router(location)

Router组件做了什么

/* Router 作用是把 history location 等路由信息 传递下去  */

class Router extends React.Component {

  static computeRootMatch(pathname) {
    return { path: '/', url: '/', params: {}, isExact: pathname === '/' };
  }
  constructor(props) {
    super(props);
    this.state = {
      location: props.history.location
    };
    //记录pending位置
    //如果存在任何<Redirect>,则在构造函数中进行更改
    //在初始渲染时。如果有,它们将在
    //在子组件身上激活,我们可能会
    //在安装<Router>之前获取一个新位置。
    this._isMounted = false;
    this._pendingLocation = null;
    /* 此时的history,是history创建的history对象 */
    if (!props.staticContext) {
      /* 这里判断 componentDidMount 和 history.listen 执行顺序 然后把 location复制 ,防止组件重新渲染 */
      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 (
      /*  这里可以理解 react.createContext 创建一个 context上下文 ,保存router基本信息。children */
      <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
        }}
      />
    );
  }
}

以上代码,可以看出,Router订阅了location对象的变化,在url发生变化时(location变化)通知Router,Router通过context,将location、history、match等信息向下传递。

Switch组件做了什么?

/* switch组件 */
class Switch extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {/* 含有 history location 对象的 context */}
        {context => {
          invariant(context, 'You should not use <Switch> outside a <Router>');
          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;
              // 子组件 也就是 获取 Route中的 path 或者 rediect 的 from
              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>
    );
  }
}

根据location中的pathname匹配switch下的多个子Route组件,找到第一个匹配的路由Route组件,将location、match作为props传递给它

Route组件做了什么

/**
 * The public API for matching a single path and rendering.
 */
class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          /* router / route 会给予警告警告 */
          invariant(context, "You should not use <Route> outside a <Router>");
          // computedMatch 为 经过 swich处理后的 path
          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"
                    ? __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>
    );
  }
}

总结: Route取出context中的数据,将location、match等数据作为props传递给将要渲染的组件,所以我们在组件中才可以使用props.history访问history对象实例

以上,其实我们已经了解了react路由的基本原理,利用原生的history对象实现不刷新跳转、记录历史状态,并利用发布订阅将更新后的路由信息,传递给Router,Router作为一个传递信息的中介,利用context向下传递,匹配唯一的路由并进行渲染替换,以此实现了不刷新跳转

使用方式(二)

另外,我们好像还看到了BrowserRouter、HashRouter

使用方式如下:

import { BrowserRouter, Switch, Route } from 'react-router-dom';

<BrowserRouter>
    <Switch>
      <Route path='/' component={<A />} />
      <Route path='/b' component={<B />} />
    </Switch>
</BrowserRouter>

BrowserRouter的原理是什么呢?

他是一个语法糖

相当于

const history = createBrowserHistory()

<Router history={history}>

其他组件

Link

<Link to='/a'/>

原理:

function handleClick(){
    // 阻止a默认跳转 调用history.push
    
}
<a href={to} onClick={handleClick}>

withRouter组件

只有直接路由组件可以在props中读到history路由对象的东西(Route组件实现的),其他非直接路由组件无法获取到(因为无法获取context,props中也没有),所以withRouter这个HOC(高阶组件)出现了。

功能:获取context中的内容,作为props传递给组件本身。