Micro View of Allium

Micro View of Allium

Jeango

Jeango 2/3/2021, 3:44:07 PM

State & Lifecycle 组件状态与生命周期

前面解析了 PureComponent 和 Component 类组件的主要差异在 shouldComponentUpdate 方法的实现逻辑上不一样,Component 的实现只是简单返回 true。而 PureComponent 的实现对 props、state 进行浅比较 shallow comparison,如果组件的 props 和 state 都没发生改变就不执行渲染,包括 componentWillUpdate 和 componentDidUpdate 函数都不执行。这样过滤了不必要行为来提高性能,但该组件也具有一定的缺陷,因为它只能进行一层浅比较。简单来说,它只比较 props 和 state 的内存地址,如果内存地址相同,就返回 false 不执行重绘。

理解组件生命周期函数,才能正确管理组件内部的状态数据。实际生产开发更多是使用其内部定义的基本生命周期函数,包括构造函数,componentDidMount 和 componentWillUnmount 生命周期函数。

在旧版本 React 中,无状态组件不支持 ref,因为在 React 调用到无状态组件的方法之前,是没有一个实例化的过程的,因此也就没有所谓的 ref。无状态组件就剩了一个 render 方法,因此也就没有没法实现组件的生命周期方法。

但是 React 16 发布引入了 Hooks API,其中有 useState 和 useReducer 等,这使得函数组件也能封装成有状态组件,所以有状态组件或无状态组件的概念越来越弱化了。而 React 新引入的 Hooks API 使用函数式编程越来越便利。,如 React.memo() 可以实现类似 PureComponent 的函数式组件,函数式组件 FunctionComponent 对象的实现就是基于 props 属性的,但是和类组件的功能越来越相似。

关于 setState() 你应该了解不要直接修改 State,而是应该使用 setState()。

// Wrong
this.state.comment = 'Hello';

// Correct
this.setState({comment: 'Hello'});

状态数据是 Immutable Data,一旦创建就不能再被更改的数据,对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure 持久化数据结构,也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing 结构共享,即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。

React 管理组件属性数据也会使用 Immutable Data,如下面的示例,如果直接将 css 样式变量绑定到组件属性将使 css 封装成只读数据:

let css = {
    display: 'block',
    background: 'yellow',
}

const UseLayoutEffect: React.FC<any> = ({children}) => {
    const box = useRef<HTMLDivElement>(null);
    useLayoutEffect(() => {
        let div = box.current!;
        console.log('useLayoutEffect');
        animate(box.current!.style, 'marginLeft', div.offsetLeft, 100, 'px');
    }, []);
    // css will be immutatble if bind to div.style directly.
    css.background = 'yellow';
    return (
    <div id="useLayout" style={{...css, background: 'yellow'}} ref={box}>{children} </div>
    );
};

构造函数是唯一可以给 this.state 赋值的地方。出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。 State 的更新可能是异步的,this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。

例如,此代码可能会无法更新计数器:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

上面使用了箭头函数,不过使用普通的函数也同样可以:

// Correct
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

State 的更新会被合并,当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state。

例如,你的 state 包含几个独立的变量:

  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      comments: []
    };
  }

然后你可以分别调用 setState() 来单独地更新它们:

  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts
      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments
      });
    });
  }

☛ LifeCycle 函数

整个 React 生命周期分 3 个阶段:创建、更新、卸载,每个阶段有对应的工作和方法。

------------------------ Mounting ----------------  Updating ------------------- Unmounting --
                            |                                                         |
“Render phase”          constructor()   New props   setState()  forceUpdate()         |
Pure and has no             |               |           |             |               |
side effects.           ===============getDerivedStateFromProps==============         |
May be paused,              |                     |                                   |
aborted orrestarted         |             shouldComponentUpdate                       |
by React.                   |                     |                                   |
                        =======================render========================         |
----------------------------|------------------------------|--------------------------|-------
                            |                              |                          |
“Pre-commit phase”          |                     getSnapshotBeforeUpdate             |
Can read the DOM.           |                              |                          |
                        ===============React updates ­D­O­M and refs============         |
----------------------------|------------------------------|--------------------------|--------
                            |                              |                          |
“Commit phase”          componentDidMount        componentDidUpdate        componentWillUnmount

Can work with DOM, run side effects, schedule updates.
-----------------------------------------------------------------------------------------------


