NervJS / taro

开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/ QQ 小程序/H5/React Native 等应用。 https://taro.zone/
https://docs.taro.zone/
Other
35.6k stars 4.8k forks source link

[微信小程序]嵌套Component不能正确触发componentWillReceiveProps生命周期 #5231

Closed Aliveing closed 4 years ago

Aliveing commented 4 years ago

问题描述

A组件嵌套了B组件,A组件中把state值当作props传给B,并在A的componentDidMount中设置 A新的state,并在B的componentDidMount中设置自己新的state,B组件的componentWillReceiveProps会触发两次,并且两次nextProps和当前props值相同,作为组件类普遍都在 componentWillReceiveProps事件中比较props值,来运行组件功能代码或者保存新的实例变量数据,这个bug就导致比较props值相同,这种触发机制用着挺难受

复现步骤

talk is cheap show you the code

A 组件

import Taro, { Component } from '@tarojs/taro';
import { View } from '@tarojs/components';
import B from './B';

class A extends Component {
    constructor(props) {
        super(props);
        this.state = {}
    }

    componentDidMount() {
        console.log('--------------class A didMount', this.state.value);
        this.setState({ value: 1 }, () => {
            console.log('--------------class A didMount & after setState', this.state.value);
        });
    }

    render() {
        const { value } = this.state;
        return <View>
            AAAAAAAAAAAAA
            <B value={value} />
        </View>
    }
}
export default A;

B组件

import Taro, { Component } from '@tarojs/taro';
import { View } from '@tarojs/components';

class B extends Component {

    state = { didMount: false }

    componentDidMount() {
        console.log('--------------class B did mount', this.props.value);
        this.setState({ didMount: true }, () => {
            console.log('--------------class B didMount & after setState', this.state.didMount);
        });
    }

    componentWillReceiveProps(nextProps) {
        console.log('--------------class B receive props', nextProps.value, this.props.value, this.state.didMount);
    }

    render() {
        console.log('--------------class B render', this.props.value);
        const { value } = this.props;
        const { didMount } = this.state;
        return <View>{value}{didMount}</View>
    }
}
export default B;

期望行为

被嵌套的组件能正确触发componentWillReceiveProps生命周期

报错信息

生命周期触发错误,没有报错信息

系统信息

👽 Taro v1.3.25 Taro CLI 1.3.25 environment info: System: OS: macOS 10.15.2 Shell: 5.7.1 - /bin/zsh Binaries: Node: 10.16.3 - ~/.nvm/versions/node/v10.16.3/bin/node Yarn: 1.19.2 - ~/.yarn/bin/yarn npm: 6.9.0 - ~/.nvm/versions/node/v10.16.3/bin/npm npmPackages: @tarojs/components: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/plugin-babel: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/plugin-csso: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/plugin-sass: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/plugin-uglifyjs: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/router: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/taro: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/taro-alipay: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/taro-h5: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/taro-qq: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/taro-quickapp: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/taro-swan: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/taro-tt: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/taro-weapp: 2.0.0-beta.13 => 2.0.0-beta.13 @tarojs/webpack-runner: 2.0.0-beta.13 => 2.0.0-beta.13 eslint-config-taro: 2.0.0-beta.13 => 2.0.0-beta.13 eslint-plugin-taro: 2.0.0-beta.13 => 2.0.0-beta.13 nerv-devtools: ^1.5.6 => 1.5.6 nervjs: ^1.5.6 => 1.5.6 stylelint-config-taro-rn: 2.0.0-beta.13 => 2.0.0-beta.13 stylelint-taro-rn: 2.0.0-beta.13 => 2.0.0-beta.13

补充信息

生命周期触发问题,没有等到A组件真正渲染完毕就触发componentDidMount

如果您有功能上的建议,可以提到 FeatHub

使用上的问题,欢迎在「Taro 社区」一起交流

taro-bot[bot] commented 4 years ago

CC @Chen-jj

taro-bot[bot] commented 4 years ago

欢迎提交 Issue~

如果你提交的是 bug 报告,请务必遵循 Issue 模板的规范,尽量用简洁的语言描述你的问题,最好能提供一个稳定简单的复现。🙏🙏🙏

如果你的信息提供过于模糊或不足,或者已经其他 issue 已经存在相关内容,你的 issue 有可能会被关闭。

Good luck and happy coding~

Aliveing commented 4 years ago

暂时的解决办法:在A的 componentDidMount 事件中用setTimeout来设置state。

A组件

componentDidMount() {
setTimeout(() => this.setState({value: 1}), 0);
}
Chen-jj commented 4 years ago

@Aliveing CLI 和依赖版本保持一致

Aliveing commented 4 years ago

