今天和大家聊一聊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次。

image-20200715142534730

假如我们将函数组件中observable对象的引用移除,即如下代码所示

const IndexPage = () => {
  const { setTest } = indexStore;
  useEffect(() => {
    setTest(true);
    console.log('执行effect')
  }, []);

  return (
    <View style='margin:100px'>{`测试页面`}</View>
  );
}

export default observer(IndexPage);

可以看到代码只执行了一次

image-20200715142928066

原因探索

为什么会有这样的现象呢?

我们尝试端点进入看看发生了什么。

taro在微信小程序触发onload事件时,执行函数组件方法。

并且利用setTimeout使effect得以异步执行。

image-20200715143502185

为此,在所有待执行的effect函数都被存储在当前组件节点的effects列表中

image-20200715144854430

我们再看看实际执行effect的方法。

可以看到,这里依次调用effect的方法,并且获取cleanup函数,之后再请客effect数组。

image-20200715145249457

好,到了关键的地方。

由于effects中执行了mobx的action,触发了mobx的reaction。

而该页面又对变化的observable对象进行了监听,从而触发了页面的更新。(注意mobx是同步更新的)

最终在doUpdate方法中再次调用了invokEffects。

image-20200715145750962

而此时上一次的effects.forEach方法并没有执行完成,因此组件的effects队列没有清空。

此时再次进入,就导致了effects执行2次的情况。

image-20200715150310551

###

原因总结及验证

将上述的源码分析用流程图总结如下

image-20200715152247277

根据这个流程图我们可以得到以下推理

推论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所执行的时机无关。

image-20200715152846022

推论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执行。

结果如下。

image-20200715154310499

为什么会这样

解决方案

  • 目前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);

image-20200715155132385


参考文档:


本文会经常更新,请阅读原文: 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 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系