本文主要讲解了下我平时在工做开发中遇到的关于 Hooks 的一些缺点和问题,并尝试配合 Mobx 解决这些问题的经历。我以为二者的配合能够极大的下降开发过程当中有可能出现的问题以及极大的提升开发体验,并且学习成本也是很是的低。若是你对 Hooks 以及 Mobx 有兴趣,想知道更进一步的了解,那么这篇文章适合你。这篇文章会介绍以下内容,方便你决定是否要仔细阅读,节省时间:html
另外 Hooks 自己真的就是一个理解上很是简单的东西,因此本文也不长,我也不喜欢去写什么万字长文,又不是写教程,并且读者看着标题就失去兴趣了。前端
首先,在这里我再也不说 Hooks 的优势,由于他的优势用过的人都清楚是怎么回事,这里主要讲解一下他存在的缺点,以及如何用 Mobx 来进行改进。node
换句话说,形成这种缘由主要是由于 Hooks 每次都会建立一个全新的闭包,而闭包内全部的变量其实都是全新的。而每次都会建立闭包数据,而从性能角度来说,此时缓存就是必要的了。而缓存又会牵扯出一堆问题。react
说到底,也就是说没有一个公共的空间来共享数据,这个在 Class 组件中,就是 this,在 Vue3 中,那就是 setup 做用域。而 Hooks 中,除非你愿意写 useRef
+ ref.current
不然是没有办法找到共享做用域。设计模式
而 mobx 和 Hooks 的结合,能够很方便在 Hooks 下提供一个统一的做用域来解决上面遇到的问题,所谓双剑合并,剑走天下。api
在传统的使用 mobx 的过程当中,你们应该都知道 observer
这个 api,对须要可以响应式的组件用这个包裹一下。一样,这个 api 直接在 hooks 中依旧能够正常使用。 可是 hooks 并不推荐 hoc 的方式。天然,mobx 也提供了 hookify 的使用方式,那就是 useObserver
。缓存
const store = observable({})
function App() {
return useObserver(() => {
return <div>{store.count}</div>
})
}
复制代码
看到这里,相信使用过 mobx 的应该能够发现,useObserver 的使用几乎和 Class 组件的 render 函数的使用方式一致。事实上也确实如此,并且他的使用规则也很简单,直接把须要返回的 Node 用该 hooks 包裹后再返回就能够了。闭包
通过这样处理的组件,就能够成功监听数据的变化,当数据变化的时候,会触发组件的重渲染。至此,第一个 api 就了解完毕了框架
简单来说,就是在 Hooks 的环境下封装的一个更加方便的 observable。就是给他一个函数,该函数返回一个须要响应式的对象。能够简单的这样理解异步
const store = useLocalStore(() => ({key: 'value'}))
// equal
const [store] = useState(() => obserable({key: 'value'}))
复制代码
而后就没有了,极其简单的一个 api 使用。然后面要讲的一些最佳实践更多的也是围绕这个展开,后文简化使用 local store 代指。
简单来说,就是在保留 Hooks 的特性的状况下,解决上面 hooks 所带来的问题。
第一点,因为 local store 的存在,做为一个不变的对象存储数据,咱们就能够保证不一样时刻对同一个函数的引用保持不变,不一样时刻都能引用到同一个对象或者数据。再也不须要手动添加相关的 deps。由此能够避免 useCallback 和 useRef 的过分使用,也避免不少 hooks 所面临的的闭包的坑(老手请自动忽略)。依赖传递性和缓存雪崩的问题均可以获得解决
直接上代码,主要关注注释部分
// 要实现一个方法,只有当鼠标移动超过多少像素以后,才会触发组件的更新
// 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)
// 固然这里也能够监听 [pos.x, pos.y],可是性能很差
}, [])
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))
}
复制代码
第二,就是针对异步数据的批量更新问题,mobx 的 action 能够很好的解决这个问题
// 组件挂载以后,拉取数据并从新渲染。不考虑报错的状况
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 */))
}
复制代码
不过也有人会说,这种状况下用 useReducer
不就行了么?确实,针对这个例子是能够的,可是每每业务中会出现不少复杂状况,好比你在异步回调中要更新本地 store 以及全局 store,那么就算是 useReducer
也要分别调用两次 dispatch ,一样会触发两次渲染。而 mobx 的 action 就不会出现这样的问题。// 若是你强行 ReactDOM.unstable_batchedUpdates
我就不说啥了,勇士受我一拜
知道了上面的两个 api,就能够开始愉快的使用起来了,只不过这里给你们一下小 tips,帮助你们更好的理解、更好的使用这两个 api。(不想用并且也不敢用「最佳实践」这个词,感受太绝对,这里面有一些我本身也没有打磨好,只能算是 tips 来帮助你们拓展思路了)
对于 store 内的函数要获取 store 的数据,一般咱们会使用 this 获取。好比
const store = useLocalStore(() => ({
count: 0,
add() {
this.add++
}
}))
const { add } = store
add() // boom
复制代码
这种方式通常状况下使用彻底没有问题,可是 this 依赖 caller,并且没法很好的使用解构语法,因此这里并不推荐使用 this,而是采用一种 no this
的准则。直接引用自身的变量名
const store = useLocalStore(() => ({
count: 0,
add() {
store.count++
}
}))
const { add } = store
add() // correct,不会致使 this 错误
复制代码
在某些状况下,咱们的 local store 可能须要获取 props 上的一些数据,而经过 source 能够很方便的把 props 也转换成 observable 的对象。
function App(props) {
const store = useLocalStore(source => ({
doSomething() {
// source 这里是响应式的,当外界 props 发生变化的时候,target 也会发生变化
if (source.count) {}
// 若是这里直接用 props,因为闭包的特性,这里的 props 并不会发生任何变化
// 而 props 每次都是不一样的对象,而 source 每次都是同一个对象引用
// if (props.count) {}
}
// 经过第二个参数,就能够完成这样的功能
}), props)
// return Node
}
复制代码
固然,这里不只仅能够用于转换 props,能够将不少非 observable 的数据转化成 observable 的,最多见的好比 Context、State 之类,好比
const context = useContext(SomeContext)
const [count, setCount] = useState(0)
const store = useLocalStore(source => ({
getCount() {
return source.count * context.multi
}
}), {...props, ...context, count})
复制代码
有的时候,默认的 observable 的策略可能会有一些性能问题,好比为了避免但愿针对一些大对象所有响应式。能够经过返回自定义的 observable 来实现。
const store = useLocalStore(() => observable({
hugeObject: {},
hugeArray: [],
}, {
hugeObject: observable.ref,
hugeArray: observable.shallow,
}))
复制代码
甚至你以为自定义程度不够的话,能够直接返回一个自定义的 store
const store = useLocalStore(() => new ComponentStore())
复制代码
默认的使用方式下,最方便高效的类型定义就是经过实例推导,而不是经过泛型。这种方式既能兼顾开发效率也能兼顾代码可读性和可维护性。固然了,你想用泛型也是能够的啦
// 使用这种方式,直接经过对象字面量推导出类型
const store = useLocalStore(() => ({
todos: [] as Todo[],
}))
// 固然你能够经过泛型定义,只要你不以为烦就行
const store = useLocalStore<{
todos: Todo[]
}>(() => ({todos: []}))
复制代码
可是这个仅仅建议用做 local store 的时候,也就是相关的数据是在本组件内使用。若是自定义 Hooks 话,建议仍是使用预约义类型而后泛型的方式,能够提供更好的灵活性。
当使用 useObserver api 以后,就意味着失去了 observer 装饰器默认支持的浅比较 props 跳过渲染的能力了,而此时须要咱们本身手动配合 memo 来作这部分的优化
另外,memo 的性能远比 observer 的性能要高,由于 memo 并非一个简单的 hoc
export default memo(function App(){
const xxx = useLocalStore(() => ({}))
return useObserver(() => {
return (<div/>)
})
})
复制代码
上面的这几个 Hooks 均可以经过 useLocalStore 代替,内置 Hooks 对 Mobx 来讲是毫无必要。并且这几个内置 api 的使用也会致使缓存的问题,建议作以下迁移
常用 useEffect 知道他有一个功能就是监听依赖变化的能力,换句话说就是能够当作 watcher 使用,而 mobx 也有本身的监听变化的能力,那就是 reaction,那么究竟使用哪一种方式更好呢?
这边推荐的是,两个都用,哈哈哈,没想到吧。
useEffect(() =>
reaction(() => store.count, () => console.log('changed'))
, [])
复制代码
说正经的,针对非响应式的数据使用 useEffect,而响应式数据优先使用 reaction。固然若是你全程抛弃原生 hooks,那么只用 reaction 也能够的。
逻辑拆分和组合,是 Hooks 很大的一个优点,在 mobx 加持的时候,这个有点依旧能够保持。甚至在还更加简单。
function useCustomHooks() {
// 推荐使用全局 Store 的规则来约束自定义 Hooks
const store = useLocalStore(() => ({
count: 0,
setCount(count) {
store.count = count
}
}))
return store
}
function App() {
// 此时这个 store 你能够从两个角度来思考
// 第一,他是一个 local store,也就是每个都会初始化一个新的
// 第二,他能够做为全局 store 的 local 化,也就是你能够将它按照全局 store 的方式来使用
const store = useCustomHook()
return (
// ui
)
}
复制代码
Mobx 自己就提供了做为全局 Store 的能力,这里只说一下和 Hooks 配合的使用姿式
当升级到 mobx-react@6 以后,正式开始支持 hooks,也就是你能够简单的经过这种方式来使用
export function App() {
return (
<Provider sa={saStore} sb={sbStore}> <Todo/> </Provider>
)
}
export function Todo() {
const {sa, sb} = useContext(MobxProviderContext)
return (
<div>{sa.foo} {sb.bar}</div>
)
}
复制代码
这句话怎么理解数据共享和组件通信呢?举个例子
曾经关注过 Hooks 的发展,发现不少人在 Hooks 诞生的时候开始尝试用 Context + useReducer 来替换掉 Redux,我以为这是对 Context 的某种曲解。
缘由就是 Context 的更新问题,若是做为全局 Store,那么必定要在根组件上挂载,而 Context 检查是否发生变化是经过直接比较引用,那么就会形成任意一个组件发生了变化,都会致使从 Provider 开始的整个组件树发生从新渲染的状况。
function App() {
const [state, dispatch] = useReducer(reducer, init)
return (
// 每次当子组件调用 dispatch 以后,会致使 state 发生变化,从而致使 Provider 的 value 变化
// 进而让全部的子组件触发刷新
<GlobalContext.Provider value={{...state, dispatch}}>
{/* child node */}
</GlobalContext.Provider>
)
}
复制代码
而若是你想避免这些问题,那就要再度封装一层,这和直接使用 Redux 也就没啥区别了。
主要是 Context 的更新是一个性能消耗比较大的操做,当 Provider 检测到变化的时候,会遍历整颗 Fiber 树,比较检查每个 Consumer 是否要更新。
专业的事情交给专业的来作,使用 Redux Mobx 能够很好的避免这个问题的出现。
知道 Redux 的应该清楚他是如何定义一个 Store 吧,官方其实已经给出了比较好的最佳实践,但在生产环境中,使用起来依旧不少问题和麻烦的地方。因而就诞生了不少基于 Redux 二次封装的库,基本都自称简化了相关的 API 的使用和概念,可是这些库其实大大增长了复杂性,引入了什么 namespace/modal 啥的,我也记不清了,反正看到这些就自动劝退了,不喜欢在已经很麻烦的东西上为了简化而作的更加麻烦。
而 Mobx 这边,官方也有了一个很好的最佳实践。我以为是颇有道理,并且是很是易懂易理解的。
但仍是那个问题,官方在有些地方仍是没有进行太多的约束,而在开发中也遇到了相似的问题,因此这里在基于官方的框架下有几点意见和建议:
是的,基本上这样就能够写好一个 Store 了,没有什么花里胡哨的概念,也没有什么乱七八糟的工具,约定俗成就足以。我向来推崇没有规则就是最大的规则,没有约束就是最大的约束。不少东西能约定俗成就约定俗成,落到纸面上就足够了。彻底不必作一堆 lint/tools/library 去约束,既增长了前期开发成本,又增长了后期维护成本,就问问你司内部有多少 dead 的工具和库?
俗话说的话,「秦人不暇自哀然后人哀之,后人哀之而不鉴之,亦使后人而复哀后人也」,这就是现状(一巴掌打醒)
不过以上的前提是要求大家的开发团队有足够的开发能力,不然新手不少或者同步约定成本高的话,搞个库去约束到也不是不行(滑稽脸)
说了这么多,也不是说是万能的,有这个几个缺点
useObserver
方法。Mobx 在我司的项目中已经使用了好久了,但 Hooks 也是刚刚使用没多久,但愿这个能给你们帮助。也欢迎你们把遇到的问题一块儿说出来,你们一块儿找解决办法。
我始终以为基于 Mutable 的开发方式永远是易于理解、上手难度最低的方式,而 Immutable 的开发方式是易维护、比较稳定的方式。这二者不必非此即彼,而 Mobx + React 能够认为很好的将二者整合在一块儿,在须要性能的地方能够采用 Immutable 的方式,而在不须要性能的地方,能够用 Mutable 的方式快速开发。
固然了,你就算不用 Mobx 也彻底没有问题,毕竟原生的 Hooks 的坑踩多了以后,习惯了也没啥问题,一些小项目,我也会只用原生 Hooks 的(防杠声明)。