@Chen-jj 统一使用1.3.34版本,issue中的问题依然存在。

taro info(更新为CLI与依赖一致)

👽 Taro v1.3.34 Taro CLI 1.3.34 environment info: System: OS: macOS 10.15.2 Shell: 5.7.1 - /bin/zsh Binaries: Node: 10.16.3 - ~/.nvm/versions/node/v10.16.3/bin/node Yarn: 1.19.2 - ~/.yarn/bin/yarn npm: 6.9.0 - ~/.nvm/versions/node/v10.16.3/bin/npm npmPackages: @tarojs/components: 1.3.34 => 1.3.34 @tarojs/plugin-babel: 1.3.34 => 1.3.34 @tarojs/plugin-csso: 1.3.34 => 1.3.34 @tarojs/plugin-sass: 1.3.34 => 1.3.34 @tarojs/plugin-uglifyjs: 1.3.34 => 1.3.34 @tarojs/router: 1.3.34 => 1.3.34 @tarojs/taro: 1.3.34 => 1.3.34 @tarojs/taro-alipay: 1.3.34 => 1.3.34 @tarojs/taro-h5: 1.3.34 => 1.3.34 @tarojs/taro-qq: 1.3.34 => 1.3.34 @tarojs/taro-quickapp: 1.3.34 => 1.3.34 @tarojs/taro-swan: 1.3.34 => 1.3.34 @tarojs/taro-tt: 1.3.34 => 1.3.34 @tarojs/taro-weapp: 1.3.34 => 1.3.34 @tarojs/webpack-runner: 1.3.34 => 1.3.34 eslint-config-taro: 1.3.34 => 1.3.34 eslint-plugin-taro: 1.3.34 => 1.3.34 nerv-devtools: ^1.5.6 => 1.5.6 nervjs: ^1.5.6 => 1.5.6 stylelint-config-taro-rn: 1.3.34 => 1.3.34 stylelint-taro-rn: 1.3.34 => 1.3.34
Chen-jj commented 4 years ago

@Aliveing Page 和 Component 的 componentDidMount 分别对应小程序的 onReady 和 ready。

如果 A 是页面、B 是组件,componentWillReceiveProps 的 nextProps 是会有可能等于 this.props。这是因为页面的 onReady 触发时间有可能在所有页面 ready 前或所有页面 ready 后,onReady 最先触发的情况下就会有问题,这里你只能换种写法。

但如果 A 和 B 都是组件,B ready 肯定比 A 早,因此 componentWillReceiveProps 的 nextProps 没有错。

至于 componentWillReceiveProps 会触发两次,复现不了。

taro-bot[bot] commented 4 years ago

Hello~

您的问题楼上已经有了确切的回答,如果没有更多的问题这个 issue 将在 15 天后被自动关闭。

如果您在这 15 天中更新更多信息自动关闭的流程会自动取消,如有其他问题也可以发起新的 Issue。

Good luck and happy coding~

Aliveing commented 4 years ago

@Chen-jj 我把复现代码更新了一下,让console更详细一些,这个问题核心就在于 B 的 componentWillReceiveProps触发了两次并且这两次nextProps和this.props都相同,并且不是概率性的,而这两次的原因只有两个来源,一个是B didMount的时候更新state,一个是A didMount时更新state导致B的props改变。 为了方便查看以下是运行的结果:

1. --------------class B render undefined   // (B.props.value)
2. --------------class B did mount undefined  // (B.props.value & B setState didMount=true)
3. --------------class A didMount undefined  // (A.state.value & A setState value=1)
4. --------------class B render 1  // (B.props.value)
5. --------------class B receive props 1 1 true  // (nextProps.value B.props.value B.state.didMount)
6. --------------class B render 1  // (B.props.value)
7. --------------class B receive props 1 1 true // (nextProps.value B.props.value B.state.didMount)
8. --------------class B render 1  // (B.props.value)
9. --------------class A didMount & after setState 1  // (A.state.value)
10. --------------class B didMount & after setState true  // (B.state.didMount)

问题就是:

  1. 第5行 B receiveProps 和 第7行 B receiveProps 分别是因为什么触发的?
  2. 为什么在A的componentDidMount中,用 setTimeout 0 设置state的方式就能正确触发componentWillReceiveProps? (经过尝试发现在B的 componentDidMount 中用同样的setTimeout方法也可以正确触发,即 A或者B 中只要其中一个在 componentDidMount 中用 setTimeout 方式更新就能正确触发componentWillReceiveProps事件)
Aliveing commented 4 years ago

@Chen-jj 今天在开发的时候发现 componentWillReceiveProps 生命周期的触发条件真的非常迷,而且传来的数据也是迷到不行,当props改变时有概率不触发,导致写的组件在某些概率下不能根据传来的数据加载值,浪费了整整两天时间。

