写在前面
React放出Fiber(2017/09/26发布的v16.0.0带上去的)到现在已经快1年了,到目前(2018/06/13发布的v16.4.1)为止,最核心的Async Rendering特性仍然没有开启,那这大半年里React团队都在忙些什么?Fiber计划什么时候正式推出?

一.渐进迁移计划
启用Fiber最大的难题是关键的变动会破坏现有代码,这个breaking change主要来自组件生命周期的变化:

// 第1阶段 render/reconciliationcomponentWillMountcomponentWillReceivePropsshouldComponentUpdatecomponentWillUpdate// 第2阶段 commitcomponentDidMountcomponentDidUpdatecomponentWillUnmount

第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环境会报Warning

17.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处理的场景

一方面降低迁移成本,另一方面提供等价的能力(避免出现之前能实现,现在实现不了或不合理的情况)

getDerivedStateFromPropsstatic getDerivedStateFromProps(props, state) {  // ...  return newState;}

注意是静态函数,实例无关。用来更新state,return null表示不需要更新,调用时机有2个:

组件实例化完成之后

re-render之前(类似于componentWillReceiveProps的时机)

配合componentDidUpdate使用,用来解决之前需要在componentWillReceiveProps里setState的场景,比如state依赖更新前后的props的场景

getSnapshotBeforeUpdategetSnapshotBeforeUpdate(prevProps, prevState) {  // ...  return snapshot;}

这个不是静态函数,调用时机是应用DOM更新之前,返回值会作为第3个参数传递给componentDidUpdate:

componentDidUpdate(prevProps, prevState, snapshot)
用来解决需要在DOM更新之前保留当前状态的场景,比如滚动条位置。类似的需求之前会通过componentWillUpdate来实现,现在通过getSnapshotBeforeUpdate + componentDidUpdate实现

三.迁移指南
除了辅助API外,React官方还提供了一些常见场景的迁移指南

componentWillMount里setState// Beforeclass ExampleComponent extends React.Component {  state = {};  componentWillMount() {    this.setState({      currentColor: this.props.defaultColor,      palette: 'rgb',    });  }}// Afterclass ExampleComponent extends React.Component {  state = {    currentColor: this.props.defaultColor,    palette: 'rgb',  };}

没必要的前置setState,直接挪出去,没什么好说的

componentWillMount里发请求// Beforeclass 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 ...    }  }}

相当常见的场景(***下也会出问题,因为用不着externalData了,没必要发请求),开启Async Rendering后,就可能会发多个请求,这样解:

// Afterclass 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里发请求,之前是因为对***不友好,而现在有2个原因了

注意,如果是为了尽早发请求(或者***下希望在render之前同步获取数据)的话,可以挪到constructor里做,同样不会多次执行,但大多数情况下(***除外,componentDidMount不触发),componentDidMount也不慢多少

另外,将来会提供一个suspense(挂起)API,允许挂起视图渲染,等待异步操作完成,让loading场景更容易控制,具体见Sneak Peek: Beyond React 16演讲视频里的第2个Demo

componentWillMount里监听外部事件// Beforeclass 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,    });  };}

在***环境还会存在内存泄漏风险,因为componentWillUnmount不触发。开启Async Rendering后可能会造成多次监听,同样存在内存泄漏风险

这样写是因为一般认为componentWillMount和componentWillUnmount是成对儿的,但在Async Rendering环境下不成立,此时能保证的是componentDidMount和componentWillUnmount成对儿(从语义上讲就是挂上去的东西总会被删掉,从而有机会清理现场),都不会多调。所以挪到componentDidMount里监听:

// Afterclass 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// Beforeclass 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来说明关联:

// Afterclass 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里执行回调// Beforeclass ExampleComponent extends React.Component {  componentWillUpdate(nextProps, nextState) {    if (      this.state.someStatefulValue !==      nextState.someStatefulValue    ) {      nextProps.onChange(nextState.someStatefulValue);    }  }}

更新时通知外界,比如通知tooltip重新定位。可以直接挪到componentDidUpdate:

// Afterclass 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里写日志// Beforeclass ExampleComponent extends React.Component {  componentWillReceiveProps(nextProps) {    if (this.props.isVisible !== nextProps.isVisible) {      logVisibleChange(nextProps.isVisible);    }  }}// Afterclass ExampleComponent extends React.Component {  componentDidUpdate(prevProps, prevState) {    if (this.props.isVisible !== prevProps.isVisible) {      logVisibleChange(this.props.isVisible);    }  }}

与上一个场景类似,时机延后一点再记日志,没什么关系,componentDidUpdate能够保证一次更新过程只触发一次

componentWillReceiveProps里发请求// Beforeclass 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里:

// Afterclass 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来解:

c```
lass 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 maintainersP.S.其它没提到的场景后面可能会更新,见Other scenarios参考资料Update on Async RenderingSneak Peek: Beyond React 16:又给看Demo

更多相关文章

  1. 面试必问:布隆过滤器的原理以及使用场景
  2. 用户画像分析与场景应用
  3. 设计模式使用场景、优缺点汇总
  4. 聊聊 Redis 使用场景
  5. 聊聊 MongoDB 使用场景
  6. HTTPS 降级***的场景剖析与解决之道
  7. 聊聊Redis使用场景
  8. PHP生成圆心图片-常用作头像圆图等场景
  9. MYSQL连接池应用场景

随机推荐

  1. Android studio图片ERROR: 9-patch image
  2. android手机中图片的拖拉及浏览功能
  3. RelativeLayout
  4. Android Wi-Fi 设置带宽代码流程
  5. inputtype
  6. Android中Dialog对话框
  7. Android自学笔记(番外篇):全面搭建Linux环境
  8. android 瀑布流简单例子
  9. Android总结篇系列:Android 权限
  10. Android WebView