乐趣区

关于javascript:理解ReactFiber架构和新旧生命周期

➣ React Fiber 原理


React 架构

  • 1)Virtual DOM 层,形容页面长什么样
  • 2)Reconciler 层,负责调用组件生命周期办法,进行 Diff 运算等
  • 3)Renderer 层,依据不同的平台,渲染出相应的页面,如 ReactDOM 和 ReactNative

React15 遗留问题

  • 1)浏览器的整体渲染是多线程的,包含 GUI 渲染线程、JS 引擎线程、事件触发线程、定时触发器线程和异步 http 申请线程。页面绘制和 JS 运算是互斥的线程,两者不能同时进行。
  • 2)React15 应用 JS 的函数调用栈 (Stack Reconciler) 递归渲染界面,因而在解决 DOM 元素过多的简单页面的频繁更新时,大量同步进行的工作 (树 diff 和页面 render) 会导致界面更新阻塞、事件响应提早、动画卡顿等,因而 React 团队在 16 版本重写了 React Reconciler 架构。

React16 问题解决

  • 1)Fiber Reconciler架构能够容许同步阻塞的工作拆分成多个小工作,每个工作占用一小段时间片,工作执行实现后判断有无闲暇工夫,有则继续执行下一个工作,否则将控制权交由浏览器以让浏览器去解决更高优先级的工作,等下次拿到工夫片后,其它子工作继续执行。整个流程相似 CPU 调度逻辑,底层是应用了浏览器 APIrequestIdleCallback
  • 2)为了实现整个 Diff 和 Render 的流程可中断和复原,单纯的 VirtualDom Tree 不再满足需要,React16 引入了采纳单链表构造的 Fiber 树,如下图所示。
  • 3)FiberReconciler 架构将更新流程划分成了两个阶段:1.diff(由多个 diff 工作组成,工作工夫片耗费完后被可被中断,中断后由 requestIdleCallback 再次唤醒) => 2.commit(diff 结束后拿到 fiber tree 更新后果触发 DOM 渲染,不可被中断)。右边灰色局部的树即为一颗 fiber 树,左边的 workInProgress 为两头态,它是在 diff 过程中自顶向下构建的树形构造,可用于断点复原,所有工作单元都更新实现之后,生成的 workInProgress 树会成为新的 fiber tree。
  • 4)fiber tree 中每个节点即一个工作单元,跟之前的 VirtualDom 树相似,示意一个虚构 DOM 节点。workInProgress tree 的每个 fiber node 都保留着 diff 过程中产生的 effect list,它用来寄存 diff 后果,并且底层的树节点会顺次向下层 merge effect list,以收集所有 diff 后果。留神的是如果某些节点并未更新,workInProgress tree 会间接复用原 fiber tree 的节点(链表操作),而有数据更新的节点会被打上 tag 标签。
<FiberNode> : {
    stateNode,    // 节点实例
    child,        // 子节点
    sibling,      // 兄弟节点
    return,       // 父节点
}

➣ React 新旧生命周期


React16.3 之前的生命周期

1.componentWillMount()
此生命周期函数会在在组件挂载之前被调用,整个生命周期中只被触发一次。开发者通常用来进行一些数据的预申请操作,以缩小申请发动工夫,倡议的代替计划是思考放入 constructor 构造函数中,或者 componentDidMount 后;另一种状况是在在应用了内部状态治理库时,如 Mobx,能够用于重置 Mobx Store 中的的已保留数据,代替计划是应用生命周期 componentWilUnmount 在组件卸载时主动执行数据清理。

2.componentDidMount()
此生命周期函数在组件被挂载之后被调用,整个生命周期中只触发一次。开发者同样能够用来进行一些数据申请的操作;除此之外也可用于增加事件订阅(须要在 componentWillUnmount 中勾销事件订阅);因为函数触发时 dom 元素曾经渲染结束,第三种应用状况是解决一些界面更新的副作用,比方应用默认数据来初始化一个 echarts 组件,而后在 componentDidUpdate 后进行 echarts 组件的数据更新。

3.componentWillReceiveProps(nextProps, nexState)
此生命周期产生在组件挂载之后的组件更新阶段。最常见于在一个依赖于 prop 属性进行组件外部 state 更新的非齐全受控组件中,非齐全受控组件即组件外部保护 state 更新,同时又在某个非凡条件下会采纳内部传入的 props 来更新外部 state,留神不要间接将 props 齐全复制到 state,否则应该应用齐全受控组件Function Component,一个例子如下:

class EmailInput extends Component {state = { email: this.props.email};

