React新生命周期

Author Avatar
GeniusFunny 1月 24, 2020
  • 在其它设备中阅读本文章

新生命周期

React在16.4推出了新的生命周期:

image-20191104204301590

挂载

调用顺序:constructor –> static getDerivedStateFromProps –> render –> React更新DOM和refs —> ComponentDidMount

constructor(props)

在constructor中设置state或绑定事件,如果两者都不需要则可忽略此函数。

一旦定义constructor则必须在函数内部调用super(props)why? 通过执行super(props)语句调用Component构造函数来设置props【this.props = props】

设置state时,不能调用setState而是直接为this.state赋值。如果state中有属性依赖于props,见下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
class MyComponent extends Component {
constructor(props) {
super(props);
/*
省略....
*/
}
}
static getDerivedStateFromProps(props, state)

此方法无权访问组件实例,只要父组件重新渲染就会触发

适用于state一直依赖于父组件传下来的props,派生state

props:父组件传递下来的props

state:组件的state

返回值:用来更新state的对象

避免派生状态的替代方案:

  • 在ComponentDidUpdate中解决props变化的副作用
  • prop 更改时重新计算某些数据memoization【占坑】
  • prop 更改时“重置”某些 state,完全受控/使用key使组件完全不受控【占坑】
  • 你可能不需要使用派生状态
render
  1. 唯一一个必须实现的方法,纯函数
  2. 不应该在这里有交互逻辑
  3. 如果shouldComponentUpdate()返回false,则不会调用render()

返回值:

  • React元素。
  • 数组或fragments
  • Portals:可以渲染组件到不同的DOM子树
  • 字符串或数值类型,在DOM中被渲染为文本节点
  • 布尔类型、null、undefiend,被忽略
React更新DOM和refs

更新DOM就是React底层为我们做的,不用开发者去考虑,但是React更新DOM背后的算法很有趣【占坑,React Diff】

componentDidMount

在组件挂载到DOM树中立即被调用

订阅、副作用切入点

  • 调用setState会触发额外的渲染,但是渲染发生在浏览器更新屏幕前;
  • 这就保证即使在一次事件中更新了两次state,用户也不会看见中间的状态,React在更新state时采用了批处理机制,先收集变化再批量更新。
  • 但是,对于modals和tooltip,在渲染赖于其大小或位置的内容之前需要测量DOM节点时,将state的初始化放在compoentDidMount是必要的

更新

顺序:static getDerivedStateFromProps —> shouldComponentUpdate —> render —> getSnapshotBeforeUpdate —> React更新DOM和refs —> componentDidUpdate

何时引起更新
  • 父组件重新渲染
  • 组件调用setState
  • 组件调用forceUpdate
shouldComponentUpdate(nextProps, nextState)

性能优化点

首次渲染和forceUpdate不会调用此函数

根据下一状态的props和state来进行判断是否需要重新渲染,如果返回fasle则组件不会调用后续的方法,默认返回true。

默认情况下,当接受到newState或newProps,这个生命钩子将被调用;但是在Mounting阶段或者调用了forceUpdate时不会被调用

手写可以,PureComponent更优;PureComponent会对state、props进行浅比较

当然,手写也是阔以的,可以比较nextProps与this.props、nextState与this.state然后返回false来跳过这一次的更新。但是返回false并不会阻止子组件重新渲染(如果子组件的state变化)

不要不要不要在这里搞一些耗时的操作,例如JSON.stringify()

以后可能这个钩子返回false只是作为一个不渲染的参考而不是现在的决定

static getSnapshotBeforeUpdate(prevProps, prevState)

在最近一次渲染之前调用,在更改DOM之前获取DOM的一些信息(例如滚动位置)。

返回snapshot的值(或null),返回值作为参数传递给componentDidUpdate()

componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate(prevProps, prevState, snapshot)在更新发生后会立即调用。

可以在这里进行网络请求或者操作DOM

调用setState时一定要将其包裹在一个条件判断中,如果state派生于props这会使得我们丢失当前对应的state

snapshot当且仅当getSnapshotBeforeUpdate实现时存在

卸载

componentWillUnmount

在这个生命周期中做清理操作【清除定时器、取消订阅、取消网络请求】

Error Handling

  • static getDerivedStateFromError

    static getDerivedStateFromError(error)在后代组件抛出错误时调用

    在render过程中被调用,所以副作用不会被提交

  • componentDidCatch

    componentDidCatch(error, info)在后台组件抛出错误时调用

    在commit阶段被调用,所以副作用会被提交;

    用于错误日志之类的

其他

setState

异步、批量处理、state浅合并

setState(updater[,callback]),延迟合并后渲染

setState会导致重新渲染,除非shouldComponentUpdate返回false

setState并不会保证state会立马改变,所以如果当前的state依赖于之前的state,可以通过回调或者componentDidUpdate(更推荐)来操作

updater: (state, props) => stateChange

forceUpdate
  • 如果render依赖于除props、state以外的数据,可以调用forceUpdate强制让组件重新渲染,这会跳过自身组件的shouldComponentUpdate()
  • 这个调用也会触发子组件正常更新的生命周期

一些属性

类属性
  • defaultProps
  • displayName
实例属性
  • props
  • state

遗留的生命周期,不推荐使用

  1. UNSAFE_componentWillMount()

    在安装前调用;

    这是唯一一个在SSR中被调用的生命周期

  2. UNSAFE_componentWillReceiveProps(nextProps)

    在一个已经安装后的组件接受到新的props是被调用

  3. UNSAFE_componentWillUpdate(nextProps, nextState)

    在更新前调用,should之后,did之前

setState的背后

