跳到主要内容

React基础知识

说明:以下这些基础知识适用于类组件和函数组件,并不是函数组件独有的。

安装react并初始化

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

注意:

  1. 目录名不允许有大写字母
  2. 初始化过程比较慢,甚至可能需要5-10分钟
  3. 如果报错:npm ERR! Unexpected end of JSON input while parsing near '...n\r\nwsFcBAEBCAAQBQJd', 解决方法:npm root -g 找到本机npm全局安装目录,cd 进入该目录,执行清除缓存:npm cache clean --force,然后再次初始化。
3、启动项目:cd hello-react、npm start

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

自定义组件基础知识

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

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

    import React,{Component,Fragment} from 'react';  
类组件:render(){return <Fragment>xxxxxxx</Fragment>}
函数组件:return <Fragment>xxxxxxx</Fragment>

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

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

4、在最新的react版本中,为了提高更新性能,推荐采用异步的方式更新数据。具体使用方式为:setXxx((prevData) => {return xxx})。其中参数prevData指之前的变量值,return的对象指修改之后的数据值。

可以将上面代码简写为:setXxx(prevData => xxx) 若没有用到prevData参数,还可以省略,即 setXxx(() => xxx);

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

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

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

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

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

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

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

"纯函数" 概念解释

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

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

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

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

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

这就是为什么react不允许直接修改某变量的原因。

"受控组件" 概念解释

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

"受控"即这些组件的可以值受到某变量的控制。

与之对应的是"非受控组件",即该组件对应的值并不能被某变量控制。

例如"<input type='file'/>",该组件的值为用户选中本地的文件信息,该值并不能直接通过某变量来进行value值的设定,因此该组件属于"非受控组件"。

"声明式开发" 概念解释

"声明式开发":基于数据定义和数据改变,视图层自动更新。
"命令式开发":基于具体执行命令更改视图,例如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等),但是请注意,只从有了hook以后,可以通过useReducer+useContext来实现类似Redux的功能。

"函数式编程" 概念解释

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

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

"虚拟DOM" 概念解释

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

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

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

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

"Diff算法" 概念解释

当变量发生改变时,需要重新生成新的虚拟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(更新完成)

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

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

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

捕获子组件错误:

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

卸载(Unmounting):

componentWillUnmount(即将被卸载)

备注:类组件继承自Component组件,Component组件内置了除render()以外的所有生命周期函数。因此自定义组件render()这个生命周期函数必须存在,其他的生命周期函数都可以忽略不写。 而使用了hook的函数组件,简化了生命周期函数调用的复杂程度。

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

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

1、只需要第一次获取数据的Ajax请求
如果类组件有ajax请求(只需请求一次),那么最好把ajax请求写在componentDidMount中(只执行一次)。因为"初始化、挂载、卸载"在一个组件的整个生命周期中只会发生一次,而"更新"可以在生命周期中多次执行。
如果是函数组件,则可以写在useEffect()中,并且将第2个参数设置为空数组,这样useEffect只会执行一次。

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

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

还可以让组件继承由React.Component改为React.PureComponent,react会自动帮我们在shouldComponentUpdate生命周期函数中做浅对比。

如果是函数组件,则在子组件导出时,使用React.memo()进行包裹,同时结合useCallback来阻止无谓的渲染,实现提高性能。

React中设置样式的几种形式

第一种:引用外部css样式

伪代码示例:

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

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

第二种:内部样式

伪代码示例:

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

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

Hook用法

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

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

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

hook表现出来特别像一个普通的JS函数(仅仅是表现出来但绝不是真的普通JS函数)。