【React】React学习笔记1(React概念)

React学习笔记1(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是什么

  1. 虚拟DOM实际上是一个JS的Object类型(一般对象)
  2. 虚拟DOM的属性比真实DOM的属性要少很多
  3. 虚拟DOM会被React转换为真实DOM

(四)JSX语法规则(JavaScript XML)

  1. 定义虚拟DOM时,不要写引号;跨行可以使用小括号。
<script type="text/babel">
 const VDOM = (
   <h1 id="title">
     <span>Hello,React</span>
   </h1>
 )
</script>
  1. 标签中混入 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>
  1. 样式的类名指定不要用class,要用className。
<style>
 .title{
   background-color: orange;
   width: 200px;
 }
</style>

<script type="text/babel">
 const VDOM = (
   <div className="title"></div>
 )
</script>
  1. 内联样式,要用style={{key:value}}的形式去写。
<script type="text/babel">
 const VDOM = (
   <div>
     <span style={{color:'white',fontSize:'29px'}}></span>
   </div>
 )
</script>
  1. 只有一个根标签

下面这样会报错:

<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>
  1. 标签必须闭合(有结束)

下面这样会报错,可以改为<input type="text" /><input type="text"></input>

<script type="text/babel">
 const VDOM = (
   <input type="text">
 )
</script>
  1. 标签首字母
    (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等 代码和资源的集合(组件化是模块化的升级版)


(一)函数式组件【简单组件】

写法要求:

  1. 函数名首字母大写
  2. 函数要有返回值
  3. 渲染时使用<函数名/>作为组件渲染

示例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>

渲染组件到页面的代码运行时发生了什么:

  1. React 解析 组件标签,找到了MyComponent组件。
  2. 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM 转换 为真实DOM,随后呈现在页面中。

(二)【重点】类式组件【复杂组件】

写法要求:

  1. 类必须要继承React.Component
  2. 类必须重写render()方法
  3. render()方法必须有返回值
  4. 渲染时使用<函数名/>作为组件渲染

示例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()方法是放在哪里的:

  1. MyComponent的原型对象上,供实例使用。

render()方法中的this是谁:

  1. MyComponent的实例对象,也即MyComponent组件实例对象。

渲染组件到页面的代码运行时发生了什么:

  1. React 解析 组件标签,找到了MyComponent组件。
  2. 发现组件是使用类定义的,随后new出来该类的实例,并通过该实例 调用 到原型上的 render方法
  3. 将render返回的虚拟DOM 转换 为真实DOM,随后呈现在页面中。

JS的类特性:

  1. 类里面可以 直接 写构造器、方法、赋值语句 ;不能直接定义变量。

(三)【重点】组件实例的三大核心属性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>

几个注意点:

  1. 类事件调用的类方法(回调操作),此时类方法中的this会是 undefined ,需要进行绑定操作。
  2. 状态 state 必须通过 setState({key:value}) 方法更新对应key的value。
  3. state属性虽然可以直接赋值,但是仍然建议使用对象进行包装,一是因为状态可能会有多个值,二是因为setState()方法只能传key-value。
  4. jsx中将html里的onclick改为了onClick
  5. onClick事件不能使用{this.changeWeather()}这种带括号的形式进行绑定,因为这算是直接方法调用,没有把方法与onClick事件绑定

示例1总结:

  1. 构造器中主要进行
    (1) 初始化赋值
    (2) 绑定this到方法
  2. render()中主要进行
    (1) 读取数据并更新页面
  3. 自定义方法中主要进行
    (1) 自定义操作

2、【总结】简化代码

提出问题:

  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、优化代码结构

提出问题:

  1. 每个元素查找的时候都要this.props.xxx
  2. 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. 如果传入了字符串再进行+1操作,就会自动转化为字符串拼接导致出错,所以要有传入类型需要限制
  2. 需要对传输的key进行必要性设置,有些属性如果没有,这个对象就没意义了
  3. 应该能给某些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>

总结:

  1. 约束不满足时,会有报错提示,但不会影响渲染
  2. 类.propTypes变量(变量名固定):设置属性类型、属性是否为空
    (1) React 15.5之后(含),PropTypes.stringPropTypes.numberPropTypes.func对应字符串类型、数字类型、函数类型
    (2) React 15.4之前(含),React.PropTypes.stringReact.PropTypes.numberReact.PropTypes.func对应字符串类型、数字类型、函数类型
    (3) 类型后面加上.isRequired代表该属性传入时不能为空
  3. 类.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="点击按钮提示数据"/>&nbsp;
         <button onClick={this.showData}>点我提示左侧的数据</button>&nbsp;
         
         <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="点击按钮提示数据"/>&nbsp;
         <button onClick={this.showData}>点我提示左侧的数据</button>&nbsp;
         <input onBlur={this.showData2} ref={c => this.input2 = c } type="text" placeholder="失去焦点提示数据"/>&nbsp;
       </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="点击按钮提示数据"/>&nbsp;
         <button onClick={this.showData}>点我提示左侧的数据</button>&nbsp;
         <input onBlur={this.showData2} ref={this.myRef2} type="text" placeholder="失去焦点提示数据"/>&nbsp;
       </div>
     )
   }
 }
 
 // 2.渲染组件到页面
 ReactDOM.render(<Demo a="1" b="2"/>,document.getElementById('test'))
