乐趣区

关于react.js:说说ReactRouter底层实现面试进阶

React-Router 根本理解

对于 React-Router 是针对 React 定义的路由库,用于将 URL 和 component 进行匹配。

React-Router 源码剖析

简略前端路由的实现

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>router</title>
</head>
<body>
    <ul> 
        <li><a href="#/">turn white</a></li> 
        <li><a href="#/blue">turn blue</a></li> 
        <li><a href="#/green">turn green</a></li> 
    </ul> 
<script>
    function Router() {        this.routes = {};        this.currentUrl = '';    }    <!--    // 针对不同的地址进行回调的匹配
    //1: 用户在调用 Router.route('address',function), 在 this.routes 对象中进行记录或者说 address 与 function 的匹配
    -->    Router.prototype.route = function(path, callback) {this.routes[path] = callback || function(){};    };    <!--    // 解决 hash 的变动,针对不同的值,进行页面的解决
    //1: 在 init 中注册过事件,在页面 load 的时候,进行页面的解决
    //2:在 hashchange 变动时,进行页面的解决
    -->    Router.prototype.refresh = function() {        this.currentUrl = location.hash.slice(1) || '/';        this.routes[this.currentUrl]();};    <!--    //1:在 Router 的 prototype 中定义 init
    //2:在页面 load/hashchange 事件触发时,进行回调解决
    //3:利用 addEventListener 来增加事件,留神第三个参数的用途
    //4:bind 的应用区别于 apply/call 的应用
    -->    Router.prototype.init = function() {        window.addEventListener('load', this.refresh.bind(this), false);        window.addEventListener('hashchange', this.refresh.bind(this), false);    }    window.Router = new Router();// 在 window 对象中构建一个 Router 对象
    window.Router.init();// 页面初始化解决
    var content = document.querySelector('body');    // change Page anything
    function changeBgColor(color) {content.style.backgroundColor = color;}    Router.route('/', function() {changeBgColor('white');    });    Router.route('/blue', function() {changeBgColor('blue');    });    Router.route('/green', function() {changeBgColor('green');    });
</script>
</body>
</html>

下面的路由零碎次要由三局部组成

  1. Router.protopyte.init 用于页面初始化(load)/ 页面 url 变动 的事件注册
  2. Router.protopyte.route 对门路 (address) 和回调函数 (function) 的注册并寄存于 Router 中,为 load/hashchange 应用
  3. Router.protopyte.refresh 针对不同的门路 (address) 进行回调的解决

React-Router 简略实现

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title> 包装形式 </title>
</head>
<body>
<script>
    var body = document.querySelector('body'),        newNode = null,        append = function(str){newNode = document.createElement("p");            newNode.innerHTML = str;            body.appendChild(newNode);        };        // 原对象(这里能够是 H5 的 history 对象)var historyModule = {listener: [],        listen: function (listener) {this.listener.push(listener);            append('historyModule listen.')        },        updateLocation: function(){            append('historyModule updateLocation tirgger.');            this.listener.forEach(function(listener){listener('new localtion');            })        }    }    // Router 将应用 historyModule 对象,并对其包装
    var Router = {source: {},        // 复制 historyModule 到 Router 中
        init: function(source){this.source = source;},        // 解决监听事件,在 Router 对页面进行解决时,利用 historyModule 中解决页面
        listen: function(listener) {append('Router listen.');            // 对 historyModule 的 listen 进行了一层包装
            return this.source.listen(function(location){append('Router listen tirgger.');                listener(location);            })        }    }    // 将 historyModule 注入进 Router 中
    Router.init(historyModule);    // Router 注册监听
    Router.listen(function(location){append(location + '-> Router setState.');    })    // historyModule 触发监听回调(对页面进行渲染等解决)
    historyModule.updateLocation();
</script>
</body>
</html>

其实上诉的操作就是只是针对前端简略路由 +historyModule 的降级解决。
其中的操作也是相似的。

  1. Router.init(historyModule) ==> Router.protopyte.init
  2. Router.listen(function()) ==> Router.protopyte.route
  3. Router.updateLocation ==> Router.protopyte.refresh

React-Router 代码实现剖析

因为 React-Router 版本之间的解决形式有些差异,所以就按最新版本来进行剖析。

historyModule(history)的实现

这里针对 react-router-dom 中的 BrowserRouter.js 进行剖析

import warning from "warning";
import React from "react";
import PropTypes from "prop-types";
import {createBrowserHistory as createHistory} from "history";// 这里的 history 就是下面第二个例子中的 historyModule
import Router from "./Router"; // 对应第二个例子中的 Router 对象

