一起学react(5) 史上最全react-router4底层源

2018-05-27  本文已影响1445人  fangkyi03

本篇文章主要分析react-router-redux 与react-router 这两个插件的底层源码部分

首先来看一下简单的react-router-redux 来了解一下它的运行机制

如果有什么问题的话 可以加我QQ:469373256

示例

import React from 'react'
import { render } from 'react-dom'
import { connect, Provider } from 'react-redux'
import {
  ConnectedRouter,
  routerReducer,
  routerMiddleware,
  push
} from 'react-router-redux'

import { createStore, applyMiddleware, combineReducers } from 'redux'
import createHistory from 'history/createBrowserHistory'

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

const history = createHistory()

const authSuccess = () => ({
  type: 'AUTH_SUCCESS'
})

const authFail = () => ({
  type: 'AUTH_FAIL'
})

const initialState = {
  isAuthenticated: false
}

const authReducer = (state = initialState , action) => {
  switch (action.type) {
    case 'AUTH_SUCCESS':
      return {
        ...state,
        isAuthenticated: true
      }
    case 'AUTH_FAIL':
      return {
        ...state,
        isAuthenticated: false
      }
    default:
      return state
  }
}

const store = createStore(
  combineReducers({ routerReducer, authReducer }),
  applyMiddleware(routerMiddleware(history)),
)

class LoginContainer extends React.Component {
  render() {
    return <button onClick={this.props.login}>Login Here!</button>
  }
}

class HomeContainer extends React.Component {
  componentWillMount() {
    alert('Private home is at: ' + this.props.location.pathname)
  }

  render() {
    return <button onClick={this.props.logout}>Logout Here!</button>
  }
}

class PrivateRouteContainer extends React.Component {
  render() {
    const {
      isAuthenticated,
      component: Component,
      ...props
    } = this.props

    return (
      <Route
        {...props}
        render={props =>
          isAuthenticated
            ? <Component {...props} />
            : (
            <Redirect to={{
              pathname: '/login',
              state: { from: props.location }
            }} />
          )
        }
      />
    )
  }
}

const PrivateRoute = connect(state => ({
  isAuthenticated: state.authReducer.isAuthenticated
}))(PrivateRouteContainer)

const Login = connect(null, dispatch => ({
  login: () => {
    dispatch(authSuccess())
    dispatch(push('/'))
  }
}))(LoginContainer)

const Home = connect(null, dispatch => ({
  logout: () => {
    dispatch(authFail())
    dispatch(push('/login'))
  }
}))(HomeContainer)

render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Switch>
        <Route path="/login" component={Login} />
        <PrivateRoute exact path="/" component={Home} />
      </Switch>
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root'),
)

先看一下初始化部分

  const history = createHistory()
  const store = createStore(
  combineReducers({ routerReducer, authReducer }),
  applyMiddleware(routerMiddleware(history)),
)
首先在这里定义你想要的history
然后将react-router-redux中对应的reduce以及这个中间件进行一下注入
然后在这里将个刚才注册的history传递给ConnectedRouter用来发起一次dispatch
 <Provider store={store}>
    <ConnectedRouter history={history}>
      <Switch>
        <Route path="/login" component={Login} />
        <PrivateRoute exact path="/" component={Home} />
      </Switch>
    </ConnectedRouter>
  </Provider>,

//绑定组件对应的dispatch用来触发一次action
const Login = connect(null, dispatch => ({
  login: () => {
    dispatch(authSuccess())
    dispatch(push('/'))
  }
}))(LoginContainer)

const Home = connect(null, dispatch => ({
  logout: () => {
    dispatch(authFail())
    dispatch(push('/login'))
  }
}))(HomeContainer)
现在开始一点点来进行一下分析

先来看一下ConnectedRouter部分

import React, { Component } from "react";
import PropTypes from "prop-types";
import { Router } from "react-router";

import { LOCATION_CHANGE } from "./reducer";

