likes
comments
collection
share

重识React — — 初次见面,我是Fiber

作者站长头像
站长
· 阅读数 42

前言

在上一篇文章里,我们回顾了经典的React 15生命周期,也在回顾时察觉到了一丝异样,像是componentWillMountcomponentWillUpdatecomponentWillReceiveProps ,都似乎不是特别的好用,或者说是不推荐使用。而且以现在的眼光回望过去的技术,我们知晓技术必定会发生迭代的结局(React 15 --> React 16.3)。我们百分百地明白变化是什么,但是我们是否也同样百分百地明白变化是为什么呢?

于是我在此向你发出邀请,一同走一遍“知其所以然”的道路

本文将React 15React 16.3的变化分为两个部分———生命周期的变化和核心算法的变化,分别探寻变化的原因。

生命周期的变化

什么变了

变化一览
  1. componentWillReceiveProps
  2. componentWillUpdate
  3. componentWillMount
  4. getDerivedStateFromProps()
  5. getSnapshotBeforeUpdate()
Mounting挂载阶段
组件挂载: 初始化渲染(Mounting)
constructor()
componentWillMount()
render()
componentDidmount()
组件挂载: 初始化渲染(Mounting)
constructor()
getDerivedStateFromProps()
render()
componentDidmount()

从上图的对比中,根据这对称的样子、这所处的位置,看起来就像是getDerivedStateFromProps替代了componentWillMount?

实际上并不是getDerivedStateFromProps替代了componentWillMount,真正被getDerivedStateFromProps替代的另有其人(componentWillReceiveProps)。

因此在这里我们达成一个共识,componentWillMount并不是被谁替代了,而就是被彻底地废弃了。

🔈"componentWillMount, Out!"

在React 15中componentWillMount给我们的感觉是“鸡肋”,举个例子,我们知道componentWillMount会在render前执行,因此会顺理成章地想在componentWillMount里进行异步请求的发起,本意是期望早点发起,早点返回数据,从而能避免进行到render时数据还没有返回的白屏现象。

这个想法是美好的,但是现实是骨感的。实际上render方法会在componentWillMount结束后立刻执行,即componentWillMount执行结束,其中的异步请求才刚刚发起,而render方法就已经执行了。这和我们美好的期望大相径庭。倒是和将此次异步请求放在componentDidmount执行没什么区别。

既然在componentWillMount中能做的事情,大部分都可以在componentDidmount中去做,componentWillMount的地位确实尴尬,用一句话评价一下它,“有点用处,但不多”。

那么是什么成为了压倒它的最后一棵稻草呢,是什么让React团队决定彻底废弃componentWillMount呢?

不妨带着这个疑问,接着往下走走~🙈🙉🙊

🔈"getDerivedStateFromProps, In!"

在React 15,我们知道componentWillReceiveProps的存在,是为了让子组件能够根据父组件的props进行自身state的派生或者更新。

举个🌰


// props:{text:'父组件文本'}
componentWillReceiveProps(props){
// 将父组件props中的text的值派生为子组件state中的textFromFather变量
// 而不会影响子组件本身已存在的变量
// 假设子组件的state是:
// { text:'子组件文本' }
//
// 则在执行完componentWillReceiveProps这一生命周期后
// 子组件的state为:
// {text:'子组件文本' ,textFromFather:'父组件文本' }
this.setState({textFromFather:props.text})

}

而在getDerivedStateFromProps,是这样使用的

举个🌰

// props:{text:'父组件文本'}
getDerivedStateFromProps(props,state){

return {textFromFather:props.text}

}

可以看到,getDerivedStateFromProps返回了一个对象,通过这个返回的对象,来进行state的派生和更新操作 (如果返回的是null,则不对state做任何操作)。

Updating更新阶段
组件更新: 由父组件触发(Updating)
componentWillReceiveProps()
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate()
组件更新: 由组件自身触发(Updating)
组件更新: NextProps(Updating)
getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
组件更新: setState(Updating)
组件更新: forceUpdate(Updating)
🔈"componentWillUpdate, Out!"
🔈"getSnapshotBeforeUpdate, In!"

举个🌰