  render() {return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = e => his.setState({email: e.target.value});

  componentWillReceiveProps(nextProps) {if (nextProps.userID !== this.props.userID) {this.setState({ email: nextProps.email});
    }
  }
}

4.shouldComponentUpdate(nextProps)
此生命周期产生在组件挂载之后的组件更新阶段。
值得注意的是子组件更新不肯定是因为 props 或 state 扭转引起的,也可能是父组件的其它局部更改导致父组件重渲染而使得以后子组件在 props/state 未扭转的状况下从新渲染一次。
函数被调用时会被传入行将更新的 nextPropsnextState对象,开发者能够通过比照前后两个 props 对象上与界面渲染相干的属性是否扭转,再决定是否容许这次更新 (return true 示意容许执行更新,否则疏忽更新,默认为 true)。常搭配对象深比拟函数用于缩小界面无用渲染次数,优化性能。在一些只须要简略浅比拟 props 变动的场景下,并且雷同的 state 和 props 会渲染出雷同的内容时,倡议应用React.PureComponnet 代替,在 props 更新时 React 会主动帮你进行一次浅比拟,以缩小不必要渲染。

class EmailInput extends Component {state = { email: this.props.email};

  render() {return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = e => his.setState({email: e.target.value});

  shouldComponentUpdate(nextProps, nextState) {
    if (
      nextProps.userID === this.props.userID &&
      nextState.email == this.state.email
    ) return false;
  }
}

5.componenetWillUpdate(newProps, newState)
此生命周期产生在组件挂载之后的更新阶段。当组件收到新的 props 或 state,并且 shouldComponentUpdate 返回容许更新时,会在渲染之前调此办法,不能够在此生命周期执行 setState。在此生命周期中开发者能够在界面理论渲染更新之前拿到最新的nextPropsnextState,从而执行一些副作用:比方触发一个事件、依据最新的 props 缓存一些计算数据到组件内、平滑界面元素动画等:

 // 须要搭配 css 属性 transition 应用
 componentWillUpdate : function(newProps,newState){if(!newState.show)
      $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'1'});
    else
      $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'0'});;
  },
  componentDidUpdate : function(oldProps,oldState){if(this.state.show)
      $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'1'});
    else
      $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'0'});;
  }

6.componenetDidUpdate(prevProps, prevState)
此生命周期产生在组件挂载之后的更新阶段,组件首次挂载不会触发。当组件的 props 和 state 扭转引起界面渲染更新后,此函数会被调用,不能够在此生命周期执行setState。咱们应用它用来执行一些副作用:比方条件式触发必要的网络申请来更新本地数据、应用 render 后的最新数据来调用一些内部库的执行(例子:定时器申请接口数据动静绘制 echarts 折线图):

  ...
  componentDidMount() {this.echartsElement = echarts.init(this.refs.echart);
    this.echartsElement.setOption(this.props.defaultData);
    ...
  }
  componentDidUpdate() {const { treeData} = this.props;
    const optionData = this.echartsElement.getOption();
    optionData.series[0].data = [treeData];
    this.echartsElement.setOption(optionData, true);
  }

7.componentWillUnmount()
此生命周期产生在组件卸载之前,组件生命周期中只会触发一次。开发者能够在此函数中执行一些数据清理重置、勾销页面组件的事件订阅等。

React16.3 之后的生命周期

React16.3 之后 React 的 Reconciler 架构被重写 (Reconciler 用于解决生命周期钩子函数和 DOM DIFF),之前版本采纳函数调用栈递归同步渲染机制即 Stack Reconciler,dom 的 diff 阶段不能被打断,所以不利于动画执行和事件响应。React 团队应用 Fiber Reconciler 架构之后,diff 阶段依据虚构 DOM 节点拆分成蕴含多个工作工作单元(FiberNode) 的 Fiber 树 (以链表实现),实现了 Fiber 工作单元之间的任意切换和工作之间的打断及复原等等。Fiber 架构下的异步渲染导致了componentWillMountcomponentWillReceivePropscomponentWillUpdate 三个生命周期在理论渲染之前可能会被调用屡次,产生不可意料的调用后果,因而这三个不平安生命周期函数不倡议被应用。取而代之的是应用全新的两个生命周期函数:getDerivedStateFromPropsgetSnapshotBeforeUpdate

1.getDerivedStateFromProps(nextProps, currentState)

  • 1)定义

此生命周期产生在组件初始化挂载和组件更新阶段,开发者能够用它来代替之前的 componentWillReceiveProps 生命周期,可用于依据 props 变动来动静设置组件外部 state。
函数为 static 动态函数,因而咱们无奈应用 this 间接拜访组件实例,也无奈应用 this.setState 间接对 state 进行更改,以此能够看出 React 团队想通过 React 框架的 API 式束缚来尽量减少开发者的 API 滥用。函数调用时会被传入行将更新的 props 和以后组件的 state 数据作为参数,咱们能够通过比照解决 props 而后返回一个对象来触发的组件 state 更新,如果返回 null 则不更新任何内容。

  • 2)滥用场景一:间接复制 props 到 state 下面