class ConnectedRouter extends Component {
  static propTypes = {
    store: PropTypes.object,
    history: PropTypes.object.isRequired,
    children: PropTypes.node,
    isSSR: PropTypes.bool
  };

  static contextTypes = {
    store: PropTypes.object
  };

  handleLocationChange = (location, action) => {
    this.store.dispatch({
      type: LOCATION_CHANGE,
      payload: {
        location,
        action
      }
    });
  };

  componentWillMount() {
    const { store: propsStore, history, isSSR } = this.props;
    this.store = propsStore || this.context.store;

    if (!isSSR)
      this.unsubscribeFromHistory = history.listen(this.handleLocationChange);

    this.handleLocationChange(history.location);
  }

  componentWillUnmount() {
    if (this.unsubscribeFromHistory) this.unsubscribeFromHistory();
  }

  render() {
    return <Router {...this.props} />;
  }
}

export default ConnectedRouter;

重点来看一下ConnectedRouter的componentWillMount部分

 componentWillMount() {
    const { store: propsStore, history, isSSR } = this.props;
    this.store = propsStore || this.context.store;

    if (!isSSR)
      this.unsubscribeFromHistory = history.listen(this.handleLocationChange);

    this.handleLocationChange(history.location);
  }

从这里可以看到 如果当前不是isSSR服务端渲染的话 那么就会发起一个监听 用来监听当前路由的变化 如果history发生变化时 触发handleLocationChange事件

handleLocationChange事件

handleLocationChange = (location, action) => {
    this.store.dispatch({
      type: LOCATION_CHANGE,
      payload: {
        location,
        action
      }
    });
  };
这个事件的作用很简单 当路由发生变化 就发起一次dispatch用来重新刷新页面 在这里得来先屡一下这个调用的先后顺序

1.react-router-redux中间件的实现部分

  export default function routerMiddleware(history) {
  return () => next => action => {
    if (action.type !== CALL_HISTORY_METHOD) {
      return next(action);
    }

    const { payload: { method, args } } = action;
    history[method](...args);
  };
}
从这里可以看到 如果action的类型不为CALL_HISTORY_METHOD就直接放行 让下一个中间件去处理 如果当前类型等于CALL_HISTORY_METHOD则触发history
下面来看一下CALL_HISTORY_METHOD这个究竟是个什么东西

2.Action定义

export const CALL_HISTORY_METHOD = "@@router/CALL_HISTORY_METHOD";

function updateLocation(method) {
  return (...args) => ({
    type: CALL_HISTORY_METHOD,
    payload: { method, args }
  });
}

/**
 * These actions correspond to the history API.
 * The associated routerMiddleware will capture these events before they get to
 * your reducer and reissue them as the matching function on your history.
 */
export const push = updateLocation("push");
export const replace = updateLocation("replace");
export const go = updateLocation("go");
export const goBack = updateLocation("goBack");
export const goForward = updateLocation("goForward");

export const routerActions = { push, replace, go, goBack, goForward };

链接上文就可以发现 我们所有调用的push replace的type都是CALL_HISTORY_METHOD
举个简单的例子
this.props.dispatch(push('./'))
那么实际上我们发送的是一个这样的一个action
  return (...args) => ({
    type: ‘@@router/CALL_HISTORY_METHOD’,
    payload: { method:'push', args }
  });


在连接上文的代码来看一下
export default function routerMiddleware(history) {
  return () => next => action => {
    if (action.type !== CALL_HISTORY_METHOD) {
      return next(action);
    }

    const { payload: { method, args } } = action;
    history[method](...args);
  };
}
这时候 你会发现 这里实际上就变成了
history.push(...args)这种方式去进行了调用

聪明的你 我想应该已经发现这个react-router-redux的运行机制了
当你用push或者replace进行任何操作的时候
最终都会被转换成history中对应的方法
然后因为我们对history进行了操作 所以会触发他对应的回调
通过这种方式来做到了页面的跳转
但是从现有的代码里面 你会发现 貌似没有任何一个地方会导致页面被重新渲染 别急 继续往下看

