react-router 源码浅析

用 react-router 也用了比较久了,对他的内部工作方式却只是了解皮毛,而且大部分还是通过别人的博客。最近两周打算自己探究一下他的实现。
注意!因为我只使用过 v3 版本的 react-router,因为对他的使用方式比较熟悉,所以这次解析也是基于这个版本。

文章目录:

  • react-router 工作模式简化流程
  • 内部具体实现 ( 最后来个图片综上)
  • Link 组件的实现方式 以及 *History.push 的实现方式
  • 为什么要这样实现?有什么别的方式?做个比较?

react-router 工作模式简化流程

聊到这个话题就离不开前端路由。关于前端路由的一些演变过程和现有的方式可以看这篇文章。前端路由的重点就是不刷新页面,现有的解决方案有 hashChange 和 popState 两种。
React 提供API也是围绕这两种方式。 共同点都是发布订阅的模式,让浏览器事件触发的时候自己添加的 listener 被调用。Router 组件包裹着 Route 组件,Route 组件负责描述每条路由的展示组件和匹配的路径。这样 Router 组件实际上会格式化出一个映射的路由表
然而这是在页面路由更新的时候,最开始进入页面的时候怎么办呢?其实刚进入页面的时候也会进行一次匹配,详细分析见下一部分。

内部具体实现

首先解答上面的遗留问题,刚进入页面的状态如何带入?这个问题我们可以和 "Router 组件是在什么时候添加的事件监听"放在一起解答。

  componentWillMount() {//来源:modules/Router.js
    this.transitionManager = this.createTransitionManager()
    this.router = this.createRouterObject(this.state)

    this._unlisten = this.transitionManager.listen((error, state) => {
      if (error) {
        this.handleError(error)
      } else {
        // Keep the identity of this.router because of a caveat in ContextUtils:
        // they only work if the object identity is preserved.
        assignRouterState(this.router, state)
        this.setState(state, this.props.onUpdate)
      }
    })
  },

Router 组件在 willMount 生命周期添加了 listener,而添加 listener 本身就会触发一次匹配路由展示的过程。匹配的过程有 match 方法,用于各种嵌套路由的匹配。
但是注意,如果使用的是 browserHistory,这种路由方式一般是/a/b 这种方式,可能需要后端同学的配合。

封装 history ——transitionManager

在上面的代码中,我们会注意到添加监听器的 listen方法来自于 transitionManager 这个生成之后被赋值到 this.router 实例的属性。实际上 react-router 的事件监听过程是用 transitionManager 套了 history 这个库,抹平各种前端路由 方式的调用差异。history库本身暴露了一些API 比如监听、取消监听、跳转等一系列方法。有基于咱们刚才提到的 hash 和 state 两种方式。我们传给 Router 组件 history 属性的值其实就是他的实例。(拿hashHistory 举栗,下面的文件是 reate-router export 的 hashHistory 的来源,也就是我们用的 hashHistory 的来源)。

//来源:modules/hashHistory.js
import createHashHistory from 'history/lib/createHashHistory'
import createRouterHistory from './createRouterHistory'
export default createRouterHistory(createHashHistory)

而 transitionManager 做的事情是针对当前的 router 实例和开发者指定的 history 对象,对 history 库给的 API 做一次二次封装,加上修改路由状态等等操作。然后开发者拿着 transitionManager 封装之后暴露出的 listen 等方法操作路由。

  createTransitionManager() {//来源:modules/Router.js
    const { matchContext } = this.props
    if (matchContext) {
      return matchContext.transitionManager
    }

    const { history } = this.props
    const { routes, children } = this.props

    invariant(
      history.getCurrentLocation,
      'You have provided a history object created with history v4.x or v2.x ' +
        'and earlier. This version of React Router is only compatible with v3 ' +
        'history objects. Please change to history v3.x.'
    )

    return createTransitionManager(//注意这个createTransitionManager才是
      history,
      createRoutes(routes || children)
    )
  },
渲染部分

渲染过程不是放在 Route 组件中负责渲染,而是把状态都放在 Router 中保存,详细可见第一部分的代码添加 listener 的部分。

     assignRouterState(this.router, state)
     this.setState(state, this.props.onUpdate)

而 Router 组件的 render 是这样写的:

    const { location, routes, params, components } = this.state
    const { createElement, render, ...props } = this.props

    return render({
      ...props,
      router: this.router,
      location,
      routes,
      params,
      components,
      createElement
    })

而 props 的值是当前 Router 组件的状态,他现在要展示的组件,对应的地址,当前跳转携带的参数 params 等等。下面是调用 render 的部分。

  return <RouterContext {...props} />

RouterContext 包装组件的主要作用就是把 props参数中存有当前路由状态的对象router存到全局。类似于 Redux 的 Provider 组件。

Link 组件的实现方式

这里小伙伴们可以猜测一下,Link是怎么做的呢?我们知道 Link最后渲染完是个 a 标签,我们通常会给 Link 组件几个参数,最常用的是跳转的路由地址和携带的参数。通过上面的讲解不难猜出,Link 在点击的时候应该是调用了一个跳转的操作(八成也是 history 库里给的),然后禁止掉默认跳转就行了。
事实也是如此,history 暴露了一个 push方法,来 push 进浏览器的历史访问栈中。
这里再提一句另外一种用法:*history.push() 的方式。这种其实就相当于直接点击了 a 标签一样的道理,只不过用 js 的方式实现了。

为什么要这样实现?有什么别的方式?做个比较?

我们可不可以尝试把展示交给 route 组件管理?router 只控制激活当前的 route?但是这样就不能支持通过 props 传给 router 路由配置的方式使用了,这是其一;其二,这样其实 route 其实负责了组件的渲染工作,而不是把所有的状态和路由信息全部放在 router 中管理了,不方便集中维护和扩展。
不知道小伙伴们还有别的看法吗?

(本来想写个浅析的……噼里啪啦写了一大堆……还捎带点语无伦次……)
(但是虽然说了一堆……不过确实挺浅的……各位有兴趣可以尝试自己扒一下源码。建议 react-router 和 history 库一起 debug,更有助于我们融会贯通)
( 各位见笑了)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容