React(一)

第一章 React基础

React简介

What

  • 用于构建用户界面的JS库 (操作DOM呈现页面,也即只关注界面)
  • 将数据渲染为HTML视图的开源JS库

Why

原生的缺点:

  • 原生JS操作DOM频繁,效率低 (DOM — API操作UI)(尽管jQuery通过包装减少了代码量的书写,但在效率上没有任何提升)

  • 使用JS直接操作DOM,浏览器会进行大量重绘重排

  • 原生JS没有组件化编码方案,代码复用率低

    模块化 —— 将JS的根据作用分成对应功能的模块

React的特点:

  • 采用组件化模式、声明式编码,提高开发效率及组件复用率

    命名式 与 声明式

  • ReactNative中可以使用React语法进行移动端开发

  • 使用虚拟DOM + 优秀的Diffing算法,尽量减少与真实DOM的交互

如下图所示,原生JS缺少代码复用,当你想改变页面中的某些元素时,结果往往是使用新的DOM将以前的完整覆盖掉,而以前的数据并没有派上用场

IMG_0087

原生JS实现

F3A6B2F5422143F03CC64B9BF37CAE96

React实现

D015342A860D3CC92E1DFB6ADFD31F49

How

前置知识:判断this的指向、class、ES6语法规范、npm包管理器、原型&&原型链、数组常用方法、模块化


Hello React

我们以实例作为引入React知识点的例子,这里我们用到三个react的js,分别是babel.minreact.developmentreact-dom.development

babel 在ES6中起到的作用是转码,即将ES6转换为ES5,而在react中起到的作用是将jsx转换为js(浏览器无法直接识别jsx文件)

react.development是react核心库,

react-dom.development是react扩展库,用来操作dom

我们写下以下代码

image-20220201102626707

进入chorme调试

image-20220201102652446

这里的警告,说明咱们的目前的入门写法存在一些问题,即当我们使用浏览器加载此文档时,浏览器发现了babel,就会立刻进行翻译,而如果jsx的代码繁多且复杂时,耗费的时间会非常长,这种状况不适合于大型开发中,至于开发中如何去做,会在下文中予以解决


虚拟DOM的两种创建方法

  • 使用jsx创建虚拟DOM(同上)

    image-20220201103919154

  • 使用js创建虚拟DOM

    image-20220201104434857

我们一般更喜欢使用jsx,因为当我们需要创建一个多层嵌套的标签时,如果使用js,我们需要在createElement中多次调用它自己,使代码变得冗长且没味,而使用jsx只需如下图所示

image-20220201104923955

或者是

image-20220201105116868


虚拟DOM与真实DOM

关于虚拟DOM:

  • 本质是Object类型的对象(一般对象)
  • 虚拟DOM比较”轻“,真实DOM比较”重“,因为虚拟DOM使React内部在用,无需真实DOM上那么多属性
  • 虚拟DOM最终会被React转化为真实DOM,呈现在页面上

jsx语法规则

  • 全称: JavaScript XML

    XML早期用于存储和传输数据,后来存储和传输数据使用JSON

  • react定义的一种类似于XML的JS扩展语法: JS + XML本质是React.createElement(component, props, …children)方法的语法糖

  • 作用: 用来简化创建虚拟DOM

​ 写法:var ele = Hello JSX!</h1>

​ 注意1:它不是字符串, 也不是HTML/XML标签

​ 注意2:它最终产生的就是一个JS对象

  • 标签名任意: HTML标签或其它标签

  • 标签属性任意: HTML标签属性或其它

  • 基本语法规则

​ 遇到 <开头的代码, 以标签的语法解析: html同名标签转换为html同名元素, 其它标签需要特别解析

