React Native 中使用Mobx实践

4,191 阅读8分钟

一、为什么我们需要使用Mobx?

  • Mobx 是 flux 实现的后起之秀. 以更简单的时候和更少的概念, 让 flux 使用起来变得更简单.
  • 相比 Redux 有mutation, action, dispatch 等概念. Mobx则更加简洁, 更符合对Store 增删改查的操作概念.
  • Mobx是学习成本更低,性能更好的状态解决方案。
  • Mobx代码量少,与TS结合性更好,可以使用更少的代码实现更复杂的页面
  • 使用好Mobx可以极大的降低页面的不必要的重绘次数

二、React Native工程集成Mobx以及TS环境的搭建

(1) 安装Mobx、Mobx-React

 npm i mobx mobx-react

(2)安装TS

npm i typescript

(3)安装TS BaBle解析器

在package.json中输入如下依赖,并重新执行npm install

"devDependencies": {
    "@babel/core": "^7.5.5",
    "@babel/plugin-proposal-decorators": "^7.4.4",
    "@babel/preset-typescript": "^7.3.3",
    "@babel/runtime": "^7.5.5",
    "@types/react-dom": "^16.8.5",
    "@types/react": "^16.9.2",
    "@types/react-native": "^0.60.7",
    "babel-jest": "^24.9.0",
    "babel-plugin-transform-class-properties": "^6.24.1",
    "jest": "^24.9.0",
    "metro-react-native-babel-preset": "^0.56.0",
    "react-test-renderer": "16.8.6"
  },

(4)配置Bable插件

在.babelrc文件中应用以下插件脚本,如没有则在工程根目录新建

{
  "presets": [
    "@babel/preset-typescript",
    [
      "module:metro-react-native-babel-preset"
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ],
    "transform-class-properties"
  ]
}

(5)配置TS校验规则

在tsconfig.json文件中应用以下校验配置,如没有则在工程根目录新建

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "jsx": "preserve",
    "strict": true,
    "noImplicitAny": false,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "esModuleInterop": true,
    "removeComments": false,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "allowJs": true,
    "checkJs": true
  },
  "exclude": [
    "node_modules"
  ],
  "types": [
    "react",
    "react-dom",
    "react-native"
  ]
}

具体字段含义参照www.html.cn/doc/typescr…

三、Mobx中一些常用的概念

(1)observable和autorun

import { observable, autorun } from 'mobx';

const value = observable(0);
const number = observable(100);

autorun(() => {
  console.log(value.get());
});

value.set(1);
value.set(2);
number.set(101);

可以看到,控制台中依次输出0,1,2。 observable可以用来观测一个数据,这个数据可以数字、字符串、数组、对象等类型(相关知识点具体会在后文中详述),而当观测到的数据发生变化的时候,如果变化的值处在autorun中,那么autorun就会自动执行。 上例中的autorun函数中,只对value值进行了操作,而并没有number值的什么事儿,所以number.set(101)这步并不会触发autorun,只有value的变化才触发了autorun。

(2)计算属性——computed

假如现在我们一个数字,但我们对它的值不感兴趣,而只关心这个数组是否为正数。这个时候我们就可以用到computed这个属性了。

const number = observable(10);
const plus = computed(() => number.get() > 0);

autorun(() => {
  console.log(plus.get());
});

number.set(-19);
number.set(-1);
number.set(1);

依次输出了true,false,true。 第一个true是number初始化值的时候,10>0为true没有问题。 第二个false将number改变为-19,输出false,也没有问题。 但是当-19改变为-1的时候,虽然number变了,但是number的改变实际上并没有改变plus的值,所以没有其它地方收到通知,因此也就并没有输出任何值。 直到number重新变为1时才输出true。

实际项目中,computed会被广泛使用到。

(3)action,runInAction

mobx推荐将修改被观测变量的行为放在action中。 来看看以下例子:

import {observable, action} from 'mobx';
class Store {
  @observable number = 0;
  @action add = () => {
    this.number++;
  }
}

const newStore = new Store();
newStore.add();

以上例子使用了ES7的decorator,在实际开发中非常建议用上它,它可以给你带来更多的便捷

接下来说一个重点action只能影响正在运行的函数,而无法影响当前函数调用的异步操作 比如官网中给了如下例子

@action createRandomContact() {
  this.pendingRequestCount++;
  superagent
    .get('https://randomuser.me/api/')
    .set('Accept', 'application/json')
    .end(action("createRandomContact-callback", (error, results) => {
      if (error)
        console.error(error);
      else {
        const data = JSON.parse(results.text).results[0];
        const contact = new Contact(this, data.dob, data.name, data.login.username, data.picture);
        contact.addTag('random-user');
        this.contacts.push(contact);
        this.pendingRequestCount--;
      }
  }));
}

重点关注程序的第六行。在end中触发的回调函数,被action给包裹了,这就很好验证了上面加粗的那句话,action无法影响当前函数调用的异步操作,而这个回调毫无疑问是一个异步操作,所以必须再用一个action来包裹住它,这样程序才不会报错。

如果你使用async function来处理业务,那么我们可以使用runInAction这个API来解决之前的问题。

import {observable, action, useStrict, runInAction} from 'mobx';
useStrict(true);

class Store {
  @observable name = '';
  @action load = async () => {
    const data = await getData();
    runInAction(() => {
      this.name = data.name;
    });
  }
}

四、结合React Native 使用

