一个由mobx observer引发的React Route路由失效问题探究

1. 问题描述

最近一直在使用React + React Router(v4.1.1) + Mobx做项目开发,相比繁琐的React + React Rotuer + Redux方案,爽的不要不要的,当然前提你得忍受Object.defineProperty拦截getter/setter带来的各种黑魔法问题。咳咳,这里不是Mobx大战Redux,就此打住。想了解的人可以去看一下女神Preethi Kasireddy在React Conf 2017上的演讲

最近开发过程中确遇到一个问题,这里跟大家分享一下。

问题页面如上,整个页面利用React Router做路由切换。当用户点击左边菜单栏进行进行路由切换的时候,虽然浏览器地址栏里URL信息已经发生变更, 但是页面并没有进行刷新。路由配置代码如下:

export default function RouterConfig() {
  const homePath = '/home';
  const getComponentRoutes = () => {
    const routeArr = [];
    const pushRoute = path => {
      routeArr.push(<Route key={path} path={path} component={PastyContainer} />);
    };
    for (const item of sideData.common) {
      if (!_.isEmpty(item.children)) {
        for (const childrenItem of item.children) {
          pushRoute(childrenItem.path);
        }
      } else {
        pushRoute(item.path);
      }
    }
    return routeArr;
  };
  return (
    <Router history={history}>
      <TopBar>
        <Switch>
          <Route exact path={homePath} component={Home} />
          <Route path="*">
            <SideBar theme="dark" data={sideData.common}>
              <Switch>
                {getComponentRoutes()}
              </Switch>
            </SideBar>
          </Route>
        </Switch>
      </TopBar>
    </Router>
  );
}

2. React Route v4.0路由原理

想最终问题根源,想来了解一下React Route原理是不可避免的了。

2.1 React Route 的核心依赖History

history is a JavaScript library that lets you easily manage session history anywhere JavaScript runs. history abstracts away the differences in various environments and provides a minimal API that lets you manage the history stack, navigate, confirm navigation, and persist state between sessions.

简而言之,React Route核心就是利用History的replace/push和listen的能力在前端完成路由的切换。这里不做详细介绍,更多关于History的介绍,可以参考其官方文档。

2.2 Link、Router、 Switch、 Route

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

2.2.1 Link

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

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

2.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

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

3. Mobx-React中的observer

The observer function / decorator can be used to turn ReactJS components into reactive components. It wraps the component's render function in mobx.autorun to make sure that any data that is used during the rendering of a component forces a re-rendering upon change.

从代码层面来看, 主要针对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比对,若数据不发生变化,则不进行重新渲染。

4. 问题分析

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

首先我们通过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过程。

5. 问题解决

其实,用户在使用connect, observer这样会重写shouldComponentUpdate或者PureComponent都会遇到相同的问题,React Router Guide针对此问题做了详细描述。总体解法思路:通过传入props绕过shouldComponentUpdate触发render。
对于Router来说,路由的变化会反应在location的变化,所有将location传入props中,会是不错的绕过shouldComponentUpdate触发render的方式。那获取location的方法目前有两种:

  1. Route如果匹配到路由,会注入location到待渲染组件的props中。所以我们可以直接将TopBar封装到Route中:

    const TopBarWithRoute = () => (
    <TopBar>
      <Switch>
        <Route exact path={homePath} component={Home} />
        <Route path="*">
          <SideBar theme="dark" data={sideData.common}>
            <Switch>
              {componentRoutes()}
            </Switch>
          </SideBar>
        </Route>
      </Switch>
    </TopBar>
    );
    return (
    <Router history={history}>
      <Route component={TopBarWithRoute} />
    </Router>
    );
    
  2. React Router提供了一个Hoc组件withRouter,利用此组件可以将location注入到TopBar中:
    const TopBarWithRouter = withRouter(TopBar);
    return (
    <Router history={history}>
      <TopBarWithRouter>
        <Switch>
          <Route exact path={homePath} component={Home} />
          <Route path="*">
            <SideBar theme="dark" data={sideData.common}>
              <Switch>
                {componentRoutes()}
              </Switch>
            </SideBar>
          </Route>
        </Switch>
      </TopBarWithRouter>
    </Router>
    );
    
    

6. 参考文章:

时间: 2024-10-03 00:29:43

一个由mobx observer引发的React Route路由失效问题探究的相关文章

vb net-VB.NET能自己定义一个事件,比如一个变量等于一个特定值时引发一个事件的触发吗?

问题描述 VB.NET能自己定义一个事件,比如一个变量等于一个特定值时引发一个事件的触发吗? VB.NET能自己定义一个事件,比如一个变量等于一个特定值时引发一个事件的触发吗? 比如定义一个变量i 当i=1时触发一个事件 解决方案 自己定义一个方法把i封装起来,要改变i需要通过这个方法修改 比如 public sub modifyI(byval i1 as integer) i = i1 '触发事件 end sub 或者 用定时器的方式 Public class Form1 Public y_c

