mobx observer导致react-router路由失效的问题

问题描述

在个人项目当中,我使用了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的原理做一个简单的了解

1.react-router

1.2 Link、Router、 Switch、 Route

Link, Router, Switch, Route是React-Route中最核心的几个API了。

1.2.1 Link

其中Link能力类比html中的标签, 利用Link可以实现页面跳转。上图中侧边栏中所有可尽心页面跳转都利用了该组件,其实现原理想必所有做过前端开发的人应该都能想到:通过监听onClick事件,在listener中执行history.replace/push完成页面跳转。

1.2.2 Router

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
  }
}

1.2.3 Route

这应该是整个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

1.2.3 Switch

这里还用到了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
  }
}

2. Mobx-React中的observer

从代码层面来看, 主要针对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);
  }
};
  • componentDidMount, componentDidUpdate里面只是提供debug相关的report。
  • componentWillMount里做两件事情

    1. 首先会拦截pros/state的get/set, 通过mobx的Atom赋予state, props Observable的能力。
    2. 重写render方法(this.render = initRender)
  • render

    1. 第一次 render 时:

      • 初始化一个 Reaction
      • 在 reaction.track 里执行 baseRender,建立依赖关系
    2. 有数据修改时:

      • 触发 render 的执行 (由于在 reaction.track 里执行,所以会重新建立依赖关系)
  • shouldComponentUpdate类似PureRenderMixin, 只做shadow比对,若数据不发生变化,则不进行重新渲染。

 问题分析

了解了这些背景知识后,我们再来看一下当前这个问题:

首先我们通过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组件外面包一个组件作为app的根组件,在组件上方@inject('appState'),就是相当于给App组件的props传入appState就是store的数据。当url发生了变化,由于使用了router,在组件外部也会包一个,如下代码。在内部,会触发history.listen的回调,这个回调会改变一个state的值,从而触发Router的render,在默认的情况下,会触发子组件重新render,也就是组件。但是,通过了mobx的封装,会对组件的shouldComponentUpdate进行优化,mobx observer的原来和vue类似。在App组件里面,由于props只有appState,shouldeComponentUpdate方法判断了props/state没有改变因此会拦截了后续的渲染,导致url改变组件没有重新渲染。

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

你可能感兴趣的:(react)