react Link跳转无效_React-Router源码解读

884d4592c43df6d4a926a88cf06acaea.png
起因

目前负责的项目中有一个微信网页,用的是react技术栈。在该项目中增加了一个微信分享功能后,线上ios出现了问题,经排查,定位到了react的路由系统。

c6ca3cc6a562d144a2462082a9bcb844.png

这次线上bug,让我决定,先从react-router-dom开始,看看它内部实现了什么。

前端目前用到的就是react-router-dom这个库,它提供了两个高级路由器,分别是BrowserRouterHashRouter,它两的区别就是一个用的history API ,一个是使用URL的hash部分,接下来我以BrowserRouter为例,做一个解读。

简易过程图

a670e512cede4a569c65e13e22c076cf.png
解读(只摘取核心代码进行展示)

首先看看整个react-router-dom提供了点啥?

export {
  MemoryRouter,
  Prompt,
  Redirect,
  Route,
  Router,
  StaticRouter,
  Switch,
  generatePath,
  matchPath,
  withRouter,
  useHistory,
  useLocation,
  useParams,
  useRouteMatch
} from "react-router";

export { default as BrowserRouter } from "./BrowserRouter.js";
export { default as HashRouter } from "./HashRouter.js";
export { default as Link } from "./Link.js";
export { default as NavLink } from "./NavLink.js";

除了下面它自己实现的四个组件外,其余的都是将react-router提供的组件做了一个引入再导出,那看来底层核心的东西还是在react-router上。

1.先从一个简单demo开始

import { BrowserRouter, Route, Switch, Link } from "react-router-dom"

function App() {
   return (
      <BrowserRouter>
        <div>主菜单</div>
        <Link to="/home">home</Link>
        <br />
        <Link to="/search">search</Link>
        <hr />
        <Switch> 
          <Route path="/home" component={Home}  />
          <Route path="/search" component={Search} />
        </Switch>
      </BrowserRouter>
      )
}

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

需要通过路由跳转来实现UI变化的组件,要用BrowserRouter作为一个根组件来包裹起来,Route用来盛放页面级的组件。

那按照这种层级关系,我们先来看下BrowersRouter里实现了什么功能。

2. BrowersRouter

import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";

class BrowserRouter extends React.Component {
  history = createHistory(this.props);
  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

非常少量的几行代码,很清晰的看到,核心点是history这个库所提供的函数。组件在render前执行了createHistory这个函数,然后它会返回一个history的对象实例,然后通过props传给Router这个路由器,另外其中包裹的所有子组件,统统传给Router。

这里其实官网上已经说的很清楚,大家用的时候可以多留意下。

26e19fbe6866fb89c303ef5d9c224cf4.png

那么思路就很清楚,重点放在Router和history库上,看看Router是怎么用这个history对象的,以及这个history对象里又包含了啥,和window.history有什么区别?让我们接着往下走。

3. Router

import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js";

Router是核心的路由器,上面我们已经看到BrowsRouter传递给它了一个history对象。

首先引入了两个context,这里其实就是创建的普通context,只不过拥有特定的名称而已。

它的内部实现是这样

const createNamedContext = name => {
  const context = createContext();
  context.displayName = name;
  return context;
};

// 上述的引用就相当于 HistoryContext = createNamedContext("Router-History")

引入了这两个context后,在来看它的构造函数。

constructor(props) {
    super(props);
    this.state = {
      location: props.history.location
    };

    this.unlisten = props.history.listen(location => {
  
       this.setState({ location });
      
    });
  
 }

Router组件维护了一个内部状态location对象,初始值为上面提到的在BrowsRouter中创建的history提供的。

之后,执行了history对象提供的listen函数,这个函数需要一个回调函数作为入参,传入的回调函数的功能就是来更新当前Router内部状态中的location的,关于什么时候会执行这个回调,以及listen函数,后面会详细剖析。

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

等这个Router组件将要卸载时,就取消对history的监听。

 render() {
    return (
      <RouterContext.Provider
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext
        }}
      > 
       <HistoryContext.Provider
          children={this.props.children || null}
          value={this.props.history}
        /> 
     </RouterContext.Provider>
    );
  }

最后生成的react树,就是由最开始引入的context组成的,然后传入history、location这些值。

3fa3757476fa97554fbe01d26bb75d40.png

