skrjs's Studio.

React学习笔记

2021/02/13

React 学习笔记

安装 react 并初始化

1、安装:npm install -g create-react-app
2、创建 hello-react 目录并初始化:npx create-react-app hello-react

注意:

  1. 目录名不允许有大写字母
  2. 初始化过程比较慢,大约需要 5-10 分钟
3、启动项目:cd hello-react、npm start

默认将启动:http://localhost:3000

自定义组件基础知识

1、自定义组件和入口文件,顶部都需要引入 “react:import React from ‘react’”;

2、自定义组件必须以大写字母开头、默认网页原生标签还以小写开头。请注意这里表述的”默认网页原生标签”本质上并不是真实的原生网页标签,他们是 react 默认定义好的、内置的自定义组件标签,只不过这些标签刚好和原生标签的作用,功能,名称一模一样而已。

3、自定义组件 “render(){return }”,必须且只能有一个最外层标签;

4、如果不希望设定最外层的标签,那么可以使用 react(16+版本)提供的占位符 Fragment 来充当最外层标签;

1
2
3
4

import React,{Component,Fragment} from 'react';
render(){return <Fragment>xxxxxxx</Fragment>}

在最新的 react 版本中,可以直接使用”<></>”来代替 Fragment。其中<>唯一可以拥有的属性为 key。即”< key=’xxx’></>”

5、如果你写了 “constructor(props){super(props);}”但是还没有写 “this.state={}”,默认会报错误警告:”Useless constructor. eslint(no-useless-constructor),constructor” 中加上 “this.state={}”即可消除警告。

6、希望数据发生改变,忘掉 DOM 操作,改成数据操作(setState())。

7、使用数组 map 循环更新 li,一定要给 li 添加对应的 key 值,否则虽然正常运行,但是会报错误警告。不建议直接使用 index 作为 key 值。

8、永远不要直接操作修改 this.state.xxx=xxx,一定要用 setState()来修改。

9、先复制出一份 this.state.xxx 的值,然后修改,修改完成后再通过 setState()赋值。

10、在最新的 react 版本中,为了提高更新性能,推荐采用异步的方式更新数据。具体使用方式为:

1
2
3

setState((prevState) => {return {xxx}})

其中参数 prevState 指之前的数据值(即 this.state),return 的对象指修改之后的数据值。pervState 参数还可以省略,即 setState(() => {return {xxx}});

setState()还可以增加第 2 个参数(回调函数),即当异步的 setState 更新完成后执行的函数。

1
2
3

setState(()=>{return {xxx}},()=>{xxxxx})

上述代码中的第 2 个参数()=>{xxxxx}就是异步执行(更新)完毕后的回调函数。

异步的目的是为了优化更新性能,react 短期内发现多条 state 发生修改,那么他会将所有修改合并成一次修改再最终执行。