​ 遇到以 { 开头的代码,以JS语法解析: 标签中的js表达式必须用{ }包含

  • babel.js的作用

​ 浏览器不能直接解析JSX代码, 需要babel转译为纯JS的代码才能运行

​ 只要用了JSX,都要加上type=”text/babel”, 声明需要babel来处理

image-20220201112334780


何为JS表达式

要注意区分:【js语句(代码)】与【js表达式】

  1. 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方

    以下都是表达式:(都有返回值)

    (1) a

    (2) a + b

    (3) demo(1)

    (4) arr.map() //map方法用于加工数组

    (5) function test() {}

  2. 语句(代码):

    下面这些都是语句(代码):(控制代码走向。没有值)

    (1) if () {}

    (2) for () {}

    (3) switch () {case: xxxx}

    以下是一个遍历的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script type="text/javascript">

const data = ['Angular', 'React', 'Vue']
//下面有一些可能出错的地方——index,我们在接下来也会j
const VDOM = {
<div>
<h1>遍历!</h1>
<ul>
{
data.map((item, index) ==> {
return <li key = {index}>{item}</li>
})
}
</ul>
</div>
}

</script>

遍历中,列表中的每一个元素都要有一个唯一值key


组件与模块&&模块化与组件化

  • 模块

    理解:向外提供特定功能的js程序,一般就是一个js

    为什么要拆成模块:随者业务逻辑增加,代码越来越 多且复杂

    作用:复用js,简化js的编写,提高js运行效率

  • 组件

    理解:用来实现局部功能效果的代码和资源的集合(html/css/js/image )等等

    为什么:一个界面的功能更复杂

    作用:复用编码,简化项目编码,提高运行效率

  • 模块化

    当应用的 js 都以模块来编写的,这个应用就是一个模块化的应用

  • 组件化

    当应用是以名组件的方式实现,这个应用就是一个组件化的应用


第二章 React面向组件编程

函数式组件

image-20220201145146419

此时,组件内部的this是undefined,其原因是,当babael翻译完代码之后,会开启严格模式,禁止自定义函数里的this指向window

执行了React.render(……)之后,发生了什么?

​ 1.React解析组件标签,找到了MyComponent组件

​ 2.发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转换为真实

​ DOM,随后呈现在页面中


类的复习

image-20220201165412378


p1.speak.call({a:1, b:2}),call有一个重要的功能,即是更改函数里的this指向,而这时由于没有a、b,this的值就是undefined

image-20220201171906269

super必须第一个用欧~

总结

  • 类中的构造器不是必须要写的,要对示例进行一些初始化操作,如添加指定属性时才写
  • 如果A类继承了B类,且A类中写了构造器,那么A类构造器中的super是必须要调用的
  • 类中所定义的方法,都是放在了类的原型对象上

类式组件

image-20220202093028922

执行了React.render(……)之后,发生了什么?

​ 1.React解析组件标签,找到了MyComponent组件

​ 2.发现组件是使用类定义的,随后new出来该类实例,并通过该实例调用到原型的render方法

​ 3.将render返回的虚拟DOM转为真实DOM,随后呈现到页面中


简单组件 && 复杂组件

如果组件是有状态(state)的,就是复杂组件,反之则为简单组件

那么,什么是state?


组件实例的三大属性之一 — state

状态 影响 行为
组件 状态 驱动 页面

React中的事件绑定

我们先来看一下原生的绑定方式

image-20220202104346136

React中,三种方法都可以,推荐第三种

image-20220202105448711


类中方法this的指向

image-20220202112639924

要想解决此问题,可以添加如下图25行的语句

image-20220202113217130

this.changWeather.bind(this)this.changWeather找到了原型中的changeWeather函数,而bind有两个作用,一个是生成新的函数,另一个是修改this的指向,而传入的this(构造器中的this),就是Weather的实例对象


setState

我们使用内置API来更改状态(state)

image-20220202120129739

简写

我们自己写的函数,大部分都是作为回调函数起到交互作用,他们的this都不是指向实例函数的,当我们想引用大量回调函数时,免不了大量使用bind进行this的转移

而类中可以直接写赋值语句,其意为,向对应的实例对象中添加一个名x值x的属性(第22行)

箭头函数本身没有this,但在其中使用this并不会报错,而是会找其外层函数的this来使用(第29行)

这样,我们便无需使用构造器了

image-20220203083816972


state总结

1.理解:

  • state是组件对象最重要的属性, 值是对象(可以包含多个key-value的组合)

  • 组件被称为”状态机”, 通过更新组件的state来更新对应的页面显示(重新渲染组件)

2.注意:

  • 组件中render方法中的this为组件实例对象

  • 组件自定义的方法中this为undefined,如何解决?

​ (1) 强制绑定this: 通过函数对象的bind()

​ (2)箭头函数

  • 状态数据,不能直接修改或更新

组件实例的三大属性之一 — props

当我们想从外部获取信息,而不是从内部状态中读出来的,这时我们就不能使用state了,就比如以下状态

image-20220203085453732

当我们想显示上述状态时

image-20220203085527908

我们需要多次渲染,这么写未免有些麻烦,React给我们提供了props属性,使用方法如下

image-20220203090341458

也可以先解构赋值,减少代码的书写量

image-20220203090628148


批量传递props

image-20220203093739383

为什么能用{…p}的形式展开一个对象?在下一个模块我们来分析这个问题


… — 展开运算符

展开运算符可以展开数组,但是不能展开一个对象

image-20220203093220382

可以看到,{…p}本来是起到复制作用的,但是React和bable使其具有了展开对象的作用,需要注意的是,这种作用仅仅限于标签的传递


对props进行限制

在下图中,我们可以看见两种对对象传入age的格式(第一种必须加引号,否则会构成语法错误)

image-20220203093804517

当我们相对所有页面元素加一时

image-20220203093947740

如果这样操作,只有第二种方式成功加一,而第一种方式的数字则只进行了字符串拼接,而如果第一种想传入数字,而非字符串,应该使用

1
ReactDOM.render(<Person name = "秦" age = {18} sex = "男" />, document.getElementById('test1'))

在此时,我们可以通过propTypes,向创建者 — Person,添加一些“要求”,或者说,“规则”

需要注意的是,要注意分清propTypesPropTypes,前者可以认为是React里的一个规矩,后者是一个内置的属性

image-20220203100210377

需要注意的是,在15版本之后,这个属性单独作为一个库存在,使用需引入,并且不用写React

image-20220203100931318

如果要求必须加某一项,就在后面写required

1
name: PropTypes.string.isRequired

同时,如果指定要求为函数时,为避免与function关键词冲突,应使用func

1
speak: PropTypes.func

指定默认值如下dexxx

image-20220203101251452


props的简写方式

props是只读的,因此我们想直接通过如this.props.name = 'jack',这样进行更改,是错误的!!!

我们可以把限制塞进类里

image-20220203103042946


类式组件中的构造器与props

image-20220203103925102

通过上述学习我们知道,constructor完全可以用 = 和 箭头函数 替代,而当我们使用constructor时,如果不传入props,其中的this.props就没有接到对象,我们也就不能通过 实例.props 修改值了,即构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props


函数式组件与使用props

对于函数function,由于没有实例,所以理论上三个实例属性(state/props/rerfs)都不能使用,但是,props可以使用,其原因在于,函数可以接收参数

1
2
3
4
5
6
7
8
9
10
11
function Person (props) {
const {name, age, sex} = props
return {
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
<ul>

}
}

props总结

理解

  • 每个组件对象都会有props(properties的简写)属性
  • 组件标签的所有属性都保存在props中

作用

  • 通过标签属性从组件外向组件内传递变化的数据

  • 注意: 组件内部不要修改props数据-


组件实例的三大属性之一 — ref

字符串型的ref

当我们引用标签之前,在原生JS中,往往会给对应的标签打上id,如下图所示

image-20220204101228434

而在React中,我们也可以向ref中写入

image-20220204102116411

image-20220204102148815

可以发现,refs中被写入了类似于id的“特征”,分别代表了其对应的标签,而此时,我们也可以通过refs使用标签,同时我们也可以使用解构赋值来减少代码书写量

image-20220204102926911

ref收集的不是虚拟DOM,而是其在转成真实DOM之后对应的标签


回调形式的ref

上面我们通过由id到ref的方式简单介绍了ref,但很遗憾的是,因为存在的效率问题,直接使用字符串形式的ref目前已经不被官方推荐使用了,甚至在将来可能被废弃掉(官方文档明确说明了“未来版本可能移除”),因此,我们还要学习两种ref的使用方式

我们将ref改写为回调函数形式,ref=()=>{console.log('@')},发现回调函数确实执行了,在打印台上输出了’@’,那么,这个回调函数是否接到了参数,这个参数又是什么?

打印一下

image-20220204104004901

image-20220204104230319

可以发现,它的参数正是ref所处的节点

因此我们可以这样写

image-20220204105320516

当实例调用render时会触发我们写的回调函数ref,并在调用时将当前所处的节点传了进去

(currtentNode)=>{this.input = currtentNode}(currentNode - 当前节点),指的是,把节点放到组件实例自身上(这里的this,由于箭头函数本身没有this,而是会找其外层函数的this来使用,所以这里的this就是render的this,即组件的实例对象),给其起名为input1

当箭头函数左侧只有一个参数,小括号可以不要;右侧只有一句函数体,花括号可以不要,最终如下所示

ref={c => this.input = c}

因此在我们取用ref时,不从ref自身取,而是在其被存放的实例对象上取用,最终成品如下

image-20220204110023666


回调形式ref调用次数的问题

先参考官网文档

image-20220204110525605

这里给一个内联函数的例子

ref={(currentNode)=>{this.input1 = currentNode; console.log(‘@’, currentNode);}}

控制台输出:(注意的是,页面渲染时的那次不算跟更新)

第一次调用:@ null — 清空动作

第二次调用:@ 标签

jsx怎样注释?

{/ …… /}

那么正确的形式该怎样写呢?

修改ref

ref={this.saveInput}

同时

image-20220204111856463


createRef的使用

我们可以通过方法创建一个容器,并把标签“塞到”里面

image-20220204112911712

由于每个容器是”专人专用“的,所以后放入的,会把前面的顶掉,所以针对不同的标签,要用不同的容器进行存取

image-20220204112703464

可以看到input存储在容器中,我们要想调用它,应该这样写

alert(this.myRef.current.value)


React中的事件处理

(1)通过onXxx属性指定事件处理函数(注意大小写!)

​ a. React使用的是自定义(合成)事件,而非原生的DOM事件

​ b. React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)