getSnapshotBeforeUpdate(prevProps, prevState){
 //...todo
}

由图可知getSnapshotBeforeUpdate的执行时机是在render方法之后,在真实DOM更新之前,也就是说,getSnapshotBeforeUpdate可以同时获取到更新前的真实DOM和更新前的state以及props 的信息。

为什么变

getDerivedStateFromProps

与componentDidUpdate一起,这个新的生命周期涵盖过时componentWillReceiveProps的所有用例

———— React官方

让我们直译一下getDerivedStateFromProps的命名:“获取从Props中派生的State”。

并且getDerivedStateFromProps又是一个static静态方法,这意味着我们无法在其中进行this.setState或者this.fetchXXX去做一些异步请求等操作,因为我们根本无法在一个静态方法中获取this

由此可见,React 16期望并限制我们,只能在getDerivedStateFromProps里做1件事情 —— —— 从Props中派生/更新State

componentWillReceiveProps功能是比getDerivedStateFromProps多,但是更多也就意味着更高的维护成本。

综上所述,我们可以将为什么总结为以下几点:

  1. getDerivedStateFromProps专心于1个功能,更符合设计模式的API设计原则,缩减了维护成本。
  2. React 16对getDerivedStateFromProps的强制推行,对其使用上的强制限制,使得React 16下的生命周期函数的行为更加可控、可预测(因为开发者只能通过getDerivedStateFromProps建立props到state的映射关系),从根源上避免了生命周期的滥用。
  3. 为全新的Fiber架构铺路
getSnapshotBeforeUpdate

与componentDidUpdate一起,这个新的生命周期涵盖过时componentWillUpdate的所有用例

———— React官方

getSnapshotBeforeUpdate可以与componentDidUpdate实现通信 举个🌰

getSnapshotBeforeUpdate(prevProps, prevState){

 console.log('getSnapshotBeforeUpdate执行')
 return '你好,componentDidUpdate!'
 
}

componentDidUpdate(prevProps, prevState,valueFromSnapshot){

console.log('componentDidUpdate执行')
// 控制台会在显示 'componentDidUpdate执行' 之后,
// 显示 '你好,componentDidUpdate!'
console.log(valueFromSnapshot)

}

getDerivedStateFromProps不同,我们可以认为React使用getSnapshotBeforeUpdatecomponentDidUpdate一起替代掉componentWillUpdate的原因,就是为了Fiber架构

核心算法的变化

变了什么

  1. React Fiber是对React 16以前的核心算法的重写。
  2. React Fiber使得先前同步的渲染过程变成异步的渲染过程。

为什么变

原因一:以前的同步渲染不好

复习一下React组件更新的渲染过程,每一次的组件更新会使React重新构建一个虚拟DOM树,并将此次新构建的树与旧的树通过diff算法进行对比,而这个对比的是个递归的过程。这也就意味着当这棵树的层数比较多,则递归调用栈就会很深,如下图所示。 重识React — — 初次见面,我是Fiber 只有最底层的调用返回了,才会慢慢接着逐层返回。而且这个更新过程无法被打断。这也就意味着,同步渲染一旦开始,主线程就将被其占据,在同步渲染期间,浏览器无法做除渲染以外的任何事情。假如在此期间,用户发生了一些交互行为,浏览器是无法处理的。

如果此次同步渲染的时间较少,那么给用户的感知就不是那么强烈,一旦同步渲染的时间较长,那么带给用户最直观的使用感受就是卡顿,而我们都很讨厌卡顿

这种一旦执行就必须得等它停下来之后才能做其他事情的感受,也许文字描述还是不能让你有多大感觉。

可以试着回想一下,是否在过去的记忆里,有那么一个场景,那是假期里的某一天,你在饭点前开了一把Moba游戏排位(不能暂停),刚开不久,你妈妈喊你出来吃饭,你却久久地无法从房间里出来。此时,请你代入你妈妈的视角,感受下她的情绪😡。

我想同步任务之于用户,由此可见一斑了。

变化后的好处

好处一:Fiber使主线程不会被渲染线程一直占据

重识React — — 初次见面,我是Fiber Fiber会将一个大的更新任务,拆解成一个个小的任务,每当执行完一个小任务的时候,渲染线程就会把主线程交出去,看看目前是否有优先级更高的任务需要占用主线程。