【翻译】基于 Create React App路由4.0的异步组件加载(Code Splitting)

基于 Create React App路由4.0的异步组件加载 本文章是一个额外的篇章,它可以在你的React app中,帮助加快初始的加载组件时间.当然这个操作不是完全必要的,但如果你好奇的话,请随意跟随这篇文章一起用Create React App和 react路由4.0的异步加载方式来帮助react.js构建大型应用. 代码分割(Code Splitting) 当我们用react.js写我们的单页应用程序时候,这个应用会变得越来越大,一个应用(或者路由页面)可能会引入大量的组件,可是有些组

每天一个linux命令(53):route命令

Linux系统的route命令用于显示和操作IP路由表(show / manipulate the IP routing table).要实现两个不同的子网之间的通信,需要一台连接两个网络的路由器,或者同时位于两个网络的网关来实现.在Linux系统中,设置路由通常是为了解决以下问题:该Linux系统在一个局域网中,局域网中有一个网关,能够让机器访问Internet,那么就需要将这台机器的IP地址设置为Linux机器的默认路由.要注意的是,直接在命令行下执行route命令来添加路由,不会永久保存,

一个微信公众号引发的网络文学变局

中介交易 SEO诊断淘宝客 站长团购 云主机 技术大厅 前言:在今年5月份时候,我曾撰写过一篇<微信5.0商业化抄了盛大文学后路>的文章,其中提到微信可能从模式上颠覆现有文学网站.最近,一个公众账号的出现,让我意识到这事越来越靠谱了.不过与当初预想不同的是,从这个账号的使用体验来看,微信对网络文学产业链的冲击最先发生在运营商身上,其次才是网络文学网站. [搜狐IT消息](文/王聪佶)提起写<盗墓笔记>的南派三叔,大家肯定都听过,他在今年5月份开通了自己的微信公众账号(微信号:pai

一个“灵异”批处理引发的思考加补充说明_DOS/BAT

批处理的要求是:随机显示的数字为(6,7,8,9,10,11,12,14,15,16,17)为其中的一个 注:里面没有13的 下面的两个代码,第一个出错,第二个却成功了,但他们的区别只是第一个(%random%)%%(%n%)+1运算后的值赋予%tn%,而第二个则将运算后的值继续赋予%n%-- 复制代码 代码如下: @echo off  set "string=6 7 8 9 10 11 12 14 15 16 17"  for %%i in (%string%) do call se

IntelliJ Idea中一个编译报错引发的

  package verify; public class Verifier { private String name; public Verifier() { this.name = getClass().getName();//getClass()在ItelliJ idea中会报错: } public static void main(String[] args) { Verifier verifier = new Verifier(); System.out.println("可以正常

React Native中ScrollView性能探究

1 基本使用 ScrollView 是 React Native(后面简称:RN) 中最常见的组件之一.理解 ScrollView 的原理,有利于写出高性能的 RN 应用. ScrollView 的基本使用也非常简单,如下: <ScrollView>      <Child1 />    <Child2 />    ...  </ScrollView>   它和 View 组件一样,可以包含一个或者多个子组件.对子组件的布局可以是垂直或者水平的,通过属性 h

社区营销案例:一个帖子引发的粉丝爆增

中介交易 SEO诊断 淘宝客 云主机 技术大厅 2009年8月14日,新浪微博开始内测.从此,微博的概念开始走进网民的生活.短短的140个字,让越来越多的网网友可以充分利用自己的碎片化时间,来抒发感慨.倾诉烦恼.分享喜乐.记录生活.这一场由明星垂范的微博盛宴,在民间得到了大的普及,成了草根的狂欢."今天你微博了吗?",成了你我见面说的第一句话. 截至2012年2月,新浪微博的用户已经超过3亿人,市场估值近40亿.腾讯凭着庞大的用户基数,全力发展空间.微博等社区化产品,凭借QQ客户端积累

Angular vs React 最全面深入对比

如今,Angular和React这两个JavaScript框架可谓红的发紫,同时针对这两个框架的选择变成了当下最容易被问及或者被架构设计者考虑的问题,本文或许无法告诉你哪个框架更优秀,但尽量从更多的角度去比较两者,尽可能的为你在选择时提供更多的参考意见. 选择的方法 在选择之前,我们尝试带着一些问题去审视你将要选择的框架(或者是任何工具),尝试用这些问题的答案来帮助我们更加了解框架,也更加让选择变得更容易 框架本身的问题: 是否成熟?谁在背后支持呢? 具备的功能? 采用什么架构和模式? 生态系统