这会导致父层级从新渲染时,SimpleInput 组件的 state 都会被重置为父组件从新传入的 props,不论 props 是否产生了扭转。如果你说应用 shouldComponentUpdate 搭配着防止这种状况能够吗?代码层面上能够,不过可能导致前期 shouldComponentUpdate 函数的数据起源凌乱,任何一个 prop 的扭转都会导致从新渲染和不正确的状态重置,保护一个牢靠的 shouldComponentUpdate 会更难。

class SimpleInput extends Component {state = { attr: ''};

  render() {return <input onChange={(e) => this.setState({attr: e.target.value})} value={this.state.attr} />;
  }

  static getDerivedStateFromProps(nextProps, currentState) {
    // 这会笼罩所有组件内的 state 更新!return {attr: nextProps.attr};
  }
}
  • 3)应用场景:在 props 变动后选择性批改 state
class SimpleInput extends Component {state = { attr: ''};

  render() {return <input onChange={(e) => this.setState({attr: e.target.value})} value={this.state.attr} />;
  }

  static getDerivedStateFromProps(nextProps, currentState) {if (nextProps.attr !== currentState.attr) return {attr: nextProps.attr};
    return null;
  }
}

可能导致的 bug:在须要重置 SimpleInput 组件的状况下,因为 props.attr 未扭转,导致组件无奈正确重置状态,体现就是 input 输入框组件的值还是上次遗留的输出。

  • 4)优化的应用场景一:应用齐全可控的组件

齐全可控的组件即没有外部状态的性能组件,其状态的扭转齐全受父级 props 管制,这种形式须要将本来位于组件内的 state 和扭转 state 的逻辑办法抽离到父级。实用于一些简略的场景,不过如果父级存在太多的子级状态治理逻辑也会使逻辑冗余复杂化。

function SimpleInput(props) {return <input onChange={props.onChange} value={props.attr} />;
}
  • 5)优化的应用场景二:应用有 key 值的齐全可控的组件

如果咱们想让组件领有本人的状态治理逻辑,然而在适当的条件下咱们又能够管制组件以新的默认值从新初始化,这里有几种办法参考:

/* 
  1. 设置一个惟一值传入作为组件从新初始化的标记
     通过比照属性手动让组件从新初始化
*/
class SimpleInput extends Component {state = { attr: this.props.attr, id=""}; // 初始化默认值

  render() {return <input onChange={(e) => this.setState({attr: e.target.value})} value={this.state.attr} />;
  }

  static getDerivedStateFromProps(nextProps, currentState) {if (nextProps.id !== currentState.id)
      return {attr: nextProps.attr, id: nextProps.id};
    return null;
  }
}

/*
  2. 设置一个惟一值作为组件的 key 值
     key 值扭转后组件会以默认值从新初始化
  */
class SimpleInput extends Component {state = { attr: this.props.attr}; // 初始化默认值

  render() {return <input onChange={(e) => this.setState({attr: e.target.value})} value={this.state.attr} />;
  }
}

<SimpleInput
  attr={this.props.attr}
  key={this.props.id}
/>

/*
  3. 提供一个内部调用函数以供父级间接调用以重置组件状态
     父级通过 refs 来拜访组件实例,拿到组件的外部办法进行调用
  */
class SimpleInput extends Component {state = { attr: this.props.attr}; // 初始化默认值

  resetState = (value) => {this.setState({ attr: value});
  }

  render() {return <input onChange={(e) => this.setState({attr: e.target.value})} value={this.state.attr} />;
  }
}

<SimpleInput
  attr={this.props.attr}
  ref={this.simpleInput}
/>

2.componentDidMount()

3.shouldComponentUpdate(nextProps, nexState)

4.getSnapshotBeforeUpdate(prevProps, prevState)
此生命周期产生在组件初始化挂载和组件更新阶段,界面理论 render 之前。开发者能够拿到组件更新前的 prevPropsprevState,同时也能获取到 dom 渲染之前的状态 (比方元素宽高、滚动条长度和地位等等)。此函数的返回值会被作为componentWillUpdate 周期函数的第三个参数传入,通过搭配 componentDidUpdate 能够齐全代替之前 componentWillUpdate 局部的逻辑,见以下示例。

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

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 判断是否在 list 中增加新的 items 
    // 捕捉滚动​​地位以便咱们稍后调整滚动地位。if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 调整滚动地位使得这些新 items 不会将旧的 items 推出视图
    // snapshot 是 getSnapshotBeforeUpdate 的返回值)if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

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

5.componenetDidUpdate(prevProps, prevState, shot)
此生命周期新增个性:getSnapshotBeforeUpdate的返回值作为此函数执行时传入的第三个参数。

6.componenetWillUnmount

退出移动版