(2)通过event.target得到发生事件的DOM元素对象 (请勿过度使用ref


受控组件与非受控组件

页面中所有输入类的DOM,随着输入,就能把内容维护到状态中去。这就是受控

而“现用现取”,就是非受控


EXP 高阶函数 && 函数柯里化

image-20220205102957171

例子:

当我们将表单中的数据添加到state时,一个一个写未免过于麻烦,过于重复,因此可以使用如下方式

image-20220205102224284

可以发现,下面的onChange调用的是saveFormData返回的对象,而为了使它正常工作,我们选择将调用函数的返回值也设置为一个函数,这样onChange最终还是调用到了函数

在这里还有个细节需要注意

image-20220205102512952

这里的”dataType”,如果不加中括号,仅仅会在state中新建一个dataType,而不会获取其内容


生命周期

挂载(mount):组件放到页面

卸载(unmount):组件被移除

引入生命周期

我们以本题为例

需求:

1.<h2>内容规律变浅,消失后出现

2.点击<button>,组件消失


如何卸载一个组件?

image-20220207094517458


实现需求

如果将定时器设置在render里,定时器会发生嵌套,变化速度越来越快,占用越来越多,我们先折中一下,再布置一个按钮,将需求先完成

image-20220207101824180

那么React是否有办法,在内容挂载到页面后,帮助我们调用一次定时器呢?

render的兄弟 — componentDidMount

我们知道,render调用的时机有两个:

(1)初始化渲染

(2)状态更新之后

componentDidMount则只在一个时候调用:组件挂载完毕

因此我们可以这样写

image-20220207102946452

但是,这样仍存在一些错误,当我们点击<button><h2>确实木大了,但是控制台提出了抗议

image-20220207103111251

组件没了,不能再更新状态了!所以我们还需要清空定时器

可以这样

image-20220207103352556

但还可以这样Quicker_20220207_103611

我们从这里可以看出,React总是提前准备了一些函数,再合适的时候做着合适的事情,而这些事情,如下图所示

image-20220207104241309

这些函数被叫做:

生命周期回调函数 <=> 生命周期钩子函数 <=> 生命周期函数 <=> 生命周期钩子

而函数也不仅仅只有这两个,我们将在下面几小节讨论


生命周期(旧)

理解:

1、组件从创建到死亡会经历一些特殊的阶段

2、React组件包含一系列钩子函数(生命周期回调函数),会在特定时刻调用

3、我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作

生命周期流程图(旧)

react生命周期(旧).png)

