美文网首页
升级异步rendering(Update on Async Re

升级异步rendering(Update on Async Re

作者: 木匠_说说而已 | 来源:发表于2019-01-27 00:51 被阅读0次

    原文地址 March 27, 2018 by Brian Vaughn

    一年多了,React小组致力于实现异步rendering。在上个月的JSConf Iceland上,Dan公布了一些关于异步rendering的令人兴奋的新可能性解锁了
    现在我们想分享在工作中使用这些特性,已经学到的一些经验和方法,以帮助你的组件为异步rendering发布做好准备。

    我们学到的一个最大的经验是,一些旧的组件生命周期会变成不安全的代码实践。它们是:

    • componentWillMount
    • componentWillReceiveProps
    • componentWillUpdate

    这些生命周期方法经常被误解和巧妙地滥用;而且,这些可能的滥用在异步rendering中会更有问题。因此,我们将在最近的版本中为这些生命周期加上“UNSAFE_”前缀。(“unsafe”不是指安全性,而是说使用这些生命周期在将来的React版本中更容易有bugs,尤其是一旦启用了异步rendering)


    渐进的迁移(升级)路线

    React遵循semantic versioning,因此此改变是渐进的。我们目前计划如下:

    • 16.3:介绍不安全生命周期的别名:UNSAFE_componentWillMount, UNSAFE_componentWillReceiveProps 和 UNSAFE_componentWillUpdate。(此版本旧名称和新别名都可以使用)
    • 某个16.x版本:开启弃用警告:componentWillMount, componentWillReceiveProps 和 componentWillUpdate。(此版本旧名称和新别名可以同时使用,但是旧名称会在开发模式打印警告)
    • 17.0:移除componentWillMount,componentWillReceiveProps 和 componentWillUpdate。(这以后,只有新的“UNSAFE_”生命周期名字可以使用)

    注意如果你是个React应用开发者,你目前不需要为旧方法做什么。即将到来的16.3版本的主要目的是,允许开源项目维护人员借助弃用警告提前升级它们的库。这些警告直到将来某个16.x版本才会启用

    我们在Facebook维护超过50000个React组件,并且我们不打算全部立即重写。我们知道升级需要时间。我们将与React社区的成员采用渐进的升级路线。


    迁移旧的生命周期

    如果你想开始使用React 16.3中新的组件APIs(或者你是个维护人员,想提前升级你的库),这里有少许例子希望帮到你,开始以不同的方式思考组件。以后,我们会在文档中继续添加其他“食谱”,展示如何避免使用问题生命周期。

    在进入正题之前,看下16.3版本的生命周期变动概览:

    • 添加以下生命周期别名:UNSAFE_componentWillMount, UNSAFE_componentWillReceiveProps 和 UNSAFE_componentWillUpdate。(旧生命周期和新别名都可使用)
    • 两个新生命周期,静态getDerivedStateFromProps和getSnapshotBeforeUpdate

    新生命周期:getDerivedStateFromProps

    class Example extends React.Component {
      static getDerivedStateFromProps(props, state) {
        // ...
      }
    }
    

    新的静态生命周期getDerivedStateFromProps,在组件实例化后和每次re-rendered之前执行。它可以返回一个对象来更新state,或者返回null表示不需要更新state。

    通过和componentDidUpdate一起使用,这个新生命周期应该涵盖了所有旧的componentWillReceiveProps使用情况。

    Note:
    旧componentWillReceiveProps和新getDerivedStateFromProps方法都增加组件复杂性。这经常引起bugs。请参考derived state简单替代方案,让组件可预见可维护。

    新生命周期:getSnapshotBeforeUpdate

    class Example extends React.Component {
      getSnapshotBeforeUpdate(prevProps, prevState) {
        // ...
      }
    }
    

    新生命周期getSnapshotBeforeUpdate在制造突变之前被调用(例如DOM被更新之前)。此生命周期返回值会作为componentDidUpdate的第三个参数。(这个生命周期不经常使用,但是在某些情况很有用,譬如在rerenders时候手动保存滚动位置)

    通过和componentDidUpdate一起使用,这个新生命周期应该会涵盖所有旧的componentWillUpdate使用情况。
    你可以在这个要点中找到他们的类型签名。

    我们来看看下面的例子中是如何使用这俩生命周期的。


    例子

    • Initializing state
    • Fetching external data
    • Adding event listeners (or subscriptions)
    • Updating state based on props
    • Invoking external callbacks
    • Side effects on props change
    • Fetching external data when props change
    • Reading DOM properties before an update

    Note
    简单起见,下面的例子们使用了实验性质的class,但不用也是一样的迁移策略。

    初始化state

    这个例子显示了一个在componentWillMount中调用setState的组件

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

    对这种组件最简单的重构是将state初始化移动到constructor或属性初始化器,例如:

    // After
    class ExampleComponent extends React.Component {
      state = {
        currentColor: this.props.defaultColor,
        palette: 'rgb',
      };
    }
    
    获取外部数据

    这是一个组件使用componentWillMount来获取外部数据的例子:

    // Before
    class ExampleComponent extends React.Component {
      state = {
        externalData: null,
      };
    
      componentWillMount() {
        this._asyncRequest = loadMyAsyncData().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 ...
        }
      }
    }
    

    上面的例子是有问题的:对服务器rendering(外部数据不会被使用的);对即将到来的异步rendering(请求可能被发起两次)。

    大多数情况下,推荐把获取数据移到componentDidMount。

    // After
    class ExampleComponent extends React.Component {
      state = {
        externalData: null,
      };
    
      componentDidMount() {
        this._asyncRequest = loadMyAsyncData().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 ...
        }
      }
    }
    

    一种常见的误解是在componentWillMount中获取可以避免第一次rendering空state。实际上这观点永远是错的,因为React一直都是在componentWillMount后直接执行render。如果在componentWillMount执行时候没有数据,第一次render将仍然显示loading state,才不会管你在哪发起的获取动作。所以大多数情况,把获取数据动作移到componentDidMount没啥影响。

    Note:
    一些高级用法(譬如Realy之类的库)想更快预获取异步数据。这里有一个如何实现这一点的示例
    长远来看,React组件获取数据的规范方式是基于JSConf Iceland上介绍的“suspense”API。无论是简单的数据获取,还是Apollo和Relay这样的库,都可以在底层使用它。它比上述任何一种解决方案都要简单,但是可能不会在16.3版本中实现。
    如果是服务器端rendering,目前来看提供同步数据是必要的,componentWillMount经常被用于这个目的,但是也可以使用constructor。即将到来的suspense APIs将完全有可能使客户端和服务器端redndering都可以异步获取数据。

    添加事件监听(或订阅)

    这个组件挂载时候订阅了一个外部的事件分发

    // 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,
        });
      };
    }
    

    很不幸的是:服务器端渲染的话,这样可能引起内存泄漏(因为componentWillUnmount永远不会被调用);而且异步rendering也会(因为rendering可能在结束前被打断,导致componentWillUnmount不会被调用)。

    大家经常认为componentWillMount和componentWillUnmount是成对出现,但其实不一定。只有componentDidMount已经调用了,React才保证以后会调用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 16.3一起发布。

    我们可以通过create-subscription传递订阅值,而不是像上面例子那样传递数据源订阅属性。

    import {createSubscription} from 'create-subscription';
    
    const Subscription = createSubscription({
      getCurrentValue(sourceProp) {
        // Return the current value of the subscription (sourceProp).
        return sourceProp.value;
      },
    
      subscribe(sourceProp, callback) {
        function handleSubscriptionChange() {
          callback(sourceProp.value);
        }
    
        // Subscribe (e.g. add an event listener) to the subscription (sourceProp).
        // Call callback(newValue) whenever a subscription changes.
        sourceProp.subscribe(handleSubscriptionChange);
    
        // Return an unsubscribe method.
        return function unsubscribe() {
          sourceProp.unsubscribe(handleSubscriptionChange);
        };
      },
    });
    
    // Rather than passing the subscribable source to our ExampleComponent,
    // We could just pass the subscribed value directly:
    <Subscription source={dataSource}>
      {value => <ExampleComponent subscribedValue={value} />}
    </Subscription>;
    

    注意
    像Realy/Apollo之类的库,应该各自使用了和create-subscription同样的技巧在底层管理订阅(参考这里)。

    基于props更新state

    注意
    旧的componentWillReceiveProps和新的getDerivedStateFromProps方法都会给组件带来复杂性。经常导致bugs。请考虑简单的替代方案,让组件可预见可维护。

    这个组件使用了旧的componentWillReceiveProps生命周期,来基于新props值更新state。

    // 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,
          });
        }
      }
    }
    

    即使上面的代码本身没什么问题,但componentWillReceiveProps生命周期经常被错误使用,带来问题。因此,这个方法将被弃用。

    基于16.3版本,响应props变化更新state的推荐方式是使用:新静态getDerivedStateFromProps生命周期。(这个生命周期在组件创建后和接受新props时候被调用)(译者:还有更新state,forUpdate等时候

    // 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;
      }
    }
    

    你可能注意到上面例子中props.currentRow被映射到state中(state.lastRow)。这允许getDerivedStateFromProps像componentWillReceiveProps一样读取上个props值。

    你可能会想为什么我们不直接把上一个props当成参数传给getDerivedStateFromProps。我们设计这个API时候考虑到了,但是最终决定反对这样,因为两个原因:

    • 一个prevProps参数在第一次getDerivedStateFromProps被调用时候可能是null(实例化后),需要在prevProps存取时候添加if-not-null检查。
    • 不传递上一个props给这个函数是为了将来:在未来的React版本中释放内存。(如果React不需要传递上一个props给生命周期,那么他不需要保存上一个props对象在内存中)

    注意
    如果你在写一个共享组件,react-lifecycles-compat polyfill 允许在旧版React中使用getDerivedStateFromProps生命周期。后面有如何使用

    执行外部回调

    当内部state变化,此组件调用了一个外部函数

    // Before
    class ExampleComponent extends React.Component {
      componentWillUpdate(nextProps, nextState) {
        if (
          this.state.someStatefulValue !==
          nextState.someStatefulValue
        ) {
          nextProps.onChange(nextState.someStatefulValue);
        }
      }
    }
    

    有时候人们把componentWillUpdate用错地方,是因为怕随着componentDidUpdate触发,更新其他组件state“太晚了”。不是这样的。React保证任何一个 componentDidMount 和 componentDidUpdate 中的setState调用,在用户看到更新后的UI前,都会被排放(flushed)。一般来说,避免这样串联升级会更好,但是有些情况是必须的(例如,如果你需要在测量rendered DOM元素后定位一个提示)。

    无论哪种方式,在异步模式下用componentWillUpdate都是不安全的,因为一次更新中外部调用可能会被调用两次。相反,应该使用componentDidUpdate生命周期,因为可以保证一次更新只被调用一次。
    我没看懂这部分,在用户看到更新后的UI前,都会被排放(flushed)是什么意思?一次更新中外部调用可能会被调用两次怎么复现?

    / After
    class ExampleComponent extends React.Component {
      componentDidUpdate(prevProps, prevState) {
        if (
          this.state.someStatefulValue !==
          prevState.someStatefulValue
        ) {
          this.props.onChange(this.state.someStatefulValue);
        }
      }
    }
    
    有副作用的props改变

    类似上面的例子,有时候props改变,会带来副作用。

    // Before
    class ExampleComponent extends React.Component {
      componentWillReceiveProps(nextProps) {
        if (this.props.isVisible !== nextProps.isVisible) {
          logVisibleChange(nextProps.isVisible);
        }
      }
    }
    

    像componentWillUpdate,componentWillReceiveProps可能在一次更新中被调用多次。因此,避免在这两个方法中有副作用很重要。相反,应该使用componentDidUpdate:因为它可以保证一次更新中只被调用一次。
    同上,上面看不懂,这个肯定就不理解了

    // After
    class ExampleComponent extends React.Component {
      componentDidUpdate(prevProps, prevState) {
        if (this.props.isVisible !== prevProps.isVisible) {
          logVisibleChange(this.props.isVisible);
        }
      }
    }
    
    当props更新时候获取外部数据

    此组件在props改变时候获取外部数据

    // 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 = loadMyAsyncData(id).then(
          externalData => {
            this._asyncRequest = null;
            this.setState({externalData});
          }
        );
      }
    }
    

    建议将数据更新移到componentDidUpdate。你也可以使用新的getDerivedStateFromProps生命周期在rendering新props之前清除旧数据。

    // 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 = loadMyAsyncData(id).then(
          externalData => {
            this._asyncRequest = null;
            this.setState({externalData});
          }
        );
      }
    }
    

    注意
    如果你使用一个支持取消动作的HTTP库,例如axios,那么在卸载时候取消一个正在进行的请求很容易。对于原生Promises,你可以使用这个方法

    更新前读取DOM属性

    此组件在更新前读取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;
      };
    }
    

    在上例中,componentWillUpdate用来读取DOM属性。然而异步rendering,可能在“render”阶段生命周期(例如componentWillUpdate 和 render)和“commit”阶段生命周期(例如componentDidUpdate)之间有延迟。如果用户在这期间做了一些操作例如改变窗口,componentWillUpdate中读取的scrollHeight值就过时了。
    不是很理解这部分,没遇到实际场景

    这个问题的解决方案是使用新的“commit”阶段生命周期,getSnapshotBeforeUpdate。这个方法在制造突变前(例如DOM更新前)会被直接调用。它可以返回一个值给React,作为突变后被直接调用的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;
      };
    }
    

    注意
    如果你在写一个共享组件,react-lifecycles-compat polyfill允许旧版React使用新的getSnapshotBeforeUpdate生命周期。后面有个例子

    其他场景

    我们在本文努力覆盖最常见的使用情况,我们认识到我们可能会遗漏一些。如果你用到了componentWillMount, componentWillUpdate, or componentWillReceiveProps的其他情况,并且不确定如何迁移这些旧生命周期,请file a new issue against our documentation提供你的代码和尽可能多的背景信息。当有新的替代模式时候,我们会更新这个文档。

    开源项目维护人员

    开源项目维护人员可能会想这些改动对共享组件意味什么。如果你实现上面的建议,有新静态getDerivedStateFromProps生命周期的组件会发生什么?你是否必须发布一个新主版并且放弃对16.2及更老版本的兼容?

    幸运的是,不需要。

    当React 16.3发布,我们同时发布一个新npm包,react-lifecycles-compat。这个polyfill可以让老版本React使用新 getDerivedStateFromProps 和 getSnapshotBeforeUpdate生命周期(0.14.9+)。

    为了使用这个polyfill,首先添加一个依赖:

    # Yarn
    yarn add react-lifecycles-compat
    
    # NPM
    npm install react-lifecycles-compat --save
    

    下一步,升级你的组件使用新的生命周期(像上面的描述)。

    最后,使用polyfill兼容旧版React。

    import React from 'react';
    import {polyfill} from 'react-lifecycles-compat';
    
    class ExampleComponent extends React.Component {
      static getDerivedStateFromProps(props, state) {
        // Your state update logic here ...
      }
    }
    
    // Polyfill your component to work with older versions of React:
    polyfill(ExampleComponent);
    
    export default ExampleComponent;
    

    相关文章

      网友评论

          本文标题:升级异步rendering(Update on Async Re

          本文链接:https://www.haomeiwen.com/subject/boawjqtx.html