本文是异步渲染之更新博文的总结整理。
在 React 16 使用 Fiber 来对 Virtual DOM 可以进行增量式渲染。由于 Fiber 能使渲染阶段暂停,中止和重新启动。所以一些生命周期可能会被调用多次。如果这些生命周期错误的被使用,可能导致 bug,这篇文章介绍如何替换它们。
如果你对 React 的生命周期还不熟悉,先阅读# React 学习笔记:React 生命周期。
替换过时的生命周期
在异步渲染(Fiber)时,有 3 个过时的声明周期函数,如下所示:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
这些声明周期经常被滥用,在异步渲染时还可能会导致 bug。所以需要替换它们。本文主要介绍替换的方法。
生命周期函数 getDerivedStateFromProps
新的生命周期函数 getDerivedStateFromProps
class Example extends React.Component {
static getDerivedStateFromProps(props, state) {
// ...
}
}
它在组件实例化后或重新渲染之前调用。返回对象更新 state, 或者返回 null 不更新 state
替换 componentWillMount
场景 1 初始化 state
以前写法, 在 componentWillMount 中初始化 state
替换写法, 在 constructor 中初始化 state
场景 2 异步获取数据
以前写法,在 componentWillMount 中异步获取数据
替换写法,在 componentDidMount 中异步获取数据
场景 3 添加订阅
以前写法,在 componentWillMount 中异步添加订阅
替换写法,在 componentDidMount 中异步添加订阅
替换 componentWillReceiveProps
场景 1 比较 props 更新 state
componentWillReceiveProps 写法
// 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,
});
}
}
}
替换为 getDerivedStateFromProps 后的写法
// After
class ExampleComponent extends React.Component {
// 在构造函数中初始化 state,
// 或者使用属性初始化器。
state = {
isScrollingDown: false,
lastRow: null,
};
static getDerivedStateFromProps(props, state) {
if (props.currentRow !== state.lastRow) {
return {
isScrollingDown: props.currentRow > state.lastRow,
lastRow: props.currentRow,
};
}
// 返回 null 表示无需更新 state。
return null;
}
}
场景 2 比较 props 更新副作用
componentWillReceiveProps 写法
// Before
class ExampleComponent extends React.Component {
componentWillReceiveProps(nextProps) {
if (this.props.isVisible !== nextProps.isVisible) {
logVisibleChange(nextProps.isVisible);
}
}
}
替换为 componentDidUpdate 后的写法
// After
class ExampleComponent extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (this.props.isVisible !== prevProps.isVisible) {
logVisibleChange(this.props.isVisible);
}
}
}
场景 3 比较 props 获取外部数据
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) {
// 渲染加载状态 ...
} else {
// 渲染真实 UI ...
}
}
_loadAsyncData(id) {
this._asyncRequest = loadMyAsyncData(id).then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
}
替换为 componentDidUpdate + getDerivedStateFromPorps 后的写法
// After
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
static getDerivedStateFromProps(props, state) {
// 保存 prevId 在 state 中,以便我们在 props 变化时进行对比。
// 清除之前加载的数据(这样我们就不会渲染旧的内容)。
if (props.id !== state.prevId) {
return {
externalData: null,
prevId: props.id,
};
}
// 无需更新 state
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) {
// 渲染加载状态 ...
} else {
// 渲染真实 UI ...
}
}
_loadAsyncData(id) {
this._asyncRequest = loadMyAsyncData(id).then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
}
替换 componentWillUpdate
场景 1 调用外部回调
componentWillUpdate 写法
// Before
class ExampleComponent extends React.Component {
componentWillUpdate(nextProps, nextState) {
if (
this.state.someStatefulValue !==
nextState.someStatefulValue
) {
nextProps.onChange(nextState.someStatefulValue);
}
}
}
替换为 componentDidUpdate 后的写法
// After
class ExampleComponent extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (
this.state.someStatefulValue !==
prevState.someStatefulValue
) {
this.props.onChange(this.state.someStatefulValue);
}
}
}
场景 2 组件更新前读取 DOM
componentWillUpdate 写法
class ScrollingList extends React.Component {
listRef = null;
previousScrollOffset = null;
componentWillUpdate(nextProps, nextState) {
// 我们正在向列表中添加新项吗?
// 捕获滚动位置,以便我们稍后可以调整滚动位置。
if (this.props.list.length < nextProps.list.length) {
this.previousScrollOffset =
this.listRef.scrollHeight - this.listRef.scrollTop;
}
}
componentDidUpdate(prevProps, prevState) {
// 如果我们刚刚添加了新项,并且设置了 previousScrollOffset。
// 调整滚动位置,以便这些新项不会把旧项挤出视图。
if (this.previousScrollOffset !== null) {
this.listRef.scrollTop =
this.listRef.scrollHeight -
this.previousScrollOffset;
this.previousScrollOffset = null;
}
}
render() {
return (
<div ref={this.setListRef}>{/* ...内容... */}</div>
);
}
setListRef = ref => {
this.listRef = ref;
};
}
替换为 getSnapshotBeforeUpdate 后的写法
class ScrollingList extends React.Component {
listRef = null;
getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们正在向列表中添加新项吗?
// 捕获滚动位置,以便我们稍后可以调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
return (
this.listRef.scrollHeight - this.listRef.scrollTop
);
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们刚刚添加了新项,并且有了快照值。
// 调整滚动位置,以便这些新项不会把旧项挤出视图。
// (此处的快照是从 getSnapshotBeforeUpdate 返回的值)
if (snapshot !== null) {
this.listRef.scrollTop =
this.listRef.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.setListRef}>{/* ...内容... */}</div>
);
}
setListRef = ref => {
this.listRef = ref;
};
}
总结
componentWillMount 主要是初始化 state,获取外部数据,订阅。 初始化 state 由 constrctor 替换;获取外部数据,订阅由 componentDidMount 替换
conponentWillReceiveProps 的两个主要作用 ** 更新副作用 ** 和 ** state **。
更新副作用由 componentDidUpdate 替换, state 由 getDerivedStateFromProps 替换.
getDerivedStateFromProps 通过把 props 存在 state,对比 props 和 存的state(旧 props),
实现 conponentWillReceiveProps 对比 props 和 nextProps 的特性
componentWillUpdate 主要是组件更新前,调用外部回调和获取 DOM 更新前调用外部回调可以用 componentDidUpdate 替换;获取更新前 DOM 用 getSnapshotBeforeUpdate 替换。
参考文章
1,异步渲染之更新