React学习笔记(概念篇)
- 零 l React 简介
- 一 l React 第一步
- 二 l React 面向组件编程 - 基础
- 三 l React 面向组件编程 - 高级
零 l React 简介
本文参考教程:[尚硅谷2021版React技术全家桶]
React的官方定义是“用于构建用户界面的 JavaScript 库”。
React有声明式、组件化、随处编写三大特点。
React通过虚拟DOM和Diffing算法,减少与真实DOM的交互来提高程序运行效率。
一 l React 第一步
(一)依赖包
react.development.js:react核心依赖
react-dom.development.js:react进行DOM操作的依赖
babel.min.js:
1.将ES6转为ES5
2.将JSX转为JS(JSX是有语法糖的写法,更加方便)
prop-types.js:props属性的类型限制依赖包(下面的 二-(四)-3 开始需要使用此包)
(二)写一个 Hello React
【注意:这里使用babel进行代码翻译(翻译速度慢,但是适合学习),后面会学习其他方法】
1、引入依赖
<script type="text/javascript" src="../js/react.development.js"></script> <script type="text/javascript" src="../js/react-dom.development.js"></script> <script type="text/javascript" src="../js/babel.min.js"></script>
2、准备一个容器
<div id="test"/>
3、写React代码
<script type="text/babel" > /* 此处一定要写babel */ // 1.创建虚拟DOM const VDOM = ( /* 此处一定不要写引号,因为不是字符串 */ <h1 id="title"> <span>Hello,React</span> </h1> ) // 2.渲染虚拟DOM到页面,替换操作 ReactDOM.render(VDOM,document.getElementById('test')) </script>
(三)虚拟DOM是什么
- 虚拟DOM实际上是一个JS的Object类型(一般对象)
- 虚拟DOM的属性比真实DOM的属性要少很多
- 虚拟DOM会被React转换为真实DOM
(四)JSX语法规则(JavaScript XML)
- 定义虚拟DOM时,不要写引号;跨行可以使用小括号。
<script type="text/babel"> const VDOM = ( <h1 id="title"> <span>Hello,React</span> </h1> ) </script>
- 标签中混入 JS表达式 时要用
{js表达式}
的格式。
一定注意区分【js语句(代码)】与【js表达式】
(1) 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方,下面这些都是表达式:
a. a
b. a+b
c. demo(1)
d. arr.map()
e. function test () {}
(2)语句(代码),下面这些都是语句(代码):
a. if(){}
b. for(){}
c. switch(){case:xxxx}<script type="text/babel"> const myId = 'aTgUiGu' const myData = 'HeLlo,rEaCt' const VDOM = ( <div> <h2 id={myId.toLowerCase()}> <span>{myData.toUpperCase()}</span> </h2> </div> ) </script>
- 样式的类名指定不要用class,要用className。
<style> .title{ background-color: orange; width: 200px; } </style> <script type="text/babel"> const VDOM = ( <div className="title"></div> ) </script>
- 内联样式,要用style={{key:value}}的形式去写。
<script type="text/babel"> const VDOM = ( <div> <span style={{color:'white',fontSize:'29px'}}></span> </div> ) </script>
- 只有一个根标签
下面这样会报错:
<script type="text/babel"> const VDOM = ( <div> <span style={{color:'white',fontSize:'29px'}}></span> </div> <div> <span style={{color:'white',fontSize:'29px'}}></span> </div> ) </script>
- 标签必须闭合(有结束)
下面这样会报错,可以改为
<input type="text" />
或<input type="text"></input>
<script type="text/babel"> const VDOM = ( <input type="text"> ) </script>
- 标签首字母
(1) 若小写字母开头,则将该标签转为html中同名元素,若html中无该标签对应的同名元素(如<good></good>
),则报错。
(2) 若大写字母开头,react就去渲染对应的组件,若组件没有定义,则报错。
(五)如何在{}中不用语句遍历列表
<script type="text/babel"> //模拟一些数据 const data = ['Angular','React','Vue'] //1.创建虚拟DOM const VDOM = ( <div> <h1>前端js框架列表</h1> <ul> { data.map((item,index)=>{ return <li key={index}>{item}</li> }) } </ul> </div> ) </script>
二 l React 面向组件编程 - 基础
简单说明模块和组件的概念:
模块:一个 提供特定功能 的 js文件 就是一个模块(以前都是模块化)
组件:一个实现页面上 一块区域 的所有html、css、js、img等 代码和资源的集合(组件化是模块化的升级版)
(一)函数式组件【简单组件】
写法要求:
- 函数名首字母大写
- 函数要有返回值
- 渲染时使用
<函数名/>
作为组件渲染
示例2.1:
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 1.创建函数式组件 function MyComponent(){ console.log(this); //此处的this是undefined,因为babel编译后开启了严格模式 return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2> } // 2.渲染组件到页面 ReactDOM.render(<MyComponent/>,document.getElementById('test')) </script>
渲染组件到页面的代码运行时发生了什么:
- React 解析 组件标签,找到了MyComponent组件。
- 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM 转换 为真实DOM,随后呈现在页面中。
(二)【重点】类式组件【复杂组件】
写法要求:
- 类必须要继承
React.Component
类 - 类必须重写
render()
方法 -
render()
方法必须有返回值 - 渲染时使用
<函数名/>
作为组件渲染
示例2.2:
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 1.创建类式组件 class MyComponent extends React.Component { render(){ return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2> } } // 2.渲染组件到页面 ReactDOM.render(<MyComponent/>,document.getElementById('test')) </script>
render()方法是放在哪里的:
- MyComponent的原型对象上,供实例使用。
render()方法中的this是谁:
- MyComponent的实例对象,也即MyComponent组件实例对象。
渲染组件到页面的代码运行时发生了什么:
- React 解析 组件标签,找到了MyComponent组件。
- 发现组件是使用类定义的,随后new出来该类的实例,并通过该实例 调用 到原型上的 render方法 。
- 将render返回的虚拟DOM 转换 为真实DOM,随后呈现在页面中。
JS的类特性:
- 类里面可以 直接 写构造器、方法、赋值语句 ;不能直接定义变量。
(三)【重点】组件实例的三大核心属性1 —— state
只有类式组件才有该属性(类式组件才有实例),所以称类式组件为复杂组件。
最新版的React中函数式组件也可以通过hooks拥有该属性,后面会讲。
state状态:state里存储状态有关的数据,数据的改变会导致state的改变,state的改变会导致页面的改变。
1、从一个示例开始
示例2.3.1:实现点击
<h1>
标签后,文字在“今天天气很炎热”和“今天天气很凉爽”直接转换。<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 1.创建组件 class Weather extends React.Component{ // 构造器调用几次? ———— 1次 constructor(props){ super(props) // 初始化状态 this.state = {isHot:false,wind:'微风'} // 解决changeWeather中this指向问题 this.changeWeather = this.changeWeather.bind(this) } // render调用几次? ———— 1+n次 1是初始化的那次 n是状态更新的次数 render(){ //读取状态 const {isHot,wind} = this.state return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1> } //changeWeather调用几次? ———— 点几次调几次 changeWeather(){ // changeWeather放在哪里? ———— Weather的原型对象上,供实例使用 // 由于changeWeather是作为onClick的回调,所以不是通过实例调用的,是直接调用 // 类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefined //获取原来的isHot值 const isHot = this.state.isHot this.setState({isHot:!isHot}) // 严重注意:状态(state)不可直接更改,下面这行就是直接更改!!! // this.state.isHot = !isHot // 这是错误的写法 } } // 2.渲染组件到页面 ReactDOM.render(<Weather/>,document.getElementById('test')) </script>
几个注意点:
- 类事件调用的类方法(回调操作),此时类方法中的this会是 undefined ,需要进行绑定操作。
- 状态 state 必须通过 setState({key:value}) 方法更新对应key的value。
- state属性虽然可以直接赋值,但是仍然建议使用对象进行包装,一是因为状态可能会有多个值,二是因为setState()方法只能传key-value。
- jsx中将html里的
onclick
改为了onClick
onClick
事件不能使用{this.changeWeather()}
这种带括号的形式进行绑定,因为这算是直接方法调用,没有把方法与onClick
事件绑定
示例1总结:
- 构造器中主要进行
(1) 初始化赋值
(2) 绑定this到方法 - render()中主要进行
(1) 读取数据并更新页面 - 自定义方法中主要进行
(1) 自定义操作
2、【总结】简化代码
提出问题:
- 当方法越来越多,构造器中函数绑定的方法就会使代码变得臃肿。
- 能不能简化掉构造器,简化代码
示例2.3.2:优雅的去掉构造器
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 1.创建组件 class Weather extends React.Component{ // 初始化状态 state = {isHot:false, wind:'微风'} render(){ const {isHot,wind} = this.state return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1> } // 自定义方法 ———— 要用赋值语句的形式+箭头函数 // 箭头函数会找外部调用函数的this changeWeather = ()=>{ const isHot = this.state.isHot this.setState({isHot:!isHot}) } } // 2.渲染组件到页面 ReactDOM.render(<Weather/>,document.getElementById('test')) </script>
(四)【重点】组件实例的三大核心属性2 —— props
类式组件和函数式组件都可以有该属性
最新版的React中函数式组件也可以通过hooks拥有该属性,后面会讲。
props属性(properties的简写):props存储外部调用组件时传入的参数,且只能读不能改
1、从一个示例开始
示例2.4.1:让React渲染时,用类似html标签赋值的方式将属性值传入组件内部,组件内部用props属性获取
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 1.创建组件 class Person extends React.Component{ render(){ return ( <ul> <li>姓名:{this.props.name}</li> <li>性别:{this.props.sex}</li> <li>年龄:{this.props.age}</li> </ul> ) } } // 2.渲染组件到页面 ReactDOM.render(<Person name="jerry" age="19" sex="男"/>,document.getElementById('test')) </script>
2、优化代码结构
提出问题:
- 每个元素查找的时候都要this.props.xxx
- React渲染时,如果Person的属性多的话,就会写很长,而且要适应后端来的数据
示例2.4.2:优化代码
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 1.创建组件 class Person extends React.Component{ render(){ // 解决问题1 const {name,age,sex} = this.props return ( <ul> <li>姓名:{name}</li> <li>性别:{sex}</li> <li>年龄:{age+1}</li> </ul> ) } } // 2.渲染组件到页面 // 解决问题2 const p = {name:'jerry',age:18,sex:'女'} ReactDOM.render(<Person {...p}/>,document.getElementById('test')) </script>
{…P}:在JSX中表示将对象展开成标签的属性;在JS中表示将对象展开
注意:在JSX中只能表示为标签的属性,与JS效果不同
3、新的问题出现了
提出问题:
- 如果传入了字符串再进行+1操作,就会自动转化为字符串拼接导致出错,所以要有传入类型需要限制
- 需要对传输的key进行必要性设置,有些属性如果没有,这个对象就没意义了
- 应该能给某些key对应的value设置一个默认值
示例2.4.3:为value添加约束
<!-- 准备“容器” --> <div id="test"/> <!-- 引入prop-types,用于对组件标签属性进行限制 --> <script type="text/javascript" src="../js/prop-types.js"></script> <script type="text/babel"> // 1.创建组件 class Person extends React.Component{ render(){ const {name,age,sex} = this.props return ( <ul> <li>姓名:{name}</li> <li>性别:{sex}</li> <li>年龄:{age+1}</li> </ul> ) } } // 对标签属性进行类型、必要性的限制 Person.propTypes = { name:PropTypes.string.isRequired, // 限制name为字符串,且必传 sex:PropTypes.string, // 限制sex为字符串 age:PropTypes.number, // 限制age为数值 speak:PropTypes.func, // 限制speak为函数 } // 指定默认标签属性值 Person.defaultProps = { sex:'男', // sex默认值为男 age:18 // age默认值为18 } // 2.渲染组件到页面 const p = {name:'jerry',age:18,sex:'女'} ReactDOM.render(<Person {...p}/>,document.getElementById('test')) </script>
总结:
- 约束不满足时,会有报错提示,但不会影响渲染
类.propTypes变量
(变量名固定):设置属性类型、属性是否为空
(1) React 15.5之后(含),PropTypes.string
、PropTypes.number
、PropTypes.func
对应字符串类型、数字类型、函数类型
(2) React 15.4之前(含),React.PropTypes.string
、React.PropTypes.number
、React.PropTypes.func
对应字符串类型、数字类型、函数类型
(3) 类型后面加上.isRequired
代表该属性传入时不能为空类.defaultProps变量
(变量名固定):设置属性默认值
4、【总结】简化代码
示例2.4.4:将
类.propTypes变量
和类.defaultProps变量
放入类中(注意是成为类的属性,不是成为实例的属性)<!-- 准备“容器” --> <div id="test"/> <!-- 引入prop-types,用于对组件标签属性进行限制 --> <script type="text/javascript" src="../js/prop-types.js"></script> <script type="text/babel"> // 1.创建组件 class Person extends React.Component{ // 对标签属性进行类型、必要性的限制 static propTypes = { name:PropTypes.string.isRequired, // 限制name为字符串,且必传 sex:PropTypes.string, // 限制sex为字符串 age:PropTypes.number, // 限制age为数值 speak:PropTypes.func, // 限制speak为函数 } // 指定默认标签属性值 static defaultProps = { sex:'男', // sex默认值为男 age:18 // age默认值为18 } render(){ const {name,age,sex} = this.props return ( <ul> <li>姓名:{name}</li> <li>性别:{sex}</li> <li>年龄:{age+1}</li> </ul> ) } } // 2.渲染组件到页面 const p = {name:'jerry',age:18,sex:'女'} ReactDOM.render(<Person {...p}/>,document.getElementById('test')) </script>
5、【了解】讨论下构造器的props参数
构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props
<script type="text/babel"> constructor(props){ super(props) console.log('constructor',this.props); } ... </script>
6、【了解】函数式组件使用props
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 1.创建组件 function Person (props){ const {name,age,sex} = props return ( <ul> <li>姓名:{name}</li> <li>性别:{sex}</li> <li>年龄:{age}</li> </ul> ) } // 对标签属性进行类型、必要性的限制 Person.propTypes = { name:PropTypes.string.isRequired, // 限制name必传,且为字符串 sex:PropTypes.string, // 限制sex为字符串 age:PropTypes.number, // 限制age为数值 } // 指定默认标签属性值 Person.defaultProps = { sex:'男', // sex默认值为男 age:18 // age默认值为18 } // 2.渲染组件到页面 ReactDOM.render(<Person name="jerry"/>,document.getElementById('test')) </script>
(五)【重点】组件实例的三大核心属性3 —— refs【尽量不使用】
只有类式组件才有该属性(类式组件才有实例),所以称类式组件为复杂组件。
最新版的React中函数式组件也可以通过hooks拥有该属性,后面会讲。
ref:ref可以用来标示组件内的标签(类似组件外标签的id)。ref存储一系列key:value值,key为自定义的ref值,value为标签所在节点值(value如<input type="text" placeholder="点击按钮提示数据"/>
(示例2.5.1代码结果))
1、【了解】key为字符串的refs(官方不推荐使用,效率不高)
示例2.5.1:
ref="字符串"
,主要看input标签中的ref属性<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 1.创建组件 class Demo extends React.Component{ // 展示左侧输入框的数据 showData = ()=>{ const {input1} = this.refs alert(input1.value) } // 展示右侧输入框的数据 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> ) } } // 2.渲染组件到页面 ReactDOM.render(<Demo a="1" b="2"/>,document.getElementById('test')) </script>
2、key为回调函数的refs
回调函数:自定义且不由用户进行调用的函数
示例2.5.2:
ref={回调函数}
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 1.创建组件 class Demo extends React.Component{ // 展示左侧输入框的数据 showData = ()=>{ const {input1} = this alert(input1.value) } // 展示右侧输入框的数据 showData2 = ()=>{ const {input2} = this alert(input2.value) } render(){ return( <div> <input ref={c => this.input1 = c } type="text" placeholder="点击按钮提示数据"/> <button onClick={this.showData}>点我提示左侧的数据</button> <input onBlur={this.showData2} ref={c => this.input2 = c } type="text" placeholder="失去焦点提示数据"/> </div> ) } } // 2.渲染组件到页面 ReactDOM.render(<Demo a="1" b="2"/>,document.getElementById('test')) </script>
了解内容,大多数情况下无关紧要:
当页面发生变化重新渲染后,如果render()函数中有ref且为内联函数回调。React此时会调用两次内联函数,且第一次调用时形参为null,第二次才是标签所在节点值。因为每次渲染都会创建一个函数实例,使用React会清空旧的ref并生成新ref。
如果不行产生此操作,可以将ref的回调函数定义成class的绑定函数。(官方文档内容)
class的绑定函数就是就是把内联函数写到类上,然后ref指向函数的引用。【如下所示】<script type="text/babel"> //创建组件 class Demo extends React.Component{ ... saveInput = (c)=>{ this.input1 = c; console.log('@',c); } render(){ return( <div> <input ref={this.saveInput} type="text"/> </div> ) } } ... </script>
3、【推荐使用】使用 createRef API 创建的 ref
createRef API 创建的 ref 的容量只有1,即一个容器只能存储一个ref,多的会替换原来的ref容器内容
ref容器的key固定为this.容器名.current
ref容器获取value固定为this.容器名.current.value
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 1.创建组件 class Demo extends React.Component{ // 定义ref容器 myRef = React.createRef() myRef2 = React.createRef() // 展示左侧输入框的数据 showData = ()=>{ alert(this.myRef.current.value); } // 展示右侧输入框的数据 showData2 = ()=>{ alert(this.myRef2.current.value); } render(){ return( <div> <input ref={this.myRef} type="text" placeholder="点击按钮提示数据"/> <button onClick={this.showData}>点我提示左侧的数据</button> <input onBlur={this.showData2} ref={this.myRef2} type="text" placeholder="失去焦点提示数据"/> </div> ) } } // 2.渲染组件到页面 ReactDOM.render(<Demo a="1" b="2"/>,document.getElementById('test')) </script>
(六)事件处理(为什么onclick要写成onClick)
- 通过onXxx属性指定事件处理函数(注意大小写)
(1) React使用的是自定义(合成)事件, 而不是使用的原生DOM事件 —— 为了更好的兼容性
(2) React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) —— 为了的高效 - 通过event.target得到发生事件的DOM元素对象 —— 不要过度使用ref
解释2:如何减少ref的使用
当标签调用的函数形参值是标签自身的值时,可以使用event
作为形参,使用event.target.value
获取数据<div id="test"/> <script type="text/babel"> class Demo extends React.Component{ showData = (event)=>{ alert(event.target.value); } render(){ return( <div> <input onBlur={this.showData} type="text" placeholder="失去焦点提示数据"/> </div> ) } } ReactDOM.render(<Demo />,document.getElementById('test')) </script>
三 l React 面向组件编程 - 高级
(一)收集表单数据
1、非受控组件
即拿即用
示例3.1.1:
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 创建组件 class Login extends React.Component{ handleSubmit = (event)=>{ // 阻止表单提交,防止页面刷新 event.preventDefault() const {username,password} = this alert(`你输入的用户名是:${username.value},你输入的密码是:${password.value}`) } render(){ return( <form onSubmit={this.handleSubmit}> 用户名:<input ref={c => this.username = c} type="text" name="username"/> 密码:<input ref={c => this.password = c} type="password" name="password"/> <button>登录</button> </form> ) } } // 渲染组件 ReactDOM.render(<Login/>,document.getElementById('test')) </script>
2、【推荐使用】受控组件
从输入DOM中获取的值存储在state中,需要的时候再从state中取
示例3.1.2:
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 创建组件 class Login extends React.Component{ // 初始化状态 state = { username:'', // 用户名 password:'' // 密码 } // 保存用户名到状态中 saveUsername = (event)=>{ this.setState({username:event.target.value}) } // 保存密码到状态中 savePassword = (event)=>{ this.setState({password:event.target.value}) } // 表单提交的回调 handleSubmit = (event)=>{ event.preventDefault() // 阻止表单提交 const {username,password} = this.state alert(`你输入的用户名是:${username},你输入的密码是:${password}`) } render(){ return( <form onSubmit={this.handleSubmit}> 用户名:<input onChange={this.saveUsername} type="text" name="username"/> 密码:<input onChange={this.savePassword} type="password" name="password"/> <button>登录</button> </form> ) } } // 渲染组件 ReactDOM.render(<Login/>,document.getElementById('test')) </script>
(二)高阶函数、函数柯里化
首先看一段代码:
示例3.2.1:
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 创建组件 class Login extends React.Component{ // 初始化状态 state = { username:'', //用户名 password:'' //密码 } // 保存表单数据到状态中,注意!!!此时不能同时接到dataType和event参数 saveFormData = (dataType)=>{ return (event)=>{ this.setState({[dataType]:event.target.value}) } } // 表单提交的回调 handleSubmit = (event)=>{ event.preventDefault() // 阻止表单提交 const {username,password} = this.state alert(`你输入的用户名是:${username},你输入的密码是:${password}`) } render(){ return( <form onSubmit={this.handleSubmit}> 用户名:<input onChange={this.saveFormData('username')} type="text" name="username"/> 密码:<input onChange={this.saveFormData('password')} type="password" name="password"/> <button>登录</button> </form> ) } } // 渲染组件 ReactDOM.render(<Login/>,document.getElementById('test')) </script>
1、高阶函数定义
如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。
2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。
常见的高阶函数有:Promise、setTimeout、arr.map()等等
(如示例3.2.1的saveFormData函数)
2、函数的柯里化定义
通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。
(如示例3.2.1的saveFormData函数(此时不能同时接到dataType和event参数) 或 示例3.2.2)
示例3.2.2:
function sum(a){ return (b)=>{ return (c)=>{ return a+b+c } } }
补充知识点:
(1)补充知识点1:概念分别:
在JSX中的有如下两行代码,
第一行代码表示将类里的 saveFormData函数 作为 onChange的回调函数 ;
第二行代码表示将类里的 saveFormData函数返回值 作为 onChange的回调(返>回的是函数就是回调函数,无返回值则不会执行)。
第二行代码的saveFormData函数函数体只会执行一次,saveFormData函数函数返回值每次触发都会执行一次。<input onChange={this.saveFormData} type="text" name="username"/> <input onChange={this.saveFormData('username')} type="text" name="username"/>
(2)补充知识点2:
[变量名]
告诉JS变量名不是字符串,而是个需要取值的变量
(3)补充知识点3:参数
类组件的箭头函数,无传入参数时,会有默认的event参数;有参数则不会有event参数
3、【推荐使用】优化示例3.2.1(不用函数柯里化)
注意看 saveFormData函数体 和 onChange的绑定
示例3.2.3:
<!-- 准备“容器” --> <div id="test"/> <script type="text/babel"> // 创建组件 class Login extends React.Component{ // 初始化状态 state = { username:'', // 用户名 password:'' // 密码 } // 保存表单数据到状态中 saveFormData = (dataType,event)=>{ this.setState({[dataType]:event.target.value}) } // 表单提交的回调 handleSubmit = (event)=>{ event.preventDefault() // 阻止表单提交 const {username,password} = this.state alert(`你输入的用户名是:${username},你输入的密码是:${password}`) } render(){ return( <form onSubmit={this.handleSubmit}> 用户名:<input onChange={ event => this.saveFormData('username',event) } type="text" >name="username"/> 密码:<input onChange={ event => this.saveFormData('password',event) } type="password" >name="password"/> <button>登录</button> </form> ) } } // 渲染组件 ReactDOM.render(<Login/>,document.getElementById('test')) </script>
(三)【重点概念】组件的生命周期
概念上:
生命周期回调函数 == 生命周期钩子函数 == 生命周期函数 == 生命周期钩子
调用上:
生命周期函数的书写位置与调用位置无关
最常用的是componentDidMount()
、render()
、componentWillUnmount()
这三个生命周期
1、旧版
图片解释:
- 初始化阶段: 由 ReactDOM.render() 触发的初次渲染
(1) constructor()
(2) componentWillMount()
(3) render()
(4) 【常用】componentDidMount()(一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息)- 更新阶段: 由组件内部 this.setSate() 、forceUpdate() 或 父组件render 触发
(1) componentWillReceiveProps()
(2) shouldComponentUpdate()
(3) componentWillUpdate()
(4) 【必用】render()
(5) componentDidUpdate()- 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
(1) 【常用】componentWillUnmount()(一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息)
挂载时:第一次渲染
父组件render:父组件渲染导致子组件渲染
shouldComponentUpdate:是否开启组件更新,默认返回true,如果使用则需要指定返回值 true / false,不然会返回undefined报错
componentWillReceiveProps:不能接受到第一个参数(初始化参数),只会从第二个参数(改变后的参数)开始接受
setState():正常更新,更改状态引起的更新,有阀门shouldComponentUpdate控制是否允许更新
forceUpdate():强制更新,不更改状态,无视shouldComponentUpdate的返回值
示例3.3.1.1:除去父组件render和componentWillReceiveProps生命周期函数
<!-- “容器” --> <div id="test"/> ...导包... <script type="text/babel"> // 创建组件 class Count extends React.Component{ // 构造器 constructor(props){ console.log('Count---constructor'); super(props) // 初始化状态 this.state = {count:0} } /****** 自定义函数 ******/ // 加1按钮的回调 add = ()=>{ //获取原状态 const {count} = this.state //更新状态 this.setState({count:count+1}) } // 卸载组件按钮的回调 death = ()=>{ ReactDOM.unmountComponentAtNode(document.getElementById('test')) } // 强制更新按钮的回调 force = ()=>{ this.forceUpdate() } /****** 组件第一次挂载相关 *****/ // 组件将要挂载的钩子 componentWillMount(){ console.log('Count---componentWillMount'); } // 组件挂载完毕的钩子 componentDidMount(){ console.log('Count---componentDidMount'); } // 组件将要卸载的钩子 componentWillUnmount(){ console.log('Count---componentWillUnmount'); } /****** 更新相关 *****/ // 控制组件更新的“阀门”,返回true允许更新,返回false不允许更新(不允许进行接下来的流程) shouldComponentUpdate(){ console.log('Count---shouldComponentUpdate'); return true } // 组件将要更新的钩子 componentWillUpdate(){ console.log('Count---componentWillUpdate'); } // 组件更新完毕的钩子 componentDidUpdate(){ console.log('Count---componentDidUpdate'); } render(){ console.log('Count---render'); const {count} = this.state return( <div> <h2>当前求和为:{count}</h2> <button onClick={this.add}>点我+1</button> <button onClick={this.death}>卸载组件</button> <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button> </div> ) } } // 渲染组件 ReactDOM.render(<Count/>,document.getElementById('test')) </script>
示例3.3.1.2:父组件render和componentWillReceiveProps生命周期函数
<!-- “容器” --> <div id="test"/> ...导包... <script type="text/babel"> // 父组件A class A extends React.Component{ // 初始化状态 state = {carName:'奔驰'} changeCar = ()=>{ this.setState({carName:'奥拓'}) } render(){ return( <div> <div>我是A组件</div> <button onClick={this.changeCar}>换车</button> <B carName={this.state.carName}/> </div> ) } } // 子组件B class B extends React.Component{ // 组件将要接收新的props的钩子 componentWillReceiveProps(props){ console.log('B---componentWillReceiveProps',props); } render(){ console.log('B---render'); return( <div>我是B组件,接收到的车是:{this.props.carName}</div> ) } } // 渲染组件 ReactDOM.render(<A/>,document.getElementById('test')) </script>
2、新版
图片解释:
- 初始化组件: 由 ReactDOM.render() 触发的初次渲染
(1) constructor()
(2) 【新】【不常用】getDerivedStateFromProps()
(3) render()
(4) 【常用】componentDidMount()(一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息)- 更新组件: 由组件内部 this.setSate() 、forceUpdate() 或 父组件render 触发
(1) 【新】【不常用】getDerivedStateFromProps()
(2) shouldComponentUpdate()
(3) 【必用】render()
(4) 【新】【不常用】getSnapshotBeforeUpdate()
(5) componentDidUpdate()- 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
(1) 【常用】componentWillUnmount()(一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息)
新版相对于旧版,主要体现在
三个即将废弃:componentWillMount()
、componentWillUpdate()
、componentWillReceiveProps()
两个添加(基本不用):getDerivedStateFromProps()
、getSnapshotBeforeUpdate()
一个了解:React更新DOM和refs
不能进行任何操作
注意事项:
新版生命周期中,旧版的componentWillMount()
、componentWillUpdate()
、componentWillReceiveProps()
更名为UNSAFE_componentWillMount()
、UNSAFE_componentWillUpdate()
、UNSAFE_componentWillReceiveProps()
且旧版名称在18版本中停用
官方文档:这里的UNSAFE前缀不是指不安全,而是在未来的版本中可能出现bug
示例3.3.2.1:
getDerivedStateFromProps()
【从参数列表获取派生状态】的用法
作用:如果返回值state的key与初始化state的key相同,则会覆盖初始化state对应key的value
可选参数列表:props【传入参数】、state【组件状态】
返回值:null / state的{key:value}【不能什么都不返回(undefined)】
示例控制台输出:getDerivedStateFromProps > {count:199} > {count:0}
注意:该生命周期函数只适用于【state的值在任何时候都被props决定的时候】,因为该生命周期的返回值不能更改<!-- “容器” --> <div id="test"/> ...导包... <script type="text/babel"> // 创建组件 class Count extends React.Component{ // 初始化状态 state = {count:0} // 若state的值在任何时候都取决于props,那么可以使用getDerivedStateFromProps static getDerivedStateFromProps(props, state){ console.log('getDerivedStateFromProps', props, state); return props } render(){ return( <div></div> ) } } // 渲染组件 ReactDOM.render(<Count count={199}/>, document.getElementById('test')) </script>
示例3.3.2.2:
getSnapshotBeforeUpdate()
【更新前获取快照】的用法
作用:获得更新前组件的props和state
可选参数列表:prevProps
【修改前的props参数值】、prevState
【修改前的state对象值】
返回值:快照值(任何变量类型都可以)【返回值会作为componentDidUpdate的参数】<!-- “容器” --> <div id="test"/> ...导包... <script type="text/babel"> // 创建组件 class Count extends React.Component{ // 初始化状态 this.state = {count:0} // 在更新之前获取快照 getSnapshotBeforeUpdate(prevProps, prevState){ console.log('getSnapshotBeforeUpdate'); return 'atguigu' } // 组件更新完毕的钩子 componentDidUpdate(prevProps, prevState, snapshotValue){ console.log('Count---componentDidUpdate', preProps, preState, snapshotValue); } render(){ return( <div></div> ) } } // 渲染组件 ReactDOM.render(<Count count={199}/>, document.getElementById('test')) </script>
(四)【重点概念】DOM的Diffing算法
1、遍历时给的key的作用
- Java版解释:
key就是Java中实例的hashcode,当且仅当两个实例hashcode()相同且equals()也相同时,虚拟DOM不会替换真实DOM,此外都会进行替换。- 萌新版解释:
当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:
(1) 旧虚拟DOM中找到了与新虚拟DOM相同的key:
(a) 若虚拟DOM中内容没变,直接使用之前的真实DOM
(b) 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
(2) 旧虚拟DOM中未找到与新虚拟DOM相同的key:
(a)根据数据创建新的真实DOM,随后渲染到到页面
2、如果key是index会产生什么问题
例子:使用index索引值作为key
初始数据:
{id:1,name:'小张',age:18}, {id:2,name:'小李',age:19}
初始的虚拟DOM:
<li key=0>小张---18<input type="text"/></li>
<li key=1>小李---19<input type="text"/></li>
更新后的数据(在数组头插入一个数据):
{id:3,name:'小王',age:20}, {id:1,name:'小张',age:18}, {id:2,name:'小李',age:19}
更新数据后的虚拟DOM:
<li key=0>小王---20<input type="text"/></li>
<li key=1>小张---18<input type="text"/></li>
<li key=2>小李---19<input type="text"/></li>
例子产生的问题1:
初始的时候index=0的数据{id:1,name:'小张',age:18}
,与更新后index=0的数据{id:3,name:'小王',age:20}
不同,导致key=index=0的真实DOM需要更新。同理,key=index=1的真实DOM也需要更新。当数据量很大的时候,这种更新就 十分 的 消耗性能 且 无意义 。
例子产生的问题2:
React是逐级对比,会认为初始虚拟DOM中<li key=0>小张---18<input type="text"/></li>
的<input type="text"/>
与更新后的虚拟DOM中<li key=0>小王---20<input type="text"/></li>
的<input type="text"/>
是相同的,所以本来应该对应于小张—18的输入框现在会对应成小王—20
3、Diffing算法的几个简单面试题
- 面试题1: 虚拟DOM中key的作用?
(1) 略,前面有说- 面试题2: 用index作为key可能会引发的问题?
(1) 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
(2) 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
(3) 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。- 面试题3:开发中如何选择key?
(1) 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
(2) 如果确定只是简单的展示数据,用index也是可以的。
4、【扩展】Diffing算法流程图
想了解更多具体内容请自行搜索