断点看源码系列:探究React-router内部工作流程

从实际出发搞清楚react-router内部原理

[toc]

前言:最近在看 react-router 的工作原理网上看了一圈文章,发现很多都是介绍了 history 库的原理和 react-router 里面的 BrowserRouter 组件、Router 组件等等,看了原理是大概知道了,但是它的内部是怎么样的一个工作流程还是一脸懵逼,又在找了一圈资料,看完还是觉得具体流程理不太清。想到平时在 js 代码的调试中可以打断点观察各个变量的情况以及程序的流程,react 程序那应该也可以这样调试。

随后写了一个小 demo 测试一下:

准备文件:

App.js 文件

import React, { PureComponent } from 'react'
import { Link, Switch, Route, withRouter } from 'react-router-dom'

// 为了看 history 的源码而引入
import history from 'history'

import Home from './pages/home';
import Profile from './pages/profile';

class App extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
        }
    }

    render() {
        console.log(this.props);
        return (
            <div>
                <Link className='mylink' to="/">首页</Link>
                <Link className='mylink' to="/profile">我的</Link>
                <button onClick={() => this.jumpToProduct()}>测试</button>

                <Switch>
                    <Route exact path="/" component={Home} />
                    <Route path="/profile" component={Profile} />
                </Switch>
            </div>
        )
    }

    jumpToProduct() {
        this.props.history.push("/profile");
    }
}

export default withRouter(App);

index.js 文件

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

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

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
  ,
  document.getElementById('root')
);

profile.js 组件, Home 一样

import React, { PureComponent } from 'react'

export default class Profile extends PureComponent {
    render() {
        return (
            <div>
                <h2>Profile</h2>
            </div>
        )
    }
}

到这里就可以开始了我们的流程探究了,在 App.js 文件的 jumpToProduct 里面的一行代码打上断点。

打断点的具体方式是:将 demo 跑起来 -> 打开浏览器的控制台 -> source -> 具体文件 -> 打上断点

jumpToProduct() {
        this.props.history.push("/profile");
}

如图:

点击 button 按钮,进行单步调试。然后程序就跳到 node_modules/history/cjs/history.jspush 函数里面了。

这是为什么呢?因为 Link 组件里面判断了是用什么方法改变路径的。这里我们模拟的是 push 方法

// 1. Link 里面的判断代码片段
var props = _extends({}, rest, {
      href: href,
      navigate: function navigate() {
        var location = resolveToLocation(to, context.location);
        var isDuplicateNavigation = history.createPath(context.location) === history.createPath(normalizeToLocation(location));
        var method = replace || isDuplicateNavigation ? history$1.replace : history$1.push;
        method(location);
      }
    }); // React 15 compat

2. 这里调用的是 history 下的 push 方法

push 方法里面创建一个新的 location 对象,然后通过 globalHistory 方法改变浏览器当前路由,最后通过setState 方法通知 React-Router 更新(这里会跳到 ReactRefreshEntry.js 这个文件),并传递当前的 location 对象

function push(path, state) {
    var action = 'PUSH';
  	/* 2. 创建location对象 */
    var location = createLocation(path, state, createKey(), history.location);
  	/* 3. 确定是否能进行路由转换 */
    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
      if (!ok) return;
      var href = createHref(location);
      var key = location.key,
          state = location.state;

      if (canUseHistory) {
        /* 改变 url */
        globalHistory.pushState({
          key: key,
          state: state
        }, null, href);

        if (forceRefresh) {
          window.location.href = href;
        } else {
          /* 改变 react-router location对象, 创建更新环境 */
          setState({
            action: action,
            location: location
          });
        }
      } 
    });

3. history 下的 createTransitionManager 函数

作用是监听判断是否能进行路由转换

/* 3. 确定是否能进行路由转换 */
function createTransitionManager() {
  var prompt = null;
  
  function confirmTransitionTo(location, action, getUserConfirmation, callback) {
    if (prompt != null) {
      var result = typeof prompt === 'function' ? prompt(location, action) : prompt;

      if (typeof result === 'string') {
        if (typeof getUserConfirmation === 'function') {
          getUserConfirmation(result, callback);
        } 
      } else {
        // Return false from a transition hook to cancel the transition.
        callback(result !== false);
      }
    } else {
      callback(true);
    }
  }
  
  var listeners = [];
  
  function appendListener(fn) {
    var isActive = true;

    function listener() {
      if (isActive) fn.apply(void 0, arguments);
    }

    listeners.push(listener);
    return function () {
      isActive = false;
      listeners = listeners.filter(function (item) {
        return item !== listener;
      });
    };
  }

  function notifyListeners() {
    listeners.forEach(function (listener) {
      return listener.apply(void 0, args);
    });
  }
}

4. historycreateBrowserHistory(props) 函数的 setState

 /* 通知listener更新,这里通知到 react-router了 */
const setState = (nextState) => {
    /* 合并信息 */
    Object.assign(history, nextState)
    history.length = globalHistory.length
    /* 通知每一个listens路由已经发生变化 */
    transitionManager.notifyListeners(
      history.location,
      history.action
    )
  }

5. 路由匹配

 /* 5. 监听并进行路由匹配 */
class Router extends React.Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);

    this.state = {
      location: props.history.location
    };

    this._isMounted = false;
    this._pendingLocation = null;

    if (!props.staticContext) {
      this.unlisten = props.history.listen(location => {
        if (this._isMounted) {
          this.setState({ location });
        } else {
          this._pendingLocation = location;
        }
      });
    }
  }
}

总结:

路由转换的大致流程就是:

  1. 模拟点击 Link
  2. 调用 history.push 方法,通过 history.pushState 来改变当前 url
  3. 接下来触发 history 下面的 setState 方法,产生新的 location 对象,
  4. 然后通知 Router 组件更新 location
  5. 路由匹配组件中监听更新的消息,找出符合的组件,最后进行页面更新。

纸上得来终觉浅,绝知此事要躬行

还得再多加油 多多 coding 多多看源码

参考:

「源码解析 」这一次彻底弄懂react-router路由原理

------------- 本文结束 感谢阅读 -------------