生命周期函数                                调用次数                    能否使用setSate()
constructor(props)                        1(实例化时调用)              ✗
getDefaultProps()                        1(creatClass()初始化)      ✗
getInitialState()                        1(creatClass()初始化)      ✗
componentWillMount()                    1(组件加载前)            ✓
getDerivedStateFromProps                >=1                          ✗
render()                                >=1(组件渲染时)              ✗
componentDidMount()                        1(组件加载时)            ✓
componentWillReceiveProps(nextProps)    >=0(props数据更新)        ✓
shouldComponentUpdate(nextProps)        >=0                          ✗
componentWillUpdate(nextProps)            >=0                          ✗
getSnapshotBeforeUpdate                    >=0                          ✗
componentDidUpdate(nextProps)            >=0                          ✗
componentWillUnmount()                    1                          ✗

React 定义的生命周期接口主要由 ComponentLifecycle 和 NewLifecycle、 DeprecatedLifecycle 两个基础接口组件,后两者分别表示新 API 和旧 API 集合,ComponentLifecycle 定义最基本的生命周期 API,这些代码组织方式都在明确告知开发者该如何去用好它们:

interface ComponentLifecycle<P, S, SS = any> extends NewLifecycle<P, S, SS>, DeprecatedLifecycle<P, S> {
    componentDidMount?(): void;
    shouldComponentUpdate?(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean;
    componentWillUnmount?(): void;
    componentDidCatch?(error: Error, errorInfo: ErrorInfo): void;
}

interface NewLifecycle<P, S, SS> {
    getSnapshotBeforeUpdate?(prevProps: Readonly<P>, prevState: Readonly<S>): SS | null;
    componentDidUpdate?(prevProps: Readonly<P>, prevState: Readonly<S>, snapshot?: SS): void;
}

interface StaticLifecycle<P, S> {
    getDerivedStateFromProps?: GetDerivedStateFromProps<P, S>;
    getDerivedStateFromError?: GetDerivedStateFromError<P, S>;
}

type GetDerivedStateFromProps<P, S> =
    (nextProps: Readonly<P>, prevState: S) => Partial<S> | null;
type GetDerivedStateFromError<P, S> =
    (error: any) => Partial<S> | null;

interface DeprecatedLifecycle<P, S> {
    componentWillMount?(): void;
    UNSAFE_componentWillMount?(): void;
    componentWillReceiveProps?(nextProps: Readonly<P>, nextContext: any): void;
    UNSAFE_componentWillReceiveProps?(nextProps: Readonly<P>, nextContext: any): void;
    componentWillUpdate?(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): void;
    UNSAFE_componentWillUpdate?(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): void;
}

这些内容在 ReactDom 的 TypeScript 类型声明文件中有明确的说明,使用带有 UNSAFE 前缀的函数表示开发者明白做什么,React 不会在控制台中给提示信息。如果在 VSCode 或 Sublime 等编辑器中安装好 TypeScript 支持插件,在编写这样方法时会将注解的内容提示出来:

@types\react-dom\node_modules\@types\react\index.d.ts

像 componentWillMount 或者 componentWillReceiveProps 这些过时 API 建议使用 getSnapshotBeforeUpdate 或静态该当 getDerivedStateFromProps 替代。

☛ Lifecycle 第一阶段

这是虚拟 DOM 创建的阶段,会依次执行数个方法,这些方法除了 render 方法,其余基本上在整个生命周期中只调用 1 次,而且一定会调用 1 次:

  • constructor() 执行组件构造函数,初始化实例对象;
  • getDefaultProps() 从此方法获取组件默认属性设置,函数没有参数,对 createClass() 创建的组件有效,否则使用 defaultProps 静态成员设置默认属性参数;
  • getInitialState() 从此方法获取组件的初始状态数据,函数没有参数,对 createClass() 创建的组件有效,否则使用 state 成员设置状态数据;
  • componentWillMount() 过时 API,可用来修改 state。React 先调用父组件的这个函数,再调用子组件的这个函数;
  • render() 开始组件渲染函数,返回一个只有一个根节点的虚拟 DOM。该函数中不能同步的修改组件的状态(state)。
  • componentDidMount() 在 render 渲染之后,通知组件已经加载完成。React 先调用子组件的这个函数,再调用父组件的这个函数。

执行创建阶段的任务后,组件就可以和其他框架交互了,比如设置计时器或者发起网络请求等等耗时的操作都可以在 componentDidMount() 中进行。

DeprecatedLifecycle 接口定义的生命周期函数已经弃用,建议转换到新的 NewLifecycle API。

新 API 的静态函数 getDerivedStateFromProps 会在组件实例化后,渲染之前或者更新渲染之前执行,要求它返回一个对象或来更新 state,或者返回 null 表示新的 props 没有可以可新的状态数据。无论是旧的 componentWillReceiveProps 还是新的 getDerivedStateFromProps,它们都是复杂的函数实现,实际应用中尽量避免使用。

唯一 getDerivedStateFromProps 存在的理解是,组件可以根据组件接收到的属性数据来更新其内部的状态数据,即派生方案 Derived State。不推荐使用派生 state 方案,这会导致代码冗长,并且会使得组件变的难以理解。

注意,前面提到的静态生命周期接口提供了两个函数,一旦在组件中定义了 StaticLifecycle 接口中的任意一个函数,就表示不使用过时的生命周期函数,即 DeprecatedLifecycle 接口的所以函数都不会被调用。一旦使用了 NewLifecycle 接口的函数,就不能使用 UNSAFE 前缀的过时函数。

另外 getSnapshotBeforeUpdate 和 componentDidUpdate 应该一起搭配使用,前者在最近一次的 render 完将要 commit 给 DOM 的时候会调用,这个方法能够使得组件可以在可能更改之前从 DOM 捕获一些信息,比如滚动的位置等等。这个方法返回的任何值,都会传递给 componentDidUpdate,这几个函数的应用可以完全替换掉旧的 componentWillReceiveProps 函数。

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 捕获滚动的位置,以便后面进行滚动 注意返回的值
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 如果有 snapshot 会进行滚动的调整,这样子就不会立即将之前的内容直接弹上去
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

☛ Lifecycle 第二阶段

此时该组件已经进入了稳定运行阶段,必要时执行组件状态更新流程,这个阶段组件可以处理用户交互,或者接收事件更新界面。

以下方法在整个生命周期中可以执行很多次,也可以一次也不执行:

  • componentWillReceiveProps() 过时 API,当父容器中对应的参数改变将会调用子组件的该函数。新的 props 将会作为参数传递进来,旧属性可以根据 this.props 来获取。可以在该函数中对 state 作一些处理,并且在该函数中更新 state 不会发生二次渲染;
  • shouldComponentUpdate() 该函数传递过来两个参数,新的 state 和新的 props,PureComponent 已经实现浅比较,根据比对结果选择更新或者不更新,从而提高效率。
  • componentWillUpdate()componentWillMount() 两个类似的过时 API,都在 render 之前触发,update 方法会接收到新的 props 或者 state,执行时歌词把数据更新到 this.props 和 this.state。
  • componentDidUpdate() 与 componentDidMount 方法类似,在 render 渲染之后被调用,真实 DOM 生成之后调用该函数。传递过来的参数是之前的 props 和 state。

shouldComponentUpdate 方法只是能够用来优化性能,不能过度依赖它来阻止组件的渲染,因为重写 shouldComponentUpdate 可能会导致 bug,不建议进行深层比较,或者是使用 JSON.stringify,因为效率很低,而且很容易引起性能问题,应该优先使用 PureComponent。需要注意的是,即使 shouldComponent 返回了 false,子组件的状态更新引起的重新渲染是不会受到影响的。

☛ Lifecycle 第三阶段

这就是消亡的阶段,主要进行内存的清理和释放的工作。这个阶段只有一个方法,该方法在整个生命周期内调用且仅调用一次。

componentWillUnmount()

当组件要被从界面上移除的时候,ReactDOM.unmountComponentAtNode(),就会调用 componentWillUnmount。在这里进行一些相关的销毁操作,比如撤销定时器,事件监听等等。

另外还有两个用于错误处理的函数 componentDidCatch 和 getDerivedStateFromError,React 引入了 Error Boundaries 错误边界的概念,两个方法配合一起使用,以将错误拦截在边界组件上,而不是破坏整个应用。

以下是各个生命周期函数示范:

import React from 'react';

interface Props {
    record?:string[]
    title?:string
}
interface State {
    record: string[]
}
let getTime = ()=>{
    return new Date().toISOString().substr(11,12)
}

export default class LifeCode extends React.Component<Props> {

    props:Props
    state:State

    static record:string[] = []

    constructor(props: Props){
        super(props);
        this.props = props;
        LifeCode.record.push(getTime()+' Tip: LifeCode constructor');
        this.state = {record: LifeCode.record};
    }

