用 react-router 也用了比较久了,对他的内部工做方式却只是了解皮毛,并且大部分仍是经过别人的博客。最近两周打算本身探究一下他的实现。 注意!由于我只使用过 v3 版本的 react-router,由于对他的使用方式比较熟悉,因此此次解析也是基于这个版本。javascript
聊到这个话题就离不开前端路由。关于前端路由的一些演变过程和现有的方式能够看这篇文章。前端路由的重点就是不刷新页面,现有的解决方案有 hashChange 和 popState 两种。 React 提供API也是围绕这两种方式。 共同点都是发布订阅的模式,让浏览器事件触发的时候本身添加的 listener 被调用。Router 组件包裹着 Route 组件,Route 组件负责描述每条路由的展现组件和匹配的路径。这样 Router 组件实际上会格式化出一个映射的路由表。
然而这是在页面路由更新的时候,最开始进入页面的时候怎么办呢?其实刚进入页面的时候也会进行一次匹配,详细分析见下一部分。前端
首先解答上面的遗留问题,刚进入页面的状态如何带入?这个问题咱们能够和 "Router 组件是在何时添加的事件监听"放在一块儿解答。java
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 这种方式,可能须要后端同窗的配合。react
在上面的代码中,咱们会注意到添加监听器的 listen方法来自于 transitionManager 这个生成以后被赋值到 this.router 实例的属性。实际上 react-router 的事件监听过程是用 transitionManager 套了 history 这个库,抹平各类前端路由方式的调用差别。history库自己暴露了一些API 好比监听、取消监听、跳转等一系列方法。有基于我们刚才提到的 hash 和 state 两种方式。咱们传给 Router 组件 history 属性的值其实就是他的实例。(拿hashHistory 举栗,下面的文件是 reate-router export 的 hashHistory 的来源,也就是咱们用的 hashHistory 的来源)。segmentfault
//来源: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 是这样写的:bash
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 的部分。react-router
return <RouterContext {...props} /> 复制代码
RouterContext 包装组件的主要做用就是把 props参数中存有当前路由状态的对象router存到全局。相似于 Redux 的 Provider 组件。ide
这里小伙伴们能够猜想一下,Link是怎么作的呢?
咱们知道 Link最后渲染完是个 a 标签,咱们一般会给 Link 组件几个参数,最经常使用的是跳转的路由地址和携带的参数。经过上面的讲解不难猜出,Link 在点击的时候应该是调用了一个跳转的操做(八成也是 history 库里给的),而后禁止掉默认跳转就好了。
事实也是如此,history 暴露了一个 push方法,来 push 进浏览器的历史访问栈中。 这里再提一句另一种用法:*history.push() 的方式。这种其实就至关于直接点击了 a 标签同样的道理,只不过用 js 的方式实现了。
咱们可不能够尝试把展现交给 route 组件管理?router 只控制激活当前的 route?可是这样就不能支持经过 props 传给 router 路由配置的方式使用了,这是其一;
其二,这样其实 route 其实负责了组件的渲染工做,而不是把全部的状态和路由信息所有放在 router 中管理了,不方便集中维护和扩展。 不知道小伙伴们还有别的见解吗?
(原本想写个浅析的……噼里啪啦写了一大堆……还捎带点语无伦次……) (可是虽说了一堆……不过确实挺浅的……各位有兴趣能够尝试本身扒一下源码。建议 react-router 和 history 库一块儿 debug,更有助于咱们融会贯通) ( 各位见笑了)