写在前面
React放出Fiber(2017/09/26
发布的v16.0.0
带上去的)到现在已经快1年了,到目前(2018/06/13
发布的v16.4.1
)为止,最核心的Async Rendering特性仍然没有开启,那这大半年里React团队都在忙些什么?Fiber计划什么时候正式推出?
一.渐进迁移计划
启用Fiber最大的难题是关键的变动会破坏现有代码,这个breaking change主要来自组件生命周期的变化:
// 第1阶段 render/reconciliation
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
// 第2阶段 commit
componentDidMount
componentDidUpdate
componentWillUnmount
第1阶段的生命周期函数可能会被多次调用
(引自生命周期hook | 完全理解React Fiber)
一般道德约束render
是纯函数,因为明确知道render
会被多次调用(数据发生变化时,再render
一遍看视图结构变了没,确定是否需要向下检查),而componentWillMount
,componentWillReceiveProps
,componentWillUpdate
这3个生命周期函数从来没有过这样的道德约束,现有代码中这3个函数可能存在副作用,Async Rendering特性开启后,多次调用势必会出问题
为此,React团队想了个办法,简单地说就是废弃这3个函数:
16.3
版本:引入带UNSAFE_
前缀的3个生命周期函数UNSAFE_componentWillMount
,UNSAFE_componentWillReceiveProps
和UNSAFE_componentWillUpdate
,这个阶段新旧6个函数都能用16.3+
版本:警告componentWillMount
,componentWillReceiveProps
和componentWillUpdate
即将过时,这个阶段新旧6个函数也都能用,只是旧的在DEV环境会报Warning17.0
版本:正式废弃componentWillMount
,componentWillReceiveProps
和componentWillUpdate
,这个阶段只有新的带UNSAFE_
前缀的3个函数能用,旧的不会再触发
其实就是通过废弃现有API来迫使大家改写老代码,只是给了一个大版本的时间来逐步迁移,果然最后也没提出太好的办法:
We maintain over 50,000 React components at Facebook, and we don’t plan to rewrite them all immediately. We understand that migrations take time. We will take the gradual migration path along with everyone in the React community.
二.新生命周期函数
v16.3
已经开始了迁移准备,推出了3个带UNSAFE_
前缀的生命周期函数和2个辅助生命周期函数
UNSAFE_前缀生命周期
UNSAFE_componentWillMount()
UNSAFE_componentWillReceiveProps(nextPropsnextProps)
UNSAFE_componentWillUpdate(nextProps, nextState)
// 对比之前的
componentWillMount()
componentWillReceiveProps(nextProps)
componentWillUpdate(nextProps, nextState)
没什么区别,只是改了个名
辅助生命周期
getDerivedStateFromProps
和getSnapshotBeforeUpdate
是v16.3
新引入的生命周期函数,用来辅助解决以前通过componentWillReceiveProps
和componentWillUpdate
处理的场景
一方面降低迁移成本,另一方面提供等价的能力(避免出现之前能实现,现在实现不了或不合理的情况)
getDerivedStateFromProps
static getDerivedStateFromProps(props, state) {
// ...
return newState;
}
注意是静态函数,实例无关。用来更新state
,return null
表示不需要更新,调用时机有2个:
组件实例化完成之后
re-render之前(类似于
componentWillReceiveProps
的时机)
配合componentDidUpdate
使用,用来解决之前需要在componentWillReceiveProps
里setState
的场景,比如state
依赖更新前后的props
的场景
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState) {
// ...
return snapshot;
}
这个不是静态函数,调用时机是应用DOM更新之前,返回值会作为第3个参数传递给componentDidUpdate
:
componentDidUpdate(prevProps, prevState, snapshot)
用来解决需要在DOM更新之前保留当前状态的场景,比如滚动条位置。类似的需求之前会通过componentWillUpdate
来实现,现在通过getSnapshotBeforeUpdate + componentDidUpdate
实现
三.迁移指南
除了辅助API外,React官方还提供了一些常见场景的迁移指南
componentWillMount里setState
// Before
class ExampleComponent extends React.Component {
state = {};
componentWillMount() {
this.setState({
currentColor: this.props.defaultColor,
palette: 'rgb',
});
}
}
// After
class ExampleComponent extends React.Component {
state = {
currentColor: this.props.defaultColor,
palette: 'rgb',
};
}
没必要的前置setState
,直接挪出去,没什么好说的
componentWillMount里发请求
// Before
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
componentWillMount() {
this._asyncRequest = asyncLoadData().then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if (this.state.externalData === null) {
// Render loading state ...
} else {
// Render real UI ...
}
}
}
相当常见的场景(SSR下也会出问题,因为用不着externalData
了,没必要发请求),开启Async Rendering后,就可能会发多个请求,这样解:
// After
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
componentDidMount() {
this._asyncRequest = asyncLoadData().then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if (this.state.externalData === null) {
// Render loading state ...
} else {
// Render real UI ...
}
}
}
请求整个挪到componentDidMount
里发就好了,算是实践原则,不要在componentWillUnmount
里发请求,之前是因为对SSR不友好,而现在有2个原因了
注意,如果是为了尽早发请求(或者SSR下希望在render
之前同步获取数据)的话,可以挪到constructor
里做,同样不会多次执行,但大多数情况下(SSR除外,componentDidMount
不触发),componentDidMount
也不慢多少
另外,将来会提供一个suspense
(挂起)API,允许挂起视图渲染,等待异步操作完成,让loading场景更容易控制,具体见Sneak Peek: Beyond React 16演讲视频里的第2个Demo
componentWillMount里监听外部事件
// Before
class ExampleComponent extends React.Component {
componentWillMount() {
this.setState({
subscribedValue: this.props.dataSource.value,
});
// This is not safe; it can leak!
this.props.dataSource.subscribe(
this.handleSubscriptionChange
);
}
componentWillUnmount() {
this.props.dataSource.unsubscribe(
this.handleSubscriptionChange
);
}
handleSubscriptionChange = dataSource => {
this.setState({
subscribedValue: dataSource.value,
});
};
}
在SSR环境还会存在内存泄漏风险,因为componentWillUnmount
不触发。开启Async Rendering后可能会造成多次监听,同样存在内存泄漏风险
这样写是因为一般认为componentWillMount
和componentWillUnmount
是成对儿的,但在Async Rendering环境下不成立,此时能保证的是componentDidMount
和componentWillUnmount
成对儿(从语义上讲就是挂上去的东西总会被删掉,从而有机会清理现场),都不会多调。所以挪到componentDidMount
里监听:
// After
class ExampleComponent extends React.Component {
state = {
subscribedValue: this.props.dataSource.value,
};
componentDidMount() {
// Event listeners are only safe to add after mount,
// So they won't leak if mount is interrupted or errors.
this.props.dataSource.subscribe(
this.handleSubscriptionChange
);
// External values could change between render and mount,
// In some cases it may be important to handle this case.
if (
this.state.subscribedValue !==
this.props.dataSource.value
) {
this.setState({
subscribedValue: this.props.dataSource.value,
});
}
}
componentWillUnmount() {
this.props.dataSource.unsubscribe(
this.handleSubscriptionChange
);
}
handleSubscriptionChange = dataSource => {
this.setState({
subscribedValue: dataSource.value,
});
};
}
这种方式只是低成本简单修改,实际上不推荐,建议要么用Redux/MobX
,要么采用类似于create-subscription的方式,由高阶组件负责打理好一切,具体原理见react/packages/create-subscription/src/createSubscription.js,用法示例见Adding event listeners (or subscriptions)第3块代码
componentWillReceiveProps里setState
// Before
class ExampleComponent extends React.Component {
state = {
isScrollingDown: false,
};
componentWillReceiveProps(nextProps) {
if (this.props.currentRow !== nextProps.currentRow) {
this.setState({
isScrollingDown:
nextProps.currentRow > this.props.currentRow,
});
}
}
}
state
关联props
变化,前面有提到过这种场景,通过getDerivedStateFromProps
来说明关联:
// After
class ExampleComponent extends React.Component {
// Initialize state in constructor,
// Or with a property initializer.
state = {
isScrollingDown: false,
lastRow: null,
};
static getDerivedStateFromProps(props, state) {
if (props.currentRow !== state.lastRow) {
return {
isScrollingDown: props.currentRow > state.lastRow,
lastRow: props.currentRow,
};
}
// Return null to indicate no change to state.
return null;
}
}
注意到一个变化是增加了lastRow
这个state
,因为getDerivedStateFromProps
拿不到prevProps.currentRow
(迁移前的this.props.currentRow
),才通过这种方式来保留上一个状态
绕这么一圈,为什么不直接把prevProps
传进来作为getDerivedStateFromProps
的参数呢?
2个原因:
prevProps
第一次是null
,用的话需要判空,太麻烦了考虑将来版本的内存优化,不需要之前的状态的话,就能及早释放
P.S.旧版本React(v16.3-
)想用getDerivedStateFromProps
的话,需要react-lifecycles-compat polyfill,具体示例见Open source project maintainers
componentWillUpdate里执行回调
// Before
class ExampleComponent extends React.Component {
componentWillUpdate(nextProps, nextState) {
if (
this.state.someStatefulValue !==
nextState.someStatefulValue
) {
nextProps.onChange(nextState.someStatefulValue);
}
}
}
更新时通知外界,比如通知tooltip
重新定位。可以直接挪到componentDidUpdate
:
// After
class ExampleComponent extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (
this.state.someStatefulValue !==
prevState.someStatefulValue
) {
this.props.onChange(this.state.someStatefulValue);
}
}
}
与componentWillUpdate
差不多等价,不会因为时机延后而出现肉眼可见的体验差异:
React ensures that any setState calls that happen during componentDidMount and componentDidUpdate are flushed before the user sees the updated UI.
componentWillReceiveProps里写日志
// Before
class ExampleComponent extends React.Component {
componentWillReceiveProps(nextProps) {
if (this.props.isVisible !== nextProps.isVisible) {
logVisibleChange(nextProps.isVisible);
}
}
}
// After
class ExampleComponent extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (this.props.isVisible !== prevProps.isVisible) {
logVisibleChange(this.props.isVisible);
}
}
}
与上一个场景类似,时机延后一点再记日志,没什么关系,componentDidUpdate
能够保证一次更新过程只触发一次
componentWillReceiveProps里发请求
// Before
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
componentDidMount() {
this._loadAsyncData(this.props.id);
}
componentWillReceiveProps(nextProps) {
if (nextProps.id !== this.props.id) {
this.setState({externalData: null});
this._loadAsyncData(nextProps.id);
}
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if (this.state.externalData === null) {
// Render loading state ...
} else {
// Render real UI ...
}
}
_loadAsyncData(id) {
this._asyncRequest = asyncLoadData(id).then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
}
数据变化时重新请求的场景,同样,可以挪到componentDidUpdate
里:
// After
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
static getDerivedStateFromProps(props, state) {
// Store prevId in state so we can compare when props change.
// Clear out previously-loaded data (so we don't render stale stuff).
if (props.id !== state.prevId) {
return {
externalData: null,
prevId: props.id,
};
}
// No state update necessary
return null;
}
componentDidMount() {
this._loadAsyncData(this.props.id);
}
componentDidUpdate(prevProps, prevState) {
if (this.state.externalData === null) {
this._loadAsyncData(this.props.id);
}
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if (this.state.externalData === null) {
// Render loading state ...
} else {
// Render real UI ...
}
}
_loadAsyncData(id) {
this._asyncRequest = asyncLoadData(id).then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
}
注意,在props
变化时清理旧数据的操作(之前的this.setState({externalData: null})
)被分离到了getDerivedStateFromProps
里,这体现出了新API的等价能力
componentWillUpdate里取DOM属性
class ScrollingList extends React.Component {
listRef = null;
previousScrollOffset = null;
componentWillUpdate(nextProps, nextState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (this.props.list.length < nextProps.list.length) {
this.previousScrollOffset =
this.listRef.scrollHeight - this.listRef.scrollTop;
}
}
componentDidUpdate(prevProps, prevState) {
// If previousScrollOffset is set, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
if (this.previousScrollOffset !== null) {
this.listRef.scrollTop =
this.listRef.scrollHeight -
this.previousScrollOffset;
this.previousScrollOffset = null;
}
}
render() {
return (
<div ref={this.setListRef}>
{/* ...contents... */}
</div>
);
}
setListRef = ref => {
this.listRef = ref;
};
}
希望在更新前后保留滚动条位置,这个场景在Async Rendering下比较特殊,因为componentWillUpdate
属于第1阶段,实际DOM更新在第2阶段,两个阶段之间允许其它任务及用户交互,如果componentWillUpdate
之后,用户resize
窗口或者滚动列表(scrollHeight
和scrollTop
发生变化),就会导致DOM更新阶段应用旧值
可以通过getSnapshotBeforeUpdate + componentDidUpdate
来解:
class ScrollingList extends React.Component {
listRef = null;
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
return (
this.listRef.scrollHeight - this.listRef.scrollTop
);
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
this.listRef.scrollTop =
this.listRef.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.setListRef}>
{/* ...contents... */}
</div>
);
}
setListRef = ref => {
this.listRef = ref;
};
}
getSnapshotBeforeUpdate
是在第2阶段更新实际DOM之前调用,从这里到实际DOM更新之间不会被打断
P.S.同样,v16.3-
需要需要react-lifecycles-compat polyfill,具体示例见Open source project maintainers
P.S.其它没提到的场景后面可能会更新,见Other scenarios