生命周期
什么是生命周期
一句话概括:从创建到销毁的过程就是生命周期
那么react的生命周期有哪些阶段呢?
- 初始化阶段
constructor
构造函数
getDefaultProps
props默认值
getInitialState
state默认值 - 挂载阶段
componentWillMount
组件初始化渲染前调用
render
组件渲染
componentDidMount
组件挂载到DOM后调用 - 更新阶段
componentWillReceiveProps
组件将要接收新 props前调用
shouldComponentUpdate
组件是否需要更新
componentWillUpdate
组件更新前调用
render
组件渲染
componentDidUpdate
组件更新后调用 - 卸载阶段
componentWillUnmount
React16新的生命周期弃用了componentWillMount
、componentWillReceiveProps
、componentWillUpdate
这三个方法,新增了getDerivedStateFromProps
、getSnapshotBeforeUpdate
来代替被弃用的三个钩子函数;
在React16中并没有删除这三个钩子,但是不能和新增的钩子函数混用,React17将会删除这三个钩子函数,新增了对错误的处理(componentDidCatch
)
那么新增的方法如何使用呢?
getDerivedStateFromProps
:组件每次被 rerender的时候,包括在组件构建之后(虚拟 dom之后,实际 dom挂载之前),每次获取新的 props或 state之后;每次接收新的props之后都会返回一个对象作为新的 state,返回null则说明不需要更新 state;配合 componentDidUpdate
,可以覆盖 componentWillReceiveProps
的所有用法。
getSnapshotBeforeUpdate
:触发时间: update发生的时候,在 render之后,在组件 dom渲染之前;返回一个值,作为 componentDidUpdate
的第三个参数;配合 componentDidUpdate
, 可以覆盖 componentWillUpdate
的所有用法。
componentDidCatch
:如果在任何生命周期方法或任何子组件的呈现阶段发生一些错误,则调用componentDidCatch
()方法。此方法用于为React应用程序实现错误边界。它在提交阶段被调用,因此与在渲染阶段被调用的getDerivedStateFromError
()不同,此方法允许使用side-effects
。此方法还用于记录错误。它接收两个参数,error
和info
;error
:这是后代组件引发的错误。info
:它存储哪个组件引发了此错误的componentStack
跟踪。
副作用
-
首先解释
纯函数(Pure function)
:给一个 function 相同的参数,永远会返回相同的值,并且没有副作用;这个概念拿到 React 中,就是给一个Pure component
相同的props
, 永远渲染出相同的视图,并且没有其他的副作用;纯组件的好处是,容易监测数据变化、容易测试、提高渲染性能等; -
副作用(
Side Effect
)是指一个 function 做了和本身运算返回值无关的事,比如:修改了全局变量、修改了传入的参数、甚至是console.log()
,所以ajax
操作,修改dom
都是算作副作用的;
Virtual Dom
Virtual Dom的定义
本质上是JS对象,这个对象就是更加轻量级的对DOM的描述。
React为解决操作DOM的痛点提出了一个新的思想,即始终整体刷新页面,当发生前后状态变化时,React会自动更新UI,但是缺点就是很慢。因此,没有改变的DOM节点让它保持原样不动,仅仅创建并替换变更过的DOM节点实现了节点的复用,因此问题转化为如何对比两个DOM节点的差异。因为DOM是树形解构,完成的树形结构diff算法复杂度为O(n^3)。
React做了三种优化来降低复杂度:
- 如果父节点不同,放弃对子节点的比较,直接删除旧节点然后添加新的节点重新渲染;
- 如果子节点有变化,VirtualDOM不会计算变化,二次重新渲染;
- 通过设定唯一的key值来比较节点;真实的DOM有非常多的属性,大部分属性对于DIff是没有任何用处的,所以如果用更轻量级的JS对象来代替复杂的DOM节点,就可以避免大量对DOM的查询操作。
Virtual Dom的作用
- 在牺牲部分性能的前提下,增加了可维护性。
- 可以使框架跨平台
- 组件的高度抽象化
Virtual Dom的缺点
- 首次渲染大量的DOM,由于多个一层虚拟DOM的计算,会稍微慢一点
- 需要在内存中维护一份DOM的副本
- 如果虚拟 DOM 大量更改,这是合适的。但是单一的,频繁的更新的话,虚拟 DOM 将会花费更多的时间处理计算的工作。所以,如果你有一个 DOM 节点相对较少页面,用虚拟 DOM,它实际上有可能会更慢。
setState
的机制
setState
是同步的还是异步的?
在 React的生命周期和合成事件中, React仍然处于他的更新机制中,这时无论调用多少次 setState
,都会不会立即执行更新,而是将要更新的值存入 _pendingStateQueue
,将要更新的组件存入 dirtyComponent
。
当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件 didmount
后会将批处理标志设置为 false。这时将取出 dirtyComponent
中的组件以及 _pendingStateQueue
中的 state进行更新。这样就可以确保组件不会被重新渲染多次。
componentDidMount() {
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index);
}
所以,如上面的代码,当我们在执行 setState
后立即去获取 state
,这时是获取不到更新后的 state
的,因为处于 React的批处理机制中, state
被暂存起来,待批处理机制完成之后,统一进行更新。
所以。setState
本身并不是异步的,而是 React的批处理机制给人一种异步的假象。
异步代码和原生事件中:
componentDidMount() {
setTimeout(() => {
console.log('调用setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index);
}, 0);
}
如上面的代码,当我们在异步代码中调用 setState
时,根据 JavaScript的异步机制,会将异步代码先暂存,等所有同步代码执行完毕后在执行,这时 React的批处理机制已经走完,处理标志设被设置为 false
,这时再调用 setState
即可立即执行更新,拿到更新后的结果。
在原生事件中调用 setState
并不会出发 React的批处理机制,所以立即能拿到最新结果。
最佳实践
setState
的第二个参数接收一个函数,该函数会在 React的批处理机制完成之后调用,所以你想在调用 setState
后立即获取更新后的值,请在该回调函数中获取
setState
接收一个新的状态- 该接收到的新状态不会被立即执行么,而是存入到
pendingStates
(等待队列)中
setState
的执行原理
可以分为两类:
- 批量更新类:即
react
内部的执行函数,执行setState
的执行逻辑,都是批量更新处理,其中包括:react内部事件(合成事件)和生命周期; - 非批量更新类:即上面两种情况以外的情况,经常见到的:原生事件、
setTimeout
、fetch
等等;
两个概念:
- 事务:可以理解为,一个正常的函数外层又被包裹了一层。这层包裹处理,包括一个或多个的函数执行前的处理函数(initialize函数)、一个和多个函数执行后的处理函数(close函数);React很多的逻辑处理,都使用了事务的概念;
- 合成事件和原生事件的关系和区别:
区别:原生事件就是addEventListener写法的事件!而合成事件,就是直接书写react中的onClick、onChange等;
关系:合成事件可以理解为react对原生事件的包裹封装;原生事件相当于上面事务概念中的正常的函数,而经过包装处理形成的事务,就是react中的合成事件。
原生事件中,setState
会直接触发render
更新,所以例子在原生事件中的执行顺序是,先render
然后执行callback
,setState
事务执行完毕,然后执行打印。打印拿到的就是setState
更新之后的状态,以此类推,所以出现了上面原生事件的打印顺序,这就很明了了。
而合成事件则不然,它直接发起事务1,在函数执行之前开始批量更新状态(isBatchedUpdates
为true
,默认值是false
!),开启之后,执行合成事件中的setState
,此时处于批量更新状态,这时setState
不会触发render
更新,而是做了两件事情:收集state
和callback
。
默认批量更新是处于关闭的状态,那么会直接执行batchedUpdates
(此函数就是更新渲染函数)。这里就是批量更新状态是否开启的分叉口:当开启批量更新时,则是把状态push
到数组(dirtyComponents
)中。
收集完状态以后,执行事务的close
函数,它里面做了些什么呢?一个是关闭批量更新状态,一个是正式发起对收集的状态的处理,这里又开启了一个新事务:即事务2。事务2,经过复杂的处理,处理更新了收集的state,也就是dirtyComponents
。处理完以后,执行事务2的close
函数,它重置了整个更新的状态,也是在这里处理执行事务1中收集的callback
;
总结setState
:
setState
的执行,分为两大类:一类是生命周期和合成函数;一类是非前面的两种情况;- 两种类型下,
setState
都是同步执行,只是在批量更新类中,state
和callback
被收集起来延迟处理了,可以理解为数据的异步执行;而非批量更新类中的setState
直接触发更新渲染。 callback
与state
同时收集,处理是在render
之后,统一处理的。
合成事件机制和原理
由于fiber
机制的特点,生成一个fiber节点时,它对应的dom
节点有可能还未挂载,onClick
这样的事件处理函数作为fiber
节点的prop
,也就不能直接被绑定到真实的DOM节点上。
为此,React提供了一种“顶层注册,事件收集,统一触发”的事件机制。
所谓“顶层注册”,其实是在root
元素上绑定一个统一的事件处理函数。“事件收集”指的是事件触发时(实际上是root
上的事件处理函数被执行),构造合成事件对象,按照冒泡或捕获的路径去组件中收集真正的事件处理函数。“统一触发”发生在收集过程之后,对所收集的事件逐一执行,并共享同一个合成事件对象。这里有一个重点是绑定到root
上的事件监听并非我们写在组件中的事件处理函数,注意这个区别,下面会提到。
以上是React事件机制的简述,这套机制规避了无法将事件直接绑定到DOM节点上的问题,并且能够很好地利用fiber
树的层级关系来生成事件执行路径,进而模拟事件捕获和冒泡,另外还带来两个非常重要的特性:
对事件进行归类,可以在事件产生的任务上包含不同的优先级
提供合成事件对象,抹平浏览器的兼容性差异
接下来会对事件机制进行详细讲解,贯穿一个事件从注册到被执行的生命周期。
- 事件处理函数不是绑定到组件的元素上的,而是绑定到root上,这和fiber树的结构特点有关,即事件处理函数只能作为fiber的prop。
- 绑定到root上的事件监听不是我们在组件里写的事件处理函数,而是一个持有事件优先级,并能传递事件执行阶段标志的监听器。
合成事件对象
在组件中的事件处理函数中拿到的事件对象并不是原生的事件对象,而是经过React合成的SyntheticEvent
对象。它解决了不同浏览器之间的兼容性差异。抽象成统一的事件对象,解除开发者的心智负担。
Fiber
大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是吧;运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用setState
更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。整个过程是一气呵成,不能被打断的。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。
解决主线程长时间被 JS 运算占用这一问题的基本思路,是将运算切割为多个步骤,分批完成。也就是说在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。
window.requestIdleCallback()
会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。
React 框架内部的运作可以分为 3 层:
- Virtual DOM 层,描述页面长什么样。
- Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
- Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。
react16之后的版本改动最大的当属 Reconciler 层了,React 团队也给它起了个新的名字,叫Fiber Reconciler。
Fiber Reconciler 在执行过程中,会分为 2 个阶段。
- 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。
- 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。
阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。
Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。
Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程。
如果过程中有优先级更高的任务需要进行,则 Fiber Reconciler 会丢弃正在生成的树,在空闲的时候再重新执行一遍。
在构造 Fiber 树的过程中,Fiber Reconciler 会将需要更新的节点信息保存在Effect List当中,在阶段二执行的时候,会批量更新相应的节点。
Fiber 大概是怎么实现的
链表、一次Fiber循环所有节点访问两次、requestIdleCallback
Fiber深度理解
需求
1. 每一个状态的改变不是都需要马上反应在UI界面上
2. 不同的状态改变应该有不同的优先级,如动画,输入等应该有更高的优先级
3. 对于大量状态的改变复杂操作应该进行更好的调度,避免UI页面卡顿
Fiber结构
fiber树的整体结构是一个双向循环链表,这种结构能够更加快速的找到相对应的节点。
在Reconcile过程中为了能够知道之前节点的信息,需要将新的fiber节点与老fiber节点进行关联。
Fiber中会同时存在两种fiber tree,每次Reconcile的过程就是新fiber tree构建的过程,当commit之后新的fiber tree就变成了current fiber tree,如此循环往复。
Fiber Effect
在Reconcile的过程中,需要给节点设置状态,与旧节点相比需要达到的状态。每个fiber节点构建完成后(设置自己的effectTag状态),如果有effect则将自己以及其子孙元素放入父节点的effects中,这样层层构建,最终新的fiber tree的effects中存储的就是所有要处理的fiber node。然后进入到commit阶段,将所有的fiber node进行到dom的转换,进行UI页面的刷新。
Fiber 调度
Fiber既然是一个虚拟栈,那么就需要进行调度。我们可以利用该函数在浏览器空闲的时候来执行我们的代码,这样可以达到不阻塞页面渲染的目的。
Fiber 优先级
为了更好的用户体验,需要让优先级更高的任务优先执行,如动画,输入等。Fiber中分为五种优先级,每种优先级对应一个过期时间。
时间分片,通俗地说,就是将任务分成几种类型,具体为:
- 立即需要执行的任务
- 用户无操作期间需要执行的任务
- 正常任务
- 低优先级任务
- 浏览器空闲时才执行的任务
按照优先级执行,如果插入了新的任务,那么也按照优先级重新排序
这个模块是用两个es6的新API实现的,分别是window.requestAnimationFrame
和window.requestIdleCallback
。具体用法可以查MDN
。每次循环,如果有过期的任务,那么无论如何要把过期的任务执行完毕,然后如果有剩余时间则按照到过期时间小的优先执行,以此类推。