React知识点总结

什么是 react

React 是一个声明式的,高效的,并且灵活的用于构建用户界面的 JavaScript 库。

react 的特点

声明式

react 采用声明式的渲染方法,当数据更改时,React 将高效地更新和正确的渲染组件。

虚拟 DOM

虚拟 DOM 是一种抽象的数据结构,在 DOM 的基础上建立了一个抽象层,对数据和状态所做的任何改动,都会被自动且高效的同步到虚拟 DOM,最后再批量同步到 DOM 中。虚拟 DOM 只有插入文档后才会变成真正的 DOM

diff 算法

react 中所有的 DOM 变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实 DOM 上,这种算法叫做 DOM diff

DOM DIFF 是 react 应用中的精华所在,DOM DIFF 在使用时有一些约定如下:

  1. DOM 节点跨层级的移动操作特别少,可以忽略不计(例如 A 原本和 B 平级,随后 A 变成 B 的子节点)

  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构(A 和 B 组件结构不一致)

  3. 同一层级的一组子节点,它们可以通过 uid 进行区分。

DIFF 算法在执行时有三个维度,分别是 Tree DIFF、Component DIFF 和 Element DIFF,执行时按顺序依次执行,它们的差异仅仅因为 DIFF 粒度不同、执行先后顺序不同。

Tree DIFF 是对树的每一层进行遍历,如果某组件不存在了,则会直接销毁。

第二层进入 Component DIFF,同一类型组件继续比较下去,发现 A 组件没有,所以直接删掉 A、B、C 组件

Element DIFF 紧接着以上统一类型组件继续比较下去,常见类型就是列表。同一个列表由旧变新有三种行为,插入、移动和删除,它的比较策略是对于每一个列表指定 key,先将所有列表遍历一遍,确定要新增和删除的,再确定需要移动的。

什么是 VDOM?

首先,DOM 文档对象模型(Document Object Model))是一种对文档进行抽象化的树形结构,文档皆在这个结构中,而 VDOM 则是对 DOM 的抽象,其本身是一个 js 的对象,拥有 dom 的一些属性例如 class,id 等。

例如 react 中的 VDOM 由 VNODE 组成,其包含:

  • 标签类型:\$\$typeof
  • 节点属性:props(包括属性,事件,子节点,类选择器等)
  • 唯一 ID 值:key
  • 引用指针:ref
    在 react 中我们通过 jsx 去编写 js 和 html 的混合,然后通过 babel 插件编译成对象再调用 createElement
    例子:
1
const element = <h1 className="greeting">Hello, world!</h1>

编译为:

1
2
3
4
5
6
//jsx书写的代码会变成js对象然后调用createElement把其作为参数传入
const element = React.createElement(
'h1',
{ className: 'greeting' },
'Hello, world!'
)

最后返回

1
2
3
4
5
6
7
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world'
}
}

使用 VDOM 的原因。

随着前端技术的发展,现在的网页应用变得越来越庞大,DOM 树也随之变得复杂。当我们要修改 DOM 的某个元素时,我们需要先遍历找个这个元素,然后才修改能修改。而且如果我们大量地去修改 DOM,每次 DOM 修改浏览器就得重绘甚至重排(repaint)页面,损耗了大量性能

数据驱动

react 通过数据驱动更新节点,通过更新虚拟的抽象数据来更新界面

组件化

每一个组件都拥有自己的状态(state)用其渲染复杂的界面。也可以声明无状态组件,在更高级的组件中保存 state,而子组件只负责视图部分。

react 生命周期

组件的生命周期大致分成三个状态:

  • Mounting:已插入真实 DOM,即 Initial Render

  • Updating:正在被重新渲染,即 Props 与 State 改变

  • Unmounting:已移出真实 DOM,即 Component Unmount

每个状态有两个处理函数,分为 will 和 did,分别在状态前和状态后调用

一个 react 组件完整的渲染过程

