当前位置: 代码迷 >> 综合 >> 【前端学习】React学习笔记-简介、state、props、ref
  详细解决方案

【前端学习】React学习笔记-简介、state、props、ref

热度:85   发布时间:2024-01-14 23:41:33.0

跟着尚硅谷的天禹老师学习React
看视频可以直接点击 b站视频地址

1、简介

原生JavaScript的问题

1、原生JavaScript操作DOM繁琐,效率低,可以将DOM操作托管给React。尽管有了jQuery,最后的代码量也是很大的。
2、使用JavaScript直接操作DOM,会触发大量的重绘重排,React采用了虚拟DOM。
3、原生JavaScript没有组件化方案,代码复用率低。

React的特点

1、采用组件化模式,声明式编码,提高开发效率和组件复用率。
2、在React Native中可以使用React语法进行移动端开发。
3、使用虚拟DOM和Diffing算法,减少与真实DOM的交互

虚拟DOM特点

本质是一个js的Object
虚拟DOM比真实DOM要”轻“一点,最终会被React转换成真实DOM

jsx注意事项
  1. 定义虚拟DOM时不要写引号,直接写标签就行
  2. 混入js表达式时用单花括号
  3. class使用className,因为js操作DOM的时候就是用的className,class是当时js的保留字
  4. 关于style使用变量需要双花括号,可以理解成先用单花括号表示这是一个变量,然后传入一个对象,或许也可以传入一个字符串。同时短横杠的属性写法要转成驼峰形式。
const className = 'test'
const VDOM = (<div><p className={
    className} style={
    {
    fontSize:'22px'}}></div>
)
  1. jsx只允许有一个根元素,可以参考Vue
  2. 标签必须闭合
  3. 标签如果用小写的标签,会被React自动理解成html的同名标签,找不到就报错;如果是组件就需要写成大写开头,React来渲染这个组件,找不到就报错。甚至还可以写一个中文标签。
  4. 单花括号内只能混入表达式,不能混入语句(比如for循环、if、switch等等)
    • 一个表达式会产生一个值 ,注意函数也可以用表达式来声明
    • 如果需要用if就用三目运算符,如果需要用for就用forEach、map等等
  • 如果要用循环输出一个jsx?(一般来讲不会用index作为key,这里只是简单地试一下)
const frame = ['React','Vue','Angular']
const VDOM = {
    <div>frame.map((item,index)=>{
    <p key={
    index}>{
    item}</p>})</div>
}

2、组件实例三大核心属性,state、proprs、refs

模块与组件

模块应该是专指js模块。
组件是实现一个完整功能的代码和资源的集合(比如会包含html、css、js、音视频等等)
模块化:所有js代码都按一定规则抽象。
组件化:各个功能按照一定规则抽象