在React中,我们一般会把和页面相关的数据放到state中,在需要改变这些数据的时候,我们会去用setState这个方法来进行改变。 先设想一个最简单的场景,页面上有个数字0和一个按钮。点击按钮我要让这个数字增加1,就让我们要用Mobx来处理这个试试。

import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import React, {PureComponent} from 'react'
import {observer} from 'mobx-react'
import {action, observable} from 'mobx'


class MyState {
    @observable num = 1

    @action
    public addNum() {
        this.num++
    }
}

const state = new MyState()

interface Props {

}

interface State {
    
}

/**
 * 注释:
 * 时间: 2019/8/21 0021 11:52
 * @author 郭翰林
 */
@observer
export default class App extends PureComponent<Props, State> {

    static propTypes = {}

    constructor(props) {
        super(props)
    }

    render() {
        return (
            <View style={{flex: 1, justifyContent: "space-between", alignItems: "center"}}>
                <View>
                    <Text style={{color: 'red', fontSize: 18, fontWeight: "bold"}}>
                        {state.num}
                    </Text>
                </View>
                <TouchableOpacity
                    style={styles.buttonStyle}
                    onPress={() => {
                        state.addNum()
                    }}>
                    <Text style={{color: '#ffffff', fontSize: 14}}>
                        增加
                    </Text>
                </TouchableOpacity>
            </View>
        )
    }

}

const styles = StyleSheet.create({
    buttonStyle: {
        justifyContent: "center",
        alignItems: "center",
        width: 250,
        borderRadius: 8,
        height: 50,
        marginBottom: 25,
        backgroundColor: '#fc704e'
    }
})

上例中我们使用了一个MyState类,在这个类中定义了一个被观测的num变量和一个action函数addNum来改变这个num值。 之后我们实例化一个对象,叫做newState,之后在我的React组件中,我只需要用@observer修饰一下组件类,便可以愉悦地使用这个newState对象中的值和函数了。

跨组件交互

在不使用其它框架、类库的情况下,React要实现跨组件交互这一功能相对有些繁琐。通常我们需要在父组件上定义一个state和一个修改该state的函数。然后把state和这个函数分别传到两个子组件里,在逻辑简单,且子组件很少的时候可能还好,但当业务复杂起来后,这么写就非常繁琐,且难以维护。而用Mobx就可以很好地解决这个问题。来看看以下的例子:

class MyState {
  @observable num1 = 0;
  @observable num2 = 100;

  @action addNum1 = () => {
    this.num1 ++;
  };
  @action addNum2 = () => {
    this.num2 ++;
  };
  @computed get total() {
    return this.num1 + this.num2;
  }
}

const newState = new MyState();

const AllNum = observer((props) => <div>num1 + num2 = {props.store.total}</div>);

const Main = observer((props) => (
  <div>
    <p>num1 = {props.store.num1}</p>
    <p>num2 = {props.store.num2}</p>
    <div>
      <button onClick={props.store.addNum1}>num1 + 1</button>
      <button onClick={props.store.addNum2}>num2 + 1</button>
    </div>
  </div>
));

@observer
export default class App extends React.Component {

  render() {
    return (
      <div>
        <Main store={newState} />
        <AllNum store={newState} />
      </div>
    );
  }
}

有两个子组件,Main和AllNum (均采用无状态函数的方式声明的组件) 在MyState中存放了这些组件要用到的所有状态和函数。 之后只要在父组件需要的地方实例化一个MyState对象,需要用到数据的子组件,只需要将这个实例化的对象通过props传下去就好了。

那如果组件树比较深怎么办呢? 查看最新Mobx5的@inject属性

关于@observer的一些说明

通常,在和Mobx数据有关联的时候,你需要给你的React组件加上@observer,你不必太担心性能上的问题,加上这个@observer不会对性能产生太大的影响,而且@observer还有一个类似于pure render的功能,甚至能起到性能上的一些优化。

所谓pure render见下例:

@observer
export default class App extends React.Component {
  state = {
    a: 0,
  };
  add = () => {
    this.setState({
      a: this.state.a + 1
    });
  };
  render() {
    return (
      <div>
        {this.state.a}
        <button onClick={this.add}>+1</button>
        <PureItem />
      </div>
    );
  }
}

@observer
class PureItem extends React.Component {

  render() {
    console.log('PureItem的render触发了');
    return (
      <div>你们的事情跟我没关系</div>
    );
  }
}

如果去掉子组件的@observer,按钮每次点击,控制台都会输出 PureItem的render触发了 这句话。

React组件中可以直接添加@observable修饰的变量

@observer
class MyComponent extends React.Component {
  
  state = { a: 0 };

  @observable b = 1;

  render() {
    return(
      <div>
       {this.state.a}
       {this.b}
      </div>
    )
  }
}

在添加@observer后,你的组件会多一个生命周期componentWillReact。当组件内被observable观测的数据改变后,就会触发这个生命周期。 注意setState并不会触发这个生命周期!state中的数据和observable数据并不算是一类。

另外被observable观测数据的修改是同步的,不像setState那样是异步,这点给我们带了很大便利。

五、总结

Mobx想要入门上手可以说非常简单,只需要记住少量概念并可以完成许多基础业务了。但深入学习下去,也还是要接触许多概念的。例如Modifier、Transation等等。 最后与Redux做一个简单的对比

1、Mobx写法上更偏向于OOP 2、对一份数据直接进行修改操作,不需要始终返回一个新的数据 3、对typescript的支持更好一些 4、相关的中间件很少,逻辑层业务整合是一个问题