前端框架React

Posted Silam Lin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端框架React相关的知识,希望对你有一定的参考价值。

前端框架React

--------------------组件基础----------------------

React事件机制

首先,React中的事件是合成事件,与原生html事件是不同的。

原生事件,譬如用addEventListener添加的click事件:

componentDidMount()
	document.body.addEventListener('click',this.handleClickBody,false);

React合成事件,React中通过JSX去绑定的事件

定义:React合成事件是React 模拟原生DOM事件所有能力 的一个对象,它根据W3C规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口。

render()
	return(
		<div>
			<button onClick=this.handleClickBotton>test</button>
		</div>
	)

区别:

  • React合成事件是小驼峰写法;原生HTML事件是全小写
  • React合成事件传入的是函数;原生HTML事件传入的是一个字符串
  • React合成事件不能用return false来阻止浏览器默认行为,需要明确调用preventDefault()来阻止

事件机制:

  • React合成事件是通过事件委托,绑定到document对象上
  • 事件触发后,先开始DOM事件流:事件捕获、处于目标、事件冒泡,因此先处理原生HTML事件
  • 当冒泡到document对象时,再由dispatchEvent去处理React合成事件
  • 最后再执行真正绑定在document对象上的事件
  • 因此执行顺序是:原生事件,React合成事件,document对象挂载事件

使用合成事件的目的:

  • 抹平浏览器之间的兼容问题
  • 可以自定义事件
  • 利用事件委托,将React事件代理到document对象,减少内存开销,提高性能

哪些方法会让React重新渲染、render会做什么

一般有三种方法

  • setState() 通过setState改变state,则会重新渲染。若传入null是不会重新渲染的
  • 父组件重新渲染/props变化
  • forceUpdate() 调用forceUpdate进行强制渲染

render会进行diff算法

React类组件和函数组件

  • 组件是React中可复用的最小代码片段,无论是类组件还是函数组件,它们都返回要渲染在页面中的元素,最后的呈现效果是完全一致的。
  • 类组件和函数组件之间可以互相改写。
  • 以前,如果需要使用到继承与生命周期等概念时,主张使用类组件;
  • 现在,有了React Hooks,使得生命周期概念逐渐淡化;另外官方也主张组合优于继承的设计概念,继承并不是组件的最佳设计模式,故函数组件完全可以替代类组件。
  • 尽管,类组件更容易上手。但React Hooks的推出,函数组件应该是未来的主流(自己做过的项目)

React高阶组件,和普通组件的区别,适用场景

  • 高阶组件HOC是复用组件逻辑的一种高级技巧,基于React组合特性的一种设计模式
  • 实际上高阶组件就是一个接收组件为参数,返回值为新组件的函数
  • 可以实现逻辑复用,又不影响WrappedComponent的内部逻辑

React受控组件和非受控组件

满足以下两个条件的表单元素,叫受控组件

  • React的state成为表单元素的唯一数据源
  • 渲染表单元素的React组件控制了用户输入过程中表单发生的操作

受控组件与state的具体表现如下:

  • 通过初始化state,为表单元素提供默认值
  • 当用户输入时,表单的值发生变化,调用onChange事件处理器
  • onChange事件处理器可以通过event.target.value拿到表单更新后的值
  • 通过setState(),用event.target.value更新state,完成视图的重新渲染,从而完成组件的更新

受控组件的缺陷:

  • 受控组件的唯一数据源是state,需要编写对应的事件处理函数去控制表单发生的变化。
  • 如果有多个表单,那么就需要编写多个事件处理函数,代码会变得臃肿。所以出现了非受控组件

非受控组件

  • 非受控组件将真实数据储存在DOM节点,而不是state
  • 不需要为状态更新编写对应的事件处理函数,而是通过ref从DOM节点对数据现取现用
  • 可以减少代码量

React有状态组件和无状态组件

有状态组件

  • 是类组件
  • 有继承,并且使用生命周期
  • 可以使用this接收状态和属性
  • 内部使用state去管理自身的状态变化
  • 缺点:容易频繁触发生命周期函数,影响性能,加载速度慢

无状态组件

  • 可以是类组件也可以是函数组件
  • 不继承,也不使用生命周期钩子
  • 可以不使用this
  • 内部不使用state去管理,仅仅是用于展示

React-Fiber

