万字文章带你了解什么是React,React有什么,如何使用React。(一)

生命周期

什么是生命周期

一句话概括:从创建到销毁的过程就是生命周期

那么react的生命周期有哪些阶段呢?

  1. 初始化阶段
    constructor 构造函数
    getDefaultProps props默认值
    getInitialState state默认值
  2. 挂载阶段
    componentWillMount 组件初始化渲染前调用
    render 组件渲染
    componentDidMount 组件挂载到DOM后调用
  3. 更新阶段
    componentWillReceiveProps 组件将要接收新 props前调用
    shouldComponentUpdate 组件是否需要更新
    componentWillUpdate 组件更新前调用
    render 组件渲染
    componentDidUpdate 组件更新后调用
  4. 卸载阶段
    componentWillUnmount

React16新的生命周期弃用了componentWillMountcomponentWillReceivePropscomponentWillUpdate这三个方法,新增了getDerivedStateFromPropsgetSnapshotBeforeUpdate来代替被弃用的三个钩子函数;
在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。此方法还用于记录错误。它接收两个参数,errorinfoerror:这是后代组件引发的错误。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做了三种优化来降低复杂度:
  1. 如果父节点不同,放弃对子节点的比较,直接删除旧节点然后添加新的节点重新渲染;
  2. 如果子节点有变化,VirtualDOM不会计算变化,二次重新渲染;
  3. 通过设定唯一的key值来比较节点;真实的DOM有非常多的属性,大部分属性对于DIff是没有任何用处的,所以如果用更轻量级的JS对象来代替复杂的DOM节点,就可以避免大量对DOM的查询操作。

Virtual Dom的作用

  1. 在牺牲部分性能的前提下,增加了可维护性。
  2. 可以使框架跨平台
  3. 组件的高度抽象化

Virtual Dom的缺点

  1. 首次渲染大量的DOM,由于多个一层虚拟DOM的计算,会稍微慢一点
  2. 需要在内存中维护一份DOM的副本
  3. 如果虚拟 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的批处理机制给人一种异步的假象。

扫描二维码关注公众号,回复: 17263617 查看本文章

异步代码和原生事件中:

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的执行原理

可以分为两类:

  1. 批量更新类:即react内部的执行函数,执行setState的执行逻辑,都是批量更新处理,其中包括:react内部事件(合成事件)和生命周期;
  2. 非批量更新类:即上面两种情况以外的情况,经常见到的:原生事件、setTimeoutfetch等等;

两个概念:

  1. 事务:可以理解为,一个正常的函数外层又被包裹了一层。这层包裹处理,包括一个或多个的函数执行前的处理函数(initialize函数)、一个和多个函数执行后的处理函数(close函数);React很多的逻辑处理,都使用了事务的概念;
  2. 合成事件和原生事件的关系和区别:
    区别:原生事件就是addEventListener写法的事件!而合成事件,就是直接书写react中的onClick、onChange等;
    关系:合成事件可以理解为react对原生事件的包裹封装;原生事件相当于上面事务概念中的正常的函数,而经过包装处理形成的事务,就是react中的合成事件。

原生事件中,setState会直接触发render更新,所以例子在原生事件中的执行顺序是,先render然后执行callbacksetState事务执行完毕,然后执行打印。打印拿到的就是setState更新之后的状态,以此类推,所以出现了上面原生事件的打印顺序,这就很明了了。

而合成事件则不然,它直接发起事务1,在函数执行之前开始批量更新状态(isBatchedUpdatestrue,默认值是false!),开启之后,执行合成事件中的setState,此时处于批量更新状态,这时setState不会触发render更新,而是做了两件事情:收集statecallback

默认批量更新是处于关闭的状态,那么会直接执行batchedUpdates(此函数就是更新渲染函数)。这里就是批量更新状态是否开启的分叉口:当开启批量更新时,则是把状态push到数组(dirtyComponents)中。

收集完状态以后,执行事务的close函数,它里面做了些什么呢?一个是关闭批量更新状态,一个是正式发起对收集的状态的处理,这里又开启了一个新事务:即事务2。事务2,经过复杂的处理,处理更新了收集的state,也就是dirtyComponents。处理完以后,执行事务2的close函数,它重置了整个更新的状态,也是在这里处理执行事务1中收集的callback

总结setState

  1. setState的执行,分为两大类:一类是生命周期和合成函数;一类是非前面的两种情况;
  2. 两种类型下,setState都是同步执行,只是在批量更新类中,statecallback被收集起来延迟处理了,可以理解为数据的异步执行;而非批量更新类中的setState直接触发更新渲染。
  3. callbackstate同时收集,处理是在render之后,统一处理的。

合成事件机制和原理

由于fiber机制的特点,生成一个fiber节点时,它对应的dom节点有可能还未挂载,onClick这样的事件处理函数作为fiber节点的prop,也就不能直接被绑定到真实的DOM节点上。
为此,React提供了一种“顶层注册,事件收集,统一触发”的事件机制。

所谓“顶层注册”,其实是在root元素上绑定一个统一的事件处理函数。“事件收集”指的是事件触发时(实际上是root上的事件处理函数被执行),构造合成事件对象,按照冒泡或捕获的路径去组件中收集真正的事件处理函数。“统一触发”发生在收集过程之后,对所收集的事件逐一执行,并共享同一个合成事件对象。这里有一个重点是绑定到root上的事件监听并非我们写在组件中的事件处理函数,注意这个区别,下面会提到。

以上是React事件机制的简述,这套机制规避了无法将事件直接绑定到DOM节点上的问题,并且能够很好地利用fiber树的层级关系来生成事件执行路径,进而模拟事件捕获和冒泡,另外还带来两个非常重要的特性:

对事件进行归类,可以在事件产生的任务上包含不同的优先级

提供合成事件对象,抹平浏览器的兼容性差异

接下来会对事件机制进行详细讲解,贯穿一个事件从注册到被执行的生命周期。

  1. 事件处理函数不是绑定到组件的元素上的,而是绑定到root上,这和fiber树的结构特点有关,即事件处理函数只能作为fiber的prop。
  2. 绑定到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中分为五种优先级,每种优先级对应一个过期时间。
时间分片,通俗地说,就是将任务分成几种类型,具体为:

  1. 立即需要执行的任务
  2. 用户无操作期间需要执行的任务
  3. 正常任务
  4. 低优先级任务
  5. 浏览器空闲时才执行的任务

按照优先级执行,如果插入了新的任务,那么也按照优先级重新排序
这个模块是用两个es6的新API实现的,分别是window.requestAnimationFramewindow.requestIdleCallback。具体用法可以查MDN。每次循环,如果有过期的任务,那么无论如何要把过期的任务执行完毕,然后如果有剩余时间则按照到过期时间小的优先执行,以此类推。

猜你喜欢

转载自blog.csdn.net/zw7518/article/details/124469084