react-router分析
这里主要介绍Prompt Router Route Redirect
其他的都是一些衍生的产物 就不过多介绍了
先来看一下Router

  class Router extends React.Component {
  static propTypes = {
    history: PropTypes.object.isRequired,
    children: PropTypes.node
  };

  static contextTypes = {
    router: PropTypes.object
  };

  static childContextTypes = {
    router: PropTypes.object.isRequired
  };

  getChildContext() {
    return {
      router: {
        ...this.context.router,
        history: this.props.history,
        route: {
          location: this.props.history.location,
          match: this.state.match
        }
      }
    };
  }

  state = {
    match: this.computeMatch(this.props.history.location.pathname)
  };

  computeMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  }

  componentWillMount() {
    const { children, history } = this.props;

    invariant(
      children == null || React.Children.count(children) === 1,
      "A <Router> may have only one child element"
    );

    // Do this here so we can setState when a <Redirect> changes the
    // location in componentWillMount. This happens e.g. when doing
    // server rendering using a <StaticRouter>.
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }

  componentWillReceiveProps(nextProps) {
    warning(
      this.props.history === nextProps.history,
      "You cannot change <Router history>"
    );
  }

  componentWillUnmount() {
    this.unlisten();
  }

  render() {
    const { children } = this.props;
    return children ? React.Children.only(children) : null;
  }
}

export default Router;

接我们上文讲到的话题 当你发起一个dispatch的时候 为什么页面就会发生变化呢
这里来看一下关键代码

  componentWillMount() {
    const { children, history } = this.props;

    invariant(
      children == null || React.Children.count(children) === 1,
      "A <Router> may have only one child element"
    );

    // Do this here so we can setState when a <Redirect> changes the
    // location in componentWillMount. This happens e.g. when doing
    // server rendering using a <StaticRouter>.
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }
看到这里 我想你应该就明白了吧
你会发现 原来Router在这里也对history进行了一个监听
只要你发起了一个dispatch并且正常调用了history以后 这边就会接收到这个更新 并且触发一次setState
这里我们知道 如果父级刷新的时候 所有的children都会进行一次render计算 所以 页面的刷新 其实就是这么来的
是不是比你想象的要简单很多呢

再来看看route部分

