写点什么

说说 React-Router 底层实现?- 面试进阶

作者:beifeng1996
  • 2022 年 10 月 10 日
    浙江
  • 本文字数:6060 字

    阅读完需:约 20 分钟

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就是上面第二个例子中的historyModuleimport 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 中根据计算的匹配结果,进行页面首次渲染/页面更新渲染处理。

用户头像

beifeng1996

关注

还未添加个人签名 2022.09.01 加入

还未添加个人简介

评论

发布
暂无评论
说说React-Router底层实现?-面试进阶_React_beifeng1996_InfoQ写作社区