组件类型和定义
- react组件首字母应当大写,因为使用时以标签形式使用,如果首字母小写会认为是html的内置标签,报错
1 | // 函数式组件 |
组件三大属性
state
箭头函数
()=>{}
和function
区别:箭头函数没有this
,其中出现this
会去找外侧的this
,function
自身有this
。可以利用该点在类中使用箭头函数,其中用this
改变类中的属性.react自定义方法一般用复制加箭头函数的方法
1 | class Foo extends React.Component{ |
- 组件对象有
state
属性用于表示组件状态,同时在不同事件后使用setState
改变state的值实现不同状态的转化。注意setState
会把相同的键合并,不同的键保留,不会覆盖
1 | class Foo extends React.Component{ |
- 同时上述代码可以不需要构造器,简写如下
1 | class Foo extends React.Component{ |
props
类组件中的props
- react 的props:我们在创建组件对象时不想用constructor,如何给组件传参?利用props属性,在使用对象标签时给标签参数就可以反映到props属性上
1 | class Person extends React.Component |
- react批量传props:使用对象传参数
1 | class Person extends React.Component |
- 拷贝时可以修改其中的某项参数
1 | let a = {name:'wx', age:18, gender:'M'} |
- 当直接使用标签属性的形式传递props时,形式为
key=value
此时value只能时字符串,如果想传递其它类型需要加{}
表示这是js表达式
1 | <Person name='wx' age='19' gender='M'/> |
- 接上述内容,传入标签有不同类型,我们对不同类型标签需要有不同的操作,需要对不同标签的数据类型做限制。同时某些参数不传的时候,也需要参数的默认值。(参考函数传参需要限制参数类型和参数默认值)。组件类中使用属性
propTypes
指定各个属性的限制,使用defaultProps
指定默认值。具体使用如下
1 | import PropTypes from 'prop-types'; |
- props是只读的,不能修改
函数组件中的props
- 函数式组件通过参数的形式可以有
props
,但没有state
和refs
,除非使用最新的hooks
1 | function Person(props) |
refs
字符串形式的ref(不推荐)
- 组件内的标签可以通过
ref
来标识自己,组件会把ref
和标签组成一对key-value放入属性refs
中
1 | class Person extends React.Component |
回调函数形式的ref
- 当
ref
为函数时,该函数作为回调函数使用,将ref
所在节点作为参数传入函数中。一般会利用回调函数,把当前节点赋值给组件属性
1 | class Person extends React.Component |
- 有关内联函数(函数体直接定义在
ref
后面的函数)的问题:- 当组件更新的时候(
state
改变,react重新调用render
函数,重新渲染),会调用两次ref
回调函数,第一次传入参数null
第二次才真正传入当前节点 - 原因:每次重新调用
render
都会重新生成回调函数,不确定之前调用的回调函数有什么影响,为了消除影响,第一次先传入null消除之前可能的效果,之后再次调用,传入当前节点。 - 只是细节,基本不会产生影响
- 如果想改,可以把回调函数改为类中定义的函数
- 当组件更新的时候(
1 | class Person extends React.Component |
createRef
- 可以使用react内部的
createRef
来定义一个ref
,createRef
相当于创建一个容器,装ref对应的标签,每个createRef
对应一个标签,createRef
创建的ref是一个对象,其中有一个key为currrent
通过myRef.current
获取到对应的标签
1 | class Person extends React.Component |
React事件处理
- React中的事件都被封装了一层
- 通过
onXxx
指定事件处理函数,如onClick
、onBlur
等- 使用的是React自定义事件,而不是原生DOM事件 (为了更好的兼容性)
- 事件都是通过事件委托的方式处理的(委托给组件最外层元素) (为了高效)
- 通过
event.target
得到发生事件的DOM元素对象 (减少ref使用)
- 避免过度使用ref:
- 发生事件的元素正好是需要操作的元素,可以不用ref
- 利用事件的回调函数传入的
event
参数直接获取DOM元素对象
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Demo extends React.Component
{
showOnBlur=(event)=>{
// 回调该函数时会传入event参数,使用event.target获得该事件的DOM对象
// 这样可以避免ref的频繁使用
alert(event.target.value)
}
render()
{
return (
<div>
<input onBlur={this.showOnBlur} type='text' placeholder='失去焦点显示内容'/>
</div>
)
}
React 受控和非受控组件
非受控组件
1 | class Login extends React.Component |
- form表单不提供参数时,默认发起GET请求同时带有query参数
- 原生js中表单具有事件
onsubmit
, React中为onSubmit
- 避免表单提交刷新页面,可以使用
event.preventDefault()
阻止默认事件不提交表单 - 非受控组件:页面中所有输入类DOM的值现用现取(点击登录后,回调函数获取表单值)属于非受控组件
受控组件
- 原生js 有事件
onchange
在改变时调用onchange
的回调函数,React中同样有onChange
- 使用
onChange
回调函数将输入内容直接存入状态中
1 | class Login extends React.Component |
- 关于受控和非受控:
- 非受控可以理解为拿到数据的行为不受程序控制,而受用户控制,只有用户进行提交等行为,组件才能最终拿到数据;
- 受控可以理解为拿数据的行为受程序控制,用户只要输入,不用提交,随着输入组件就能就拿到数据
- 更建议使用受控组件,因为非受控组件有几个输入,就有几个
ref
,受控组件可以减少ref
的使用
- 上述代码有重复实现的功能,可以改善,代码如下,具体使用了函数的柯里化
1 | class Login extends React.Component |
- 不用柯里化函数,也可以实现
1 | // 修改changeFormData,接受dataType和需要的参数 |
直接利用回调函数给changeFormData
传入不同的参数实现代码复用
组件生命周期(重要)
旧版本
组件第一次被渲染到页面上时,React中称之为挂载 (mount) ,从页面移除时称之为 卸载 (unmount)
挂载组件使用
render()
,卸载使用ReactDOM.unmountComponentAtNode()
希望在组件挂载到页面上时执行一些操作,比如设置定时器等。可以使用
componentDidMount()
进行操作,该函数在组件挂载时调用,并仅调用这一次。例:希望组件中的文本周期性的透明度减小,到0时恢复为1。点击按钮使组件消失
1 | class Life extends React.Component |
- 以上代码会出现新的问题,在点击删除节点后,控制台报错:不能更新一个未挂载的组件(unmounted component)的状态
- 解决以上问题:
- 先把定时器绑定到
this
上,即初始定义为this.timer = setInterval()
,之后在删除组件时加上删除定时器clearInterval(this.timer)
使状态停止更新 - 直接使用
componentWillUnmount
做这项工作,在组件将要被卸载时执行的操作
- 先把定时器绑定到
1 | // ... |
1 | componentWillUnmount() |
- 组件的生命周期实际就是组件从创建、挂载到卸载,其中重要的函数(如
render
、componentDidMount
、componentWillUnmount
等)被称为:生命周期钩子函数、生命周期函数等- 组件从创建到死亡有一些特定的阶段
- 组件包含一系列钩子函数会在特定阶段调用
- 定义组件时会在特定的生命周期回调函数中,做特定的工作。
- 对于上图:左侧好理解,不做赘述
- 右侧:
setState
流程- 调用后,首先会通过
shouldComponentUpdate
阀门,检查是否应当更新当前组件,若返回true则能够进行下去,反之会被阻拦,无法调用后面的函数。如果该函数未定义,永远返回true - 检查返回true后会依次调用
componentWillUpdate
、render
、componentDidUpdate
表示组件更新前、更新、更新后的操作
- 调用后,首先会通过
forceUpdate
流程- 跳过
shouldComponentUpdate
强制更新 - 直接调用
componentWillUpdate
、render
、componentDidUpdate
- 一般用做不更改状态数据,强制更新一下
- 通过
this.forceUpdate
调用
- 跳过
- 父组件调用
render
- 首先组件要形成父子关系:在A组件中调用B组件,则A为B的父组件
- 父组件状态改变,重新render后,调用子组件钩子
componentWillReceiveProps
(组件将要接收props) - 注意! 上述钩子在第一次传入props 时不会调用!只有更新传入新的props时才会调用(可以认为父组件重新render后调用)
1 | // 父组件调用render展示 |
- 以上都为旧版本的生命周期,做如下总结
- 初始化阶段,由
ReactDOM.render()
触发 (初次渲染)constructor
componentWillMount
render
componentDidMount
常用,经常在该钩子中进行初始化,如:开启定时器、发起请求、订阅消息
- 更新阶段,由
this.setState
或父组件render
触发shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
- 卸载组件,通常由
ReactDOM.unmountComponentAtNode
触发componentWillUnmount
常用,通常做一些收尾工作,例:关闭定时器、取消订阅
新版本
componentWillMount
、componentWillReceiveProps
、componentWillUpdate
前都需要加上UNSAFE_
记忆:除了
componentWillUnmount
,所有带will 的钩子都需要加UNSAFE_
unsafe:未来版本后加入异步渲染后,这三个钩子可能带来一些bug(现在不会),因此加入UNSAFE标志,同时提醒减少这三个钩子的使用
新生命周期如下图
对于新生命周期,可以理解为:
- 废弃
componentWillMount
、componentWillReceiveProps
、componentWillUpdate
三个旧的钩子 - 提出
getDerivedStateFromProps
、getSnapshotBeforeUpdate
两个新的钩子 - 注:实际使用中,两个新的钩子使用情况极其罕见
- 废弃
对于
getDerivedStateFromProps
:- 定义在类上,因此需要是静态方法
static getDerivedStateFromProps
;同时返回值也必须是状态对象或null
- 接受参数
props
和state
,得到标签参数 - 当返回
null
时,不会产生影响 - 当返回状态对象,会将返回的对象设置为当前的state
- 理解:从props中得到派生的状态,即通过props的值得到状态,state的值在任何时候都取决于props,修改就没作用了
- 注意:容易造成代码冗余并且难以维护
- 定义在类上,因此需要是静态方法
1 | class A extends React.Component |
- 对于
getSnapshotBeforeUpdate
:- 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给
componentDidUpdate()
。 - 说明:
componentDidUpdate
实际有三个参数componentDidUpdate(preProps, preState, snapshot)
接收更新前的props、更新前的state和getSnapshotBeforeUpdate
传递进来的快照 snapshot
- 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给
- 案例说明:
- 需求:有一新闻列表,长度固定;不断有新的新闻刷新,新的新闻在列表最上方;超出列表长度,使用滚动条。
- 问题:当内容不断刷新出现时,滚动条相对于最高点位置是固定的,反映到内容上,旧的内容会被新的内容挤到下方
- 解决:需要固定内容相对不动,即每次更新内容时,需要滚动条相对顶部的高度加上新内容的高度
提前定义好列表和新闻样式为
1
2
3
4
5
6
7
8
9
10.newsList{
width: 200px;
height:150px;
background:skyblue;
overflow: auto;
}
.news{
height: 30px;
}
代码 为
1 | class NewsList extends React.Component |
- 新版本生命周期总结如下
- 初始化阶段,由
ReactDOM.render()
触发 (初次渲染)constructor
getDerivedStateFromProps
render
componentDidMount
常用,经常在该钩子中进行初始化,如:开启定时器、发起请求、订阅消息
- 更新阶段,由
this.setState
或父组件render
触发getDerivedStateFromProps
shouldComponentUpdate
render
getSnapshotBeforeUpdate
componentDidUpdate
- 卸载组件,通常由
ReactDOM.unmountComponentAtNode
触发componentWillUnmount
常用,通常做一些收尾工作,例:关闭定时器、取消订阅
Diffing算法
React每次更新不是直接更新真实DOM,而是修改虚拟DOM,比较修改前后的虚拟DOM,只找不同的地方在真实DOM修改,不用真实DOM每次都全部更新,效率高
Diffing算法最小更新粒度是标签,但是会比较多层,比如
1
2
3
4<span>
现在的时间是:{this.state.time}
<input type="text"/>
</span>
其中this.state.time
一秒钟更新一次,表示当前时间,span
标签内容改变,但是内部的input
标签并没有改变,react只更新改变的内容,内部的input
不会更新。虚拟DOM中
key
的作用- 简单:
key
是虚拟DOM对象的标识,更新显示时key
起着极其重要的作用 - 详细:
- 当状态数据发生变化时,react会根据新数据生成新的虚拟DOM,随后React将【新虚拟DOM】与【旧虚拟DOM】的diff比较,规则如下:
- 旧DOM找到了与新DOM相同的
key
,:- 若虚拟DOM内容不变,则仍用原本的真实DOM
- 若虚拟DOM内容改变了,则根据改变的虚拟DOM生成真实DOM,替换页面内容
- 未找到相同的
key
:根据数据创建真实DOM并渲染到页面
- 旧DOM找到了与新DOM相同的
- 简单:
之前的代码 提到用index
作为key
会产生一些问题,下分析其低效原因
代码示例:
1 | class Person extends React.Component { |
分析其中的更新步骤:
1. 初始化创建, state
内为
1 | [ |
那么虚拟DOM中标签为
1 | <li key=0>小王---18</li> |
- 更新后,数据变为
1 | [ |
新生成的虚拟DOM标签为
1 | <li key=0>小张---20</li> |
按照之前的diffing比较步骤:
1. 首先取 key=0
旧虚拟和新虚拟比较:有该key
,但内容不同,则重新渲染到真实DOM中
2. 再取 key=1
旧虚拟和新虚拟比较:有该key
,但内容不同,则重新渲染到真实DOM中
3. 取 key=2
旧虚拟和新虚拟比较:无该key
,直接渲染到真实DOM中
可以发现,实际将三个标签全部重新渲染,但是我们可以看出,两个标签是重复的,不必要再次渲染,这样冗余的渲染造成了效率低下
进一步看造成冗余的原因:使用index
作为key
,在数据列表等顺序改变时,会直接改变已有数据的key
,导致新加入数据后,原本数据对应标签的key
也发生变化,造成多余的真实DOM渲染,没有复用之前的标签
最好不要使用index
,而是用数据的唯一标识,如id
等作为key
的值
以上的问题,不只会带来效率降低,当有输入框等嵌套结构时,还会造成数据错位:
1 | class Person extends React.Component { |
上述代码,上半部分在输入后点击“添加小张” 会造成输入框数据的错位;下半部分则不会
原因:
* 在用index作为key的部分,由于相同的key内部值不同,节点都做了更新,但因为虚拟DOM中 input
没有 value
属性,直接比较内部 input
标签都一样,因此没有更新 input
标签,这样相当于保留输入但改变前面内容,造成了最终的错位
* 使用id
为key
,比较后id相同不做更新,直接在最前方加入 li
标签,输入框和文本信息统一后移,不会有错位
总结:用index
作为key
可能的问题
1. 若对数据进行排序、逆序、删除、插入等操作破坏原本顺序:会造成不必要的真实DOM更新,降低效率
2. 如果结构中还包含输入类DOM:会产生错误的DOM更新造成信息的错误显示
3. 如果不对数据进行排序、逆序、删除、插入等操作,仅用于渲染展示信息,可以使用index
作为key
React脚手架
文件介绍
punlic/index.html
head部分内容解释
1 | <!-- 以链接形式指定图标,%PUBLIC_URL%为public文件夹 --> |
punlic/manifest.json
如果网页用作应用加壳,使用的配置文件(即,直接使用网页套壳作为安卓、iOS应用等)
src/App.js
存放最外层App组件,用脚手架开发之后,所有小组件都作为App的子组件,最后只加载App组件到页面
src/App.test.js
对App组件做测试的脚本,几乎不用
src/index.js
webpack的入口文件
其中加载App组件,用<React.StrictMode>
包裹:这样会检查App组件中不合理的部分(比如使用字符串形式的ref会弹出警告等)
src/reportWebVital.js
用于分析、记录、显示页面性能
src/setupTests.js
用于模块的整体测试
编写方式
- 不同组件放到
src
统一的文件夹下,文件夹内部,一个组件一个文件夹,用于存放组件使用的外部js和样式等 - 将不同组件在App中进行组合
- 区分组件和业务逻辑文件:
- 组件文件名大写
- 组件文件后缀可以使用
jsx
- 多层文件引入麻烦,可以不同组件文件夹不同,但定义组件的文件都叫
index.jsx
,这样只要引入文件夹即可
样式模块化
避免不同模块之间样式冲突(不同模块指定相同名字的class不同样式,后引入的会覆盖之前引入的),要使用样式的模块化
模块化css:文件名改为
xxx.module.css
这样引入时可以将样式作为模块引入,使用时按照模块使用(xxx.class
等形式)
1 | /* xxx.module.css */ |
1 | import xxx from './xxx.module.css' |
应用案例 TodoList
- 上方Header组件为一个输入框,接受用户输入需要完成的任务,将任务加入到列表中
- 中间List展示已经有的任务,并且每个任务可以标志是否完成、可以删除
- 下方Footer展示已完成可全部任务数量,可以删除所有已完成任务,可以给所有任务打勾
困难:Header和List之间为兄弟,Header的数据无法传给List,也就无法新加入任务
解决:Header用某种方式将数据传给父组件App,再由App用props的形式将数据传给List
子组件向父组件传递数据
将父组件类内部的处理、显示数据的函数传给子组件,子组件将数据作为函数参数传入并调用,相当于给父组件传入了参数,代码如下
1 | // App.js |
具体实现见:Todo_src
React 和 axios
跨域请求和代理配置
- 当出现以下情况,都属于跨域访问
跨域原因 | 示例 |
---|---|
域名不同 | www.jd.com 与 www.taobao.com |
域名相同,端口不同 | www.jd.com:8080 与 www.jd.com:8081 |
二级域名不同 | item.jd.com 与 miaosha.jd.com |
我们在测试请求数据时:在不同端口实现前端(3000)和后端服务器(5000),造成跨域访问,导致数据获取失败。
Ajax 对于跨域请求:请求可以发出,后端服务器能够接收到,但是后端服务器发往前端的数据会被ajax阻止
对于跨域,我们可以设置代理:
- 代理将前端请求转发给后端,将后端响应返回给前端
- 代理与前端端口和域名一致:前端仅与代理通信,不会造成跨域,请求不会被阻拦
- 代理访问后端实际为跨域,但是由于没有ajax阻拦所以可以实现
做法:在package.json 中最后加入
"proxy":"后端服务器地址"
发送数据时就直接给前端端口(不会产生跨域)发送数据,代理自动将数据转发
注意:请求会首先在真正的前端端口下寻找资源,如果找到是不会转发给后端服务器的
附
原生js基础
- 展开运算符:
...
用于展开可迭代的变量,如数组等,但字面量对象(可以理解为字典)无法直接展开
1 | let a = [1,2,3,4] |
对象相关
- 对象的复制:形式类似于展开运算符,但需要在外层加上
{}
1 | let a = {name:'ccc', age:'3'} |
- 所有对象中
key:value
表达式,默认key
为字符串,name:"wx"
等价于"name":"wx"
。如果想使变量作为key
可以使用方括号[]
1 | let dataType = "username"; |
- 当对象key和value相同时,有简写形式
1 | opacity = 0.5 |
常用
setInterval
定时器,两个参数,第一个参数为函数,为执行的操作;第二个参数为执行函数间隔的时间
1 | time = 200 //ms |
特殊概念
- 高阶函数
- 如果一个函数接受一个函数作为参数,那么就是高阶函数
- 如果一个函数将一个函数作为返回值,也是高阶函数
- 函数的柯里化:通过函数调用最终返回函数的方式,实现多次接收参数,最后统一处理的函数编码形式。如受控和非受控组件中的代码
资源
- 库的js可以通过官网找到,下载
- 还可以在BootCDN 找到,该网站提供了常用js库的加速访问