1.1.React简介
1.1.1.介绍描述
-
用于动态构建用户界面的 JavaScript 库(只关注于视图)
-
由Facebook开源
实现一个页面需求通常有三步:
1.发送请求获取数据
2.处理数据(过滤、整理格式等)
3.React就是操作DOM呈现页面
。
是一个将数据渲染为HTML视图的开源 JavaScript 库。
1.1.2.React的特点
- 采用
组件化模式
、声明式编码
,提高开发效率及组件复用率
命令式编码:获取节点再改样式;
声明式编码:用特殊语法表达更改的样式,react会帮我们操作dom - React Native 编写原生应用,进行
移动端开发
- 使用
虚拟DOM
+优秀的Diffing 算法
,尽量减少与真实DOM交互
1.1.3.React高效的原因
- 使用虚拟(virtual)DOM, 不总是直接操作页面真实DOM。
- DOM Diffing算法, 最小化页面重绘。
原生JavaScript操作DOM繁琐、效率低(DOM-API操作 UI)
使用JavaScript直接操作DOM,浏览器会进行大量的重绘重排
原生JavaScript没有组件化编码方案,代码复用率低
1.2.React的基本使用
1.2.1.效果
1.2.2.相关js库
- react.development.js:React核心库。
- react-dom.development.js:提供操作DOM的react扩展库。
- babel.min.js:解析JSX语法代码转为JS代码的库。
<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom,用于支持react操作DOM -->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!-- 引入babel,用于将jsx转为js -->
<script type="text/javascript" src="../js/babel.min.js"></script>
1.2.3.创建虚拟DOM的两种方式
-
纯JS方式(一般不用)
-
JSX方式
1.2.4.虚拟DOM与真实DOM
- 虚拟DOM本质是object类型的对象(一般对象)
- 虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。
- 虚拟DOM最终会被React转化为真实DOM,呈现在页面上。
// js创建虚拟节点
React.createElement(标签名,标签属性,标签体内容)
1.3.React JSX
全称: JavaScript XML
react定义的一种类似于XML的JS扩展语法: JS + XML
本质是React.createElement(component, props, …children)方法的语法糖
1.3.1.作用:用来简化创建虚拟DOM
- 写法:
var ele = <h1>Hello JSX!</h1>
- 注意1:它不是字符串, 也不是HTML/XML标签
- 注意2:它最终产生的就是一个JS对象
1.3.2.基本语法规则
-
定义虚拟DOM时,不要写引号。
-
标签中混入Js表达式时要用{}。
-
样式的类名指定不要用class,要用className。
-
内联样式,要用style={{key:value}}的形式去写
-
只有一个根标签
-
标签必须闭合
-
标签首字母
若小写字母开头,则将改标签转为htm1中同名元素,若htm1中无该标签对应的同名元素,则报错。
若大写字母开头,react就去渲染对应的组件,若组件没有定义,则报错。
1.3.3.js中的表达式和语句区分
为什么这段代码会报红呢?
因为在react {}包裹的必须是js表达式
才可以,for循环是语句,不是表达式
- 表达式:一个表达式会产生一个值,
左侧用一个变量能接到值
就是表达式,可以放在任何一个需要值的地方- (1).a
- (2).a+b
- (3)demo(1) 如果函数没有返回值会自动返回underfined,所以也是能接到值的
- (4)arr.map()
- (5).function test(){}
- 语句(代码),控制代码走向,没有值
- (1).if(){}
- (2).for(){}
- (3).switch(){case:xxxx}
所以上面的代码改用map方法并返回就没问题了
注意:data数据是数组的形式react会帮我们遍历,如果是对象则不行,控制台报错
1.3.4.babel.js的作用
1)浏览器不能直接解析JSX代码, 需要babel转译为纯JS的代码才能运行
2)只要用了JSX,都要加上type=“text/babel”, 声明需要babel来处理
<body>
<!-- 准备容器 -->
<div id="test"></div>
<!-- 引入babel,用于将jsx转为js -->
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel"> /* 此处一定要写babel */
// 1.创建虚拟DOM
const VDOM = <h1>Hello,React</h1>/* 此处一定不要写引号,因为不是字符串 */
// 2.渲染虚拟DOM到页面
// ReactDOM.render(VDOM,document.getElementById('test'))
</script>
</body>
1.3.5.渲染虚拟DOM(元素)
语法: ReactDOM.render(virtualDOM, containerDOM)
作用: 将虚拟DOM元素渲染到页面中的真实容器DOM中显示
参数说明
1)参数一: 纯js或jsx创建的虚拟dom对象
2)参数二: 用来包含虚拟DOM元素的真实dom元素对象(一般是一个div)
<body>
<!-- 准备容器 -->
<div id="test"></div>
<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom,用于支持react操作DOM -->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!-- 引入babel,用于将jsx转为js -->
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel"> /* 此处一定要写babel */
// 1.创建虚拟DOM
const VDOM = <h1>Hello,React</h1>/* 此处一定不要写引号,因为不是字符串 */
// 2.渲染虚拟DOM到页面
ReactDOM.render(VDOM,document.getElementById('test'))
</script>
</body>
1.4.模块与组件、模块化与组件化的理解
1.4.1.模块
把庞大的js文件拆分成多份
- 理解:向外提供特定功能的js程序, 一般一个js文件就是一个模块
- 为什么要拆成模块:随着业务逻辑增加,代码越来越多且复杂。
- 作用:复用js, 简化js的编写, 提高js运行效率
1.4.2.组件
页面某个能复用的区域,将其封装成组件,就是该区域的所有资源代码功能全部打包到一个组件里
- 理解:用来实现局部功能效果的代码和资源的集合(html/css/js/image等等)
- 为什么要用组件: 一个界面的功能更复杂
- 作用:复用编码, 简化项目编码, 提高运行效率
1.4.3.模块化
当应用的js都以模块来编写的, 这个应用就是一个模块化的应用
1.4.4.组件化
当应用是以多组件的方式实现, 这个应用就是一个组件化的应用
第2章:React面向编程
2.1基本理解和使用
2.1.1使用React开发工具调试
在谷歌浏览器安装React调试工具
- 打开谷歌浏览器进入谷歌商店
- 搜索 React Developer Tools,添加到Chrome
- 在扩展程序中查看
- 将该工具固定在浏览器上方(绿色框的图标表示当前网页是否用React编写的,如果是图标则会变亮)
图标橙色,没有经过打包,开发者模式
正常图标,打包并且部署到服务器上线了
5. 开发者模式使用该工具
F12,点击展开箭头
Compontent:网页由多少个组件组成,每个组件拥有的属性
Profiler:记录网站的性能,渲染时间,组件加载时间
2.1.2定义组件
- 函数式组件
<script type="text/babel">
// 1.创建函数式组件(组件名首字母要大写)
function MyCompontent(){
console.log(this) //undefined
return <h2>我是函数式组件</h2>
}
// 2.渲染组件到id为test的标签内部
ReactDOM.render(<MyCompontent/>,document.getElementById('test'))
</script>
在函数组件中如果打印this,改this是undefined
,而不是像我们写js函数一样,this指向window,原因是jsx代码需要经过babel翻译,babel翻译
会开启严格模式
,禁止自定义函数里的this指向window。试一试
执行了ReactDOM.render(<MyCompontent…之后,发生了什么?
- React解析组件标签,标签为大写,找到对应的MyCompontent组件
- 发现组件是使用函数定义的,随后
调用该函数
(我们并没有调用函数,是react帮我们调的),将返回的虚拟DOM转为真实DOM,呈现在页面上
- 类式组件
<script type="text/babel">
// 1.创建类式组件
class MyComponent extends React.Component{
//render是放在MyComponent的原型对象上
render() {
console.log(this) //谁调用this指向谁,此处指向MyComponent的实例对象,也叫组件实例对象
return <h2>我是类式组件</h2>
}
}
// 2.渲染组件到指定容器中
ReactDOM.render(<MyComponent/>,document.getElementById('test'))
</script>
注意:必须要继承React.Component
父类,必须要写render
方法,而且render要有返回值
执行了ReactDOM.render(<MyCompontent…之后,发生了什么?
- React解析组件标签,标签为大写,找到对应的MyCompontent组件
- 发现组件是类定义的,随后
React
帮我们new出来该类的实例对象
,并通过实例对象调用原型对象中的render方法
- 将render返回的虚拟DOM转为真实DOM,随后呈现在页面上
原型链知识点复习:实例对象中都有_proto_(对象原型)属性,该属性指向prototype(原型对象),原型对象中存有类的共享的属性和方法其中的constructor属性指向该类的构造函数,如果在实例对象中的原型对象中找不到对应的方法,则会继续往上查找,直到找到原型对象是Object的为止,如果Object中也没有找到该方法,则会返回null
- 简单组件与复杂组件
有状态(state)的组件为复杂组件,反之为简单组件
2.2. 组件实例三大核心属性state
state是在组件的实例对象中的
2.2.1状态的基本使用
创建一个天气类 Weather,该类继承自 React.Component,则该类的实例身上会有 props、refs、state等的属性。
state 作为一个对象在我们的项目中使用,用于存储类组件内部的数据。
在类Weather 的构造器中,使用this.state 可以获取到 state。初始时state 为 null,我们在构造器中将 state 定义为对象类型,并为其指定若干属性。
<script type="text/babel">
// 1.创建组件
class Weather extends React.Component{
// 构造器
constructor(props) {
super(props)
// 借助构造器初始化状态
this.state = {isHot:true}
}
render(){
// 读取状态
const {isHost} = this.state
return <h1 onClick={demo}>今天天气很{ isHost?'炎热':'凉爽' }</h1>
}
}
// 2.渲染组件
ReactDOM.render(<Weather/>,document.getElementById('test'))
function demo(){
console.log('标题被点击了')
}
</script>
事件绑定: 如果demo函数写了括号,如onClick={demo()},此时demo()是一个表达式,表示调用该函数,函数没有返回值会返回undefined,相当于onClick=undefined,把undefined返回给onClick作为回调,这样的话对应的事件会没有任何效果,所以实际绑定的时候函数不需要写括号
2.2.2类中方法中this指向
- 自定义函数this指向
babel翻译开启了严格模式,禁止自定义函数指向window,所以this指向的是undefined
- 类中的方法this指向
解决这个问题需要将自定义函数,改写成类中的方法,将function去掉,代码写到类中,
但是此时控制台还是报同样的错误this还是undefined,原因是onClick是直接调用(window调用)的,changeWeather是作为onclick的回调
,也是直接调用
(window调用),不是实例对象调用
,再加上类中的方法默认开启了局部的严格模式
,this不允许指向window,所以this是undefined
解析:通过this将找到weather实例对象原型上的方法,作为onclick的回调,实际上就是吧changeWeather方法作为属性赋值给了onclick,然后直接调用(window调用)的onclick,相当于点击h1标签和直接调用方法而不是通过实例对象调用,类中的方法默认会开启严格模式,不是实例对象调用,this会指向undefined,可以看下面js代码的示例来理解
- 解决类中this指向undefined问题
使用bind方法,将this指向更正为实例对象,并赋值到实例对象自身的方法
类中的方法是放在实例对象的原型对象上的,实例对象本身是没有该方法的
2.2.3更改状态setState
- 在构造器中声明setState
通过原型对象找到React.Component组件上的setState()方法
进行更改,React不能直接赋值给state更改
注意:更新状态的逻辑不是整个对象覆盖,而是同名的替换,没有同名的属性保持不变
class Weather extends React.Component{
constructor(props) {
super(props)
this.state = {isHot:true, wind:'微风'}
this.changeWeather = this.changeWeather.bind(this)
}
changeWeather(){
let isHot = this.state.isHot
// 状态必须通过setState就行修改是一种合并的操作,同名的替换掉,没有同名的则保留,setState是React.Component组件上的方法
this.setState ({isHot: !isHot})
// 严重注意:状态不可直接更改,下面这行就是直接更改
// this.state.isHot = !isHot// 错误写法!!
}
}
- 类中方法执行次数分析
- 构造器调用次数:1次
在ReactDOM.render渲染组件时,会帮我们new出Weather组件的示例对象,同时调用构造器,new了几次就调用了几次构造器 - render调用次数:1+n次,1是初始化次数,n是状态更新次数
执行过程先调构造器,构造器调完了实例对象才出来,实例对象出来了才能掉render - changeWeather调用次数:点击的次数
调用的是自身的changeWeather 而不是原型上的changeWeather
- 构造器调用次数:1次
class Weather extends React.Component{
constructor(props) { //调用1次
super(props)
this.state = {isHot:true, wind:'微风'}
this.changeWeather = this.changeWeather.bind(this)
}
changeWeather(){ //点击了几次就调用几次
let isHot = this.state.isHot
this.setState ({isHot: !isHot})
}
render(){ //调用1+n次
const {isHot,wind} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{ isHot?'炎热':'凉爽' },{wind}</h1>
}
}
ReactDOM.render(<Weather/>,document.getElementById('test'))
- state简写方式
不用把状态写在构造器中
类中方法使用箭头函数,箭头函数没有自己的this,会往上一层找,则函数内的this指向的就是这个类的实例,不需要用bind再来改变this指向了
class Weather extends React.Component{
//初始化状态,直接在类中给state赋值
state = {isHot:true, wind:'微风'}
//类中自定义方法————要用赋值语句+箭头函数的形式
changeWeather = () => { //箭头函数没有自己的this,就会往上一层找this
let isHot = this.state.isHot
this.setState ({isHot: !isHot})
console.log(this.state.isHot)
}
render(){
const {isHot,wind} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{ isHot?'炎热':'凉爽' },{wind}</h1>
}
}
2.2.3state总结
- state是组件对象最重要的属性, 值是
对象
(可以包含多个key-value的组合) - 组件被称为"状态机", 通过
更新组件的state
来调用render方法更新对应的页面
显示(重新渲染组件) - 组件中
render
方法中的this为组件实例对象
- 组件
自定义的方法
中this为undefined
,如何解决?- 强制绑定this: 通过函数对象的
bind()
箭头函数
- 强制绑定this: 通过函数对象的
- 状态数据,不能直接修改或更新,必须要借助
setState
来更改
2.3 组件实例三大核心属性props
每个组件对象都会有props(properties的简写)属性
组件标签的所有属性都保存在props中
2.3.1 props基本使用
在组件标签中设置属性并赋值,将会添加到props对象中,在组件类中就能使用this.props.属性名来获取到对应属性的值。
注意,props 是只读的,在类中不允许修改。
//创建组件
class Person extends React.Component{
render() {
// 拿到 props 中想要的属性,并用其渲染
const {name, age, sex} = this.props
return (
<ul>
<li>{name}</li>
<li>{age}</li>
<li>{sex}</li>
</ul>
)
}
}
// 在组件标签中设置的属性会添加到props对象中
ReactDOM.render(<Person name="jerry" age="18" sex="女"/>,document.getElementById('test'))
ReactDOM.render(<Person name="tom" age="22" sex="男"/>,document.getElementById('test2'))
2.3.2 批量传递props
使用展开运算符批量传递props,在react中,因为babel翻译+react允许展开运算符展开一个对象(原生js展开运算符不能展开对象),但是不能随意使用,仅仅适用于标签属性的传递
const person = {name:'张三' ,age: '18',sex: '男'}
ReactDOM.render(<Person {...persons} sex="女"/>,document.getElementById('test'))
原生js展开运算符,展开对象时会报错
展开运算符不能展开对象,
ES6新语法是使用展开运算符+{}
,构造一个新对象,一般用来浅拷贝对象
原生js和react, {}+展开运算符的区别
react中{…person}花括号的含义是里面需要写js表达式,
而原生js{…person}花括号的含义是ES6的语法
2.3.3 对props限制
需要引入props-types库,对组件标签属性进行限制
- 类型限制
类名.propTypes ={
PropTypes.xxx
}
//对标签属性进行类型、必要性限制
Person.propTypes = {
name: PropTypes.string, //声明类型
age: PropTypes.number
speack: PropTypes.func //声明speack是函数类型,此处使用的是func,不能使用function,因为function的定义函数的关键字
}
如果属性是字符串,可以直接用引号的方式赋值
如果是数值类型(age)或者是函数类型(speak)的,需要用一对花括号进行包裹
- 必要性限制
PropTypes.xxx.isRequired
//对标签属性进行类型、必要性限制
Person.propTypes = {
name: PropTypes.string.isRequired, //必要性限制
age: PropTypes.number
}
- 默认值
类名.defaultProps
={
键:值
}
//指定标签属性默认值
Person.defaultProps = {
sex: '男', //sex默认值为男
age:18//age默认值为18
}
注意:props是只读的不能修改
2.3.4 props简写方式
将props对标签属性的限制写在类的内部,使用static修饰,加到类的自身可以直接用类名调用,如果不写static就是加给了类的实例对象
2.3.5 构造器中props
构造器是否需要传递props取决于,构造器中是否需要通过this访问props,如果需要则必须要传递props否则会报错
//这种场景很少见
constructor(props) {
super(props);
//如果在构造器中想要通过实例取到props(this.props),必须要在constructor和super中传递props
console.log('constructor',this.props)
}
constructor() {
super();
//constructor和super中没有传递props,则以下代码会this.props会是undefined
console.log('constructor',this.props) //打印constructor undefined
}
2.3.6 函数式组件使用props
函数式组件只能使用props(不能使用state),对标签属性的限制也只能写在外侧
2.4 组件实例三大核心属性refs和事件处理
2.4.1 ref理解
组件内的标签可以定义ref属性来标识自己(相当于原生js中的id)好处是不需要再获取节点
原生js写法:
react中的ref写法
在控制台打印当前实例对象this时,可以看到refs属性的值是一个对象,键表示ref的名称,值表示ref当前处在的节点,如input节点(input整个html标签),refs可以收集多个ref
2.4.2 字符串形式ref
分别使用点击事件和失去焦点事件获取文本框的值,先给标签绑定ref属性,在事件的方法中通过实例对象的refs属性获取当前ref所在的节点
不推荐使用,写的多了效率可能会变低
<script type="text/babel">
class Demo extends React.Component{
//展示左侧输入框数据
showData = ()=>{
const {input1} = this.refs
alert(input1.value)
console.log(this)
}
//展示右侧输入框数据
showData2 = ()=>{
const {input2} = this.refs
alert(input2.value)
}
render(){
return(
<div>
<input ref="input1" type="text" placeholder="点击提示数据"/>
<button onClick={this.showData}>点击提示左侧数据</button>
<input ref="input2" onBlur={this.showData2} type="text" placeholder="失去焦点提示数据"/>
</div>
)
}
}
ReactDOM.render(<Demo/>, document.getElementById('test'))
</script>
2.4.3 内联函数回调形式的ref
-
执行过程:
React渲染页面时先new Demo的实例,通过实例帮我们调用render方法,调用的同时会执行return里面jsx语法,同时也就帮我们触发了ref函数的执行(只有ref属性的回调函数React才会帮我们调用其他的不会),并把ref当前所处的节点(currentNode)传入,如图所示控制的打印的就是当前input节点 -
基本使用
一般使用时,会将这个节点挂载到实例对象的某个属性上
//this是在render方法中,render的this是当前的实例对象
<input ref={(currentNode)=>{this.input1 = currentNode}} type="text" placeholder="点击提示数据"/>
- ref调用次数
在更新过程中(更新状态才会重新调用render)会执行两次,第一次传参数null,第二次传入参数DOM元素。这是因为每次渲染时会创建新的函数实例,所有react清空旧的ref并设置新的,定义成class绑定的形式可以避免这个问题,但是大多数情况下是无关紧要的- 下面代码中页面第一次加载时会自动调用render一次并出发ref的内联回调函数,所以控制台输出了当前ref节点
- 当更新状态后,控制台变化,会先打印一个null,再打印当前节点
- 下面代码中页面第一次加载时会自动调用render一次并出发ref的内联回调函数,所以控制台输出了当前ref节点
2.4.4 create方式的ref
使用React中的createRef方法,该方法可以返回一个容器,容器中可以存储被ref标识的节点
2.4.5 create方式的ref
注意:一个容器只能存储一个节点
,多次使用该容器则会被覆盖
2.4.6 事件处理
-
通过onXxx属性指定事件处理函数(注意大小写)
- React使用的是自定义(合成)事件, 而不是使用的原生DOM事件——————————为了更好的兼容性
- React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)——————————为了更高效
-
不能过度使用Ref,可以通过event.target得到发生事件的DOM元素对象(就是事件对象)
什么时候可以避免使用Ref?发生事件的元素正好是我们需要操作的元素
2.5 收集表单数据
2.5.1 非受控组件
使用 ref 来从 DOM 节点中获取表单数据(访问节点,通过节点访问值),与状态没有任何关系,这种就是非受控组件。
2.5.2 受控组件(建议使用,因为可以防止过度使用Ref)
页面中输入类的DOM,随着输入,将内容维护到状态中,需要用时直接在状态中取出,这就是受控组件
不用函数柯里化,优化代码
2.5.3高阶函数
高阶函数
:如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数,
常见的高阶函数有:Promise、setTimeout、arr.map()等等
- 若A函数,接收的
参数是一个函数
,那么A就可以称之为高阶函数。 - 若A函数,调用的
返回值依然是一个函数
,那么A就可以称之为高阶函数。
函数的柯里化
:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。
- 普通JS代码演示函数柯里化
//普通函数
function sum(a,b,c){
return a+b+c
}
//函数柯里化
function sumK(a){
return(b)=>{
return(c)=>{
return a+b+c
}
}
}
const result = sum(1,2,3)
const resultK = sumK(1)(2)(3)
console.log(result,resultK)
-
使用高阶函数优化受控组件代码
-
易错点分析
- onchange事件回掉如果写了小括号就是将函数的返回值作为onchange的回掉,函数没有返回值就是undefined调用会报错,解决思路就是将返回值写成函数,真正的回掉是返回值里的函数当触发事件时react帮我们调用的,所以将事件对象作为真正回掉函数的形参
- 这里dataType没有使用方括号,会导致存储状态时是将dataType作为字符串读取的,而不是变量我们传入的参数,对象中所有的属性都是字符串,只不过我们平常简写把引号去掉了,实际上他还是字符串,所以他读取的是dataType字符串而不是这个变量,从而导致状态中多了一个属性dateType,而不是根据我们传入的参数更改的,解决这个问题需要使用[]来读取变量
let a = 'name' let obj = {} obj.a = '小美' //错误写法 打印的会是{a:'小美'} obj[a] = '小美' //正确写法,使用[]读变量, 打印的会是{name:'小美'} console.log(obj)
- onchange事件回掉如果写了小括号就是将函数的返回值作为onchange的回掉,函数没有返回值就是undefined调用会报错,解决思路就是将返回值写成函数,真正的回掉是返回值里的函数当触发事件时react帮我们调用的,所以将事件对象作为真正回掉函数的形参
2.5 组件的生命周期
2.5.1 引出生命周期
- 挂载(mount):当组件第一次被渲染到DOM中,就会为其设置一个计时器,就称为挂载
- 卸载(unmount):当DOM中组件被删除时,清除计时器,就成为卸载
- ReactDOM.unmountComponentAtNode():卸载组件
- componentDidMount:当组件挂载到页面上时只调用一次,调用时机:组件挂载完毕
- componentWillUnmount:,调用时机:组件将要卸载时
一个完整生命周期的案例
<script type="text/babel">
//创建组件
class Life extends React.Component{
state = {
opacity:1
}
//调用时机:组件挂载完毕
componentDidMount(){
this.timer = setInterval(()=>{
let {opacity} = this.state
opacity -= 0.1
if(opacity <= 0) opacity = 1
this.setState({
opacity:opacity
//opacity 简写:对象的key和value同名可以只写key
})
},200)
}
//调用时机:组件将要卸载
componentWillUnmount(){
//关闭定时器 方式二
clearInterval(this.timer)
}
//调用时机:初始化渲染、状态更新
render(){
this.death = () =>{
//关闭定时器 方式一
clearInterval(this.timer)
//卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
return(
<div>
<h2 style={{opacity:this.state.opacity}}>React学不会怎么办?</h2>
<button onClick={this.death}>不活了</button>
</div>
)
}
}
//渲染组件
ReactDOM.render(<Life/>,document.getElementById('test'))
</script>
关键点分析:
- 定时器的位置,当想要实现一打开页面自动触发执行一次定时器时,需要将定时器写到componentDidMount函数中,这个函数是与render相同级别的函数,如果不使用这个函数,而是将定时器写在render中会引发无限循环的递归,造成render执行了无数次,这与render的执行次数有关render执行次数是1+n次,n指状态更新的次数,一次执行中在定时器中更新状态,又会重新再执行render,再开启一个定时器,无限循环
- 在卸载组件前需要先关闭定时器,如果没有关闭点击按钮卸组件会报错,原因点击后组件卸载了,但是组件的定时器还一直在跑,并且定时器中还在更改状态,所以导致报错
2.5.2 理解
常见说法:生命周期回调函数、生命周期钩子函数、生命周期函数、生命周期钩子(钩子的含义就是,在合适的时间点把挂载好的这个函数勾出来执行一下)
- 组件对象从创建到死亡它会经历特定阶段。
- React 组件对象包含一系列勾子函数(生命周期回调函数),在特定的时刻调用,
- 我们在定义组件时,在特定的生命周期回调的数,中做持定的工作。
2.5.3 生命周期(旧)
- 挂载(初始化)阶段————由ReactDOM.render()触发,初次渲染
构造器constructor ——> 组件将要挂载componentWillMount ——> 渲染render ——> 组件挂载完毕componentDidMount
- 卸载组件————由ReactDOM.unmountComponentAtNode()触发
点击卸载按钮卸载组件,卸载之前会调用componentWillUnmount - 更新阶段
- 从setState开始,当状态发生更新,会先调用shouldComponentUpdate,如果返回false,则会中断,不会继续执行调用render重新渲染,不写默认为true
- 从forceUpdate开始,强制更新,不更改状态也能更新组件,或者shouldComponentUpdate阀门为false时也能进行更新
- 从父组件更新开始,父组件更新了才会执行该流程线,第一次父组件没有更新时不会执行
- 从setState开始,当状态发生更新,会先调用shouldComponentUpdate,如果返回false,则会中断,不会继续执行调用render重新渲染,不写默认为true
常用钩子
render
:初始化渲染或更新渲染调用
componentDidMount
:一般在这个钩子中做一些初始化
的事,例如:开启定时器,发送网络请求、订阅消息
componentWillUnmountMount
:一般在这个钩子中做一些收尾
的事,例如关闭定时器、取消订阅消息
reder
:必须用的一个
2.5.4 生命周期(新)
新的生命周期需要更新使用的相关js库
推荐进入BootCDN下载
-
废弃的三个生命周期函数
新版本中,以下三个生命周期函数需要加上UNSAFE_前缀,并不建议使用,因为在异步渲染时误用钩子,可能会出现Bug
UNSAFE_componentWillMount
UNSAFE_componentWillUpdate
UNSAFE_componentWillReceiveProps所有will相关的钩子需要加前缀,除了willUnmount
-
新增了两个生命周期函数(实际使用的频率都比较低)
statis getDerivedStateFromProps(props, state)
它应该返回一个对象来更新 state,或者返回 null 就不更新任何内容。
静态方法,需要使用static修饰。
该方法接收到两个参数,一个是props(组件的传入的属性如<CustomButton count={10} / >,这里props就是{count:10}),一个是当前组件的state。
如果定义了静态 getDerivedStateFromProps, React将在调用render之前调用它,无论是在初始挂载时还是在后续更新时。
罕见用例,即state的值在任何时候都取决于props
getSnapshotBeforeUpdate(preProps, preState)
当组件即将要渲染到页面上时,将调用该方法。
该方法接收到两个参数,一个是旧的props,一个是当前组件旧的 state。
如使用该方法,则必须要返回一个快照值(snapshot)。可以是任何类型的数据,如数值、字符串、对象等,也可以是null,但不能是undefined。该快照值将作为执行 componentDidUpdate的第三个参数。 -
新增的两个钩子演示
-
getSnapshotBeforeUpdate使用场景
class NewsList extends React.Component{
state = { newsList: [] }
listRef = React.createRef()
getSnapshotBeforeUpdate(){
console.log(this.listRef.current.scrollHeight)
return this.listRef.current.scrollHeight
}
// 解决看指定新闻时,新的新闻进来导致的滚动问题
componentDidUpdate(preProps,preState,snapshotValue){
// 方式一
if(snapshotValue >= 150){
console.log("scrollTop", this.listRef.current.scrollTop)
this.listRef.current.scrollTop += 30
}
// 方式二
// this.listRef.current.scrollTop += this.listRef.current.scrollHeight - snapshotValue
}
componentDidMount() {
setInterval(() => {
const { newsList } = this.state
const newNewsItem = "新闻" + (newsList.length + 1)
// 在数组前面加元素,push是在数组后面加元素
this.setState({ newsList: [newNewsItem, ...newsList] })
}, 1000)
}
render(){
const { newsList } = this.state
return(
<div className="list" ref={this.listRef}>
{newsList.map((item,index)=>(
<div className="news" key={index}>{item}</div>
))}
</div>
)
}
}
2.6 DOM的Diffing算法
2.6.1 Diffing算法
在 React 中,DOM 的 Difing(差异比较)算法是一种优化手段,用于确定 虚拟 DOMQ 树与实际 DOM 树之间的差异,并仅更新必要的部分,以提高性能
Diffing算法基本原理
React 在进行 ViualDOM 更新时,会通过 Difing 算法对新l日 Virtual DOM 进行比较,找出变化并仅更新这部分差异,而不是整个重新渲染。这个算法的基本原理可以概括为:
- Tree Reconciliation(树协调):对比新旧树的结构,找出变化的部分。
- Component Reconciliation(组件协调): 对比同一层级的组件只,找出需要更新的组件
基本原理图
验证Diffing算法
<script type="text/babel">
/* 根据页面显示时间一直在更新,input框没有更新来验证diff算法,逐层对比虚拟DOM不同就重新生成新的真实DOM 相同就复用旧的真实DOM,最小的粒度是标签(节点)*/
class Time extends React.Component{
state = {data: new Date()}
componentDidMount(){
setInterval(()=>{
this.setState({
data: new Date()
})
},1000)
}
render(){
return(
<div>
<h1>hello</h1>
<input text="text"/>
<span>
现在是:{this.state.data.toTimeString()}
<input text="text"/>
</span>
</div>
)
}
}
ReactDOM.render(<Time/>,document.getElementById('test'))
</script>
2.6.2 Key
- Key的作用
简单的说:key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用。
详细的说:当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】随后React进行【新虚拟DOM】分【旧虚拟DOM】的diff比较,比较规则如下:
a.旧虚拟DOM中找到了与新虚拟DOM机同的key:
若虚拟DOM中内容没变,直接使用之前的真实DOM
若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
b.旧虚拟DOM中未找到与新虚拟DOM相同的key
根据数据创建新的真实DOM,随后渲染到到页面
-
用index作为key可能会引发的问题:
- 1). 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新==>界面效果没问题,但效率低。
- 2). 如果结构中还包含输入类的DOM:会产生错误D0M更新 ==>界面有问题。
- 3). 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
-
开发中如何选择key:
- 1). 最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值。
- 2). 如果确定只是简单的展示数据,用index也是可以的。
第3章 React应用
3.1.使用 create-react-app 创建 react 应用
3.1.1 react脚手架
- xxx 脚手架: 用来帮助程序员快速创建一个基于 xxx 库的模板项目
- 包含了所有需要的配置(语法检查、jsx编译、devServer…)←
- 下载好了所有相关的依赖
- 可以直接运行一个简单效果
- react 提供了一个用于创建react 项目的脚手架库: create-react-app←
- 项目的整体技术架构为: react+webpack+es6+eslinte
- 使用脚手架开发的项目的特点: 模块化,组件化,工程化
3.1.2 创建项目并启动
windows+R----->输入cmd进入命令控制窗口
第一步,全局安装:npm i -g create-react-app
第二步,切换到想创项目的目录,使用命令:create-react-app react_staging
第三步,进入项目文件夹:cd react_staging
第四步,启动项目:npm start
3.1.3 脚手架项目结构
- public ---- 静态资源文件夹
- favicon.icon ------ 网站页签图标
index.html -------- 主页面
- logo192.png ------- logo图
- logo512.png ------- logo图
- manifest.json ----- 应用加壳的配置文件
- robots.txt -------- 爬虫协议文件
- src ---- 源码文件夹
- App.css -------- App组件的样式
App.js --------- App组件
- App.test.js ---- 用于给App做测试
- index.css ------ 样式
index.js ------- 入口文件,引入App渲染到页面
- logo.svg ------- logo图
- reportWebVitals.js
— 页面性能分析文件(需要web-vitals库的支持) - setupTests.js
---- 组件单元测试的文件(需要jest-dom库的支持)
3.1.4 脚手架执行顺序
3.1.5 小案例
组件可以使用.jsx后缀方便区分,index.js是入口文件
3.1.6 样式模块化
当多个css文件中出现名字一样的,会导致样式覆盖问题,所以要使用样式的模块化
3.1.7 功能界面的组件化编码流程(通用)
- 拆分组件: 拆分界面,抽取组件
- 实现静态组件: 使用组件实现静态页面效果
- 实现动态组件
- 3.1 动态显示初始化数据
- 3.1.1 数据类型
- 3.1.2 数据名称
- 3.1.2 保存在哪个组件?
- 3.2 交互(从绑定事件监听开始)
- 3.1 动态显示初始化数据