React Fiber架构 出现的原因

  • 在React V15,当我们调用setState()更改state导致页面更新时,React会比对新旧 Virtual DOM Tree,找出需要变更的节点并更新它们。
  • 而这个过程是一气呵成不能被打断的(Stack Reconciler),也就使得JS运算任务持续占用主线程,阻塞了浏览器其它事件 。如果时间过长,那么就会一些问题,如:导致用户触发的事件得不到响应、渲染掉帧,从而让用户觉得卡顿。

Fiber架构实际上是 Fiber Reconciler,使得这个执行过程是可中断的,适当地让出CPU控制权,可以让浏览器做以下事情:

  • 及时响应用户的交互事件
  • 让优先级更高的任务先执行,避免掉帧

--------------------数据管理----------------------

setState调用的原理 **

setState是同步还是异步

setState并不是单纯表现为绝对是同步或者绝对是异步。是同步,还是异步,取决于setState的调用场景。

观察源码,内部是通过isBatchingUpdates来判断setState是先存进state队列(异步代码)还是直接更新(同步代码)

  • isBatchingUpdates如果为true,则存进state队列,是异步代码
  • isBatchingUpdates如果为false,则直接更新,是同步代码

表现为异步 (多次setState会合并操作延迟更新):

  • React生命周期
  • React合成事件

表现为同步 (在React无法控制的地方)

  • 原生事件如 addEventListener、setTimeout、setInterval

设计为异步的好处:

  • 性能优化,减少渲染次数 : 如果是同步,每次调用setState时,都会进行一次 vnode DiffDOM更新。频繁执行,显然效率低下。最好则是异步,将多次操作合并成一次操作,批量更新。

关于批量更新:

  • 某个属性进行setState,会进入state队列。而对相同属性的多次设置,React只会为其保留最后一次的更新

this.state和setState

  • this.state是用来初始化state的
  • this.setState是用来修改state的

直接修改this.state,state是不会更新的,页面不会更新。

state和props的区别

state:

  • state用于组件去保存、控制和修改组件自身的状态
  • 在constructor中用this.state进行初始化
  • 在组件内部用this.setState方法进行修改
  • 它是组件内部的私有属性,不可以直接通过外部访问和修改

props:

  • props是外部传进组件的参数,主要就是父组件向子组件传递参数
  • props具有只读性,只能通过外部组件主动传入新的props给子组件去重新渲染

props为什么是只读的

props的只读性

  • props是外部传进组件的参数,主要是父组件向子组件传递的参数。
  • props只能从父组件流入子组件”。就像函数式编程中纯函数一般

纯函数:

  • 给定相同的输入,获得的一定是相同的输出
  • 不会有副作用
  • 不对外部产生依赖

props的只读性,只能从父组件流入子组件,保证了相同的输入,组件或者说页面的内容一定是相同的。

props改变时更新组件的方法

  • componentWillReceiveProps 已经被废弃
  • getDerivedStateFromProps 16.3引入
static getDerivedStateFromProps(nextProps, prevState) 
    const type = nextProps;
    // 当传入的type发生变化的时候,更新state
    if (type !== prevState.type) 
        return 
            type,
        ;
    
    // 否则,对于state不进行任何操作
    return null;

getDerivedStateFromProps

  • 是一个静态函数,不能通过this去访问
  • 通过nextProps和prevState进行判断,是否要将新传入的props映射到state
  • 如果传进来的props不影响state,应该在末尾返回个null

--------------------生命周期----------------------

React的生命周期

组件的生命周期可以分为三个大的阶段

  • 挂载阶段 Mount,即组件第一次在DOM树中被渲染
  • 更新阶段 Update,即组件的状态发生变化,重新更新渲染
  • 卸载阶段 Unmount,即组件从DOM树中移除

组件挂载阶段 (只发生一次)

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount (发起网络请求、执行依赖DOM的操作、添加订阅)

组件更新阶段 (可发生多次)

  • getDerivedStateFromProps
  • shouldComponentUpdate (性能优化)
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

组件卸载阶段

  • componentWillUnmount

废弃的生命周期

  • componentWillMount
  • componentWillReceiveProps 被getDerivedStateFromProps替代
  • componentWillUpdate

① 废弃componentWillMount,因为它完全可以被constructor和componentDidMount代替:

  • 如果是要初始化数据,那么constructor对this.state操作即可
  • 如果是要发起网络请求,应该放到componentDidMount进行操作