    static getDerivedStateFromProps(props: Props){
        LifeCode.record.push(getTime()+' Tip: getDerivedStateFromProps');
        props = {...props, title: 'getDerivedStateFromProps'};
        return props;
    }
    shouldComponentUpdate(nextProps: Props, nextState: State, nextContext: any): boolean {
        LifeCode.record.push(getTime()+' Tip: shouldComponentUpdate');
        // check props & state ...
        return true;
    }
    // UNSAFE_componentWillMount(){
    //   LifeCode.record.push(getTime()+' Tip: componentWillMount');
    // }
    // UNSAFE_componentWillUpdate(pp:Props, ps:State){
    //   LifeCode.record.push(getTime()+' Tip: componentWillUpdate');
    // }
    getSnapshotBeforeUpdate(pp:Props, ps:State){
        LifeCode.record.push(getTime()+' Tip: getSnapshotBeforeUpdate');
        return {...pp, ...ps}
    }
    componentDidMount(){
        LifeCode.record.push(getTime()+' Tip: componentDidMount');
    }
    componentDidUpdate(){
        LifeCode.record.push(getTime()+' Tip: componentDidUpdate');
        fetch("https://api.leancloud.cn/1.1/classes/Counter")
            .then(response => response.json())
            .then(data => {
                console.log(data);
            })
    }
    componentWillUnmount(){
        let msg = getTime()+' Tip: componentWillUnmount';
        console.log(msg);
        LifeCode.record.push(msg);
    }

    doSetState(ev:React.MouseEvent<HTMLElement, MouseEvent>){
        LifeCode.record.push(getTime()+' Tip: setState');
        this.setState({...this.state, title:'setState'});
    }
    doForceUpdate(ev:React.MouseEvent<HTMLElement, MouseEvent>){
        LifeCode.record.push(getTime()+' Tip: forceUpdate');
        this.forceUpdate();
    }

    render(){
        // readonly props
        // this.props = this.state;
        return(
            <ul>
            <li>
                <button onClick={ev => this.doSetState(ev)}>setState</button>
                <button onClick={ev => this.doForceUpdate(ev)}>forceUpdate</button>
            </li>
            {LifeCode.record.map( (it, th) => <li key={th}>{th} - {it}</li>)}
            </ul>
        );
    }
}

☛ shouldComponentUpdate & Object.is

再来看看源代码,shouldComponentUpdate 的内部主要实现在 react-reconciler 模块,主要是 shallowEqual 函数进行比较操作:

// react-17.0.0\packages\react-reconciler\src\ReactFiberClassComponent.new.js
function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext,
) {
  const instance = workInProgress.stateNode;

  // 执行用户的实现
  if (typeof instance.shouldComponentUpdate === 'function') {

    // if (__DEV__) ....
    const shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );

    // if (__DEV__) ....
    return shouldUpdate;
  }

  // 执行内部的实现
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }

  return true;
}

// react-17.0.0\packages\shared\shallowEqual.js
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import is from './objectIs';

const hasOwnProperty = Object.prototype.hasOwnProperty;

/**
 * Performs equality by iterating through keys on an object and returning false
 * when any key has values which are not strictly equal between the arguments.
 * Returns true when the values of all keys are strictly equal.
 */
function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}

export default shallowEqual;

其中 Object.is 是同一值比较,这对于 JavaScript 这样的动态语言是个问题,比如 1/0 == 1/-0 或者 NaN == NaN 都返回 false。而 "" == false 或者 "" == [] 比较返回非期望的 true,因为有类型转换。

/**
 * inlined Object.is polyfill to avoid requiring consumers ship their own
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
 */
function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

const objectIs: (x: any, y: any) => boolean =
  typeof Object.is === 'function' ? Object.is : is;

export default objectIs;

Object.is 算法要求符合以下条件:

  • both undefined
  • both null
  • both true or both false
  • both strings of the same length with the same characters in the same order
  • both the same object (meaning both values reference the same object in memory)
  • both numbers and
    • both +0
    • both -0
    • both NaN
    • or both non-zero and both not NaN and both have the same value

MDN 给出一个更容易理解的写法:

if (!Object.is) {
  Object.defineProperty(Object, "is", {
    value: function (x, y) {
      // SameValue algorithm
      if (x === y) { // Steps 1-5, 7-10
        // Steps 6.b-6.e: +0 != -0
        return x !== 0 || 1 / x === 1 / y;
      } else {
        // Step 6.a: NaN == NaN
        return x !== x && y !== y;
      }
    }
  });
}