Hooks 邂逅 Mobx,代码变得更丝滑了

26,628 阅读7分钟

图怪兽_0c36d56c241a6d1261e186dff2de465e_30262

React 16.8 正式推出 Hooks 至今已经两年多了,有些朋友却一直觉得这是个新技术,对上手使用 Hooks 仍然处于观望状态,即使大多数使用React 技术栈的公司,他们所开发的项目也是多数采用React.Component的形式。

还有些朋友想要使用 React Hooks 来重构升级部分业务,或者封装优化一些通用的业务组件,来提升项目的可扩展性,但是却困于不知如何 在 Hooks 中继续使用Mobx 这一状态管理库了,使用过程中感觉畏手畏脚奇奇怪怪的。

其实吧,Mobx 作为当下炙手可热的状态管理库,很早就推出了 v6 版本,紧跟技术潮流,极大的方便了我们在 Hooks 环境下,更好的对 React 进行状态管理。我想这也是它炙手可热的原因之一吧!

这篇文章主要想深入研究一下,MobxReact Hooks 两者的一个配合使用,可以极大的提高开发体验,学习成本也相对偏低。

如果你对 MobxHooks 感兴趣,想要更进一步了解并使用,那么这边文章非常适合你。

阅读之前请注意:

  • 本文不会介绍太过于基础的内容,你需要对 Mobx 以及 Hooks 有基础的了解;
  • 本文会介绍一些配合应用的最佳实践,方便小伙伴们有一个更加深入的认识。

接下来我们开始学习吧!

Hooks很强大,为何还需要Mobx?

不可否认,Hooks很强大,而且我认为,尤大大的 Vue3 很大程度上也参考了 React Hooks 的实现,虽然两者实现存在差异,但是思想是可以借鉴的。(仅代表个人观点,望各位大佬不想吐槽我)

但是呢,在实际开发过程中,纯粹使用Hooks 的话,还是会遇到一些问题:

  • 依赖传染性 —— 这导致了开发复杂性的提高、可维护性的降低
  • 缓存雪崩 —— 这导致运行性能的降低
  • 异步任务下无法批量更新 —— 这也会导致运行性能的降低

没明白啥意思对不对?很正常!再听我来给你解释。

使用Hooks 编写代码时候,你必须清楚代码中useEffectuseCallback的“依赖项数组”的改变时机。有时候,你的useEffect 依赖某个函数的不可变性,这个函数的不可变性又依赖于另一个函数的不可变性,这样便形成了一条依赖链。一旦这条依赖链的某个节点意外地被改变了,你的 useEffect 就被意外地触发了。

是不是感觉比 传统的React.Component 更伤脑细胞?

为什么说是缓存雪崩呢? 造成这个问题主要是因为 Hooks 函数运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面,这里也可以理解成闭包。每次都会创建闭包数据,从性能角度来讲,此时缓存就是必要的了。而缓存就会牵扯出一堆问题。另外当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的,于是就导致无法批量更新。

其实 Hooks 这些问题都是因为没有一个公共的空间来共享数据导致的,在 Class 组件中,我们有 this , 在 Vue3 中,我们有 setup作用域Hooks 你只能依靠useRef + ref.currenthack 了。但它极其不优雅,丢失了函数编程的味道。

我们是有追求的程序猿,当然不能这样就了事。

这时候你是不是也想到了我们的 Mobx ,它不就是提供统一作用域的神器吗?

这就是Hooks很强大,还是需要Mobx 的原因!

Mobx 为 Hooks 准备的倚天屠龙 API

Hooks 存在的问题,我们刚刚介绍过了,Mobxv6 版本中推出的API 又是如何在保留 Hooks 的强大特性的前提下,帮她搞定这些问题的呢?

我们先介绍一下这两个 API:

useLocalStore

Mobx 推荐使用 useLocalStore 来组织组件的状态。其实它就是在 Hooks 的环境下封装的一个更加方便的 observable。作用就是给它一个函数,函数返回一个需要响应式的对象。

const store = useLocalStore(() => ({key: 'value'}))

它就等价于

const [store] = useState(() => observable({key: 'value'}))

这个 API 看着是不是平平无奇,但是就是它的存在,为Hooks 解决了 依赖传递缓存雪崩 的问题。

它作为一个不变的对象存储数据,可以保证不同时刻对同一个函数的引用保持不变,任意时刻都可以引用到同一个对象或者数据。不再需要手动添加相关的 deps 。 可以避免 useCallbackuseRef 的滥用,同时解决了 Hooks 带来的闭包相关的坑。

useObserver

Mobx 使组件响应数据状态的变化主要有以下三种方式:

  • observer HOC
  • Observer Component
  • useObserver hooks

传统React.Component 中使用 mobx 时候 我们使用 observer HOC 的方式 ,它的主要能力是给类组件提供 pure component 的能力,可以将类组件的 propsstate 转换为 observable 态,从而来响应数据状态的变化。

同样,这种 HOC 形式也可以直接在 Hooks 中正常使用。 但是 Hooks 并不推荐 HOC 的方式。于是乎就出现了 useObserver

import React from 'react';
import { useObserver, useLocalStore } from 'mobx-react';
import {store} from './store';