这就让这一次的更新渲染可以被打断,也就避免了卡顿的情况。

怎么变的

既然在React 16中,更新导致的渲染是可以被打断的,那么我们会很自然地抱有一些疑问:

我可以随心所欲地打断吗? 这个打断是否有一个判断的条件呢?

带着上面的问题,让我们一起再来看看React 16.3的生命周期图吧!

React 16.3生命周期图

重识React — — 初次见面,我是Fiber 由图可知,react 16的生命周期被划分为了2个大的阶段:render 和 commit,commit中又划分了2个子阶段:pre-commit 和 commit。

render

纯净且不包含副作用,可能会被React暂停,中止或重新启动

pre-commit

可以读取DOM

commit

可以使用DOM,运行副作用,安排更新

提问❓:为什么render阶段可以被React暂停,中止或重新启动?

回答

  1. 为什么render阶段行?

因为render阶段对用户来说是不可见的,因此哪怕暂停,中止或重新启动了,对用户来说也是0感知。

  1. 为什么commit阶段不行?

commit阶段涉及到了真实DOM的操作,这是可以被用户感知到的。如果对commit阶段也暂停,中止或重新启动,这会直接将视图的变动暴露在用户眼前,显然是不可取的。

驻足于此,回头看

render阶段可能会被React暂停,中止或重新启动,如何理解“重新启动”?

假设渲染线程在执行render阶段的某个任务A时,主线程被优先级更高的任务B占据,导致任务A执行被打断了,渲染线程只能等待任务B执行完毕。

当渲染线程重新占据主线程后,对于之前被打断的任务A,渲染线程并不是从任务A被打断的那行代码接着往下继续执行,而是把A任务从头开始再执行一遍。

这就导致render阶段的生命周期都存在被重复执行的可能

说到这里,让我们再回望前文提到的3个被废弃的生命周期

  • componentWillReceiveProps
  • componentWillUpdate
  • componentWillMount

他们3个的共同点就在于它们都处于render阶段,即它们都可能被重复执行

而开发者对于这3个生命周期的滥用,又让“可能”成为“可怕

假如在这3者之中的某个生命周期里fetch了一个异步请求,而又因为Fiber带来的异步渲染机制,会导致这个生命周期函数被多次打断+重启,那么最终就会多次发起这个异步请求。

假如这是个付款功能的请求,则相当于用户点了1次付款按钮,期望执行1次付款,实际上却付了可能10次款😰。

又或者在其中执行的是对DOM的操作,比如添加一个DOM元素。但是由于该生命周期函数被多次打断+重启,用户只是点了1次操作按钮,页面上却多了5个展示模块。

总结

React 16改造生命周期的主要目的,是为了配合Fiber架构带来的全新的异步渲染机制。

废弃了旧的生命周期函数,确保了Fiber机制下数据和视图的安全性

推出了getDerivedStateFromProps这一具有强制性的生命周期函数,提高了生命周期函数行为的可预测性和可控性。

通过这次对生命周期的变化原因的探寻,我们算是和Fiber初次打了个照面。我们也开始慢慢走向React最精巧、最核心的部分。我本人也在努力地慢慢消化,慢慢理解,试图用轻松、平常的语言和大家聊一聊自己站在巨人的肩膀上的见解。

后言

这篇分享前后写了2天,我一边进行知识的回顾消化,一边进行知识的理解输出,确实是有些困难。但是颇有点乐在其中的感觉。之前为了面试去背题,背图,去一上来就抓着Fiber的调度方法源码,concurrent等等面经去背,实在是太过痛苦,也根本没有为Fiber的设计巧妙而有眼前一亮的感觉。

我想这也许就是心态的问题,我觉得这就像交朋友一样,带有功利性质而结交的朋友,相处起来就是没那么自然的。 这一次我想为自己学一遍React,我想好好地和这位陪伴了我近3年的框架交个朋友。

希望你也可以轻松地面对技术,如果读完此文给你的感觉是轻松的,那可再好不过了~

转载自:https://juejin.cn/post/7270460172995723322
评论
请登录