class Route extends React.Component {
  static propTypes = {
    computedMatch: PropTypes.object, // private, from <Switch>
    path: PropTypes.string,
    exact: PropTypes.bool,
    strict: PropTypes.bool,
    sensitive: PropTypes.bool,
    component: PropTypes.func,
    render: PropTypes.func,
    children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
    location: PropTypes.object
  };

  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.object.isRequired,
      route: PropTypes.object.isRequired,
      staticContext: PropTypes.object
    })
  };

  static childContextTypes = {
    router: PropTypes.object.isRequired
  };

  getChildContext() {
    return {
      router: {
        ...this.context.router,
        route: {
          location: this.props.location || this.context.router.route.location,
          match: this.state.match
        }
      }
    };
  }

  state = {
    match: this.computeMatch(this.props, this.context.router)
  };

  computeMatch(
    { computedMatch, location, path, strict, exact, sensitive },
    router
  ) {
    if (computedMatch) return computedMatch; // <Switch> already computed the match for us

    invariant(
      router,
      "You should not use <Route> or withRouter() outside a <Router>"
    );

    const { route } = router;
    const pathname = (location || route.location).pathname;

    return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
  }

  componentWillMount() {
    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"
    );

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

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

  componentWillReceiveProps(nextProps, nextContext) {
    warning(
      !(nextProps.location && !this.props.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(
      !(!nextProps.location && this.props.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.'
    );

    this.setState({
      match: this.computeMatch(nextProps, nextContext.router)
    });
  }

  render() {
    const { match } = this.state;
    const { children, component, render } = this.props;
    const { history, route, staticContext } = this.context.router;
    const location = this.props.location || route.location;
    const props = { match, location, history, staticContext };

    if (component) return match ? React.createElement(component, props) : null;

    if (render) return match ? render(props) : null;

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

    if (children && !isEmptyChildren(children))
      return React.Children.only(children);

    return null;
  }
}

export default Route;

我们在上面已经知道了一个大概的dispatch刷新页面的流程以后 
我们这边要继续深入一下 来了解一下大概的刷新逻辑
这里主要是关注几点
1.
    componentWillReceiveProps(nextProps, nextContext) {
    warning(
      !(nextProps.location && !this.props.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(
      !(!nextProps.location && this.props.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.'
    );

    this.setState({
      match: this.computeMatch(nextProps, nextContext.router)
    });
  }

这里你会发现 当我们刚才发起一个dispatch的时候 因为父执行了setState以后 导致所有的children都触发了一个更新
这里子就会重新执行computeMatch来判断当前Route这个组件对应的children或者component render等函数是否要执行并且显示对应的页面

Route computeMatch部分

  computeMatch(
    { computedMatch, location, path, strict, exact, sensitive },
    router
  ) {
    if (computedMatch) return computedMatch; // <Switch> already computed the match for us

    invariant(
      router,
      "You should not use <Route> or withRouter() outside a <Router>"
    );

    const { route } = router;
    const pathname = (location || route.location).pathname;

    return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
  }

Route matchPath路径匹配规则

const matchPath = (pathname, options = {}, parent) => {
  //如果options类型为string类型的话 则path变成options
  if (typeof options === "string") options = { path: options };
  
  const { path, exact = false, strict = false, sensitive = false } = options;
  //如果path为空的时候 则使用this.context的内容
 //这里就是404的关键所在
  if (path == null) return parent;

  const { re, keys } = compilePath(path, { end: exact, strict, sensitive });
  const match = re.exec(pathname);
  //如果不匹配则直接返回null表示你当前这个组件的route不符合也就不会刷新出来
  if (!match) return null;
  
  //分解url
  const [url, ...values] = match;
  const isExact = pathname === url;
  //如果设置为强制匹配 但是实际结果不强制的话 也直接null不刷新显示
  if (exact && !isExact) return null;
  //一切正常的时候 返回对应的match来刷新页面
  return {
    path, // the path pattern 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;
    }, {})
  };
};

export default matchPath;

compilePath 部分代码

const patternCache = {};
const cacheLimit = 10000;
let cacheCount = 0;

const compilePath = (pattern, options) => {
 // 这里会将你的参数变成一个文本
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
   //这里会根据传进来的end strict sensitive来进行分类
  //如有两条数据
  1.end:true,strict:false, sensitive:true
  2.end:true,strict:false, sensitive:false
  那么就会在patternCache里面保存两条这个数据 并且将这个对应
  进行返回
  const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});
  这里返回的这个cache也就是指定类的集合 里面放的都是同等类型的
  如果cache中有相同的话 就直接返回相同的 不进行其他多余的运算
  if (cache[pattern]) return cache[pattern];
  
  首次初始化的时候 这边肯定是为空的 所以这边进行运算 生成一个最新的值
  const keys = [];
  const re = pathToRegexp(pattern, keys, options);
  const compiledPattern = { re, keys };
  // 这里要注意 你的每次路由跳转以及初始化都会使用cacheCount
最大值是10000 也就是说 如果超过了10000 则下次进行不会使用cache里面的值 而是每次都进行计算返回最新的数据
  if (cacheCount < cacheLimit) {
    cache[pattern] = compiledPattern;
    cacheCount++;
  }
 //返回最新的计算结果
  return compiledPattern;
};
image.png
image.png

Route render

我们已经了解了整个router的更新机制 现在来看一下这个是如何被render的
  render() {
    const { match } = this.state;
    const { children, component, render } = this.props;
    const { history, route, staticContext } = this.context.router;
    const location = this.props.location || route.location;
    const props = { match, location, history, staticContext };
    只要你的match为true 就会显示出来
   但是这里比较特殊的是404那种为匹配到的页面
    如果你的props中没有path的话 会返回parent的match
    这个时候只要你有component就会直接给你显示出来
    
    if (component) return match ? React.createElement(component, props) : null;

    if (render) return match ? render(props) : null;

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

    if (children && !isEmptyChildren(children))
      return React.Children.only(children);

    return null;
  }

Redirect 部分源码讲解

class Redirect extends React.Component {
  static propTypes = {
    computedMatch: PropTypes.object, // private, from <Switch>
    push: PropTypes.bool,
    from: PropTypes.string,
    to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
  };

  static defaultProps = {
    push: false
  };

  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,
        replace: PropTypes.func.isRequired
      }).isRequired,
      staticContext: PropTypes.object
    }).isRequired
  };
  //只有你的父级为Route的时候 才会有staticContext
  isStatic() {
    return this.context.router && this.context.router.staticContext;
  }

  componentWillMount() {
    invariant(
      this.context.router,
      "You should not use <Redirect> outside a <Router>"
    );
    if (this.isStatic()) this.perform();
  }

  componentDidMount() {
    if (!this.isStatic()) this.perform();
  }

  componentDidUpdate(prevProps) {
    const prevTo = createLocation(prevProps.to);
    const nextTo = createLocation(this.props.to);

    if (locationsAreEqual(prevTo, nextTo)) {
      warning(
        false,
        `You tried to redirect to the same route you're currently on: ` +
          `"${nextTo.pathname}${nextTo.search}"`
      );
      return;
    }

    this.perform();
  }

  computeTo({ computedMatch, to }) {
   // 跳转的时候 分为两种
  如果有computedMatch的话 说明你有参数要传递
 如果没有的话直接使用to的数据
    if (computedMatch) {
      if (typeof to === "string") {
        return generatePath(to, computedMatch.params);
      } else {
        return {
          ...to,
          pathname: generatePath(to.pathname, computedMatch.params)
        };
      }
    }

    return to;
  }

  perform() {
    const { history } = this.context.router;
    const { push } = this.props;
    const to = this.computeTo(this.props);
   //如果push为真的话就push否则替换
    if (push) {
      history.push(to);
    } else {
      history.replace(to);
    }
  }

  render() {
    return null;
  }
}