这里以求和为案例

挂载时:

image-20220207111359969

image-20220207111421731


更新时

我们添加

image-20220207112553312

可以发现,页面开始并没有打印,而当我们按下\

image-20220207112650035

打印执行,这就是更新的含义

而在更新时,有三条路可以走

image-20220207112031404

线路2

setState已经很熟悉了,pass

shouldComponentUpdate

其中的shouldComponentUpdate(是否更新),类似于一个阀门,返回true,代表更新可以进行,而返回false,代表更新不被允许(默认返回true

我们添加这个钩子(返回true),再点击 + 1

image-20220207113722669

image-20220207113003173

正如上面所说,每次更新之前都询问了“阀门状态”

componentWillUpdate

添加钩子,点击 +1 之后的控制台

image-20220207113732494

image-20220207113447355

componentDidUpdate

添加钩子,点击 +1 之后的控制台

image-20220207113624606

线路3

forceUpdate(强制更新)

不对状态做出更改(不管阀门),强制更新组件

我们把阀门设为false,尝试一下

image-20220207114605377

image-20220207114552209

果然绕过了阀门

线路1

我们要先在两个组件之间构建父子关系

image-20220207115126627

image-20220207115245571

我们想通过A组件,展示B组件的信息

componentWillReceiveProps

由于是B组件从外部(A)接收标签,我们添加钩子查看

image-20220207121205899

点开之后,没有???!!!

实际上,这个钩子有个“坑“,第一次传的不算

我们点击按钮(由于挂载页面时已经render一回了,这次便是第二次执行,B组件也接收到了新的props),

点之前

image-20220207121613655

点之后

image-20220207121626480


总结:旧生命周期

1. 初始化阶段: 由ReactDOM.render()触发—-初次渲染

\1. constructor()

\2. componentWillMount()

\3. render() ===> 必须

\4. componentDidMount() ===> 常用

​ 用于初始化,如:开启定时器、发送网络请求、订阅消息

2. 更新阶段: 由组件内部this.setSate()或父组件重新render触发

\1. shouldComponentUpdate()

\2. componentWillUpdate()

\3. render() ===> 必须

\4. componentDidUpdate()

3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发

\1. componentWillUnmount() ===> 常用

​ 用于”收尾“,如:关闭定时器、取消订阅消息


新旧生命周期对比

我们将上面求和的例子中的引入的React换为新版本,发现代码可以正常挂载,but

image-20220207142824820

这些警告说明了旧钩子在新版本中也可以用,但不被推荐使用,同时两个函数名称发生改变

image-20220207143338050

image-20220207143349943

更改之后,下面的两个警告disappear了

简记:所有带”will“的钩子(3个),在新版本都推荐前置UNSAFE_(无关安全性,是警告可能在未来版本中出现bug),除了 componentWillUnmount

官方文档中提出,那3个需要加前缀的钩子,都即将过时!!!

我们在下面重新展示新旧的生命周期图,进行对比

.png)

