React新生命周期
新生命周期
React在16.4推出了新的生命周期:
挂载
调用顺序: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 | function Component(props, context, updater) { |
static getDerivedStateFromProps(props, state)
此方法无权访问组件实例,只要父组件重新渲染就会触发
适用于state一直依赖于父组件传下来的props,派生state
props:父组件传递下来的props
state:组件的state
返回值:用来更新state的对象
避免派生状态的替代方案:
- 在ComponentDidUpdate中解决props变化的副作用
- prop 更改时重新计算某些数据,memoization【占坑】
- prop 更改时“重置”某些 state,完全受控/使用key使组件完全不受控【占坑】
- 你可能不需要使用派生状态
render
- 唯一一个必须实现的方法,纯函数
- 不应该在这里有交互逻辑
- 如果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
遗留的生命周期,不推荐使用
UNSAFE_componentWillMount()
在安装前调用;
这是唯一一个在SSR中被调用的生命周期
UNSAFE_componentWillReceiveProps(nextProps)
在一个已经安装后的组件接受到新的props是被调用
UNSAFE_componentWillUpdate(nextProps, nextState)
在更新前调用,should之后,did之前
setState的背后
setState的机制是如何?源码层面是怎么实现的?
初始化updater时,真正的updater在renderer中设置,构造函数中设置的this.updater = updater || ReactNoopUpdateQueue
只是占位。
1 | Component.prototype.setState = function(partialState, callback) { |
可以看出,this.setState只是简单调用组件的updater的enqueueSetState
方法,那么…
1 | enqueueSetState(inst, payload, callback) { |
流程:
先获取组件的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
74export 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
71export 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的原理…