11、在 “render(){return }”中写注释,格式为:”{/_ xxxxx _/}或{//xxxx}”,注意如果使用单行注释,最外的大括号必须单独占一行。注释尽在开发源代码中显示,在导出的网页中不会有该注释。

12、给标签添加样式时,推荐使用 className,不推荐使用 class。如果使用 class 虽然运行没问题,但是会报错误警告,因为样式 class 这个关键词和 js 中声明类的 class 冲突。类似的还有标签中 for 关键词,推荐改为 htmlFor。

13、直接将 xxx 赋值给某标签,作为该标签的内容时会进行转义(若 xxx 为”

xxx

“,则以纯文本形式展示)。若不想转义,则在属性中设置

1
dangerouslySetInnerHTML={{__html:xxx}}

,如无必要,尽量不要这样设置,容易造成 xss 攻击。

14、通常情况下,react 是针对组件开发,并且只负责对 html 中某一个 div 进行渲染,那么意味着该 html 其他标签不受影响,这样引申出来一个结果:一个 html 既可以使用 react,也可以使用 vue,两者可以并存。

15、为了方便调试代码,可以在谷歌浏览器中安装 React Developer Tools 插件。安装后可在谷歌浏览器调试模式下,查看 component 标签下的内容。 若访问本机 react 调试网页则该插件图标为红色、若访问导出版本的 React 网页则该插线显示为蓝色、若访问的网页没使用 react 框架则为灰色。

16、给组件设定属性 “ref={(xxx) => {this.xxx=xxx}}”,之后如果写处理函数中,想获得 DOM 元素可以不需要写 eve.target,而是直接写 this.xxx 即可找到该 DOM。但是不建议这样操作,还应该以更改数据而非直接操作 DOM 为原则。除非你想给该 DOM 元素设置焦点。

17、给组件设定属性,只有属性名没有属性值,那么默认 react 会将该属性值设置为 true。在 ES6 中如果只有一个属性对象没有属性值,通常理解为该属性名和属性值是相同的。 为了避免混淆,不建议不给属性不设置属性值。

18、属性设置中,使用{…props} 表示展开 props(props 可以是自己定义的对象,也可以值默认传递过来的参数 props),并继续向下传递。

19、若组件 “render(){return xxx}中 xxx” 为函数,则会将运行结果作为组件内容输出出去,但是如果 xxx 的值为 true、false、null、undefined 中的任意一种,这种输出结果是允许的,但是不会进行任何渲染。 结合这个特性,可以做条件渲染,例如 “{isxxx && }”,只有 isxxx 为 true 时,才会输出 “

请注意:aa && bb 这种 JS 原生语法,只有 aa 为布尔值(true 或 false)时才会按照预期来确定是否返回 bb 还是 false。aa 是数字(数字 1 除外)或字符串或空数组(只要不是布尔值),那么就都会返回 bb(而不是返回 false),为了保证每次都是预期返回,建议要用 Boolean(aa) 形式来将 aa 转化为布尔值。

相反,如果在输出时想输出布尔值 boo 为字符串,那么应该用 String(boo)将布尔值转化为字符串”true”或”false”

20、使用”<React.StrictMode></React.StrictMode>”标签包括元素,表示被包裹的元素将使用严格模式(若有非严格模式的错误,将会有对应错误警告提示)。”<React.StrictMode>”标签并不会显示在前台页面中,并且该元素仅在开发模式下启作用,在生产模式下将忽略被包裹元素的非严格模式下的错误提示。

21、ReactDOM.createPortal()用来将元素渲染到任意 DOM 元素中(包括顶级组件之外的其他 DOM 中)。

“纯函数” 概念解释

JS 中定义的所有函数都可以增加参数,所谓”纯函数”是指函数内部并未修改过该参数的函数。

例如以下函数:function myFun(a){let c=a },该函数内部从未更改过参数 a,那么这个函数就是纯函数。

反例,非纯函数 例如:function myFun(a){a=a+2; let c=a},该函数内部修改过参数 a,那么这个函数就不再是纯函数了。

纯函数的特殊意义是什么?
因为纯函数内部从不会直接修改参数,那么无论运行多少次,执行结果永远是一致的。

若仅仅有一个函数,那么也无所谓,但是如果有多个函数都是都需要调用执行同一个变量(参数),为了确保多个函数执行结果是符合预期的,那么就要求每个函数都不能在自己内部修改该变量(参数)。

在 react 中定义的 this.state 就会被作为参数供内部多个函数使用,react 要求任何函数不能直接修改 this.state 的值,确保各个函数在收到参数 this.state 时是一致的 this.state。

这就是为什么 react 不允许直接修改 this.state 的原因。

“受控组件” 概念解释

像 input、select、textarea、form 等将自身 value 与 state 进行绑定的组件,称之为受控组件。

“受控”即这些组件的可以值受到 state 的控制。

与之对应的是”非受控组件”,即该组件对应的值并不能被 state 控制。

例如”<input type=’file’/>“,该组件的值为用户选中本地的文件信息,该值并不能直接通过 state 来进行控制(设置),因此该组件属于”非受控组件”。

“声明式开发” 概念解释

“声明式开发”:基于数据定义和数据改变,视图层自动更新。
“命令式开发”:基于具体执行命令更改视图,例如 DOM 操作修改。

注意:声明式开发并不是不进行 DOM 操作,而是把 DOM 操作频率降到最低。

“单项数据流” 概念解释

react 框架的原则中规定,子组件只可以使用父组件传递过来的 xxx 属性对应的值或方法,不可以改变。

数据只能单向发生传递(父传向子,不允许子直接修改父),若子组件想修改父组件中的数据,只能通过父组件暴露给子组件的函数(方法)来间接修改。

react 框架具体实现方式是设置父组件传递给子组件的”数据值或方法”仅仅为可读,但不可修改。

为什么要做这样的限制?
因为一个父组件可以有多个子组件,如果每个子组件都可修改父组件中的数据(子组件之间彼此共用父组件的数据),一个子组件的数据修改会造成其他子组件数据更改,最终会让整个组件数据变得非常复杂。

为了简化数据操作复杂程度,因此采用单向数据流策略,保证父组件数据的唯一最终可修改权归父组件所有。

“视图层渲染框架” 概念解释

react 框架自身定位是”视图层渲染框架”,单向数据流概念很好,但是实际项目中页面会很复杂。

例如顶级组件 Root 中分别使用了组件 A(由子组件 A0、A1、A2 构成)、组件 B(由子组件 A0、A1、A2 构成)、组件 C(由子组件 C0、C1、C2 构成),若此时组件 A 的子组件 A2 想和组件 C 的子组件 C1 进行数据交互,那么按照单向数据流的规范,数据操作流程为 A2 -> A -> Root -> C - C1,可以看出操作流程非常复杂。

**所以实际开发中,React 框架必须结合其他”数据层框架”(例如 Redux、Flux 等)**。

“函数式编程” 概念解释

react 自定义组件的各种交互都在内部定义不同的函数(js 语法规定:类 class 中定义的函数不需要在前面写 function 关键词),因此成为函数式编程。不像原生 JS 和 html 交互那样,更多侧重 html 标签、DOM 操作来实现视图和交互。

函数式编程的几点好处:
1、可以把复杂功能的处理函数拆分成多个细小的函数。
2、由于都是通过函数来进行视图层渲染和数据交互,更加方便编写”前端自动化测试”代码。

“虚拟 DOM” 概念解释

虚拟 DOM(Virtual Dom)就是一个 JS 对象(数组对象),用来描述真实 DOM。相对通过 html 标签创建的真实 DOM,虚拟 DOM 是保存在客户端内存里的一份 JS 表述 DOM 的数组对象。

用最简单的一个 div 标签来示意两者的差异,数据格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
//真实DOM数据格式(网页标签)
<div id='mydiv'>hell react</div>

//虚拟DOM数据格式(JS数组对象)
//虚拟DOM数组对象格式为:标签名+属性集合+值
['div',{id:'mydiv'},'hell react']

//在JSX的创建模板代码中,通常代码格式为
render(){return <div id='mydiv'>hello react</>}

//还可以使用react提供的,更加底层的方法来实现
render(){return React.createElement('div',{id:'mydiv'},'hello react')}

虚拟 DOM 更新性能快的原因并不是因为在内存中(理论上任何软件都是运行在内存中),而是因为虚拟 DOM 储存的数据格式为 JS 对象,用 JS 来操作(生成/查询/对比/更新)JS 对象很容易。用 JS 操作(生成/查询/对比/更新)真实 DOM 则需要调用 Web Action 层的 API,性能相对就慢。

react 运行(更新)步骤,大致为:
1、定义组件数据 state
2、定义组件模板 JSX
3、数据与模板结合,生成一份虚拟 DOM
4、将虚拟 DOM 转化为真实 DOM
5、将得到的真实 DOM 挂载到 html 中(通过真实 DOM 操作),用来显示
6、监听 state 发生改变,若有改变重新执行第 3 步(数据与模板结合,生成另外一份新的虚拟 DOM)
7、在内存中对比前后两份虚拟 DOM,找出差异部分(diff 算法)
8、将差异部分转化为真实的 DOM
8、将差异化的真实 DOM,通过真实 DOM 操作进行更新

当 state 发生更改时,虚拟 DOM 减少了真实 DOM 的创建和对比次数(通过虚拟 DOM 而非真实 DOM),从而提高了性能。

“Diff 算法” 概念解释

当 state 发生改变时,需要重新生成新的虚拟 DOM,并且对旧的虚拟 DOM 进行差异化比对。
Diff 算法就是这个差异化比对的算法。

Diff 算法为了提高性能,优化算法,通常原则为:

同层(同级)虚拟 DOM 比对

先从两个虚拟 DOM(JS 对象)同层(即顶层)开始比对,如果发现同层就不一致,那么就直接放弃下一层(级别)的对比,采用最新的虚拟 DOM。

疑问点:假如两心虚拟 DOM 顶层不一致,但下一级别以及后面的更多级别都一致,如果仅仅因为顶层不一致而就该放弃下一级别,重新操作真实 DOM 从头渲染,岂不是性能浪费?

答:同层(同级)虚拟 DOM 比对,”比对”算法相对简单,比对速度快。如果采用多层(多级)比对,”比对”算法会相对复杂,比对速度慢。 同层虚拟 DOM 比对就是利用了比对速度快的优势来抵消”操作真实 DOM 操作性能上的浪费”。

列表元素使用 key 值进行比对

这里的 key 值是值”稳定的 key 值(是有规律的字符串,非数字)”,若 key 值为索引数字 index,那么顺序发生改变时,索引数字也会发生变化,无法判断之前的和现在的是否是同一个对象。

如果 key 值是稳定的,那么在比对的时候,比较容易比对出是否发生变化,以及具体的变化是什么。

Diff 算法还有非常多的其他性能优化算法,以上列出的”同层比对、key 值比对”仅仅为算法举例。

“高阶组件” 概念解释

高阶组件是一种组件设计方式(设计模式),就是将一个组件作为参数传递给一个函数,该函数接收参数(组件)后进行处理和装饰,并返回出一个新的组件。

简单来说就是,普通组件是根据参数(props)生成一个 UI(JSX 语法支持的标签)。而高阶组件是根据参数(组件)生成一个新的组件。

“生命周期函数” 概念解释

生命周期函数指在某一时刻组件会自动调用执行的函数。

这里的”某一时刻”可以是指组件初始化、挂载到虚拟 DOM、数据更改引发的更新(重新渲染)、从虚拟 DOM 卸载这 4 个阶段。

生命周期 4 个阶段和该阶段内的生命周期函数:

初始化(Initialization)

constructor()是 JS 中原生类的构造函数,理论上他不专属于组件的初始化,但是如果把它归类成组件组初始化也是可以接受的。

挂载(Mounting)

componentWillMount(即将被挂载)、render(挂载)、componentDidMount(挂载完成)

更新(Updation):

props 发生变化后对应的更新过程:componentWillReceiveProps(父组件发生数据更改,父组件的 render 重新被执行,子组件预测到可能会发生替换新数据)、shouldComponentUpdate(询问是否应该更新?返回 true 则更新、返回 flash 则不更新)、componentWillUpate(准备要开始更新)、render(更新)、componentDidUpdate(更新完成)

states 发生变化后对应的更新过程:shouldComponentUpdate(询问是否应该更新?返回 true 则更新、返回 flash 则不更新)、conponentWillUpdate(准备要开始更新)、、render(更新)、componentDidUpdate(更新完成)

props 和 states 发生变化后的更新过程,唯一差异是 props 多了一个 componentWillReceiveProps 生命周期函数。

componentWillReceiveProps 触发的条件是:
1、一个组件要从父组件接收参数,并且已存在父组件中(子组件第一次被创建时是不会执行 componentWillReceiveProps 的)
2、只要父组件的 render 函数重新被执行(父组件发生数据更改,子组件预测到可能会发生替换新数据),componentWillReceiveProps 就会被触发

捕获子组件错误:

componentDidCatch(捕获到子组件错误时被触发)

卸载(Unmounting):

componentWillUnmount(即将被卸载)

备注:自定义组件继承自 Component 组件,Component 组件内置了除 render()以外的所有生命周期函数。因此自定义组件 render()这个生命周期函数必须存在,其他的生命周期函数都可以忽略不写。

生命周期函数的几个应用场景:

1、只需要第一次获取数据的 Ajax 请求
如果组件有 ajax 请求(只需请求一次),那么最好把 ajax 请求写在 componentDidMount 中(只执行一次)。因为”初始化、挂载、卸载”在一个组件的整个生命周期中只会发生一次,而”更新”可以在生命周期中多次执行。

2、防止子组件不必要的重新渲染
若父组件发生 state 改变,那么会调用 render(),会重新渲染所有子组件。但是如果 state 改变的某个值与某子组件并不相关,如果此时也重新渲染该子组件会造成性能上的浪费。为了解决这个情况,可以在子组件中的 shouldComponentUpdate 生命周期函数中,做以下操作:

shouldComponentUpdate(nextProps,nextStates){
  //判断xxx值是否相同,如果相同则不进行重新渲染
  return (nextProps.xxx !== this.props.xxx); //注意是 !== 而不是 !=
}

对于类组件(由 class 创建的)和函数组件(由 function 创建的),他们对于生命周期的调用方法不同。

类组件:直接在类中定义函数;
函数组件:需要用到 hook(钩子),具体用法参见 Hook 用法;

React 中设置样式的几种形式

第一种:引用外部 css 样式

伪代码示例:

1
2
import from './xxx.css';
return <div className='xxx' /\>

注意:在 jsx 语法中,使用驼峰命名。例如原生 html 中的 classname 需要改成 className、background-color 要改成 backgroundColor。

第二种:内部样式

伪代码示例:

1
return <div style={{backgroundColor:'green',width:'100px'}} /\>

注意:内联样式值为一个对象,对象属性之间用”,”分割而不是原生 html 中的”;”。
因为是一个对象,因此下面代码也是可行的:
const mystyle = {backgroundColor:’green’,width:’100px’};
return <div style={mystyle} />

React 中数据传递的几种方式

在实际场景中,组件往往是由多级组件组合而成。组件之间数据传递(数据绑定)有多重形式,需要根据具体也许需求来选择使用哪种传递方式。

注:这里说的”数据传递”包含以下几层意思:
1、数据的获取
2、数据的修改(通过父级暴露给子组件函数来实现修改,这种方式也称为 “render props”)
3、根据数据变化重新渲染

以下文字描述中:
1、将顶级组件称之为”父组件”,用来代替
2、将实际业务组件称之为”子组件”,用来代替
3、将子组件中的子组件称之为”孙组件”,用来代替

以上对组件的称呼仅仅是为了区别组件,事实上”子组件相对孙组件也可以称之为父组件”。

伪代码提示:
1、为了简化示例代码,省略了组件代码中 import 相关代码。
2、只演示数据向下传递,不演示修改上级数据
3、修改上级数据的方式是通过父组件定义修改数据的函数,并将该函数像数据一样传递给子组件或孙组件,子组件或孙组件通过调用该函数并传入修改值来实现上级数据修改。

第 1 种:默认设置属性传递

实现方式:父组件通过对子组件添加自定义属性和属性值来传递数据。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
//父组件给子组件添加属性num,值为2
<Parent>
constructor(props){
super(props);
this.state = {
num:'2'
}
}
<Me num={this.state.num}/>
</Parent>

//子组件获取num的值
{this.props.num}

若父组件要给孙组件传递数据,则每一层都需要进行接力传递。

1
2
3
4
5
6
7
8
9
10
11
<Parent>
constructor(props){
super(props);
this.state = {
num:'2'
}
}
<Me num={this.state.num}>
<Son num={this.props.num} />
</Me>
</Parent>

优点:简单直接
缺点:需要层层传递,即使中间级别的组件不需要该数据,但是他也必须添加该属性,以保证数据能够接力向下传递。

第 2 种:使用组件组合传递

实现方式:若中间级别的组件不需要某属性,那么他可以采用{…}的形式将自身属性值传递给下一级组件中。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
<Parent>
constructor(props){
super(props);
this.state = {
num:'2'
}
}
<Me num={this.state.num}>
<Son num={...} />
</Me>
</Parent>

有点:中间级别的组件减少代码冗余(只是看上去减少了一些而已)
缺点:数据依然需要层层传递

第 3 种:使用 Context 传递

实现方式:

第 1 步:首先使用 React.createContext([defaultValue])来声明一个公共数据对象,例如 GlobalContext,可单独保存为 global-context.js。

说明如下:
1、可以声明多个不同的公共数据对象,并不要求必须全局唯一。
2、[defaultValue]为可选默认值,若父组件中未定义 value 属性值,则使用 defaultValue 作为默认要向下传递的数据值。

第 2 步:父级组件(顶级组件)中,进行以下操作:
1、引入该公共数据对象
2、添加静态对象 static contextType = GlobalContext
3、使用<GlobalContext.Provider value=’xxx’></GlobalContext.Provider>标签包裹要输出的组件代码,value=’xxx’就是定义要传递给子组件或孙组件的数据。

第 3 步:子组件或孙组件中,若不需要获取 GlobalContext 的值可以不做任何特殊处理,仅在需要获取 GlobalContext 的值的组件中,进行以下操作:
1、引入该公共数据对象
2、添加静态对象 static contextType = GlobalContext
3、使用<GlobalContext.Consumer value=’xxx’>{context => {//xxxxx }}</GlobalContext.Consumer>标签包裹要输出的组件代码,使用{this.context}来获取 GlobalContext 数据的值。

代码示例:

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
//父组件给子组件或孙组件添加属性num,值为2
import GlobalContext from './global-context'; //引入GlobalContext
<Parent>
static contextType = GlobalContext; //设置静态属性contextType
constructor(props){
super(props);
this.state = {
num:'2'
}
}
<Me value={this.state.num} />
</Parent>

//子组件或孙组件想获取num的值,操作如下
import GlobalContext from './global-context'; //引入GlobalContext
<Me>
static contextType = GlobalContext; //设置静态属性contextType
render() {
return <GlobalContext.Consumer>
{
context => {
return xxxxx; //xxxxx为具体的业务jsx,若想获取num,则使用{this.context}即可
}
}
</GlobalContext.Consumer>
}
</Me>

再次提醒:父组件使用<GlobalContext.Provider></GlobalContext.Provider>、子组件或孙组件使用<GlobalContext.Consumer></GlobalContext.Consumer>,且格式为<GlobalContext.Consumer>{context => { return xxxxx;}}</GlobalContext.Consumer>

第 4 种:使用第三方数据管理 Redux

实现方式:引入第三方该数据管理模块 redux。

Hook 用法

Hook 是 react16.8 以上版本才出现的新特性,可以在函数组件中使用组件生命周期函数,且颗粒度更加细致。

可以把 Hook 逻辑从组件中抽离出来,多个组件可以共享该 hook 逻辑。

请注意 hook 本质上是为了解决组件之间共享逻辑,并不是为了解决组件之间共享数据。

hook 表现出来特别像一个普通的 JS 函数(仅仅是表现出来但绝不是真的普通 JS 函数),如果加以扩展,是可以做到一定程度上组件之间共享数据的。

诞生 Hook 特性的背景原因:

第一个原因:在以前版本中,函数组件不支持组件生命周期函数,若想使用生命周期函数必须使用类组件,即由 class Xxx extends Component 这种形式来定义的组件。

第二个原因:类组件在使用过程中,常见有以下几个”痛点”:
1、函数中的 this 用法稍显复杂(js 中的 this 都很复杂),函数必须 bind(this)或者使用箭头函数。

2、类组件中生命周期函数固定,无法提供更细致的颗粒度。
举个例子:
假设你在 componentDidUpdate 函数中需要自定义 a,b,c 3 个变量,那么你必须把 a,b,c 3 件事情都写在 this.state = { a,b,c}中。

若你需要维护一个已经写好的组件,而该生命周期函数中存在 a,b 两个变量,那么你新增加 c 时,你还必须完整维护好 a,b 两个变量(添加 c 时千万别手滑修改了 a 或 b)。

我们更希望能够将 c 变量从 this.state 中分离出去,只去关心 c 就好,无需维护保持 a,b。因此就需要对生命周期函数有更加细致的颗粒度控制能力。

把上面这段话中的”变量”替换为”执行代码”也是合理的。

使用 Hook 以后,你可以单独把 c 从 state 中抽离出去、或者可以表达为:单独把 c 执行代码从某个生命周期函数中抽离出去。

3、类组件在个别情况下容易引发无意义的重新渲染。

4、类组件使用内部自定义数据稍显复杂(需要使用 this.state.xxx 这种形式)。使用 Hook 以后直接使用{xxxx}即可获取。

5、类组件中内部定义 state 数据会随着变化而变化,最初的 state 数据不会做备份保留。

6、若希望在类组件挂载后或数据更新后执行一些操作,那么你需要分别在 componentDidMount 和 componentDidUpdate 函数中分别写入相同的执行代码。使用 Hook 以后则 2 者合并为一个函数 useEffect 中。

7、类组件操作原生 DOM 不方便。使用 Hook 以后,就像普通 JS 函数操作原生 DOM 一样,非常简单。

基于以上原因,在 react 16.8 中引入了 Hook 这个概念。React 官方声明不会因为 Hook 的出现而取消或削弱类组件。

备注:像其他前端库,例如 Vue 也都有 Hook 用法。

Hook 的用法:

useState:用来在函数组件中自定义变量

使用方法:数组结构的语法 + useState(value)来实现组件内部自定义数据。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React,{useState} from 'react';
function Example(){
//通过以下语法来内部自定义数据
//xxxx:定义的变量名
//setXxxx:定义更改xxxx对应的函数名
//xx:定义的默认值
const [xxxx,setXxxx] = useState(xx);

//函数组件想获取xxxx变量值,即:{xxxx}

//想修改xxxx变量值需要做以下判断
//若xxxx为简单值(string、number),可以直接采用setXxxx(newValue)进行修改
//若xxxx为包含引用类型的值(例如为object {a:xx,b:xx}),若仅想修改属性b(其他属性不变)
//修改方式为setXxxx({...xxxx,b:newBValue})

return (//...)
}

数组结构赋值的用法,在上面代码中 const [xxxx,setXxxx] = useState(xx)
等价于以下代码:

1
2
3
const resArr = useState(xx);
const xxxx = resArr[0];
const setXxxx = resArr[1];

由此可见数组结构赋值可以使代码更加简洁。

useEffect:组件某些生命周期函数后执行额外的代码,也称为”副作用(额外的操作)”

“某些生命周期函数”是指这 3 个:componentDidMount、componentDidUpdate、componentWillUnmount

使用方法:直接在函数组件中使用 useEffect(() => {//…});,若该方法中有 return 返回函数,那么表示当组件生命周期函数执行完毕后,调用此 retrun 的返回函数。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
import React,{useState,useEffect} from 'react';
function Example(){
useEffect(()=>{
//组件生命周期函数执行前 对应的自定义处理代码
return () => {
//组件生命周期函数执行完毕后 对应的自定义处理代码
}
});

return (//...)
}

请注意,在 useEffect 中第 1 个参数()=>{},你可能会有疑惑,为什么不在其他地方单独定义一个函数,而是选择每次都返回一个不同的函数?
这是为了确保 effect 每次获取到的都是最新的值,而不用担心过期。 可以将每次新生成的()=>{}当成专门为此次渲染之后打造的执行函数。

若函数组件或自定义 Hook 中出现多个 useEffect,则会按照他们定义的先后顺序,依次执行。

使用 useEffect,会在组件渲染之后就执行一次。这里的”渲染之后”包括 3 个生命周期函数(componentDidMount、componentDidUpdate、componentWillUnmount)。

对于同一处的 useEffect,每次执行 useEffect 之前,会清理掉上一次执行的效果。

为什么要这样做?
为了确保代码的执行正确、一致性,为了避免可能因为忘记添加更新逻辑而出现的 bug。

该如何优化性能?
如果是类组件,可以在 componentDidUpdate 中添加 prevProps 或 prevState 值的对比,如果值未发生变化则通过 return false 来阻止重新渲染,以提高性能。
那么函数组件则可以通过给 useEffect()添加第 2 个参数(只有第 2 个参数发生变化时再会执行 useEffect 里的内容),以达到优化性能的目的。第 2 个参数通常被称为”依赖列表”。

代码示例:useEffect(()=>{//…},[xxxx]);
只有在 xxxx 发生真的变化后才会执行 useEffect 中第 1 个参数 ()=>{//…} 中的代码

如果你传入第 2 个参数是一个空数组[],那么表示该 useEffect 只会在第一次被挂载时执行一次。以后发生的 componentDidUpate、componentWillUnmount 生命周期函数时,均不会再次执行该 useEffect。

函数组件中定义(出现)多个 useState 和多个 useEffect,他们是如何对应(关联)的?
是按照他们定义(出现)的先后顺序进行 useState 和 useEffect 一一对应的。

代码示例:

1
2
3
4
useState('Aa');
useEffect(()=>{});
useState('Bb');
useEffect(()=>{});

通过定义的先后顺序(成对出现),useEffect 会找到上一个最近定义的 useState,认为该 useState 是自己所需要关注的。当这个 useState 发生变化时则会执行自身(useEffect)。

这也是为什么不允许在 if/for/嵌套函数中使用 hook 的原因,因为那些语句本身包含有条件判断或逻辑,不能保证每一次执行的结果都是按照”固定顺序定义 hook”的,这样做很容易造成 bug。如果你这样做了,react 自带的 lint 插件会有错误警告提示。

如果就是需要用到 if 做一些判断来决定是否执行某些代码,可以将 if 放到 useEffect 中。这样是比较安全稳妥的做法。

useContext:获取 react 中自定义的共享数据对象 Context

若使用 函数组件 + Hook,不建议再使用 Context 来共享数据。Hook 已经可以让我们非常自由的去定义自己的数据(Hook 本身就像一个普通的 JS 函数)。

使用方法:const xxx = useContext(XxxContext)

代码示例:

1
2
3
4
5
6
7
8
9
10
//假设我们有一个自定义GlobalContext,文件名为global-context.js
//先引入GlobalContext
import GlobalContext from './global-context.js';

//函数组件或自定义Hook函数
function Example(){
//订阅GlobalContext
const globalContext = useContext(GlobalContext);
//备注:若在类组件中订阅,则使用代码 static globalContext = GlobalContext;
}

以上为基础的 Hook 函数,以下为其他 Hook 函数

useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebuValue
Hook 使用规则

1、Hook 只能在函数组件或自定义 Hook 中使用(类组件或者普通 JS 函数中均不可使用 Hook);
2、必须在函数内的最外层(不允许在 if/for/嵌套函数中使用)
3、使用次数不限(可多次使用),但是他们的定义先后顺序很重要。应该让 useEffect 紧随着需要关注的 useState;
4、可以从函数组件中抽离出来到一个单独的函数中,该函数通常称为”自定义 Hook”;

自定义 Hook

函数命名:约定自定义 Hook 函数名采用驼峰命名法,且必须以 use 开头,即 useXxxx;
补充说明:
1、react 自带 linter 插件来检测是否按照要求来命名,若不以 use 开头则会有错误警告。
2、实际中为了更加语义化,还可以命名为 useXxxStatus,表明该函数是用于处理 XxxStatus 状态的函数。

使用方法:将函数组件中 Hook 相关函数抽离出来,单独放在一个函数内,该函数即自定义 Hook。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React,{useState,useEffect} from 'react';
import GlobalContext from './global-context.js';

function useXxxx([param]){
const [xxx,setXxx] = useState(defaultValue);
const globalContext = useContext(GlobalContext);
useEffect(() => {
//...
return () => {
//...
}
});
}

// 当然你也可以使用箭头函数来自定义hook
useXxxx = ([param]) => {
}

将 Hook 抽离出函数组件的一个好处是可以实现多个组件共用该 hook。

CATALOG
  1. 1. React 学习笔记
    1. 1.1. 安装 react 并初始化
      1. 1.1.0.0.1. 1、安装:npm install -g create-react-app
      2. 1.1.0.0.2. 2、创建 hello-react 目录并初始化:npx create-react-app hello-react
      3. 1.1.0.0.3. 3、启动项目:cd hello-react、npm start
  • 1.2. 自定义组件基础知识
  • 2. xxx
    1. 2.1. “纯函数” 概念解释
    2. 2.2. “受控组件” 概念解释
    3. 2.3. “声明式开发” 概念解释
    4. 2.4. “单项数据流” 概念解释
    5. 2.5. “视图层渲染框架” 概念解释
    6. 2.6. “函数式编程” 概念解释
    7. 2.7. “虚拟 DOM” 概念解释
    8. 2.8. “Diff 算法” 概念解释
      1. 2.8.0.0.1. 同层(同级)虚拟 DOM 比对
      2. 2.8.0.0.2. 列表元素使用 key 值进行比对
  • 2.9. “高阶组件” 概念解释
  • 2.10. “生命周期函数” 概念解释
    1. 2.10.0.1. 生命周期 4 个阶段和该阶段内的生命周期函数:
      1. 2.10.0.1.1. 初始化(Initialization)
      2. 2.10.0.1.2. 挂载(Mounting)
      3. 2.10.0.1.3. 更新(Updation):
      4. 2.10.0.1.4. 捕获子组件错误:
      5. 2.10.0.1.5. 卸载(Unmounting):
      6. 2.10.0.1.6. 生命周期函数的几个应用场景:
  • 3. React 中设置样式的几种形式
    1. 3.0.0.0.1. 第一种:引用外部 css 样式
    2. 3.0.0.0.2. 第二种:内部样式
  • 4. React 中数据传递的几种方式
    1. 4.1. 第 1 种:默认设置属性传递
    2. 4.2. 第 2 种:使用组件组合传递
    3. 4.3. 第 3 种:使用 Context 传递
    4. 4.4. 第 4 种:使用第三方数据管理 Redux
  • 5. Hook 用法
    1. 5.1. 诞生 Hook 特性的背景原因:
    2. 5.2. Hook 的用法:
      1. 5.2.0.0.1. useState:用来在函数组件中自定义变量
      2. 5.2.0.0.2. useEffect:组件某些生命周期函数后执行额外的代码,也称为”副作用(额外的操作)”
      3. 5.2.0.0.3. useContext:获取 react 中自定义的共享数据对象 Context
      4. 5.2.0.0.4. useReducer
      5. 5.2.0.0.5. useCallback
      6. 5.2.0.0.6. useMemo
      7. 5.2.0.0.7. useRef
      8. 5.2.0.0.8. useImperativeHandle
      9. 5.2.0.0.9. useLayoutEffect
      10. 5.2.0.0.10. useDebuValue
      11. 5.2.0.0.11. Hook 使用规则
      12. 5.2.0.0.12. 自定义 Hook