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 ()=>[
    <Route  path="/" render={() => <Redirect to="/list" />} exact key="home" />,
    <Route path="/list" component={TopList} key="toplist" />,
    <Route path="/detail" component={TopDetail} key="topdetail" />,
    <Route path="/test" component={TestApi} key="test" />,
]

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(
            <div className="app">
             
                    <Link to="/">首页</Link>
                    <Link to="/detail">详情页</Link>
             
                <Routes />
            </div>
        )
    }
}

首先我们需要对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中的<a>标签, 利用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的作用是渲染第一个子组件(<Route>, <Redirect>)

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

const root = document.getElementById('root');
const render = Component => {
    const renderMethod = module.hot ? ReactDom.render : ReactDom.hydrate;
    ReactDom.render(
        <AppContainer>
        <Provider appState={new AppState()}>
        <BrowserRouter>
          <Component />  
        </BrowserRouter>
        </Provider>
        </AppContainer>
    ,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(
            <div className="app">
             
                    <Link to="/">首页</Link>
                    <Link to="/detail">详情页</Link>
             
                <Routes />
            </div>
        )
    }
}

export default withRouter(App)

问题解决~

 

参考:https://yq.aliyun.com/articles/147474?t=t1

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React-Mobx是React应用程序和Mobx状态管理库的结合。它帮助开发人员在应用程序中轻松管理和更新状态,同时提高应用程序的性能。 下面是使用React-Mobx的基本步骤: 1. 安装React-Mobx:使用npm安装React-Mobx库。 ``` npm install mobx mobx-react --save ``` 2. 创建一个store:一个store是包含应用程序状态的对象。它可以包含数据和方法,用于更新和管理状态。 ```javascript import { observable, action } from 'mobx'; class CounterStore { @observable count = 0; @action increment() { this.count++; } @action decrement() { this.count--; } } const counterStore = new CounterStore(); export default counterStore; ``` 3. 在React组件中使用store:使用`Provider`组件将store传递给React组件,然后使用`inject`和`observer`高阶组件包装组件。 ```javascript import React from 'react'; import { Provider, inject, observer } from 'mobx-react'; import counterStore from './counterStore'; @inject('counterStore') @observer class Counter extends React.Component { render() { const { counterStore } = this.props; return ( <div> <h1>Count: {counterStore.count}</h1> <button onClick={counterStore.increment}>+</button> <button onClick={counterStore.decrement}>-</button> </div> ); } } const App = () => ( <Provider counterStore={counterStore}> <Counter /> </Provider> ); export default App; ``` 在这个例子中,`inject`高阶组件将store作为props传递给`Counter`组件,`observer`高阶组件将组件转换为响应式组件,使组件能够响应store中状态的更改。 4. 更新状态:使用store中的方法更新状态。 ```javascript import counterStore from './counterStore'; counterStore.increment(); counterStore.decrement(); ``` 这些是使用React-Mobx的基本步骤。使用React-Mobx可以更轻松地管理和更新状态,同时提高应用程序的性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值