useEffect(fn, []) 不等于 componentDidMount()

引言

React 从 16.8 版本开始引入了新增特性 Hooks,可以在不编写 Class 的情况下使用 state 以及其他 React 特性。所有 React 使用者都得经历从 Class 组件到 Hooks 模式的过渡期,但是这一时期令我们很容易走进一个误区。

误区:哪个 Hook 的功能 等价于【某个生命周期函数】?

问出这个问题证明我们的思维模式还停留在 ”我需要一个 Hook 来代替 componentDidMount( )“ 的阶段。但是 Hooks 是一种范式转换,从“生命周期和时间”的思维模式转变为“状态和与DOM的同步”的思维模式。如果尝试采用旧的思维模式并找到与其对应的钩子,可能会阻碍我们正确的理解和使用 Hooks,甚至带来一些问题。

这种思维模式会带来下面几个方面的问题:

  • 它们实际上在原理上是不同的,所以如果把它们看作相同的,有可能不会得到期望的结果。
  • 从时间的角度考虑问题,比如“一旦mount就就调用一次useEffect”的思维模式,会阻碍你学习钩子。
  • 从类到钩子的重构并不意味着简单地用useEffect(fn,[]) 替换组件 componentDidMount()。

执行时机不同

componentDidMount在组件挂载之后运行。如果立即(同步)设置 state,那么React就会触发一次额外的render,并将第二个render的响应用作初始UI,这样用户就不会看到闪烁。假设需要使用componentDidMount读取一个DOM元素的宽度,并希望更新state来反映宽度。事件的执行顺序应该是下面这样的:

  1. 首次执行render
  2. 此次 render 的返回值 将用于更新到真正的 Dom 中
  3. componentDidMount 执行而且执行setState
  4. state 变更导致 再次执行 render,而且返回了新的 返回值
  5. 浏览器只显示了第二次 render 的返回值,这样可以避免闪屏

可以理解为上面的过程都是同步执行的,会阻塞到浏览器将真实DOM最终绘制到浏览器上,当我们需要它的时候,这样的工作模式是合理的。但大多数情况下,我们可以在UI Paint 完毕之后,再执行一些异步拉取数据之后setState之类的副作用。

useEffect也是再挂载后运行,但是他更往后,它不会阻塞真实 DOM 的渲染,因为 useEffect 在 Paint(绘制)之后延迟异步运行,这意味着如果需要从 DOM 读取数据,然后同步设置 state 生成新的 UI,有可能会有闪烁的问题发生。React 也提供了同步执行模式的 useLayoutEffect,它更加接近 componentDidMount()表现。

如果想通过同步设置状态来避免闪烁,那么可以使用 useLayoutEffect。但是大部分时间都需要使用 useEffect 比较好。

Props 和 State 的捕获(Capturing)

在 React 应用程序中,会存在许多的异步操作。当多个异步操作执行时,props 和 state 的值可能会有点混乱。

假设我们有很多异步代码操作流程,在执行时需要指定 count 的状态:

class App extends React.Component {
    state = {
        count: 0
    }
componentDidMount() {
    longResolve().then(() => {
      alert(this.state.count);
    });
  }

  render() {
    return (
      <div>
        <button
          onClick={() => {
            this.setState(state => ({ count: state.count + 1 }));
          }}
        >
          Count: {this.state.count}
        </button>
      </div>
    );
  }
};

页面加载完成后,在 longResolve 执行完成之前,假设大概有几秒钟的时间点击按钮几次。如果我在此期间点了 5 次,那么最后 alert 最终显示的也是在最新的值,也就是 5。

同样的场景,我们用 hooks 重构的代码如下:

function App() {
    const [count, setCount] = useState(0)
    useEffect(() => {
        longResolve().then(() => {
          alert(this.state.count);
        });
    }, [])

    return (
        <div>
          <button
            onClick={() => {
              setCount(count + 1);
            }}
          >
            Count: {count}
          </button>
        </div>
      )
};

但是运行后会发现,它的表现和 class 版本有所不同,无论你在 longResove 执行完毕前点击多少次,最后 alert 的 count 都是 0。

造成这种差异的原因是 useEffect 在创建时就已经捕获了 count 的值。当我们把回调函数赋值给 useEffect时,它会存在于内存中,在内存中它只知道 count 在创建时是 0(闭包的原因)。不管经过了多少时间,以及 count 在这个时间内改变多少次,闭包的本质是只跟创建闭包时这个值的状态有关,我们称之为捕获。而在 calss 组件中,componentDidMount()中没有闭包,每次读取的都是当前的 count 的值。

可以用下面的函数来理解,在内存中,useEffect 的回调函数中的 count 在创建时赋予了初始值 0,此时 count 的值不受外界影响。

()=> {
    const count = 0
    longResolve().then(() => {
        alert(count)
    })
}

更多useEffect内容: A Complete Guide to useEffect

捕获(capturing)模式好还是不好

当使用捕获而不是当前值时,其实是可以避免一些错误的。以dan abramov的例子为例,展示了捕获是如何达到预期行为的,而不是之前的类组件使用的每次都用当前值得模式。在这个例子中,我们可以查看不同的人物信息,并且可以点击 follow 关注对应的人。如果我们点击 follow,在接口返回响应前修改查看人物的信息,这时 class 组件返回的关注成功的消息其实是当前最新的,显然是一个 bug,因为我们当前最新切换的资料不是我们点击 follow 对应的资料。所以有了时效性以及增加对应的依赖,反而能让我们把复制的情况更容易理清,而不是一味的只用最新值。
Dan 对此相关的一片文章

到底应该怎么通过 Hooks 重构已经用 class 实现的组件

下面是一段很常见的class 组件代码

class UserInfo extends  React.Component {
    state = { user: null }
    
    componentDidMound() {
        getUser(this.props.uid).then((res) => {
            this.setState({ user: res })
        })
    }
    render() {
        //...
    }
}

很容易就发现一个需要改进的地方,如果 uid 发生改变我们应该怎么办,这种情况通常需要再写一个 componentDidUpdate 来配合处理,其实很容易忘记,而且内部处理的逻辑是一样的,而且都是副作用,代码看起来很冗余。

如果我们提前了解 useEffect 的执行时机以及 对于props的捕获(capturing)特性,之后的思考是更连续,更符合 Hooks 模式的心智模型,就会有下面的重构后的代码:

useEffect (() => {
    let isCurrent = true
    getUser(uid).then((res) => {
        if(isCurrent) setUser(res)
    })
    return () => isCurrent = false
}, [uid])

总结

使用 Hooks 模式进行编程时,我们需要忘记生命周期和时间线的概念,使用以状态为中心,以及对应状态发生变化时。那些副作用需要重新执行的思想来进行编码。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340