类调用:

此过程仅在类创建时被一次,即无论创建多少个 ReactElement,此过程均只会执行一次

  • getDefaultProps

实例化:

此过程仅执行一次,执行完毕后,React 组件真正被渲染到 DOM 中
期间执行生命周期函数如下:

  • getInitialState

  • componentWillMount: 组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次,此时可以修改 state。

  • render: 创建虚拟 dom,进行 diff 算法,更新 dom 树都在此进行。此时就不能更改 state。

  • componentDidMount:组件渲染完后调用,整个生命周期只调用一次

变更

此过程会在 this.state 或 this.props 变更时执行
期间执行生命周期函数如下:
this.state 变更

  • shouldComponentUpdate(nextProps, nextState):react 性能优化非常重要的一环。组件接受新的 state 或者 props 时调用,我们可以设置在此对比前后两个 props 和 state 是否相同,如果相同则返回 false 阻止更新,因为相同的属性状态一定会生成相同的 dom 树,这样就不需要创造新的 dom 树和旧的 dom 树进行 diff 算法对比,节省大量性能,尤其是在 dom 结构复杂的时候

  • componentWillUpdate(nextProps, nextState):组件初始化时不调用,只有在组件将要更新时才调用,此时可以修改 state

  • render

  • componentDidUpdate

this.props 变更

  • componentWillReceiveProps(nextProps)

  • shouldComponentUpdate

  • componentWillUpdate(nextProps, nextState)

  • render

  • componentDidUpdate

图例

react Q&A

调用 setState 之后发生了什么?

在代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。

React 中 Element 与 Component 的区别

React Element 是描述屏幕上所见内容的数据结构,是对于 UI 的对象表述。典型的 React Element 就是利用 JSX 构建的声明式代码片然后被转化为 createElement 的调用组合。而 React Component 则是可以接收参数输入并且返回某个 React Element 的函数或者类

Class Component&Functional Component

函数式组件中不包含 state 以及生命周期函数,只负责视图的部分,数据全靠外部父组件传递 props

React 中 refs 的作用

设置 ref 可以获取节点。Refs 是 React 提供给我们的安全访问 DOM 元素或者某个组件实例的句柄。我们可以为元素添加 ref 属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CustomForm extends Component {
handleSubmit = () => {
console.log('Input Value: ', this.input.value)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" ref={input => (this.input = input)} />
<button type="submit">Submit</button>
</form>
)
}
}

react 中的合成事件

React 合成事件系统
React 快速的原因之一就是 React 很少直接操作 DOM,浏览器事件也是一样。原因是太多的浏览器事件会占用很大内存。

React 为此自己实现了一套合成系统,在 DOM 事件体系基础上做了很大改进,减少了内存消耗,简化了事件逻辑,最大化解决浏览器兼容问题。

其基本原理就是,所有在 JSX 声明的事件都会被委托在顶层 document 节点上,并根据事件名和组件名存储回调函数(listenerBank)。每次当某个组件触发事件时,在 document 节点上绑定的监听函数(dispatchEvent)就会找到这个组件和它的所有父组件(ancestors),对每个组件创建对应 React 合成事件(SyntheticEvent)并批处理(runEventQueueInBatch(events)),从而根据事件名和组件名调用(invokeGuardedCallback)回调函数。

因此,如果你采用下面这种写法,并且这样的 P 标签有很多个:

1
2
3
4
5
6
7
listView = list.map((item, index) => {
return (
<p onClick={this.handleClick} key={item.id}>
{item.text}
</p>
)
})

react 会帮你进行事件委托,所以这样也是可以的

react 中 key 的作用

Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。列表渲染中每一个元素都必须带上 key 属性

受控组件&非受控组件