</script>

(六)事件处理(为什么onclick要写成onClick)

  1. 通过onXxx属性指定事件处理函数(注意大小写)
    (1) React使用的是自定义(合成)事件, 而不是使用的原生DOM事件 —— 为了更好的兼容性
    (2) React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) —— 为了的高效
  2. 通过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="失去焦点提示数据"/>&nbsp;
       </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、旧版

3.3.1

图片解释:

  1. 初始化阶段: 由 ReactDOM.render() 触发的初次渲染
    (1) constructor()
    (2) componentWillMount()
    (3) render()
    (4) 【常用】componentDidMount()(一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息)
  2. 更新阶段: 由组件内部 this.setSate() 、forceUpdate() 或 父组件render 触发
    (1) componentWillReceiveProps()
    (2) shouldComponentUpdate()
    (3) componentWillUpdate()
    (4) 【必用】render()
    (5) componentDidUpdate()
  3. 卸载组件: 由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、新版

3.3.2

图片解释:

  1. 初始化组件: 由 ReactDOM.render() 触发的初次渲染
    (1) constructor()
    (2) 【新】【不常用】getDerivedStateFromProps()
    (3) render()
    (4) 【常用】componentDidMount()(一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息)
  2. 更新组件: 由组件内部 this.setSate() 、forceUpdate() 或 父组件render 触发
    (1) 【新】【不常用】getDerivedStateFromProps()
    (2) shouldComponentUpdate()
    (3) 【必用】render()
    (4) 【新】【不常用】getSnapshotBeforeUpdate()
    (5) componentDidUpdate()
  3. 卸载组件: 由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的作用

  1. Java版解释:
      key就是Java中实例的hashcode,当且仅当两个实例hashcode()相同且equals()也相同时,虚拟DOM不会替换真实DOM,此外都会进行替换。
  2. 萌新版解释:
      当状态中的数据发生变化时,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的输入框现在会对应成小王—203.4.1

3、Diffing算法的几个简单面试题

  1. 面试题1: 虚拟DOM中key的作用?
    (1) 略,前面有说
  2. 面试题2: 用index作为key可能会引发的问题?
    (1) 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
    (2) 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
    (3) 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
  3. 面试题3:开发中如何选择key?
    (1) 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
    (2) 如果确定只是简单的展示数据,用index也是可以的。

4、【扩展】Diffing算法流程图

想了解更多具体内容请自行搜索
3.4.2







(本文完)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值