② componentWillReceiveProps

constructor

组件的构造函数,是第一个被执行的。

如果没有显示定义它,则会默认添加一个构造函数。

如果显示定义了构造函数,由于ES6继承机制的不同,必须写 super(props),否则无法拿到this对象

  • ES6的继承机制,是先将父类的属性和方法添加到一个空对象上,再将这个对象作为子类的的实例。
  • 调用super,实际上就是调用父类的构造函数,从而创建了“空对象”,再为其添加属性和方法,最后作为子类的实例。
  • 如果不写super,那父类没有对象可以给子类,子类自然没有this对象。

constructor生命周期通常只做两件事

  • 通过this.state去初始化组件的state
  • 为事件处理方法绑定this
constructor(props) 
  super(props);
  // 不要在构造函数中调用 setState,可以直接给 state 设置初始值
  this.state =  counter: 0 
  this.handleClick = this.handleClick.bind(this)

React发起网络请求应该在哪个生命周期

一个组件的挂载阶段,通常有以下四个生命周期:

constructor -> componentWillMount(废弃) -> render -> componentDidMount

  • constructor通常只用于初始化state和为事件处理方法绑定this
  • componentWillMount发生在render之前,即使加载了数据,进行setState也不会触发render重新渲染,是无效的
  • componentDidMount的代码,是组件完全挂载到网页上才会被执行。完全可以保证数据的加载,另外进行setState也是有效的

所以,发起网络请求、执行依赖DOM的操作、添加订阅消息,都是在componentDidMount阶段执行。

React性能优化在哪个生命周期

以下三种情况会导致组件的重新渲染

  • 调用this.setState
  • 父组件更新/props变化
  • forceUpdate

以下两种情况是值得考虑,是否仍然需要重新渲染组件的

情况一、如果调用setState是以下的情况

this.setState(number: this.state.number)

情况二、如果父组件重新渲染,但是props并没有发生变化

很显然这时候组件重新渲染并没有实际意义,倒是降低了性能。

组件更新阶段:
getDerivedStateFromProps -> shouldComponentUpdate -> render -> getSnapshotBeforeUpdate -> componentDidUpdate

可以看到,组件更新阶段,render方法前有一个shouldComponentUpdate方法,可以避免不必要的组件渲染。

shouldComponentUpdate可以进行性能优化

shouldComponentUpdate(nextProps) 
    if (this.props.num === nextProps.num) 
        return false
    
    return true;

  • 比较this.props和nextProps,比较this.state和nextState
  • 如果发生变化,返回true,则需要重新渲染
  • 如果没有发生变化,返回false,没有必要重新渲染

注意:

  • shouldComponentUpdate的比较是进行浅比较
  • 不建议进行深度比较,譬如用JSON.parse(JSON.stringfy())。因为深比较的效率很低,可能比重新渲染组件的效率还低。

--------------------组件通信----------------------

总的来说,通信方式有:

  • 父组件通过props向子组件通信
  • 子组件通过props+回调函数,向父组件通信
  • 使用context
  • 使用redux
  • 使用发布订阅模式
  • 兄弟组件可以通过共同的父组件进行转发

父子组件

父组件向子组件通信:通过props向下,向子组件传送数据

// 子组件: Child
const Child = props =>
  return <p>props.name</p>

// 父组件 Parent
const Parent = ()=>
    return <Child name="react"></Child>

子组件向父组件通信:通过props,向子组件传输回调函数

// 子组件: Child
const Child = props =>
  const cb = msg =>
      return ()=>
          props.callback(msg)
      
  
  return (
      <button onClick=cb("你好!")>你好</button>
  )

// 父组件 Parent
class Parent extends Component 
    callback(msg)
        console.log(msg)
    
    render()
        return <Child callback=this.callback.bind(this)></Child>    
    

跨级组件

父组件向子组件的子组件(“孙子组件”)进行通信

  • 使用props逐层往下传递。但是如果层数较深,中间的每一层都需要去传递props,增加复杂度,而且中间层不一定需要用到props
  • 使用context。context的设计,是让不管嵌套多深的子组件,都可以访问context中的内容。

非嵌套关系组件

非嵌套关系组件,就是没有包含关系的组件。

如:兄弟组件和非兄弟组件

  • 可以使用redux等进行全局状态管理
  • 可以使用发布订阅模式
  • 如果是兄弟组件,可以通过公共的父节点去转发信息