/** * 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} />;
  }
}

export default BrowserRouter;

追踪一下 history 的实现
文件门路在源码中的 history 中 index.ts

// 定义一个接口
export interface History {
    length: number;
    action: Action;
    location: Location;
    push(path: Path, state?: LocationState): void;
    push(location: LocationDescriptorObject): void;
    replace(path: Path, state?: LocationState): void;
    replace(location: LocationDescriptorObject): void;
    go(n: number): void;
    goBack(): void;
    goForward(): void;
    block(prompt?: boolean): UnregisterCallback;
    listen(listener: LocationListener): UnregisterCallback;
    createHref(location: LocationDescriptorObject): Href;
}

除去 interface 这种类型,是不是对 History 中定义的属性有点相熟。参考 前端 react 面试题具体解答

listen 函数的注册

React-Router/Router.js

/** * The public API for putting history on context. // 这里的情理相似于例子二中第二步 */
class Router extends React.Component {

  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;
    // 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;

下面须要有几处须要留神的中央

  1. React-Router 是利用 React 的 Context 进行组件间通信的。childContextTypes/getChildContext
  2. 须要特地次要 componentWillMount,也就是说在 Router 组件还未加载之前,listen 曾经被注册。其实这一步和第一个例子中的 init 情理是相似的。
  3. 在 componentWillUnmount 中将办法进行登记,用于内存的开释。
  4. 这里提到了,其实就是 用于 url 和组件的匹配。

理解 Redirect.js

react-router/Redirect.js

// 这里省去其余库的援用
import generatePath from "./generatePath";
/** * The public API for updating the location programmatically * with a component. */
class Redirect extends React.Component {
// 这里是从 Context 中拿到 history 等数据
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,
        replace: PropTypes.func.isRequired
      }).isRequired,
      staticContext: PropTypes.object
    }).isRequired
  };

  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}) {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;
    //Router 中拿到须要跳转的门路,而后传递给 history
    const to = this.computeTo(this.props);

    if (push) {history.push(to);
    } else {history.replace(to);
    }
  }

  render() {return null;}
}

export default Redirect;

note :

  1. 针对 h5 的 history 来讲,push/replace 只是将 url 进行扭转,然而不会触发 popstate 事件

generatePath 函数的解决

// 该办法只是对门路进行解决
/** * Public API for generating a URL pathname from a pattern and parameters. */
const generatePath = (pattern = "/", params = {}) => {if (pattern === "/") {return pattern;}
  const generator = compileGenerator(pattern);
  return generator(params);
};

针对门路进行页面渲染解决

须要看一个 Router 的构造

// 这里的 Router 只是一个容器组件,用于从 Redux/react 中获取数据,而真正的门路 / 组件信息寄存在 Route 中
 <Router>
      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/topics" component={Topics}/>
  </Router>

看一下 Route 对组件的解决

/** * The public API for matching a single path and rendering. */
class Route extends React.Component {
    // 从 Router 中获取信息
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.object.isRequired,
      route: PropTypes.object.isRequired,
      staticContext: PropTypes.object
    })
  };
// 本人定义了一套 Contex 用于子组件的应用
  static childContextTypes = {router: PropTypes.object.isRequired};
// 本人定义了一套 Contex 用于子组件的应用
  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)// matching a URL pathname to a path pattern. 如果不匹配,返回 null, 也就是找不到页面信息
  };
  render() {const { match} = this.state;
    const {children, component, render} = this.props;// 从 Router 构造中获取对应的解决办法
    const {history, route, staticContext} = this.context.router;// 从 Context 中获取数据
    const location = this.props.location || route.location;
    const props = {match, location, history, staticContext};
    // 如果页面匹配胜利,进行 createElement 的渲染。在这里就会调用 component 的 render===> 页面刷新 这是解决第一次页面渲染
    if (component) return match ? React.createElement(component, props) : null;
    // 这里针对首页曾经被渲染,在进行路由解决的时候,依据 props 中的信息,进行页面的跳转或者刷新
    if (render) return match ? render(props) : null;

    return null;
  }
}

export default Route;

Buzzer

针对 React-Router 来讲,其实就是对 H5 的 History 进行了一次封装,使可能辨认将 url 的变动与 componet 渲染进行匹配。

  1. 依据 BrowserRouter 等不同的 API 针对 H5 的 history 的重构
  2. 构造的构建,同时对 history 属性进行注册。
  3. 在 Router 的 componentWillMount 中注册 history 的事件回调。
  4. 在 Redirect 中进行门路的计算,调用 history.push/history.replace 等更新 history 信息。
  5. Route 中依据计算的匹配后果,进行页面首次渲染 / 页面更新渲染解决。
退出移动版