今天和大家聊一聊taro2.x版本useEffect中改变observable对象导致的Effect执行2次问题
问题现象
话不多说,先看代码。
代码很简单,Taro+mobx在函数组件内使用,并且在useEffect中更新test的状态。注意此时useEffect传入了空数组
import Taro, { useEffect } from '@tarojs/taro';
import { View } from '@tarojs/components';
import { observer } from '@tarojs/mobx';
import { indexStore } from './store';
const IndexPage = () => {
const { test, setTest } = indexStore;
console.log('test状态', test)
useEffect(() => {
setTest(true);
console.log('执行effect')
}, []);
return (
<View style='margin:100px'>{`测试页面${test}`}</View>
);
}
export default observer(IndexPage);
import { observable, action } from 'mobx'
class IndexStore {
@observable test: boolean = false
@action
setTest = (value: boolean) => {
this.test = value
}
}
export const indexStore = new IndexStore();
我们看一下此时的输出。
可以看到effect执行了2次。
假如我们将函数组件中observable对象的引用移除,即如下代码所示
const IndexPage = () => {
const { setTest } = indexStore;
useEffect(() => {
setTest(true);
console.log('执行effect')
}, []);
return (
<View style='margin:100px'>{`测试页面`}</View>
);
}
export default observer(IndexPage);
可以看到代码只执行了一次
原因探索
为什么会有这样的现象呢?
我们尝试端点进入看看发生了什么。
taro在微信小程序触发onload事件时,执行函数组件方法。
并且利用setTimeout使effect得以异步执行。
为此,在所有待执行的effect函数都被存储在当前组件节点的effects列表中
我们再看看实际执行effect的方法。
可以看到,这里依次调用effect的方法,并且获取cleanup函数,之后再请客effect数组。
好,到了关键的地方。
由于effects中执行了mobx的action,触发了mobx的reaction。
而该页面又对变化的observable对象进行了监听,从而触发了页面的更新。(注意mobx是同步更新的)
最终在doUpdate方法中再次调用了invokEffects。
而此时上一次的effects.forEach方法并没有执行完成,因此组件的effects队列没有清空。
此时再次进入,就导致了effects执行2次的情况。
###
原因总结及验证
将上述的源码分析用流程图总结如下
根据这个流程图我们可以得到以下推理
推论1:只要在effects中更改了页面监听的mobx对象,就会导致2次执行
我们修改下demo,添加一个test2对象,让effects在test1变化时才执行
const IndexPage = () => {
const { test1, test2, setTest1, setTest2 } = indexStore;
console.log('test1状态', test1)
console.log('test2状态', test2)
useEffect(() => {
console.log('执行effect')
if (test1) {
setTest2(true)
}
}, [test1]);
return (
<View style='margin:100px' onClick={() => {
console.log('点击页面');
setTest1(true);
}}>{`测试页面${test2}`}</View>
);
}
export default observer(IndexPage);
class IndexStore {
@observable test1: boolean = false
@action
setTest1 = (value: boolean) => {
this.test1 = value
}
@observable test2: boolean = false
@action
setTest2 = (value: boolean) => {
this.test2 = value
}
}
export const indexStore = new IndexStore();
我们看到点击后effects果然执行了2次。
说明这个和effects所执行的时机无关。
推论2:只要在effects中更改了页面监听的mobx对象,当前可执行的所有effects都会执行2次
推论3:effects执行顺序会变化,首先先于action的执行2次,接着后于action的执行2次
还是对第一个demo进行修改,这次我们不改变store,而只添加2个effects。
const IndexPage = () => {
const { test, setTest } = indexStore;
console.log('test状态', test)
useEffect(() => {
console.log('执行effect1')
}, []);
useEffect(() => {
console.log('执行effect2,并且变更test状态')
setTest(true)
}, []);
useEffect(() => {
console.log('执行effect3')
}, []);
return (
<View style='margin:100px' onClick={() => setTest(true)}>{`测试页面${test}`}</View>
);
}
export default observer(IndexPage);
由于effects2执行时改变了test状态,因此重新触发invokeEffects之后,完整执行所有的effects,接着再回到第一次调用的invokeEffects方法,将剩余的effects执行。
结果如下。
为什么会这样
- 首先这个一定是taro的bug,而且github上也承认了Taro Hooks useEffect 第二个参数设置为
[]
执行一次的语法实际上执行两次 · Issue #4493 · NervJS/taro - 第二,猜测reaction触发invokeEffects的目的是为了再下一次刷新组件之前,将异步中为执行的effects都执行完成,避免状态错误。
解决方案
- 目前taro官方宣传在taro3中已经修复该问题。(未进行证实,有兴趣的小伙伴可以试试)
- 在taro2.x的情况下,函数组件中useEffect尽量不要(同步)改变mobx状态。(对,意思就是异步处理)
对于我们的demo,可以使用这样的方法。
可以看到Effects执行了一次。
const IndexPage = () => {
const { test, setTest } = indexStore;
console.log('test状态', test)
useEffect(() => {
console.log('执行effect2,并且异步变更test状态')
setTimeout(() => {
setTest(true)
}, 0);
}, []);
return (
<View style='margin:100px' onClick={() => setTest(true)}>{`测试页面${test}`}</View>
);
}
export default observer(IndexPage);
参考文档:
本文会经常更新,请阅读原文: https://xinyuehtx.github.io/post/%E5%85%B3%E4%BA%8Etaro2.x%E7%89%88%E6%9C%ACuseEffect%E4%B8%AD%E6%94%B9%E5%8F%98observable%E5%AF%B9%E8%B1%A1%E5%AF%BC%E8%87%B4%E7%9A%84Effect%E6%89%A7%E8%A1%8C2%E6%AC%A1%E9%97%AE%E9%A2%98.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名黄腾霄(包含链接: https://xinyuehtx.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 。