--------------------虚拟DOM----------------------

虚拟DOM

Real DOM , 即真实DOM。

DOM即 Document Object Modal,文档对象模型,是对文档的结构化表述,并提供了对文档结构的访问方式。

Virtual DOM,本质上是一个JavaScript对象,以JS对象的形式去表示DOM结构

在React当中,有JSX这一特性,即支持在JS中通过XML的方式去声明页面的DOM结构。

const vDom = <h1>Hello World</h1> // 创建h1标签,右边千万不能加引号
const root = document.getElementById('root') // 找到<div id="root"></div>节点
ReactDOM.render(vDom, root) // 把创建的h1标签渲染到root节点上

JSX实际上是一种语法糖,它会被babel编译转化成JS代码:

const vDom = React.createElement(
  'h1' className: 'hClass', id: 'hId' ,
  'hello world'
)

可以看出:

  • JSX实际上就是简化调用React.createElement方法
  • React.createElement方法返回的是一个JS对象,也就是Virtual DOM

Virtual DOM长什么样:

const VDom = 
	key:null,
	type:"div", // 标签名or组件名
	props:
		children:[
			 type:"span",,
			 type:"span",
		],
		className:'red',
		onClick: ()=>
	

Virturl DOM 与 Real DOM的区别

  • Virtual DOM不会进行重排重绘;Real DOM会重排重绘
  • Virtual DOM的总损耗:Virtual DOM的增删改 + DOM DIFF + 较少的重排重绘
  • Real DOM的总损耗:Real DOM的增删改 + 较多的重排重绘

Virtual DOM的优点:

① 减少DOM操作的次数

  • 如果手动操作真实DOM,需要一次一次操作DOM,繁琐且容易出错
  • 使用Virtual DOM则可以将多次操作合并成一次,避免频繁更新DOM

② 减少DOM操作的范围

  • Virtual DOM借助 DOM Diff,不会去更新不需要更新的DOM,只更新发生变化的DOM
  • 避免更新不必要的DOM

①和②都可以减少重排重绘,从而提升性能

③ 跨平台

  • 可跨平台:Virtual DOM本身是JS对象,它可以变成ios应用、安卓应用、小程序等,即具有跨平台的能力

React Diff 算法

当虚拟DOM树发生变化后,会根据比较新旧虚拟DOM树的变化,生成patch

patch是一个结构化数组,它记录了最小成本更新DOM的方式。

Diff有三个策略,Tree Diff、Component Diff、Element Diff:

Tree Diff

  • 对新旧两棵树进行对比,找出需要更新的节点
  • 如果是组件,则看Component Diff
  • 如果是标签,则看Element Diff

Component Diff

  • 节点是组件,则先比较两棵树的组件类型
  • 如果组件类型就不同,则直接删除旧的
  • 如果类型相同,则只更新属性,递归进行Tree Diff

Element Diff

  • 节点是原生标签,看两棵树的标签名是否相同
  • 不相同直接删除
  • 相同的话,只更新属性,递归进行Tree Diff

React的key

Key是React用于追踪哪些元素是被修改、添加或者移除的标识,我们需要保证某一个元素的Key在同级元素中具有唯一性

<div>
	<span>1</span>
	<span>2</span>
</div>

如果没有设置key,删除掉span 1
diff的比较是:

  • div是一样的
  • 旧的虚拟DOM树中是,1;新的是 2;进行一次修改,把1改成2
  • 新的树中没有2,进行一次删除

所以就有两次操作。

从数组的角度来看
[1,2,3] 删除掉第二项2,变成[1,3]
实际上发生了什么:

  • array[0]还是1,没有变化
  • array[1]从2变成了3,所以要进行一次修改
  • array[2]没有了,所以要进行一次删除

有了key值,则可以准确判断是哪个元素被修改、删除、增加,因此可以减少不必要的DOM操作

总结:

  • key得在同级元素中是唯一的
  • 避免使用index作为key
  • key不能使用随机数(不稳定的)

--------------------hooks----------------------

--------------------redux----------------------

以上是关于前端框架React的主要内容,如果未能解决你的问题,请参考以下文章

前端框架论短长

2020年,Vue.js会接管React吗?

前端框架真的好吗?

前端框架React、Vue对比

开箱即用的React前端框架——ReactAdmin

前端框架React