js类
class Person{
    // 构造方法constructor(name,age){
    // this指向new出来的实例对象this.name = name;this.age = age}// 一般方法speak(){
    // 一般方法在原型上,供实例使用// this指向实例对象console.log(`my name is ${
      this.name}`)}
}
(new Person('zzy',22)).speak.call({
    a:1,b:1}) // my name is
class Student extends Person{
    // 即使不写构造器也会直接调用父类的构造方法,当写了子类构造方法,必须明确地调用supercontructor(name,age,grade){
    super(name,age); // super要置于最顶端this.grade = grade;}// 重载speak(){
    console.log(`my name is ${
      this.name}, im in grade ${
      this.grade}`)}
}
const s1 = new Student('zzy',22,16)
s1.speak() // my name is zzy,im in grade 16
  • 类的构造方法不是必须要写的,除非要对实例属性进行一些初始化
  • 如果子类写了构造方法,那么必须调用super,并且放在最顶端
  • 类中定义的所有方法,都是放在实例上面
组件
  1. 函数式组件
function Demo(){
    console.log(this) // undefinedreturn <p>函数式组件</p>
}
React.render(<Demo/>,document.getElementById('someid'))

注意这里的this指向,为undefined,因为开启了严格模式,禁止了this指向window。
另外中文在产物中会被转成unicode码

  • 函数式组件的渲染过程:
    • React解析组件标签Demo,然后找到了Demo
    • 发现是函数式的组件,立即调用这个函数,获取返回值,转为真实DOM。
  1. 类组件(比较适用于复杂组件)
class Demo extends React.component{
    render(){
    return (<p>{
     'this is demo'}</p>)}
}
ReactDOM.render(<Demo/>,document.getElementById('id'))
  • 必须写render并有返回值
  • 函数式组件的渲染过程:
    • React解析组件标签Demo,然后找到了Demo
    • 发现是类定义的组件,立即调用new出一个实例,并通过这个实例调用render函数。
    • 将render函数返回的虚拟DOM渲染成真实DOM

注意组件的三个属性,refs、state、props,这些在React.component中已经被定义过。
Demo也称组件实例对象。
关于简单组件和复杂组件,有状态的就是复杂组件,适用类组件,没有状态的就是简单组件,适用函数组件。虽然hooks也提供了函数组件保存状态的能力,还是推荐按照上面的建议来定义组件。
React通过组件的状态驱动视图。

state
初始化state
<div id="container">
</div>
<script> // 创建组件 class Weather extends React.component{
     constructor(props){
     super(props)// 初始化状态this.state = {
     isHot:false}}render(){
     return (<h1>今天天气很{
     this.isHot?'热':'冷'}</h1>)} } ReactDOM.render(<Weather/>,document.getElementById('container')) </script>
js中绑定事件处理函数
<div><button id='btn1'><button id='btn2'><button id='btn3' onclick="test()">
</div>
<script>const btn1 = document.getElementById('btn1')const btn2 = document.getElementById('btn2')const test = function(){
     alert('click')}btn1.addEventListener('click',test)btn2.onclick = ()=>{
     test()} </script>

注意React将所有的绑定的事件名都给封装了,比如onclick应该用onClick来写。

<div id="container"></div>
<script>// 创建组件 class Weather extends React.component{
     constructor(props){
     super(props)// 初始化状态this.state = {
     isHot:false}}render(){
     const {
      isHot } = this.statereturn (// 注意这里一定不要写成带括号的,不然会将clickHandler的返回值绑定到onClick上<h1 onClick={
     this.clickHandler}>今天天气很{
     isHot ?'热':'冷'}</h1>)}clickHandler(){
     this.state.isHot = !this.state.isHot;} } ReactDOM.render(<Weather/>,document.getElementById('container'))</script>

书写上述代码时会发现,控制台报错了,这是因为等到事件处理时,相当于一个普通的函数,不会被,可以参考下面的两段代码

class Person{
    // 构造方法constructor(name,age){
    // this指向new出来的实例对象this.name = name;this.age = age}// 一般方法speak(){
    // 一般方法在原型上,供实例使用// this指向实例对象console.log(`my name is ${
      this.name}`)}
}const p1 = new Person('tom',12)
p1.speak() // my name is tom
speak = p1.speak
// speak() // Cannot read properties of undefined (reading 'name')
// 这是因为class中代码,会自动将转为严格模式,防止this指向window
const f1 = function(){
    console.log(this)
}
const f2 = function(){
    'use strict'console.log(this)
}
f1() // window
f2() // undefined
  • 定义事件处理函数时,将其放在原型对象上(类里面),供实例使用
  • 由于事件处理函数是作为onClick的回调,所以不是通过实例直接调用的,是直接调用
  • 类内部默认开始了局部的严格模式,所以事件处理函数中的this是undefined
解决类中this指向的问题

比如上面可以改成下面的代码,使用bind改变this指向并返回新的函数。

 constructor(props){
    super(props)// 初始化状态this.state = {
    isHot:false}// 解决clickHandler中this指向问题this.clickHandler = this.clickHandler.bind(this)}

相当于实例上加了这么一个方法,注意在clickHandler依然需要在类内声明。

setState

state是不可以被直接赋值的,需要借助setState。
修改clickHandler代码:

clickHandler(){
    //this.state.isHot = !this.state.isHot;this.setState({
    isHot:!isHot   	})}

注意这里setState是一个更新/合并操作,并不是整体的赋值
这里的construcor只调用了一次,而不是每次setState都要重新new,而render会除了初始化的那一次,只要setState修改了状态,render就会重新被调用。

state的简写方法

实例对象的属性的简写

class Car{
    constructor(name,price){
    this.name = namethis.price = pricethis.wheel = 4}// 类中可以写赋值语句,下方代码就是给实例添加一个属性aa = 1}
const c1 = new Car('宾利',299)
const c2 = new Car('宝马',199)

下面的代码才是开发中会常用的形式

<div id="container"></div>
<script>
class Weather extends React.component{
    // 直接在这里声明state即可,不用写在构造函数中state = {
    isHot:false}constructor(props){
    super(props)// 初始化状态}render(){
    const {
     isHot } = this.statereturn (// 注意这里一定不要写成带括号的,不然会将clickHandler的返回值绑定到onClick上<h1 onClick={
    this.clickHandler}>今天天气很{
    isHot?'热':'冷'}</h1>)}// 自定义方法写成用赋值语句+箭头函数的形式// 这里的自定义方法一定要写成箭头函数clickHandler = ()=>{
    const isHot = this.state.isHotthis.setState({
    isHot:!isHot})console.log(this) // weather实例}
}
ReactDOM.render(<Weather/>,document.getElementById('container'))
</script>
理解State

1、组件可以被理解为“状态机”,通过更新组件的State来更新对应页面的显示(重新渲染组件)
2、组件中render方法中的this为组件的实例对象
3、组件自定义方法中this为undefined,如何解决?

  • 强制绑定this,通过bind方法返回一个新的具有this指向的方法
  • 箭头函数(以及赋值语句,也是最常用的形式)
    4、状态(state)数据不能直接修改或者更新
Props
简介
<div id="container"></div>
<script>class Person extends React.Component{
     render(){
     const {
     name,sex,age}return (<ul><li>{
     name}</li><li>{
     sex}</li><li>{
     age}</li></ul>)}}const person = {
     name:'1',age:'1',sex:'1'}// 下面两种方式是等价的ReactDOM.render(<Person name={
     person.name} age={
     person.age} sex={
     person.sex}/>),document.getElementById('container') ReactDOM.render(<Person {
     ...person}/>),document.getElementById('container') </script>

注意"…"运算符

const person = {
    name:'1',age:'1',sex:'1'
}
// 构造字面量时使用展开语法
const person2 = {
    ...person}
const person3 = {
    ...person,name:"3"} // 合并操作
console.log(...person) // 报错
console.log(person2) // person的复制

补充,这里的"…"运算符并非数组的扩展运算符,而是使用对象字面量复制一个对象。详见展开运算符。
但是要注意,React中的这段代码,单花括号是指在此处使用js表达式,而非上面的复制对象,是使用reactbabel来编译过的。

ReactDOM.render(<Person {
    ...person}/>),document.getElementById('container')
对Props进行限制

注意,向标签种传入的值,都被默认成字符串,像下面的age就会被认为是字符串19,如果在一些操作的里边,使用"+"运算符,会被认为成字符串运算

ReactDOM.render(<Person age="
19" name="1" sex="1"/>),document.getElementById('container')

所以应当改成下面的代码

ReactDOM.render(<Person age={
    19}
/>),document.getElementById('container')

当别人复用我们的组件时,不知道如何使用,这就要加一些限制。在Person类同级目录下中添加下面的代码。

// 需要引入 prop-types库
// 在React16版本后单独分出一个prop-types库
// 之所以修改是因为,React官方不希望React库变得太大,并且一般组件也有可能不需要限制Props
Person.propTypes = {
    // 注意这里都是需要写成小写,为了和内置类型区分name:React.propTypes.string.isRequired, // 必填且为字符串 sex:React.propTypes.string,sex:React.propTypes.number, speak:propTypes.func, // func是为了和function关键字区分
}
// 赋默认值
Person.defaultProps = {
    sex:"unknown",age:18,name:"unknown"
}
Props的简写方式

注意props是禁止修改的,这里好像和Vue是一样的。
要把props加入到类的静态属性中(static

Person extends React.Component{
    state = {
      }render(){
      }static propTypes = {
    // 注意这里都是需要写成小写,为了和内置类型区分name:React.propTypes.string.isRequired, // 必填且为字符串 sex:React.propTypes.string,sex:React.propTypes.number, speak:propTypes.func, // func是为了和function关键字区分}static defaultProps = {
    sex:"unknown",age:18,name:"unknown"}
}
类式组件中的构造函数

通常,在React中,构造函数仅用于以下两种i情况:

  • 通过给 this.state 赋值对象来初始化内部state
  • 为事件处理函数绑定实例

在React组件挂载之前,会调用它的构造函数。在为React.Compnent子类实现构造函数时,应在其他域据之前调用super(props),否则,this.proprs在构造函数中可能会出现未定义的bug。
总结:类中的构造函数完全可以省略,但是如果写了构造函数,并且在构造函数的声明中写了props,那就需要在super中传入props。也就是说,构造函数是否接受过props,是否传给super,取决于是否希望在构造函数中通过this访问props。
但既然传入了props,也没有必要使用this来访问props。
这样来看,class中的构造函数写不写都行。

函数式组件使用props

函数式组件因为没有this,所以不可以访问state,refs,但是可以接收props,通过传参来实现。

function Person(props) {
    const {
     name, age, sex }return (<h1><p>{
    name}</p><p>{
    age}</p><p>{
    sex}</p></h1>)
}
// 函数式组件只能这样来限制props
Person.defaultProps = {
    sex: "unknown",age: 18,name: "unknown"
}
Person.propTypes = {
    // 注意这里都是需要写成小写,为了和内置类型区分name: React.propTypes.string.isRequired, // 必填且为字符串 sex: React.propTypes.string,sex: React.propTypes.number,speak: propTypes.func, // func是为了和function关键字区分
}
理解Props
  • 一定要写好propTypes,如果写成小写t,也不会报错,但是效果就没有了,defaultProps同理。
  • 每个组件对象都会有props属性,保存了通过标签传入的属性。
  • 组件标签的所有属性都保存在props上
  • 在组件内一定不要修改props数据
  • props的作用是从组件外部向组件内传递变化的数据
refs的基本使用
字符串形式的ref:

实现点击按钮弹窗和input框失焦弹窗

<div id="container"></div>
<script>class Demo extends React.Component{
     showData = () => {
     const {
      input1 } = this.refsalert(input1.value) // 注意这里拿到的是真实的DOM}showData2 = () => {
     const {
      input2 } = this.refsalert(input2.value)}render(){
     return (<div><input ref="input1" type="text" placeholder="点击按钮"/><button ref="btn" onClick={
     this.showData}>点击</button><input onBlur={
     this.showData2} ref="input2" type="text" placeholder="失去焦点"/></div>)} } React.DOM(<Demo />,docuemnt.getElementById('container')) </script>
回调形式的ref:过时的API

React官方未来可能会废弃掉上面的字符串形式的ref,可能是因为字符串形式的ref效率不高。一般会考虑使用回调函数形式或者createRef这个API

<div id="container"></div>
<script>class Demo extends React.Component{
     showData = () => {
     const {
      input1 } = this // 注意这里修改了,不是原来的this.refsalert(input1.value)}showData2 = () => {
      const {
      input2 } = this // 注意这里修改了,不是原来的this.refsalert(input2.value)}render(){
     return (<div><input ref={
     currentNode => this.input1 = currentNode } type="text" placeholder="点击按钮"/><button ref="btn" onClick={
     this.showData}>点击</button><input ref={
     currentNode => this.input2 = currentNode } onBlur={
     this.showData2} type="text" placeholder="失去焦点"/></div>)} } React.DOM(<Demo />,docuemnt.getElementById('container'))</script>

关于这里的ref回调函数中的this,因为写的是箭头函数,所以this和render中的this相同,render的this实际指向了实例本身。
回调函数中直接在实例内部声明了input1、input2,所以在事件处理函数中直接用this.input1、this.input2来拿数据。

回调函数执行次数问题

如果ref回调函数(就是上面的写法)是用内联函数形式定义的,更新过程中它会被执行两次。这是因为,在每次渲染时会创建一个新的函数实例,所以React清空旧的ref并且设置新的。通过将ref的回调函数定义成class绑定的函数的方式,可以避免上述问题,但大多数情况下它是无关紧要的。
第一次传入了null,这是为了确认原来的旧值能够确认被清空;第二次传入了节点信息。
试一下"class的绑定函数":

saveInput = (currentNode) => {
    this.input1 = currentNode}render(){
    return (<div><input ref={
    this.saveInput} type="text" placeholder="点击按钮"/><button ref="btn" onClick={
    this.showData}>点击</button></div>)}

补充:如何在jsx中写注释:

render(){
    return (<div>{
     // 加上花括号以写注释 }</div>)
}

这里只是来看一下class绑定函数,实际开发最好还是写成内联的,不然逻辑就更多了。

createRef API : 最推荐的形式

React.createRef调用后可以返回一个容器,这个容器可以存储被ref所标识的节点
可以理解成,当input的ref传入了myRef,这个input节点会被存入React专门存放节点的容器中
但是要注意,这个容器最好只保存一个节点,“专人专用”,不然就被其他节点给占用

<div id="container"></div>
<script>class Demo extends React.Component{
     /*React.createRef调用后可以返回一个容器,这个容器可以存储被ref所标识的节点可以理解成,当input的ref传入了myRef,这个input节点会被存入React专门存放节点的容器中但是要注意,这个容器最好只保存一个节点,“专人专用”,不然就被其他节点给占用*/myRef = React.createRef()showData = () => {
     alert(this.myRef.current.value) // 注意这里有改动}}render(){
     return (<div><input ref={
      this.myRef } type="text" placeholder="点击按钮"/><button ref="btn" onClick={
     this.showData}>点击</button></div>)} } React.DOM(<Demo />,docuemnt.getElementById('container')) </script>
理解ref
  • 字符串形式的尽量避免但老代码中肯定很常见,经常用的是回调形式并且内联的很方便,createRef API被官方推荐
  相关解决方案