在个人项目当中,我使用了mobx作为状态管理,react-router作为前端路由。在这两者配合使用时,我发现了在点击Link进行路由切换的时候,url已经改变了,但是并没有渲染对应的组件,路由切换无效。原代码:
router.jsx
import React from 'react'
import {
Route,
Redirect,
} from 'react-router-dom'
import TopList from 'view/topic-list/index'
import TopDetail from 'view/topic-detail/index'
import TestApi from 'view/test/api-test'
export default ()=>[
} exact key="home" />,
,
,
,
]
App.jsx
import React from 'react'
import {
Link,
Switch,
Route,
Redirect,
withRouter
} from 'react-router-dom'
import Routes from 'config/router'
import {
observer,
inject,
} from 'mobx-react'
import TopList from 'view/topic-list/index'
import TopDetail from 'view/topic-detail/index'
import TestApi from 'view/test/api-test'
@inject('appState') @observer
export default class App extends React.Component {
render(){
return(
首页
详情页
)
}
}
首先我们需要对react-router和mobx的原理做一个简单的了解
Link, Router, Switch, Route是React-Route中最核心的几个API了。
其中Link能力类比html中的标签, 利用Link可以实现页面跳转。上图中侧边栏中所有可尽心页面跳转都利用了该组件,其实现原理想必所有做过前端开发的人应该都能想到:通过监听onClick事件,在listener中执行history.replace/push完成页面跳转。
Router组件的是整个路由结构中顶层组件,其主要作用是通过监听history.listen,捕获路由变换,并将其置于React Context中,其核心代码如下:
class Router extends React.Component {
getChildContext() {
return {
router: {
...this.context.router,
history: this.props.history,
route: {
location: this.props.history.location,
match: this.state.match
}
}
}
}
computeMatch(pathname) {
return {
path: '/',
url: '/',
params: {},
isExact: pathname === '/'
}
}
componentWillMount() {
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
})
})
}
componentWillUnmount() {
this.unlisten()
}
render() {
const { children } = this.props
return children ? React.Children.only(children) : null
}
}
这应该是整个React Router中最核心的功能了。基本作用就是从context中捞取pathname并与用户定义的path进行匹配,如果匹配成功,则渲染响应组件。
class Route extends React.Component {
getChildContext() {
return {
router: {
...this.context.router,
route: {
location: this.props.location || this.context.router.route.location,
match: this.state.match
}
}
}
}
computeMatch({ computedMatch, location, path, strict, exact }, router) {
}
componentWillReceiveProps(nextProps, nextContext) {
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
})
}
render() {
const props = { match, location, history, staticContext }
return (
component ? ( // component prop gets first priority, only called if there's a match
match ? React.createElement(component, props) : null
) : render ? ( // render prop is next, only called if there's a match
match ? render(props) : null
) : children ? ( // children come last, always called
typeof children === 'function' ? (
children(props)
) : !isEmptyChildren(children) ? (
React.Children.only(children)
) : (
null
)
) : (
null
)
)
}
}
export default Route
这里还用到了Switch方法,Switch的作用是渲染第一个子组件(
class Switch extends React.Component {
render() {
React.Children.forEach(children, element => {
// 遍历子组件的props, 只渲染低一个匹配到pathname的Route
const { path: pathProp, exact, strict, from } = element.props
const path = pathProp || from
if (match == null) {
child = element
match = path ? matchPath(location.pathname, { path, exact, strict }) : route.match
}
})
return match ? React.cloneElement(child, { location, computedMatch: match }) : null
}
}
从代码层面来看, 主要针对ComponentDidMount, componentWillUnmount, componentDidUpdate(mixinLifecicleEvents)三个接口进行修改。同时如果用户没有重写shouldComponentUpdate, 也会优化shouldeComponentUpdate
export function observer(arg1, arg2) {
const target = componentClass.prototype || componentClass;
mixinLifecycleEvents(target)
componentClass.isMobXReactObserver = true;
return componentClass;
}
function mixinLifecycleEvents(target) {
patch(target, "componentWillMount", true);
[
"componentDidMount",
"componentWillUnmount",
"componentDidUpdate"
].forEach(function(funcName) {
patch(target, funcName)
});
if (!target.shouldComponentUpdate) {
// 如果没有重写, 则利用覆盖
target.shouldComponentUpdate = reactiveMixin.shouldComponentUpdate;
}
}
那在详细看一下,Mobx针对这几个接口都做了哪些事情:
function patch(target, funcName, runMixinFirst = false) {
const base = target[funcName];
const mixinFunc = reactiveMixin[funcName];
const f = !base
? mixinFunc
: runMixinFirst === true
? function() {
mixinFunc.apply(this, arguments);
base.apply(this, arguments);
}
: function() {
base.apply(this, arguments);
mixinFunc.apply(this, arguments);
}
;
target[funcName] = f;
}
const reactiveMixin = {
componentWillMount: function() {
makePropertyObservableReference.call(this, "props")
makePropertyObservableReference.call(this, "state")
const initialRender = () => {
reaction = new Reaction(`${initialName}#${rootNodeID}.render()`, () => {});
reactiveRender.$mobx = reaction;
this.render = reactiveRender;
return reactiveRender();
};
const reactiveRender = () => {
reaction.track(() => {
rendering = extras.allowStateChanges(false, baseRender);
return rendering;
};
this.render = initialRender;
},
componentWillUnmount: function() {
this.render.$mobx && this.render.$mobx.dispose();
this.__$mobxIsUnmounted = true;
},
componentDidMount: function() {
if (isDevtoolsEnabled) {
reportRendering(this);
}
},
componentDidUpdate: function() {
if (isDevtoolsEnabled) {
reportRendering(this);
}
},
shouldComponentUpdate: function(nextProps, nextState) {
if (this.state !== nextState) {
return true;
}
return isObjectShallowModified(this.props, nextProps);
}
};
componentWillMount里做两件事情
render
第一次 render 时:
有数据修改时:
了解了这些背景知识后,我们再来看一下当前这个问题:
首先我们通过history.listen(()=>{})观察发现,用户触发Link点击事件时,路由变化被我们的回调函数所捕获。问题并不可能出现在Link 和 listen过程。
那么React Router是在Router这个组件中创建history.listen回调的。当Url发生变化,触发history.listen注册的回调后,会通过修改state, 触发Router Render过程,默认情况下,会触发他的子组件Render过程。而当Route发生componentWillReceiveProps时,会通过Router的getChildContext方法,拿到变化的URL。
通过Debug我们发现,TopBar的render,Switch, Route的render过程都没有触发。而TopBar中有部分状态托管在mobx model中,所有问题差不多可以定位到:因为TopBar外层封装了observer,而observer又会重写shouldComponentUpdate,shouldComponentUpdate拦截了后续render过程,导致没有触发到后续Route组件的shouldComponentUpdate过程。
我个人理解:
使用了mobx observer后,会给App组件外面包一个
const root = document.getElementById('root');
const render = Component => {
const renderMethod = module.hot ? ReactDom.render : ReactDom.hydrate;
ReactDom.render(
,root);
}
render(App);
if(module.hot){
module.hot.accept(() => {
const NextApp = require('view/App').default;
render(NextApp);
})
}
通过给App组件传入props为location、history等路由相关的属性,每次切换路由都会引起这些属性的改变,shouldeComponentUpdate就是识别到props发生了改变,可以继续渲染流程,从而使得子组件可以重新render。
React Router提供了一个Hoc组件withRouter,利用此组件可以将location注入到App中:
import React from 'react'
import {
Link,
Switch,
Route,
Redirect,
withRouter
} from 'react-router-dom'
@inject('appState') @observer
class App extends React.Component {
componentWillReceiveProps(props){
console.log(props)
}
componentDidUpdate(){
console.log('update',this.props)
}
render(){
return(
首页
详情页
)
}
}
export default withRouter(App)
问题解决~
参考:https://yq.aliyun.com/articles/147474?t=t1