export default Redirect;

这个比较简单 就不细讲了 看一遍应该就明白了

Prompt 部分源码

这个组件唯一的作用就是在页面改变的时候 去给个提醒
class Prompt extends React.Component {
  static propTypes = {
    when: PropTypes.bool,
    message: PropTypes.oneOfType([PropTypes.func, PropTypes.string]).isRequired
  };

  static defaultProps = {
    when: true
  };
 //这句话意味着 你这个组件 永远不能是顶层组件 因为如果自己是顶层的话 是不会有context的
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        block: PropTypes.func.isRequired
      }).isRequired
    }).isRequired
  };

  enable(message) {
    if (this.unblock) this.unblock();

    this.unblock = this.context.router.history.block(message);
  }

  disable() {
    if (this.unblock) {
      this.unblock();
      this.unblock = null;
    }
  }

  componentWillMount() {
    invariant(
      this.context.router,
      "You should not use <Prompt> outside a <Router>"
    );

    if (this.props.when) this.enable(this.props.message);
  }

  componentWillReceiveProps(nextProps) {
   只有当this.props.when不为空
   并且 前后两次显示的message都不一样的时候 才会开启
    if (nextProps.when) {
      if (!this.props.when || this.props.message !== nextProps.message)
        this.enable(nextProps.message);
    } else {
      this.disable();
    }
  }

  componentWillUnmount() {
    this.disable();
  }

  render() {
    return null;
  }
}

export default Prompt;

ok 到这里 整个react-router源码就分析完毕 如果有什么问题的话 可以加我QQ:469373256

上一篇下一篇

猜你喜欢

热点阅读