前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React Async Rendering

React Async Rendering

作者头像
ayqy贾杰
发布2019-06-12 14:49:22
1.5K0
发布2019-06-12 14:49:22
举报
文章被收录于专栏:黯羽轻扬

写在前面

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

一.渐进迁移计划

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

代码语言:javascript
复制
// 第1阶段 render/reconciliation
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate// 第2阶段 commit
componentDidMount
componentDidUpdate
componentWillUnmount

第1阶段的生命周期函数可能会被多次调用

(引自生命周期hook | 完全理解React Fiber)

一般道德约束render是纯函数,因为明确知道render会被多次调用(数据发生变化时,再render一遍看视图结构变了没,确定是否需要向下检查),而componentWillMountcomponentWillReceivePropscomponentWillUpdate这3个生命周期函数从来没有过这样的道德约束,现有代码中这3个函数可能存在副作用,Async Rendering特性开启后,多次调用势必会出问题

为此,React团队想了个办法,简单地说就是废弃这3个函数

  • 16.3版本:引入带UNSAFE_前缀的3个生命周期函数UNSAFE_componentWillMountUNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate,这个阶段新旧6个函数都能用
  • 16.3+版本:警告componentWillMountcomponentWillReceivePropscomponentWillUpdate即将过时,这个阶段新旧6个函数也都能用,只是旧的在DEV环境会报Warning
  • 17.0版本:正式废弃componentWillMountcomponentWillReceivePropscomponentWillUpdate,这个阶段只有新的带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_前缀生命周期

代码语言:javascript
复制
UNSAFE_componentWillMount()
UNSAFE_componentWillReceiveProps((nextPropsnextProps))
UNSAFE_componentWillUpdate(nextProps, nextState)
// 对比之前的
componentWillMount()
componentWillReceiveProps(nextProps)
componentWillUpdate(nextProps, nextState)

没什么区别,只是改了个名

辅助生命周期

getDerivedStateFromPropsgetSnapshotBeforeUpdatev16.3新引入的生命周期函数,用来辅助解决以前通过componentWillReceivePropscomponentWillUpdate处理的场景

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

getDerivedStateFromProps
代码语言:javascript
复制
static getDerivedStateFromProps(props, state) {
 // ...
 return newState;
}

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

  • 组件实例化完成之后
  • re-render之前(类似于componentWillReceiveProps的时机)

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

getSnapshotBeforeUpdate
代码语言:javascript
复制
getSnapshotBeforeUpdate(prevProps, prevState) {
 // ...
 return snapshot;
}

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

代码语言:javascript
复制
componentDidUpdate(prevProps, prevState, snapshot)

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

三.迁移指南

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

componentWillMount里setState

代码语言:javascript
复制
// 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里发请求

代码语言:javascript
复制
// 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后,就可能会发多个请求,这样解:

代码语言:javascript
复制
// 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里监听外部事件

代码语言:javascript
复制
// 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后可能会造成多次监听,同样存在内存泄漏风险

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

代码语言:javascript
复制
// 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

代码语言:javascript
复制
// 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来说明关联:

代码语言:javascript
复制
// 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里执行回调

代码语言:javascript
复制
// Before
class ExampleComponent extends React.Component {
 componentWillUpdate(nextProps, nextState) {
   if (
     this.state.someStatefulValue !==
     nextState.someStatefulValue
   ) {
     nextProps.onChange(nextState.someStatefulValue);
   }
 }
}

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

代码语言:javascript
复制
// 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里写日志

代码语言:javascript
复制
// 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里发请求

代码语言:javascript
复制
// 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里:

代码语言:javascript
复制
// 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属性

代码语言:javascript
复制
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窗口或者滚动列表(scrollHeightscrollTop发生变化),就会导致DOM更新阶段应用旧值

可以通过getSnapshotBeforeUpdate + componentDidUpdate来解:

代码语言:javascript
复制
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

参考资料

  • Update on Async Rendering
  • Sneak Peek: Beyond React 16:又给看Demo
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-07-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 一.渐进迁移计划
  • 二.新生命周期函数
    • UNSAFE_前缀生命周期
      • 辅助生命周期
        • getDerivedStateFromProps
        • getSnapshotBeforeUpdate
    • 三.迁移指南
      • componentWillMount里setState
        • componentWillMount里发请求
          • componentWillMount里监听外部事件
            • componentWillReceiveProps里setState
              • componentWillUpdate里执行回调
                • componentWillReceiveProps里写日志
                  • componentWillReceiveProps里发请求
                    • componentWillUpdate里取DOM属性
                      • 参考资料
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档