componentWillUpdate可以直接修改state的值吗?

componentWillUpdate 会在 render 前被触发,它和 componentWillMount 类似,允许你在里面做一些不涉及真实 DOM 操作的准备工作

这个是肯定不可以的,会造成死循环,当然这是显而易见的,但是还有其他原因

为什么呢?

这个方法在react16中被标记为了UNSAFE,与之一样的还有 componentWillMount,componentWillRecieveProps 为啥呢?

究其原因,有如下两点:

  • 这三个钩子经常被错误使用,并且现在出现了更好的替代方案(这里指新增的getDerivedStateFromProps与getSnapshotBeforeUpdate)。
  • React从Legacy模式迁移到Concurrent模式后,这些钩子的表现会和之前不一致

为了让开发者能平稳从旧版本迁移到新版本,React推出了三个模式:

  • legacy模式 – 通过ReactDOM.render创建的应用会开启该模式。这是当前React使用的方式。这个模式可能不支持一些新功能。
  • blocking模式 – 通过 ReactDOM.createBlockingRoot创建的应用会开启该模式。开启部分concurrent模式特性,作为迁移到concurrent模式的第一步。
  • concurrent模式 – 通过ReactDOM.createRoot创建的应用会开启该模式。面向未来的开发模式。

concurrent模式相较我们当前使用的legacy模式最主要的区别是将同步的更新机制重构为异步可中断的更新

updated

在React源码中,每次发起更新都会创建一个Update对象,同一组件的多个Update(会以链表的形式保存在updateQueue中,首先了解下他们的数据结构。

Update有很多字段,当前我们关注如下三个字段

const update: Update<*> = {
  // ...省略当前不需要关注的字段
  lane,
  payload: null,
  next: null
};

  • lane:代表优先级。即图中红色节点与蓝色节点的区别。
  • payload:更新挂载的数据。对于this.setState创建的更新,payload为this.setState的传参。
  • next:与其他Update连接形成链表。

updateQueue结构如下:

const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null,
    },
    // 其他参数省略...
};

  • baseState:更新基于哪个state开始。上图中版本控制的例子中,高优bug修复后提交master,其他commit基于master分支继续开发。这里的master分支就是baseState。
  • firstBaseUpdate与lastBaseUpdate:更新基于哪个Update开始,由firstBaseUpdate开始到lastBaseUpdate结束形成链表。这些Update是在上次更新中由于优先级不够被留下的
  • shared.pending:本次更新的单或多个Update形成的链表。

其中baseUpdate + shared.pending会作为本次更新需要执行的Update

一次更新

在某个组件updateQueue中存在四个Update,其中字母代表该Update要更新的字母,数字代表该Update的优先级,数字越小优先级越高

baseState = '';

A1 - B2 - C1 - D2

首次渲染时,优先级1。B D优先级不够被跳过。

为了保证更新的连贯性,第一个被跳过的Update(B)及其后面所有Update会作为第二次渲染的baseUpdate,无论他们的优先级高低,这里为B C D

baseState: ''
Updates: [A1, C1]
Result state: 'AC'

接着第二次渲染,优先级2。
由于B在第一次渲染时被跳过,所以在他之后的C造成的渲染结果不会体现在第二次渲染的baseState中。所以baseState为A而不是上次渲染的Result state AC。这也是为了保证更新的连贯性。

baseState: 'A'          
Updates: [B2, C1, D2]  
Result state: 'ABCD'

我们发现,C同时出现在两次渲染的Updates中,他代表的状态会被更新两次。

如果有类似的代码:

componentWillReceiveProps(nextProps) {
    if (!this.props.includes('C') && nextProps.includes('C')) {
        // ...do something
    }
}

则很有可能被调用两次,这与同步更新的React表现不一致!

基于以上原因,componentWillXXX被标记为UNSAFE。

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()

从Reactv16开始,componentWillXXX钩子前增加了UNSAFE_前缀。

究其原因,是因为Stack Reconciler重构为Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。

这种行为和Reactv15不一致,所以标记为UNSAFE_。

那为什么getSnapshotBeforeUpdate就可以呢?

那是因为getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以不会遇到多次调用的问题

getDerivedStateFromProps

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容

请注意,不管原因是什么,都会在每次渲染前触发此方法。这与 UNSAFE_componentWillReceiveProps 形成对比,后者仅在父组件重新渲染时触发,而不是在内部调用 setState 时

为什么父组件重新渲染时时触发?

让我们看看源码,这段代码出自updateClassInstance方法

if (
  unresolvedOldProps !== unresolvedNewProps ||
  oldContext !== nextContext
) {
  callComponentWillReceiveProps(
    workInProgress,
    instance,
    newProps,
    nextContext,
  );
}

其中callComponentWillReceiveProps方法会调用componentWillRecieveProps

可以看到,是否调用的关键是比较unresolvedOldProps与 unresolvedNewProps是否全等,以及context是否变化。

其中unresolvedOldProps为组件上次更新时的props,而unresolvedNewProps则来自ClassComponent调用this.render返回的JSX中的props参数。

可见他们的引用是不同的。所以他们全等比较为false

基于此原因,每次父组件更新都会触发当前组件的componentWillRecieveProps,而不是每次props变化后触发

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值