受控组件(Controlled Component)代指那些交由 React 控制并且所有的表单数据统一存放的组件。譬如下面这段代码中 username 变量值并没有存放到 DOM 元素中,而是存放在组件状态数据中。任何时候我们需要改变 username 变量值时,我们应当调用 setState 函数进行修改。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ControlledForm extends Component {
state = {
username: ''
}
updateUsername = (e) => {
this.setState({
username: e.target.value,
})
}
handleSubmit = () => {}
render () {
return (
<form onSubmit={this.handleSubmit}>
<input
type='text'
value={this.state.username}
onChange={this.updateUsername} />
<button type='submit'>Submit</button>
</form>
)
}
}

非受控组件则是由 DOM 保存数据,通过 ref 来操作节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class UnControlledForm extends Component {
handleSubmit = () => {
console.log("Input Value: ", this.input.value)
}
render () {
return (
<form onSubmit={this.handleSubmit}>
<input
type='text'
ref={(input) => this.input = input} />
<button type='submit'>Submit</button>
</form>
)
}
}

际开发中我们并不提倡使用非受控组件,因为实际情况下我们需要更多的考虑表单验证、选择性的开启或者关闭按钮点击、强制输入格式等功能支持,而此时我们将数据托管到 React 中有助于我们更好地以声明式的方式完成这些功能

React 中的事件处理逻辑

react 将浏览器原生时间封装为合成事件(SyntheticEvent)传入设置的事件处理器中。。这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。另外有意思的是,React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的。

在 componentDidMount 中发送 AJAX 请求

  • React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易见其会被触发多次,自然也就不是好的选择。

  • 如果我们将 AJAX 请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了 setState 函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题。

setState 之后发生了些什么

当调用 setState 时,React 会做的第一件事情是将传递给 setState 的对象合并到组件的当前状态。这将启动一个称为和解(reconciliation)的过程。和解(reconciliation)的最终目标是以最有效的方式,根据这个新的状态来更新 UI。 为此,React 将构建一个新的 React 元素树(您可以将其视为 UI 的对象表示)。

一旦有了这个树,为了弄清 UI 如何响应新的状态而改变,React 会将这个新树与上一个元素树相比较( diff )。

通过这样做, React 将会知道发生的确切变化,并且通过了解发生什么变化,只需在绝对必要的情况下进行更新即可最小化 UI 的占用空间。

render Props

Redux

react 状态管理机制

三大原则

单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

State 是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

使用纯函数来执行修改
为了描述 action 如何改变 state tree ,你需要编写 reducers。

React16

Error Boundary

Error Boundary 错误边界 当某个组件发生错误时,我们可以通过 Error Boundary 捕获到错误并对错误做优雅处理,如使用 Error Boundary 提供的内容替代错误组件。而不用整个组件树全部 unmount

利用新的生命周期 componentDidCatch 抓取错误,可以封装一个错误处理类,在容易出错的地方把组件包裹起来,可以用错误处理组件替换掉报错的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//最佳实践:将ErrorBoundary抽象为一个公用的组件类

import React, { Component } from 'react'

export default class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
componentDidCatch(err, info) {
this.setState({ hasError: true })
//sendErrorReport(err,info)
}
render(){
if(this.state.hasError){
return <div>Something went wrong!</div>
}
return this.props.children
}
}

render(){
return (
<div>
<ErrorBoundary>
<Profile user={this.state.user} />
</ErrorBoundary>
<button onClick={this.onClick}>Update</button>
</div>
)
}

##16.3 后的变化
在 react16.3 后,三个声明周期不在推荐使用

  • componentWillUpdate
  • componentWillReciveProps
  • lcomponentWillMount

取而代之新增了两个 api

  • getDerivedStateFromProps(nextProps, prevState)
  • getSnapshotBeforeUpdate(prevProps, prevState)