总结就是整个Router就是一个传入了history、locaiton和其它一些数据的context的提供者,然后它的子组件作为消费者就可以共享使用这些数据,来完成后面的路由跳转、UI更新等动作。

3. histroy库

在Router组件可以看到已经用到了createBrowserHistory函数返回的history实例了,如:history.location和history.listen,这个库里的封装的函数那是相当多了,细节也很多,我仍然挑最重要的解读。

首先是咱们这个出镜率较高的history提供了哪些属性和方法

72427e3b02080d76dfaba88fef3a83fb.png

看起来都是些熟悉的东西,如push、replace、go这些,都是window对象属性history所提供的。但有些属性其实是重写了的,如push、replace,其它的是做了一个简单封装。

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

d18239bb55936345d697318992d899ee.png

Router内部状态location的初始数据,是使用window.location与window.history.state做的重组。

路由系统最为重要的两个切换页面动作,一个是push,一个是replace,我们平时只用Link组件的话,并没有确切的感受,其中Link接受一个props属性,to :string 或者to : object

<link to='/course'>跳转</link>

此时点击它时,调用的就是props.history中重写的push方法。

<Link to='/course' replace>跳转</Link>

如果增加replace属性,则用的就是replace方法

这两个方法主要用的是pushStatereplaceState这两个API,它们提供的能力就是可以增加新的window.history中的历史记录和浏览器地址栏上的url,但是又不会发起真正的网络请求。

这是实现单页面应用的关键点。

然后让我们看一下这两个路由跳转方法

2ecd39f1cbacb4491e6ad60efa510cea.png

精简后,代码还是不少,我解读下。

push中的入参path,是接下来准备要跳转的路由地址。createLocation方法先将这个path,与当前的location做一个合并,返回一个更新的loation。

然后就是重头戏,transitionManager这个对象,让我们先关注下成功回调里面的内容。

通过更新后的location,创建出将要跳转的href,然后调用pushState方法,来更新window.history中的历史记录。

如果你在BrowserRouter中传了forceRefresh这个属性,那么之后就会直接修改window.lcoation.href,来实现页面跳转,但这样就相当于要重新刷新来进行网络请求你的文件资源了。

如果没有传的话,就是调用setState这个函数,注意这个setState并不是react提供的那个,而是history库自己实现的。

function setState(nextState) {
    history.length = globalHistory.length;
    transitionManager.notifyListeners(history.location, history.action);
  }

还是用到了transitionManager对象的一个方法。

另外当我们执行了pushState后,接下来所获取到的window.history都是已经更新的了。

接下来就剩transitionManager这最后的一个点了。

transitionManager是通过createTransitionManager这个函数实例出的一个对象

  function createTransitionManager() {
   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() {
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }
    listeners.forEach(function (listener) {
      return listener.apply(void 0, args);
    });
  }

  return {    appendListener: appendListener,    notifyListeners: notifyListeners  };
    
}

还记的开始时我们在Router组件中已经用过一个history.listen方法,其中内部实现就是用的transitionManager.appendListener方法

function listen(listener) {
    var unlisten = transitionManager.appendListener(listener);
    checkDOMListeners(1);
    return function () {
      checkDOMListeners(-1);
      unlisten();
    };
  }

当时我们给listen传入了一个回调函数,这个回调函数是用来通过React的setState来更新组件内部状态的locaton数据,然后又因为这个lcoation传入了Router-context的value中,所以当它发生变化时,所有的消费组件,都会重新render,以此来达到更新UI的目的。

listen的执行细节是,把它的入参函数(这里指更新Rrouter的state.location的函数)会传入到appendListener中。

执行appendListener后,appendListener将这个入参函数推到listeners这个数组中,保存起来。然后返回一个函数用来删除掉推进该数组的那个函数,以此来实现取消监听的功能。

所以当我们使用push,切换路由时,它会执行notifyListeners并传入更新的location。

然后就是遍历listeners,执行我们在listen传入的回调,此时就是最终的去更新Router的location的过程了。

后面的流程,简单说下,Router里面的Route组件通过匹配pathname 和 更新的location ,来决定是否渲染该页面组件,到此整个的路由跳转的过程就结束了。

总结

第一次阅读源码,尽管删减了很多,但还是写了不少。

希望大家可以沿着这个思路,自己也去看看,还是有很多细节值得推敲的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值