react生命周期(新).png)

首先,带有will的几个”_UNSAFE“钩子没有再新周期图中出现

其次,新周期图多了getDerivedStateFromProps(挂载时)、getSnapshotBeforeUpdate(更新时)这两个新钩子


生命周期(新)

这里先说一下新生命周期的几个钩子

getDerivedStateFromProps(从props得到派生的状态)

我们将其添加进组件

image-20220207145517490

意想不到的事情发生了

image-20220207145543760

警告告诉我们,这个钩子挂到了实例上,请定义它,把它作为一个静态方法

加上 static之后

image-20220207145827057

返回又不对了,它要求只能返回一个状态对象,或者是null

image-20220207150013930

我们先返回一个null

image-20220207150040494

流程按照流程图正常显示了

但当我们返回一个状态对象

1
return {count: 108}

我们发现显示的值不变了,+ 1 按钮也失灵了

实际上,这个钩子就收到的参数,是props

1
static getDerivedStateFromProps(props)

此方法适用于罕见的用例,即state的值在任何时刻都取决于props

需要注意的是,派生状态会导致代码冗杂,导致组件难以维护

getSnapshotBeforeUpdate(更新之前获取快照)
1
2
3
getSnapshotBeforeUpdate() {
console.log('getSnapshotBeforeUpdate')
}