取消的原因:

  1. componentWillUpdate:假如组件在第一次渲染的时候被中断,由于组件没有完成渲染,所以并不会执行 componentWillUnmount 生命周期(注:很多人经常认为 componentWillMount 和 componentWillUnmount 总是配对,但这并不是一定的。只有调用 componentDidMount 后,React 才能保证稍后调用 componentWillUnmount 进行清理)。因此 handleSubscriptionChange 还是会在数据返回成功后被执行,这时候 setState 由于组件已经被移除,就会导致内存泄漏。所以建议把异步获取外部数据写在 componentDidMount 生命周期里,这样就能保证 componentWillUnmount 生命周期会在组件移除的时候被执行,避免内存泄漏的风险。

  2. componentWillReceiveProps

    componentWillReceiveProps 生命周期是在 props 更新时触发。一般用于 props 参数更新时同步更新 state 参数。但如果在 componentWillReceiveProps 生命周期直接调用父组件的某些有调用 setState 的函数,会导致程序死循环。

  3. componentWillUpdate

    componentWillUpdate 和 componentDidUpdate 这两个生命周期函数有一定的时间差(componentWillUpdate 后经过渲染、计算、再更新 DOM 元素,最后才调用 componentDidUpdate),如果这个时间段内用户刚好拉伸了浏览器高度,那 componentWillUpdate 计算的 previousScrollOffset 就不准确了。如果在 componentWillUpdate 进行 setState 操作,会出现多次调用只更新一次的问题,把 setState 放在 componentDidUpdate,能保证每次更新只调用一次。

render 方法新增返回类型

在 React 16 中,render 方法支持直接返回 string,number,boolean,null,portal,以及 fragments(带有 key 属性的数组),这可以在一定程度上减少页面的 DOM 层级。

setState 传入 null 时不会再触发更新

比如在一个选择城市的函数中,当点击某个城市时,newValue 的值可能发生改变,也可能是点击了原来的城市,值没有变化,返回 null 则可以直接避免触发更新,不会引起重复渲染,不需要在 shouldComponentUpdate 函数里面去判断。

新的 react fiberfiber

如果有 ABC 三个组件,A 在最外层,然后是 B,C 在最里层。原先组件挂载时的生命周期渲染顺序是:A 调用到 render()执行 B,B 调用到 render(),在渲染 C,C 是最里层的子组件所以生命周期会到 didmout,这时候再返回来执行 b 的 didmount 再执行 A 的 didmount。导致如果这是一个很大,层级很深的组件,react 渲染它需要几十甚至几百毫秒,在这期间,react 会一直占用浏览器主线程,任何其他的操作(包括用户的点击,鼠标移动等操作)都无法执行。

加入 fiber 的 react 将组件更新分为两个时期

phase 1
phase 2
这两个时期以 render 为分界,

render 前的生命周期为 phase1,
render 后的生命周期为 phase2
phase2 的生命周期是不可被打断的,React 将其所有的变更一次性更新到 DOM 上。
phase1 的生命周期是可以被打断的,每隔一段时间它会跳出当前渲染进程,去确定是否有其他更重要的任务。

高阶组件

代理模式
操纵 prop
抽取状态
包装组件

一个函数接收一个组件返回一个组件

  1. 抽取状态,例如一个 input 组件把它的 onchange 和 state 都写在外层包装组件里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Hocinput(InputComponent) {
return class extends Component {
state = {
name: ''
}
onInputChange = e => {
this.setState({
name: e.target.value
})
console.log(e.target.value)
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onInputChange
}
}
return <InputComponent {...this.props} {...newProps} />
}
}
}
  1. 操纵 props
    对 props 进行修改然后传入新的 props
1
2
3
4
5
6
7
8
function HocRemoveProp(WrappedComponent) {
return class WrappingComPonent extends Component {
render() {
const { user, ...otherProps } = this.props
return <WrappedComponent {...otherProps} />
}
}
}

反向继承

外层组件去继承要被包装的组件,使用 super.render 调用包装组件的 render 方法

1
2
3
4
5
6
const MyContainer = WrappedComponent =>
class extends WrappedComponent {
render() {
return super.render()
}
}