如果taro在这个生命周期上有问题,应该写在文档里告诉开发尽量不去使用,或者什么情况下才能避免这种情况的发生,而在官方开发文档的 组件生命周期 中只字未提,我个人其实不在意taro因为各平台的原因而导致某些写法不能支持,只不过taro说是可以用react编写不同平台,但是这种常用生命周期的差异性为何不提?为何不在开发文档中提前告知开发者,而是让开发者自己在开发时发现问题,并在github上以为是bug提交后再被贴一个 question 的标签,希望taro能将这种差异性补充到各端开发前注意

终于还是放弃了这个生命周期,用shouldComponentUpdate + componentDidUpdate 来模拟,虽然写的麻烦点,但是没有所谓概率性触发问题。

Chen-jj commented 4 years ago

@Aliveing 还是维持我上述回答,componentWillReceiveProps 并没复现调用多次。你没说 A 是页面还是组件,如果 A 是页面,那不要这样写,小程序 Page 的 onReady 触发时机不稳定。但如果 A 是组件,那完全没有问题。

  1. A 是页面,情况一:

image

  1. A 是页面,情况二:

image

  1. A 是组件:

image

均没有复现 componentWillReceiveProps 调用多次。

Aliveing commented 4 years ago

@Chen-jj 好的,我依次说明下:

  1. A是界面,B是组件,微信开发工具版本(1.02.1911180)
  2. 关于没有两次 componentWillReceiveProps 的问题,我不太清楚你的结果为何只有一次,但是咱们从代码角度分析也应该是两次(以下分析不分顺序,因为我想问的就是触发顺序和传参的正确性)
    • 第一个是A在componentDidMount中更改自己state而导致B的props value变化
    • 第二个是B在componentDidMount中更新自己state

以下是我运行的console截图:

奇怪的B.props

image

正常的B.props

image

Chen-jj commented 4 years ago

@Aliveing 额,setState 怎会触发 componentWillReceiveProps。这里只应触发一次。

A 是页面,componentDidMount 由 onReady 触发,微信小程序页面 onReady 触发时机不稳定。这时你不要强依赖 componentDidMount 的顺序做逻辑便是。

Aliveing commented 4 years ago

@Chen-jj 抱歉是我记错了,去查了查 react 文档,和getDerivedStateFromProps生命周期记混了,不过触发两次componentWillReceiveProps通过复现代码在我这边是100%的概率,我再重新建一个新项目试试。

以下是我当前工程中的package.json文件

package.json 截图 image

Aliveing commented 4 years ago

@Chen-jj 首先感谢一直跟踪回复,我试了试,新建的项目确实没问题,挨个排查以后,十分惊奇的是,taro-listview这个组件会导致两次触发componentWillReceiveProps,只要新建的项目安上了这个组件,即使工程中没有引用taro-listview也会发生这个触发两次的bug。 原因目前不详,猜测是taro-listview的package.json引用了老版本的taro,编译的时候导致taro版本被覆盖了,如果是这样的话,taro有没有什么方法可以获取到当前编译使用的taro版本是什么? 还有就是有个疑问,一个三方组件,为什么可以影响到taro整体的生命周期?

Aliveing commented 4 years ago

重新fork了一下 taro-listview,把其中 package.json 做了以下改动

这样安装完 taro-listview 后 node_modules/taro-listview/node_modules 里就没有老旧版本的taro了,两次触发componentWillReceiveProps的问题也就好了。 但是我还是依然想知道taro在编译三方组件的时候如果版本不统一,为何会出现这种奇怪的问题? @Chen-jj

Garfield550 commented 4 years ago

Understanding the npm dependency model

Aliveing commented 4 years ago

嗯... 我理解npm的依赖处理机制,不过经过测试有意思的来了,如果只在 taro-listview 环境下(taro@1.3.15)就不会出现触发两次的情况,只有在这两个版本(taro@1.3.15 & taro@1.3.34)同时存在时,才会触发两次,可能之前我没表达清楚,重新描述一下问题: 为什么在工程中我引用了一个 taro 的三方组件,而这个组件使用了与当前工程不同的 taro 版本,经过 npm run dev:weapp 编译后会导致工程内组件的生命周期 componentWillReceiveProps 触发两次?

Garfield550 commented 4 years ago

一定程度上是 CLI 的 AST 分析问题。

Aliveing commented 4 years ago

所以现阶段只能先这样,然后等待 taro-cli AST能正确转译?

Aliveing commented 4 years ago

这... 关闭了?CLI 的 AST 分析问题已经解决了?