image-20220207151348202

要求我们返回null或者快照,null的话更上一个相似,都pass了

image-20220207152843950

案例

当我们想实现滚动条增长,同时停在某一行时,我们可以用快照保存之前的长度,并使用componentDidUpdate(preProps, preState, height)接受快照,令当前的scrollTop += scrollHeight - 传入的长度,实现停留


总结:新生命周期

1. 初始化阶段: 由ReactDOM.render()触发—-初次渲染

\1. constructor()

2. getDerivedStateFromProps

\3. render()

\4. componentDidMount()

2. 更新阶段: 由组件内部this.setSate()或父组件重新render触发

1. getDerivedStateFromProps

\2. shouldComponentUpdate()

\3. render()

4. getSnapshotBeforeUpdate

\5. componentDidUpdate()

3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发

\1. componentWillUnmount()


DOM的diffing算法 与 key

DOM的diffing算法 && key

当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】, 随后React进行【新虚拟DOM】与

【旧虚拟DOM】的diff比较,规则如下:

key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用

a. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
(1).若虚拟DOM中内容没变, 直接使用之前的真实DOM
(2).若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM

b. 旧虚拟DOM中未找到与新虚拟DOM相同的key
根据数据创建新的真实DOM,随后渲染到到页面

我们以下面的题目为例

image-20220208093136188

需求如图所示

image-20220208093150236

由于两者用的key不同,导致其效率有着较大差异,且会发生问题

image-20220208093826423

可以发现,上面的发生了数据偏移,这是个严重错误

image-20220208093321765

image-20220208093335416

通过上面两幅图进行对比,

首先是效率问题,我们发现,由于p1是用index(索引值)作为key,当新元素放到首位时,全体元素的所引发生变化,导致所有元素都重新加载了一遍,而p2则只需加载一遍

其次是数据问题,由于diffing是分层比较的,因此在p1中,“更新小王”和“初始小张”比较,发现外层不同后,仍然进入内层比较,并认为type相同,因此保留下来了“初始小张”的框,而到最后,React发现没有索引值为2的元素,所以把”小李“和一个没输入的框挂了上去;而p2使用id比较,各个标签都能找到对应的元素

我们进行总结:

(1)用index作为key可能会引发的问题:

  • 若对数据进行:逆序添加、逆序删除等破坏顺序操作: DOM更新 ==> 界面效果没问题, 但效率低。

  • 如果结构中还包含输入类的DOM会产生错误DOM更新 ==> 界面有问题。

  • 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的

(2)开发中如何选择key?

  • 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
  • 如果确定只是简单的展示数据,用index也是可以的。