function Demo1() { 
    const localStore = useLocalStore(() => store);
    return useObserver(() => <div onClick={localStore.setCount}>{localStore.count}</div>)
}

有没有感觉很丝滑,直接将要返回的 Node 使用useObserver 包裹后再 return 就 ok 了。

如此简单的一步就可以使得这个组件成功的监听数据变化了,当数据变化的时候,组件自动 re-render 当前组件。

关于Observer Component 这种方式在最新版本的 Mobx 中,已经变为基于useObserver 来实现了。也可以配合 Hooks 丝滑使用,好像最新版本的 Mobx 更加推荐这种方式。

import React from 'react';
import { Observer, useLocalStore } from 'mobx-react';
import {store } from './store';

// 更新Observer包裹的位置,注意这里包裹的必须是一个函数
function Demo2() { 
    const localStore = useLocalStore(() => store);
    return <Observer>{() => <span>{localStore.count}</span>}</Observer>
}

Hooks + Mobx = 效率

有了以上 两个API 后,我们开发一个组件时候,只需要这么几步:

1、创建store

import { action, observable } from 'mobx';

class Store {
    @observable
    count = 1;
    
    @action
    setCount = () => {
        this.count++;
    }
}
export const store = new Store();

2、注入store,既可以在class中使用,也可以在hooks中使用

// 注入store
import { Provider } from 'mobx-react';
import {store} from './store';

function App () {
  return (
  	<Provider store={store}>
  		<Demo />
		</Provider>
  )
}

3、Demo 使用

  • Class 使用方法
import React, { Component } from 'react';
import { inject, observer } from 'mobx-react';

@inject('scope')
@observer
class Demo1 extends Component { 
    render() {
        return <div>{this.props.count}</div>
    }
}
  • Hooks 使用方法
import React from 'react';
import { useObserver, Observer, useLocalStore } from 'mobx-react';
import {store } from './store';

// 方法1
function Demo1() { 
    const localStore = useLocalStore(() => store);
    return useObserver(() => <div onClick={localStore.setCount}>{localStore.count}</div>)
}

// 方法2
function Demo2() { 
    const localStore = useLocalStore(() => store);
    return <Observer>{() => <span>{localStore.count}</span>}</Observer>
}

通过以下两个例子,可以观摩下, Hooks 配合 Mobx 的那种丝滑感:

mobx 通过两个 API 避免了 useRef 的滥用。

/**
* 实现一个方法,只有当鼠标移动超过多少像素之后,才会触发组件的更新
*/

// props.size 控制移动多少像素才触发回调
function MouseEventListener(props) {
    const [pos, setPos] = useState({x: 0, y: 0})
    const posRef = useRef()
    const propsRef = useRef()
    // 这里需要用 Ref 存储最新的值,保证回调里面用到的一定是最新的值
    posRef.current = pos
    propsRef.current = propsRef

    useEffect(() => {
        const handler = (e) => {
            const newPos = {x: e.xxx, y: e.xxx}
            const oldPos = posRef.current
            const size = propsRef.current.size
            if (
                Math.abs(newPos.x - oldPos.x) >= size
                || Math.abs(newPos.y - oldPos.y) >= size
            ) {
                setPos(newPos)
            }
        }
        // 当组件挂载的时候,注册这个事件
        document.addEventListener('mousemove', handler)
        return () => document.removeEventListener('mousemove', handler)
    }, [])

    return (
        props.children(pos.x, pos.y)
    )
}

// 用 mobx 改写之后,这种使用方式远比原生 hooks 更加符合直觉。
// 不会有任何 ref,任何 current 的使用,任何依赖的变化
function MouseEventListenerMobx(props) {
    const state = useLocalStore(target => ({
        x: 0,
        y: 0,
        handler(e) {
            const nx = e.xxx
            const ny = e.xxx
            if (
                Math.abs(nx - state.x) >= target.size ||
                Math.abs(ny - state.y) >= target.size
            ) {
                state.x = nx
                state.y = ny
            }
        }
    }), props)

    useEffect(() => {
        document.addEventListener('mousemove', state.handler)
        return () => document.removeEventListener('mousemove', state.handler)
    }, [])

    return useObserver(() => props.children(state.x, state.y))
}

针对异步数据的批量更新问题,mobxaction 可以很好的解决这个问题。

// 组件挂载之后,拉取数据并重新渲染。不考虑报错的情况
function AppWithHooks() {
    const [data, setData] = useState({})
    const [loading, setLoading] = useState(true)
    useEffect(async () => {
        const data = await fetchData()
        // 由于在异步回调中,无法触发批量更新,所以会导致 setData 更新一次,setLoading 更新一次
        setData(data)
        setLoading(false)
    }, [])
    return (/* ui */)
}

function AppWithMobx() {
    const store = useLocalStore(() => ({
        data: {},
        loading: true,
    }))
    useEffect(async () => {
        const data = await fetchData()
        runInAction(() => {
            // 这里借助 mobx 的 action,可以很好的做到批量更新,此时组件只会更新一次
            store.data = data
            store.loading = false
        })
    }, [])
    return useObserver(() => (/* ui */))
}

好了,放心的把 Mobx+Hooks 加入到自己的项目中去吧~