setState的机制是如何?源码层面是怎么实现的?

初始化updater时,真正的updater在renderer中设置,构造函数中设置的this.updater = updater || ReactNoopUpdateQueue只是占位。

1
2
3
4
5
6
7
8
9
10
Component.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

可以看出,this.setState只是简单调用组件的updater的enqueueSetState方法,那么…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);

const update = createUpdate(expirationTime, suspenseConfig);
update.payload = payload;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
update.callback = callback;
}

enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
}

流程:

  • 先获取组件的Fiber实例、当前时间、当前的suspense

  • 计算得到更新组件的expirationTime

    • 复杂,需先了解React-Fiber
  • 根据expirationTime和suspenseConfig创建一个update

  • 将这个update放进update队列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
    // Update queues are created lazily.
    const alternate = fiber.alternate;
    let queue1;
    let queue2;
    if (alternate === null) {
    // There's only one fiber.
    queue1 = fiber.updateQueue;
    queue2 = null;
    if (queue1 === null) {
    queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
    } else {
    // There are two owners.
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {
    if (queue2 === null) {
    // Neither fiber has an update queue. Create new ones.
    queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    queue2 = alternate.updateQueue = createUpdateQueue(
    alternate.memoizedState,
    );
    } else {
    // Only one fiber has an update queue. Clone to create a new one.
    queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
    }
    } else {
    if (queue2 === null) {
    // Only one fiber has an update queue. Clone to create a new one.
    queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
    } else {
    // Both owners have an update queue.
    }
    }
    }
    if (queue2 === null || queue1 === queue2) {
    // There's only a single queue.
    appendUpdateToQueue(queue1, update);
    } else {
    // There are two queues. We need to append the update to both queues,
    // while accounting for the persistent structure of the list — we don't
    // want the same update to be added multiple times.
    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
    // One of the queues is not empty. We must add the update to both queues.
    appendUpdateToQueue(queue1, update);
    appendUpdateToQueue(queue2, update);
    } else {
    // Both queues are non-empty. The last update is the same in both lists,
    // because of structural sharing. So, only append to one of the lists.
    appendUpdateToQueue(queue1, update);
    // But we still need to update the `lastUpdate` pointer of queue2.
    queue2.lastUpdate = update;
    }
    }

    if (__DEV__) {
    if (
    fiber.tag === ClassComponent &&
    (currentlyProcessingQueue === queue1 ||
    (queue2 !== null && currentlyProcessingQueue === queue2)) &&
    !didWarnUpdateInsideUpdate
    ) {
    warningWithoutStack(
    false,
    'An update (setState, replaceState, or forceUpdate) was scheduled ' +
    'from inside an update function. Update functions should be pure, ' +
    'with zero side-effects. Consider using componentDidUpdate or a ' +
    'callback.',
    );
    didWarnUpdateInsideUpdate = true;
    }
    }
    }
  • 根据expirationTime调度工作

  • 了解完React-Fiber后再来填坑【占坑】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    export function scheduleUpdateOnFiber(
    fiber: Fiber,
    expirationTime: ExpirationTime,
    ) {
    checkForNestedUpdates();
    warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);

    const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
    if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
    }

    checkForInterruption(fiber, expirationTime);
    recordScheduleUpdate();

    // TODO: computeExpirationForFiber also reads the priority. Pass the
    // priority as an argument to that function and this one.
    const priorityLevel = getCurrentPriorityLevel();

    if (expirationTime === Sync) {
    if (
    // Check if we're inside unbatchedUpdates
    (executionContext & LegacyUnbatchedContext) !== NoContext &&
    // Check if we're not already rendering
    (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
    // Register pending interactions on the root to avoid losing traced interaction data.
    schedulePendingInteractions(root, expirationTime);

    // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
    // root inside of batchedUpdates should be synchronous, but layout updates
    // should be deferred until the end of the batch.
    performSyncWorkOnRoot(root);
    } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
    if (executionContext === NoContext) {
    // Flush the synchronous work now, unless we're already working or inside
    // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
    // scheduleCallbackForFiber to preserve the ability to schedule a callback
    // without immediately flushing it. We only do this for user-initiated
    // updates, to preserve historical behavior of legacy mode.
    flushSyncCallbackQueue();
    }
    }
    } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
    }

    if (
    (executionContext & DiscreteEventContext) !== NoContext &&
    // Only updates at user-blocking priority or greater are considered
    // discrete, even inside a discrete event.
    (priorityLevel === UserBlockingPriority ||
    priorityLevel === ImmediatePriority)
    ) {
    // This is the result of a discrete event. Track the lowest priority
    // discrete update per root so we can flush them early, if needed.
    if (rootsWithPendingDiscreteUpdates === null) {
    rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
    } else {
    const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
    if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
    rootsWithPendingDiscreteUpdates.set(root, expirationTime);
    }
    }
    }
    }
    export const scheduleWork = scheduleUpdateOnFiber;
为什么不是直接更新而是异步批量更新?

异步批量更新的原因:

  • 批处理更新是有益的,避免不必要的中间状态,减少不必要的DOM渲染次数有益于提高性能。
  • 保持内部一致性,props只会在父组件重新渲染后·才会更新
  • 增加并发update的可行性

与老生命周期的差异?为什么有新生命周期?它解决了什么问题?它存在什么问题?未来会如何发展?

与老生命周期的差异
  • UNSAFE_componentWillReceiveProps只会在父组件重新渲染时触发,而static getDerviedStateFromProps在每次更新都会触发(父组件重新渲染、setState、forceUpdate)
  • 还是涉及到React-Fiber 【占坑】

总结

坑位有点多,后面会补上React-Fiber的原理…