react 重要知识点总结

352 阅读34分钟

第一板块

函数组件的渲染过程

render 函数解析标签时, 如果发现首字母是大写开头的, 就会当做组件来解析, 同时如果发现是一个函数组件, 内部又会直接调用这个函数, 得到函数中 return 的 jsx 代码, 由 Babel 转成 React.createElement() 的形式, React.createElement() 调用完毕后的结果是虚拟 DOM, render 负责把虚拟 DOM 转成真实 DOM 并渲染

import ReactDOM from 'react-dom/client'
// 首字母大写
function Hello() {
  // 必须要有 return
  // return 什么表示渲染什么,这儿返回的就是一段 JSX
  return <h1>Hello React</h1>
}

ReactDOM.createRoot(document.querySelector('#root')).render(<Hello />)

类组件的渲染过程

import React, { Component } from 'react'
import ReactDOM from 'react-dom/client'

// !#1定义
// class Hello extends React.Component {
class Hello extends Component {
  // 必须要有 render 方法
  render() {
    // console.log(this) // render 中的 this 是类组件实例?
    // return 什么就表示渲染什么
    return <h1>Hello React</h1>
  }
}

// !#2 使用
// 单标签闭合或双标签
// render 渲染类组件的过程
// render 解析 <Hello/>,发现是大写字母开头的,所以会被当做组件解析
// 又发现是 class 声明的,所以会被当做类组件解析
// 它内部会 const hello = new Hello() 得到一个实例,接下来内部会调用 实例.render()
// 得到 JSX,由 Babel 转成 React.createElement() 这种形式,React.createElement() 调用完毕之后返回虚拟 DOM
// render 负责把虚拟 DOM 转成真实 DOM 并渲染
ReactDOM.createRoot(document.querySelector('#root')).render(<Hello />)

实例属性/方法 原型属性/方法 静态属性/方法 (es5 和 es6 对比)

es5 中

// 实例属性/方法
// 原型属性/方法
// 静态属性/方法

// 继承

function Person(name, age) {
  // this => 就是实例
  // !#1 实例属性/方法:挂载到实例上的就是实例属性/方法
  // name、age
  this.name = name
  this.age = age

  // 实例方法,一般方法不会往这儿挂,因为往这儿挂的话,多个实例的方法 show 发现不相等,内存浪费
  // this.show = function () {}
}
// 方法一般挂载到原型
// !#2 原型属性/方法:挂载到原型上的
Person.prototype.color = 'red'
Person.prototype.show = function () {
  console.log('~~~~')
}

// !#3 静态属性/方法:直接挂载到构造函数上的属性或方法,只能通过构造函数本身访问,实例访问不到
Person.version = 888
Person.css = function () {}

/* const p = new Person('刘德华', 50)
console.log(p.color)
p.show()
console.log(p.css, Person.version) */

function Star(name, age) {
  // 构造函数继承 => 继承是属性
  Person.call(this, name, age)
}

// 原型继承 => 继承的父类的原型上的方法
Star.prototype = new Person()
Star.prototype.construcor = Star

// 组合继承 = 构造函数继承 + 原型继承
const zxy = new Star('张学友', 19)
zxy.show()

es6 中

// 实例属性/方法
// 原型属性/方法
// 静态属性/方法

// 继承
class Person {
  // 构造器
  // new Person 的时候就会执行
  // new Person('ifer', 18) => ifer 就对应 name,age 就对应 18
  constructor(name, age, address) {
    // 构造函数中 this 就是实例
    // !#1 实例属性
    this.name = name
    this.age = age
    // 实例方法,一般方法不会往这儿挂,因为往这儿挂的话,多个实例的方法 show 发现不相等,内存浪费
    // this.show = function () {}

    // ###
    // 写到这儿的能接收参数,例如 address
    /* this.state = {
      address,
      city: '周口',
    } */

    // ###2
    // this.handleClick = () => {}
  }

  // 【称为实例属性】
  // 这种写法也是挂载到实例上面的,等价于 ### 处的代码
  // 写到这儿没法接收参数,例如 constructor 中的 address
  state = {
    address: '河南',
    city: '周口',
  }
  // 【称为实例方法】,等价于 ###2 处的代码
  handleClick = () => {}

  // !#2 原型方法
  show() {
    console.log('~~~')
  }

  // !#3 静态属性/方法
  static version = 888
  static css() {
    console.log('CSS')
  }
}

const p1 = new Person('ifer', 18, '河南')
console.log(p1.state)

补充

  1. vue 中对于类名设置的多重写法

image.png

  1. react 要通过下载一个包后, 就能够实现和 vue 中类名设置的效果
  • 下载 classNames
  • 导入
  • 在需要的地方 classNames(要用的部分)
  1. 组件的状态
  • 状态就是用来描述事物在某一时刻的的数据;
  • 状态能被改变,改变了之后视图会有对应的变化。
  • 类组件就是有状态组件, 函数组件一般叫做无状态组件
  • React v16.8 中引入了 React Hooks,从而函数式组件也能定义自己的状态了。

注意

  1. 在 react jxs 的 {} 中 写数组如 ['aa','bb'] , 能在自动拼接成 aabb 的形式
  2. 函数组件的 this 是 undefined, 原因是代码编译后, 会开启严格模式
  3. 构造函数中的 this 就是实例
  4. 如果子类内部有 constructor 就要在内部调用 super,注意 super 的调用必须在 this 挂载的上面 (这里的 Star 为子类, Person 为父类, 这里的 super 类似于父类的 constructor)

image.png

  1. 注意如果用类组件, 组件中一定要有 render 函数
  2. 在函数中的方法只能挂载到实例上, 这个时候的 this 为 undefined; 但如果是构造函数, 可以将方法挂载到原型上(当然也可以挂载到实例上, 并且 this 优先拿到实例上的方法), 并通过 this.方法 调用, 这个时候的 this 为实例

image.png

  1. 注意在 jsx 中写属性名时, 要使用小驼峰

image.png

第二板块

类组件中方法默认开启严格模式

类组件中 原型 方法中的 this 指向是 undefined, 因为类组件中开启了严格模式, 在严格模式下的 this 指向从 window 指向 undefined

<button onClick={this.handleClick}>+1</button>,这样写,本质上是把 this.handleClick 这个方法赋值给了 onClick 这个属性,当点击按钮的时候,由 React 内部直接调用 onClick,那么 this 指向就是 undefined(class 的内部,开启了局部严格模式,所以 this 不会指向 window )。 image.png

如何让类组件中的 this 指向实例 (五种方法实现)

  1. 利用高阶函数(这种方式能够实现在点击的时候传参), 这里是在啊 26 行 直接调用函数, 并且点击事件函数 return 一个函数, 这个函数只会在按钮点击的时候触发, 而此时的事件函数中的 this 就指向实例了

image.png

  1. 在 onclick 函数中写函数
export default class App extends Component {
  state = {
    count: 0,
  }
  handleClick() {
    console.log(this.state.count)
  }
  render() {
    return (
      <div style={{ textAlign: 'center' }}>
        <h3>计数器:{this.state.count}</h3>
        <button onClick={() => this.handleClick}>+1</button>
      </div>
    )
  }
}
  1. 通过 bind 修改 handleClick 的 this 指向为实例, 并通过 bind 实现传参(注意 bind 能实现多次传参)
export default class App extends Component {
  state = {
    count: 0,
  }
  handleClick(a, b, e) {
    // 对应不上的那个参数就是事件对象
    // ...
    console.log(this.state.count, a + b, e)
  }
  render() {
    return (
      <div style={{ textAlign: 'center' }}>
        <h3>计数器:{this.state.count}</h3>
        <button onClick={this.handleClick.bind(this, 1, 2)}>+1</button>
      </div>
    )
  }
}
  1. 将事件函数写成箭头函数的形式 (因为在类组件的内部, this 指向就为实例)
export default class App extends Component {
  state = {
    count: 0,
  }
  // 实例方法
  handleClick = () => {
    console.log(this.state.count)
  }
  /* handleClick() {
    console.log(this.state.count)
  } */
  render() {
    return (
      <div style={{ textAlign: 'center' }}>
        <h3>计数器:{this.state.count}</h3>
        <button onClick={this.handleClick}>+1</button>
      </div>
    )
  }
}
  1. 通过实例方法获取原型方法, 并用 bind 将 this 指向原型方法
export default class App extends Component {
  constructor() {
    super()
    this.state = {
      count: 0,
    }
    // 根据原型上的 handleClick 生成一个新方法给了实例属性 aaa,并把原型方法 handleClick 内部的 this 改成了实例
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    console.log(this.state.count)
  }
  render() {
    return (
      <div style={{ textAlign: 'center' }}>
        <h3>计数器:{this.state.count}</h3>
        <button onClick={this.handleClick}>+1</button>
      </div>
    )
  }
}

setState() 方法

setState() 是一个继承来自 Component 的方法, 能够用来修改 state 中的数据 import React, { Component } from 'react'

  1. setState() 会对一个组件的 state 对象安排一次更新。当 state 改变了,该组件就会重新渲染。 2.不要直接修改原数据,而是要产生一份新数据,然后通过 setState() 用新的数据覆盖原数据,这么做的其中一个重要原因就是为了 SCU(shouldComponentUpdate),为了性能优化。 (这里是在原来 list 数据的基础上, 添加一个'Hello React')
this.setState({
    list: [...this.state.list, 'Hello React'],
})
  1. setState 方法中包裹一个对象, 对象中写需要修改 state 中的数据, 左边为属性名, 右边属性值, 属性值的书写要求不会直接对 state 中数据进行修改, 并且是书写同样数据类型和格式, 如果是复杂数据类型, 要先将原来的数据用展开运算符获取

受控表单组件

这里的 input 标签通过 value 属性绑定数据, 如果没有 onchange 事件, 页面中的输入框就不能输入内容, 这就是受控的效果, 此时还没有能够改变所绑定数据的方法, onchange 事件中书写修改绑定数据的代码, 实现类似 vue 中双向绑定的效果

import React, { Component } from 'react'

export default class App extends Component {
    state = {
        username : ''
    }
    handleInput = (e) => {
        this.setState(
            {username : e.target.value}
        )
    }
    render(){
        return(
            <ul>
                <li>
                    <input type='text' 
                    value={this.state.username} 
                    onChange={this.handleInput}>
                    </input>
                </li>
            </ul>
        )
    }
}

非受控组件

非受控组件则是通过操作 DOM 的方式来获取数据,表单中的 value 并没有和 state 中的数据进行绑定。通过 React.createRef() 获取 DOM。

import React, { Component } from 'react'

export default class App extends Component {
    // Step1
    input = React.createRef()
    handleChange = () => {
        // Step3
        console.log(this.input.current.value)
    }
    render() {
        return (
            <div>
                {/* Step2 */}
                <input ref={this.input} type='text' placeholder='输入内容' onChange={this.handleChange} />
            </div>
        )
    }
}

几种表单的受控组件

这里包括 文本输入框 文本域输入框 下拉选择框 单选框 复选框

import React from 'react'

export default class App extends React.Component {
    state = {
        username: '',
        content: '',
        frame: 'react',
        checkedRadio: 'male',
        checkedFruit: ['apple'],
    }
    changeText = (e) => {
        this.setState({
            username: e.target.value,
        })
    }
    changeTextArea = (e) => {
        this.setState({
            content: e.target.value,
        })
    }
    changeOption = (e) => {
        this.setState({
            frame: e.target.value,
        })
    }
    changeRadio = (e) => {
        this.setState({
            checkedRadio: e.target.value,
        })
    }
    changeCheckBox = (e) => {
        const checkedFruit = [...this.state.checkedFruit]
        const idx = checkedFruit.indexOf(e.target.value)
        if (idx === -1) {
            // 数组中没有找到,说明没有被选中,那就把数据添加到数组,进行选中的操作
            checkedFruit.push(e.target.value)
        } else {
            // 找到了,说明已被选中,通过删除数组中的数据取消选中
            checkedFruit.splice(idx, 1)
        }
        this.setState({
            checkedFruit,
        })
    }
    render() {
        const { username, content, frame, checkedRadio, checkedFruit } = this.state
        return (
            <ul>
                <li>
                    <label htmlFor='username'>用户名</label>
                    <input id='username' type='text' value={username} onChange={this.changeText} />
                </li>
                <li>
                    <label htmlFor='content'>其他信息</label>
                    <textarea id='content' cols='30' rows='10' value={content} onChange={this.changeTextArea}></textarea>
                </li>
                <li>
                    <label htmlFor='frame'>框架</label>
                    <select id='frame' value={frame} onChange={this.changeOption}>
                        <option value='vue'>Vue</option>
                        <option value='react'>React</option>
                        <option value='angular'>Angular</option>
                    </select>
                </li>
                <li>
                    <input id='male' type='radio' value='male' checked={checkedRadio === 'male'} onChange={this.changeRadio} />
                    <label htmlFor='male'></label>
                    <input id='female' type='radio' value='female' checked={checkedRadio === 'female'} onChange={this.changeRadio} />
                    <label htmlFor='female'></label>
                    <input id='unknow' type='radio' value='unknow' checked={checkedRadio === 'unknow'} onChange={this.changeRadio} />
                    <label htmlFor='unknow'>未知</label>
                </li>
                <li>
                    <input id='apple' type='checkbox' value='apple' checked={checkedFruit.includes('apple')} onChange={this.changeCheckBox} />
                    <label htmlFor='apple'>Apple</label>
                    <input id='orange' type='checkbox' value='orange' checked={checkedFruit.includes('orange')} onChange={this.changeCheckBox} />
                    <label htmlFor='orange'>Orange</label>
                </li>
            </ul>
        )
    }
}

react 和 vue 的区别

链接一

链接二

image.png

补充

  1. 在严格模式下 (书写 'use strict'可以在全局 或 局部 开启, 局部开启可以在函数中书写)
  • 原来 指向 window 的 this, 严格模式下, 变成 undefined
  • 原来在函数中没有声明就赋值而变成的全局数据, 严格模式下, 就会报错了
  1. 高阶函数: 返回的函数, 或者函数当做参数传递的函数

  2. 函数柯里化: 通过函数调用继续返回函数, 实现多次接收参数最后统一处理的编码形式

  3. ngins 反向代理的好处

  • 负载均衡, 能够在服务器大量访问时, 将流量分至闲置分支服务器
  • 服务器受到攻击时能起到保护作用
  1. EventBus 是为了实现兄弟通信 (是使用了发布订阅的模式)

$on 为订阅 $emit 为发布 image.png

  1. 常见 js 设计模式
  • 工厂模式
  • 单例模式
  • 发布订阅
  • 观察者模式
  1. CSRF 和 XSS 攻击
  • CSRF指的是跨站请求伪造,是一种挟制用户在当前已经登录的Web应用程序上执行非本意操作的攻击方法。CSRF利用的是:一旦用户通过网站服务的身份认证,网站就完全信任该用户,受害者持有的权限级别决定了CSRF攻击的影响范围。
  • XSS攻击即Cross Site Script可以译为跨站脚本攻击,为了和CSS区分开来,所以叫做XSS,XSS攻击指的是攻击者在网站上注入恶意的客户端代码,通过恶意脚本对客户端网页进行篡改,从而在用户浏览网页时对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。攻击者对客户端网页注入的恶意脚本一般包括JS,有时也会包含HTML。XSS攻击的共同点是将一些隐私数据像cookie、session发送给攻击者,将受害者重定向到一个由攻击者控制的网站,在受害者的机器上进行一些恶意操作。

注意

  1. 类组件中要修改的数据就放入 state 中, 如果不用修改, 还是可以不用放入 state 中

  2. 数组 和 字符串 都具有 indexof() 方法, 能够根据指定的元素, 返回元素的在数组或字符串中的下标 ( 和数组的 findIndex 对比, findIndex(item => item... ) 能是进行根据方法内的逻辑操作后返回的布尔值, 获取布尔值为 true 的第一个元素下标 )

第三板块

组件通信

常见通信: 父传子、子传父、兄弟相传、跨组件通信等

父传子

  1. 父组件中
<Child salary={this.state.salary}></Child>
  1. 子组件中
  • 类组件中(通过 this.props 拿到传过来的数据, 为一个对象)
export default class Child extends Component {
    render() {
        return <div style={{ border: '1px solid red' }}>子:{this.props.salary}</div>
    }
}
  • 函数组件中(通过回调函数中参数 props 拿到数据, 为一个对象)
export default const Child = (props) => {
    return <div>子:{props.salary}</div>
}

子传父

  1. 父组件准备一个方法并传递给子组件

  2. 子组件调用传递过来的方法, 并传参

  3. 父组件接收传参并做修改

兄弟通信(状态提升)

  1. 准备 A、B 兄弟组件。
  2. 把需要操作的 B 组件中的数据 count 提升到公共的父组件里面。
  3. 父组件提供数据和操作数据的方法
  4. 把数据传递给 B 组件,把操作数据的方法传递给 A 组件。

跨层级组件通信(通过 Context 实现跨级组件通讯)

  1. 祖先组件通过 React.createContext() 创建 Context 并导出
  • React.createContext({数据}), 如果在这里写数据, 那么这个数据会当做默认值
  • 优化:提取 React.createContext() 到单独的文件里面
import React from 'react'
export const Context = React.createContext()
  1. 祖先组件通过 <Context.Provider value={数据}> 配合 value 属性提供数据(这里的 value 只能传递一个数据)

将数据需要传过去的子组件都包裹在 <CounterContext.Provider> 标签中, 这样子组件就能通过<Context.Consumer> 获取数据

<CounterContext.Provider value={this.state.count}>
  <Child1 />
</CounterContext.Provider>
  1. 后代组件通过 <Context.Consumer>{(value) => <div>{value}</div> }</Context.Consumer> 配合函数获取数据

将需要用到数据的地方, 包裹在<Context.Consumer>标签中, 并通过函数的形式返回 jsx

<CounterContext.Consumer>
  {(value) => <div>Child2: {value}</div>}
</CounterContext.Consumer>

补充

  1. 通过在自组件的类组件中书写 static contextType = CounterContext 也能在子组件中获取传过来的 value 值, 左边static contextType 的写法是固定的, 右边是创建实例的名字, 写了这句话之后就能在组件中通过 this.content 获取传过来的 value 值
import React, { Component } from 'react'
import Child2 from './Child2'
import { CounterContext } from './Context'

// #4 使用 Provider 传递的数据的第二种方式
export default class Child1 extends Component {
  static contextType = CounterContext
  render() {
    return (
      <div>
        Child1: {this.context}
        <Child2 />
      </div>
    )
  }
}
  1. 设置传值默认值

注意默认值生效的条件:并不是不传递 value,而是没有找到包裹 Context.Provider 的祖先元素

import React from 'react'
export const Context = React.createContext({
    age: 88,
})

children 属性

  • 组件的子节点会被当做是 children 属性传递到子组件内部。
  • 在传递数据的时候 children 属性与普通的 prop 一样,值可以是任意类型例如数字、字符串、数组、JSX、函数等。
;<Hello children='我是子节点' />
// children 是一个特殊的 prop,上面的写法和下面等价,当内容比较多的时候,下面的写法更加直观
;<Hello>我是子节点</Hello>

这种写法类似作用域插槽

image.png

props 校验

主动抛出校验错误

  1. 安装并导入 prop-types 包。
  2. 使用 组件名.propTypes = {} 来给组件的 props 添加校验规则。
  3. 校验规则通过 PropTypes 对象来指定。(这里的 Test 为组件, 并且是进行静态属性的设置, 也就是说还可以用 static propTypes... 设置)
Test.propTypes = {
    colors: PropTypes.array,
}

常见校验规则

  • 常见类型:number、string、bool、array、func、object。
  • React 元素类型(JSX):element。
  • 必填项:isRequired。
  • 特定结构的对象:shape({})。
{
    // 常见类型
    fn1: PropTypes.func,
    // 必选
    fn2: PropTypes.func.isRequired,
    // 特定结构的对象
    obj: PropTypes.shape({
        color: PropTypes.string,
        fontSize: PropTypes.number
    })
}

props 指定默认值的两种方式

  1. js 自身语法(推荐, 这里能够在 age 没有传过来值的时候默认值设为 18 )
import React, { Component } from 'react'
class Test extends Component {
    render() {
        const { age = 18 } = this.props
        return <div>{age}</div>
    }
}

export default Test
  1. 通过 defaultProps 设置
import React, { Component } from 'react'
class Test extends Component {
    render() {
        return <div>{this.props.age}</div>
    }
}

Test.defaultProps = {
    age: 18,
}
export default Test

// 没有传入 pageSize 属性
;<Test />

生命周期

三大阶段 五个钩子 image.png

常用的钩子函数 钩子函数 | 触发时机 | 作用 | | ----------------- | ---------------- | ---------------------------------- | | constructor | 创建组件时,最先执行 | 1. 初始化 state 2. 创建 Ref 等 | | render | 每次组件渲染都会触发 | 渲染 UI(注意: 不能直接调用 setState()  ) | | componentDidMount | 组件挂载(完成 DOM 渲染)后 | 1. 发送网络请求 2.DOM 操作

注意

  1. 更新阶段的两个钩子函数(render 和 componentDidUpdate), 不能在这两个函数中直接写 setState(), 会触发死循环

  2. props 是父组件传值的情况也会触发更新, 即便父组件使用 setState({}) 没有传值也会触发子组件的 render 和 componentDidUpdate 函数

  3. 永久定时器即便在组件销毁时也不会自动清除, 所以一般在 componentWillUnmount (相当于 vue 中的 beforeDestroy) 清除定时器

注意

  1. react 中的删除思路
  • copy 一份数据再覆盖
  • 利用 filter 筛选数据, 保留需要的数据就是删除了不需要的数据
  1. 在 construtor 中使用 super(data) 相当于 this.data = data (super 将 super 中的数据挂载到this 上 )

image.png

  • 这里通过...args, 拿到所有父类的参数, 并再通过super(...args)挂载到 this 上 (因为13 代码代码将 salary 放在前面, 所以14 行中的 ...args 能够拿到 salary, 15 行的代码就不要了)
  1. tofixed() 转化之后会得到字符串(返回一个新数据, 不会改变原数据, 这个方法一般用在不需要对新数据进一步修改的位置)

  2. 兄弟通信用的是状态提升, 所以每次要做兄弟通信时, 就将数据放到父组件中

  3. 由于时间对象不能写在 {new Data()} 中, 可以导入 dayjs 对时间对象处理下 {dayjs(new Data()).format('YYYY-MM-DD')}

  4. 由于 render 每次渲染的时候都会执行, 所以不能在 render 函数中直接调用 setState, 不然会触发死循环

补充

  1. 长列表优化的包 react vitualized (康哥仓库面试题有介绍)
  • 滑动页面很长, 由于 dom 不断加载累积造成内存负担, 应用卡顿
  • 所以可以利用长列表优化, 划到一定位置就删除前面的 dom
  1. 两个时间对象相减能够拿到两个时间毫秒差

  2. 数组的排序方法 sort()

const arr = [2,4,5,1,3]
arr.sort((a,b) => b - a)  // [5,4,3,2,1] 从大到小排列
arr.sort((a,b) => a - b)  // [1,2,3,4,5] 从小到大排列

第四板块

setState 可能 是异步的

setState 是一个同步方法(下面两种情况是修改数据的异步表现), 同时也会出现相应的问题: 不能立即拿到更新后的数据, 多次进行 setState 会进行合并的操作

  • 生命周期 合成事件 setState 修改数据表现是异步的,
  • 原生事件(如click) 定时器中同步的

image.png

image.png

问题解决(如何拿到更新之后的结果)

在 setState() 的第一个参数可以是一个函数, 函数中的 prevState 参数, 能够拿到上一次 setState 修改数据后的结果 (注意这里的几次 setState 的操作依然是异步的, 所以打印结果还是状态中原有的数据, 视图中会呈现最终叠加修改的值)

image.png

在 setState() 的第二个参数为回调函数, 在回调函数中做数据修改之后的进一步处理

this.setState({修改数据的操作}, () => {
    console.log('这个回调函数会在状态更新后立即执行')
})

注意

  1. 18 版本中所有情况都为异步的, 17 版本前原生事件(如click) 定时器中同步的

  2. 多次调用 setState(),只会触发一次重新渲染,所以无需担心多次进行 setState 会带来性能问题。

Hooks

  • 作用:为函数组件提供状态、生命周期等原本 class 组件中才有的功能

  • 注意:Hooks 只能在函数组件中使用,虽然有了 Hooks,但 React 官方并没有计划从 React 库中移除 class。

  • hooks 解决了什么问题?

  1. 组件的状态逻辑复用问题
  2. class 组件自身的问题
  3. 相比于函数组件来说,类组件不利于代码压缩和优化,也不利于 TS 的类型推导。

useState()

返回值数组里面包含两个值,状态和修改该状态的方法

import React, { useState } from 'react'

const App = () => {
    const [count, setCount] = useState(0)
    return (
        <div style={{ textAlign: 'center' }}>
            <h3>计数器:{count}</h3>
            <div>
                <button onClick={() => setCount(count + 1)}>+1</button>
            </div>
        </div>
    )
}

export default App
  • 参数:初始状态,比如传入 0 就表示该状态的初始值为 0。
  • 注意:此处的状态可以是任意值(比如,数值、字符串、对象等),注意 class 组件中的 state 必须是对象。
  • 返回值:数组,数组里面包含两个值,状态和修改该状态的方法。
  • 约定:修改状态的方法以 set 开头,后面跟上状态的名称。
  • useState() 的第一个参数可以写成一个函数, 并且这个函数只会执行一次 (之后每次 state 变化, 组件会重新走一遍, 会将这个函数上一次返回结果当做起始值, 如果状态是经过一些计算得到的, 就可以考虑用这种函数形式)

实现累计

image.png

修改 State 数据

还是有状态不可变性, 不能直接修改状态

import React, { useState } from 'react'

const App = () => {
    const [obj, setObj] = useState({
        count: 0,
    })
    const handleClick = () => {
        // Error
        obj.count++
        setObj(obj)
        // Right
        /* setObj({
            count: obj.count + 1,
        }) */
    }
    return (
        <div>
            <p>{obj.count}</p>
            <button onClick={handleClick}>click</button>
        </div>
    )
}

export default App

useState 使用细则

  1. 不能嵌套在 if/for/其他函数 中! (if 的条件判断、for 循环的次数、函数的调用与否都可能会影响 hook 的顺序)。

  2. React 是按照 Hooks 的调用顺序来识别每一个 Hook,如果每次调用的顺序不同,导致 React 无法知道是哪一个状态和修改状态的方法。

  3. 可以通过开发者工具进行查看 React 对 Hook 的管理。

useEffect

  • 执行时机:初始化时和数据变化的时候执行。
  • 相当于 class 中的 componentDidMount + componentDidUpdate。(也就是说在初始化 和 数据变化的时候会走)
useEffect(() => {
    console.log('执行了 useEffect ~~~')
    document.title = count
}, [count])

注意

  1. 第二个参数可以传一个数组,表示只有当数组中的选项/依赖项改变时,才会重新执行该 effect (只有在 count 变化时,才执行 effect)
  2. 第二个参数,还可以是一个空数组([]),表示只有在组件第一次渲染后执行,一般会进行事件绑定发送请求等。

倒计时需求时遇到问题

这里要将 setCount() 写成函数的形式, 而不是用 setCount(count-1)(定时器中的 count 永远是第一次被引用着的那个 count), 避免闭包所依赖的变量造成内存不释放

image.png

注意

  1. this 的指向, 谁调用就指向谁(如果有多个 this 嵌套, 就需要不断回溯找源头)

  2. hooks 只能写在函数组件内部

  3. 强调:useState 的初始值(参数)只会在组件第一次渲染时生效,也就是说,以后的每次渲染,useState 获取到都是最新的状态值,React 组件内部会记住每次更新后的最新状态值 (点击按钮,调用 setCount(count + 1) 来修改状态,因为状态发生改变,所以,该组件会重新渲染。)

  4. 闭包会使依赖的数据在内存当中不释放, 即便是函数中的代码还没有走, 只要代码被解析了, 那么数据拿到的是解析时的数据

第五板块

useEffect 副作用之清理函数

在副作用回调函数中如果有定时器或事件, 那么就可以在清理函数 (return 的函数)中写清理定时器获取解绑事件的代码 (清理函数的执行时机, 下一次副作用回调函数调用时以及组件卸载时, 也就是说副作用中的清理函数 return 在初始化时不会执行))

useEffect(() => {
    const timer = setInterval(() => {
        console.log(1)
        setCount((count) => count - 1)
    }, 1000)
    return () => {
        clearInterval(timer)
    }
}, [])
  • 这个案例写的是在组件卸载的时候, 才会执行 return 回调函数, 清理定时器

利用 useRef 获取类组件实例

能够利用 useRef 获取 dom 或者类组件实例, 这种方式不能获取函数组件实例

import React, { useRef } from 'react'
import Test from './Test'

const App = () => {
    const testClassCmp = useRef(null)
    const add = () => {
        testClassCmp.current.handleClick()
    }
    return (
        <section>
            <Test ref={testClassCmp} />
            <button onClick={add}>添加</button>
        </section>
    )
}

export default App

useRef 解决定时器闭包问题

可以做到组件多次渲染之间的状态共享(类似于全局变量, 和全局变量不同的是: 即使多个组件实例也不会相互响应)

问题(还不是太懂)

每次点击按钮整个 Test 函数都会执行 1 次, 导致每次都会产生 1 个闭包变量 timer, 其实每次清除的是自己的闭包变量 timer,而不是上一次的

import React, { useState } from 'react'

export default function Test() {
    const [count, setCount] = useState(10)
    let timer = null
    const handleClick = () => {
        // 每次点击按钮整个 Test 函数都会执行 1 此(当然也包括这儿)
        // 导致每次都会产生 1 个闭包变量 timer
        // !其实每次清除的是自己的闭包变量 timer,而不是上一次的
        clearInterval(timer)
        timer = setInterval(() => {
            setCount((count) => count - 1)
        }, 1000)
    }
    return (
        <div>
            <h3>{count}</h3>
            <button onClick={handleClick}>开启</button>
        </div>
    )
}
  • 参考(这里每次点击都会创建一个定时器, 也就说有没有 timer 变量接收不影响新定时器的创建, 不是太懂)

image.png

解决

useRef:可以实现多次渲染之间进行数据共享,保证了更新期间共用同一个 ref 对象(可以先理解为是一个全局变量)的同时,多个组件实例之间又不会相互影响(因为它是在组件内部的)。

import React, { useState, useRef } from 'react'
export default function App() {
    // #1
    const timer = useRef(null)
    const [count, setCount] = useState(10)
    const handleStart = () => {
        // #2
        clearInterval(timer.current)
        // #3
        timer.current = setInterval(() => {
            setCount((count) => count - 1)
        }, 1000)
    }
    return (
        <div>
            <h3>{count}</h3>
            <button onClick={handleStart}>开始倒计时</button>
        </div>
    )
}

useContent 的使用

还是需要从父组件中通过 <Context.Provider value={数据}/> 提供数据, 然后通过下面的写法调用数据, 这种写法比类组件中 <Context.consumer >() => { return jsx代码 } <Context.consumer /> 更加扁平化

import { useContext } from 'react'
import { Context } from './countContext'

export default function Child() {
    const value = useContext(Context)
    return (
        <div>
            Child
            <h3>{value.count}</h3>
        </div>
    )
}
  • 作用:在函数组件中,获取 <Context.Provider/> 提供的数据。
  • 参数:Context 对象,即通过 React.createContext 函数创建的对象。
import React from 'react'
export const Context = React.createContext()
  • 返回:<Context.Provider/> 提供的 value 数据。

redux 板块

image.png

action 的使用

action 用来描述要做的事情,项目中的每一个功能都是一个 action

export const incremen = {
    type: 'INCREMENT',
    payload: 5,
}

export const decrement = {
    type: 'DECREMENT',
    payload: 5,
}

reducer 的使用

  • 本质上是一个函数,作用是根据 action 来更新状态;
  • 不要在 reducer 函数内部直接修改 state。
  • reducer 要求自身就必须是一个纯函数。
export default function counter(state = 10, action) {
    // 处理各种各样的 action
    switch (action.type) {
        case 'INCREMENT':
            return state + action.payload
        case 'DECREMENT':
            return state - action.payload
        default:
            // 记得要有默认返回的处理
            return state
    }
}

store 的使用

// store: 整个数据的仓库,负责关联 reducer 和 action,通过 store 对象可以给 reducer 分配 action
import { createStore } from 'redux'
import reducer from './reducers'
const store = createStore(reducer)
export default store
  1. 一个应用只有一个 Store。

  2. 创建:const store = createStore(reducer)

  3. 获取数据:store.getState()

  4. 更新数据:store.dispatch(action)

状态监听

  1. 订阅(监听)状态变化:const unSubscribe = store.subscribe(() => {}),注意要先订阅,后续的更新才能被观测到。

  2. 取消订阅状态变化:unSubscribe()

纯函数

相同的输入总是得到相同的输出

纯函数特点

  1. 不得改写参数,不能使用全局变量。

  2. 不能调用 Date.now() 或者 Math.random() 等不纯的方法,因为每次会得到不一样的结果。

  3. 不包含副作用的处理,副作用:AJAX 请求、操作本地数据、或者操作函数外部的变量等。

注意

  1. 清理函数的执行时机, 下一次副作用回调函数调用时以及组件卸载时, 也就是说副作用中的 return 在初始化时不会执行)

  2. 副作用的 return 要写函数

  3. hook 只能在 函数组件自定义 hook 中使用 (所以说 hook 不能在类组件中使用)

  4. useRef 每次都会返回相同的引用,而 createRef 每次渲染都会返回一个新的引用。(每次渲染都返回新的引用, 也就是说操作后的数据没有得到保存)

  5. 全局变量的问题:多个组件实例之间会共用一个全局变量,以至于会相互影响

  6. 一个副作用就做一个逻辑, 可以在一个组件中写多个副作用

补充

  1. 控制组件销毁创建的一种写法
import React, { useState } from 'react'
import Test from './Test'

export default function App() {
    const [flag, setFlag] = useState(true)
    return (
        <div>
            {flag && <Test />}
            <button onClick={() => setFlag(!flag)}>销毁/创建</button>
        </div>
    )
}
  1. 若需要使用 async/await 语法,可以在 useEffect 回调内部再次创建 async 函数并调用。
useEffect(() => {
    async function fetchMyAPI() {
        let url = 'http://something/' + productId
        const response = await myFetch(url)
    }

    fetchMyAPI()
}, [productId])
  1. fetch 是浏览器内置的 API(可以用来发请求), 用来替代 XMLHttpRequest, 工作中用得最多的是 Axios (fetch 的返回值是一个 promise)
  • 如果后台返回的是 JSON 格式的字符串, 那么就可以直接用 res.json() 进行转化得到结果
  • 如果后台返回的是普通字符串, 那么通过 res.text() 将返回内容转化为字符串
  1. BFF,即 Backend For Frontend(服务于前端的后端)(例如,我们加入 BFF 层,原本每次访问发送 3 请求页面,变成一个请求, 就是说 bff 用来减少发送的请求次数)

  2. useRef 与 createRef

  • createRef每次都会返回个新的引用;而useRef不会随着组件的更新而重新创建
  • useRef 能够在多次渲染之间共享数据
  • const refTimeId = useRef(null)只会执行一次
  1. 媒体查询主要用于 pc 网站, rem 主要用于移动端

第六板块

react-redux

更好的实现在 React 中使用 Redux 进行状态管理, 一旦使用了 react-redux,获取和更新数据的方式就变化了

image.png

替代了原来的监听数据更新刷新视图的方式

// 用了 react-redux 下面手动触发更新的方式就没用了
/* store.subscribe(() => {
    ReactDOM.render(<App />, document.querySelector('#root'))
}) */
  1. 下包, 按需获取其中的 provider 组件
  2. 在 render 函数中将 App 节点包裹在 Provider 组件中, 并通过 Provider 传递数据
  3. 在需要使用的组件中通过导入 useSelector 方法获取所有状态( 如果 reducers 做了合并处理, 这里拿到的就是一个对象)
  4. 通过 useDispatch 方法导入修改数据的方法

redux 目录结构

  • 这里将 action 或 reducer 中的 type 提取出来, 写在 constants 文件夹中(这个文件中写每个模块中 type 值), 可以在写代码时 type 写错时及时报错
  • 并且这里在 actions 和 reducers 文件夹中都有一个 index.js 作为整合文件, 能够导入导出同级文件中所有变量, 能够简化在引用时的路径书写 image.png
  1. App.js 是整个项目的入口文件, 所有组件的挂载都在这个文件中, 并且最终导出 App 函数组件
  2. src/index.js 是项目的挂载文件, 这里会引入所有项目中的 全局样式 / 挂载 App 函数组件 / 挂载 redux
  3. actions 文件封装了每个模块的命令, 能够通过 store.dispatch(action) 其中的 action 就来自这个文件夹中, 并且其中的 index.js 用来整合所有模块的 actions
  4. reducers 文件封装了命令对应的数据处理逻辑, 能够在 store.dispatch(action) 调用后, 返回处理后的数据, 并且其中的 index.js 用来整合所有模块的 reducers (通过 combineReducers 合并)
  5. constants 文件封装了 actions 中的 type 值, 能够在打错命令时及时报错, 如果没这么处理就不会有报错, 并且其中的 index.js 用来整合所有模块的 type

redux 数据操作(常 爱 瑞 组 => constants actions reducers 组件)

写代码的时候按照这个文件顺序写代码(constants => actions => reducers => 组件), 但描述的时候应该从视图到常量的变化(反着来)

image.png

redux-thunk

  • redux-thunk 中间件可以处理函数形式的 action,而在函数形式的 action 中就可以执行异步操作代码,完成异步操作。(相当于在调用 dispatch 的时候, 不是直接调用命令, 而是通过函数拓展了更大的操作空间, 在这个函数中进行需要的逻辑并调用命令)
  • 在全局的 index.js 中 applyMiddleWare(中间件) 插入中间件, 然后全局的 dispatch 就支持以下这种写法
  • 在 index.js 中
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers'
export default createStore(rootReducer, applyMiddleware(thunk))
  • 在需要用到的组件中
export const clearTodo = () => {
    return (dispatch) => {
        setTimeout(() => {
            dispatch({
                type: CLEAR_TODO,
            })
        }, 1000)
    }
}

使用步骤

  1. 安装:yarn add redux-thunk
  2. 导入 import thunk from 'redux-thunk'
  3. 从 redux 中导入 applyMiddleware 函数。
  4. 将 applyMiddleware(thunk) 调用的结果作为 createStore 函数的第二个参数。
  5. 修改 action creator,返回一个函数。
  6. 在函数形式的 action 中执行异步操作,在异步操作成功后,分发 action 更新状态。

注意

  1. 如果要拿到数据更新后的 dom 元素, 可以用 useEffect (比如说 todoList 中的自动聚焦 功能)

第七板块

redux-promise (了解)

redux-promise 可以用来替换 redux-thunk 中异步代码的写法

  • redux-thunk 写法
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
export const changeActiveAc = (item) => ({
    type: FILTER_ACTIVE,
    item,
})
export const changeActive = (item) => {
    return async (dispatch) => {
        await sleep(2000)
        dispatch(changeActiveAc(item))
    }
}
  • redux-promise 写法

记得在全局的 index.js 中 applyMiddleWare(中间件) 插入中间件, 然后全局的 dispatch 就支持以下这种写法

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

// 保证 changeActive 调用完毕后的结果是一个 Promise 就 ok
export const changeActive = async (item) => {
    await sleep(2000)
    return {
        type: FILTER_ACTIVE,
        item,
    }
}

redux-devtools-extension

开发 React 项目时,能够通过 Chrome 开发者工具调试跟踪 Redux 状态。

  1. 保证浏览器安装了 Redux 的开发者工具。
  2. 通过包管理器在项目中安装 yarn add redux-devtools-extension
  3. 在 store/index.js 中进行配置。
import { createStore, applyMiddleware } from 'redux'
import reducer from './reducers'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
export default createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))
  1. 启动 react 项目,打开 chrome 开发者工具,测试

spa

  • SPA: Single Page Application 单页面应用程序,整个应用中只有一个页面(index.html)。

  • MPA : Multiple Page Application 多页面应用程序,整个应用中有很多个页面(*.html)。

  • 优势:页面响应速度快,体验好(无刷新),降低了对服务器的压力。

    a,传统的多页面应用程序,每次请求服务器返回的都是一整个完整的页面。

    b,单页面应用程序只有第一次会加载完整的页面,以后每次请求仅仅获取必要的数据。

  • 缺点:不利于 SEO 搜索引擎优化。

    a,因为爬虫只爬取 HTML 页面中的文本内容,不会执行 JS 代码。

    b,可以通过 SSR(服务端渲染 Server Side Rendering)来解决 SEO 问题,即先在服务器端把内容渲染出来,返回给浏览器的就是纯 HTML 内容了。

react-router

配置步骤

  1. 装包 yarn add react-router-dom@5.3.0
  2. react-router-dom 这个包提供了三个核心的组件。 import { HashRouter, Route, Link } from 'react-router-dom'\
  3. 使用 HashRouter 包裹整个应用,一个项目中只会有一个 Router。
<HashRouter>
    <div className="App">App</div>
</HashRouter>
  1. 使用 Link 指定导航链接
<Link className='nav-link active' aria-current='page' to='/findmusic'>
    发现音乐
</Link>
  1. 使用 Route 指定路由规则。
// 在哪里写的 Route,最终匹配到的组件就会渲染到哪里
<Route path='/findmusic' component={FindMusic} />

路由执行过程

  1. 点击 Link 组件(a 标签),浏览器地址栏中的 url 发生变化。
  2. ReactRouter 通过 hashchange 或 popState 监听到了地址栏 url 的变化。
  3. ReactRouter 内部遍历所有 Route 组件,使用路由规则(path)与 pathname(hash)进行匹配。
  4. 当路由规则(path)能够匹配地址栏中的 pathname(hash)时,就展示该 Route 对应的组件。

link 与 navLink

  • Link 组件最终会渲染成 a 标签,用于指定路由导航。

    1. to 属性,将来会渲染成 a 标签的 href 属性。

    2. Link 组件无法实现导航的高亮效果。

  • NavLink 组件,一个更特殊的 Link 组件,可以用于指定当前导航高亮。

    1. to:用于指定地址,会渲染成 a 标签的 href 属性。

    2. activeClass:用于指定高亮的类名,默认 active

    3. exact:精确匹配,表示必须精确匹配类名才会应用 class,默认是模糊模糊匹配。

Route 匹配规则

  • Route 组件 path 属性对应的值表示:默认是以此值开头的路径就会被匹配,添加 exact 属性可以开启精确匹配。
  • 所以默认情况下,path 为 / 能够匹配所有路由组件,因为所有路由组件都是以 / 开头的,一般来说,如果路径配置了 /,往往都需要配置 exact 属性。
  • 如果 path 的路径匹配上了,那么对应的组件就会被 render,否则就会 render null。
  • 如果没有指定 path,那么一定会被渲染,例如 <Route component={NotFound}></Route>

前端路由

现代的前端应用大多都是 SPA,也就是只有一个 HTML 页面的应用程序,因为它的用户体验更好、对服务器的压力更小,所以更受欢迎。

为了有效的使用单个页面来管理原来多页面的功能,前端路由应运而生,功能:让用户从一个视图(页面)导航到另一个视图(页面)。

  • 前端路由是一套映射规则,是 URL 路径 与组件之间的对应关系。
  • 使用 React 路由简单来说就是:配置路径和组件(配对)。

重定向 与 404

  • 这里的重定向需要加上 exact, 不然会陷入到死循环 (当代码匹配到 '/' 后, 重定向代码会用 '/findmusic' 从头再匹配)
  • 404 页面需要加在最后, 并且不加 path(也就是说任何跳转都会到 404 页面, 但由于 switch 的存在, 会在第一次匹配上路由的时候就不往下面走了, 因此这种写法需要加上 switch )
<Switch>
    {/* from 表示默认模糊匹配,表示以 '/' 开头就匹配了 Redirect */}
    {/* 然后跳转到了 /findmusic,发现又是以 / 开头的,死循环... */}
    {/* <Redirect from='/' to='/findmusic' /> */}
    {/* 解决 */}
    <Redirect exact from='/' to='/findmusic' />
    <Route path='/findmusic' component={FindMusic} />
    <Route path='/mymusic' component={MyMusic} />
    <Route path='/myfollow' component={MyFollow} />
    <Route component={NotFound}></Route>
</Switch>

hash 与 history 路由模式

image.png

嵌套路由的设置

这里设置的是二级路由, 直接在一级路由 '/findmusic' 后面继续添加一级路由路径; 要注意这里重定向写法, 并且要加上 exact

<Switch>
    {/* 默认展示推荐 */}
    <Redirect exact from='/findmusic' to='/findmusic/recommend' />
    <Route path='/findmusic/recommend' component={Recommend} />
    <Route path='/findmusic/ranking' component={Ranking} />
    <Route path='/findmusic/songlist' component={SongList} />
</Switch>

编程式导航

  • 第一种方式通过 props 拿到 history 进行跳转,props.history.push('/comment')
  • 第二种方式通过 react-router-dom 提供的 useHistory 勾子进行跳转。
import React from 'react'
import { useHistory } from 'react-router-dom'

export default function SongList(props) {
    const history = useHistory()
    const handleClick = () => {
        history.push('/mymusic')
    }
    return (
        <div>
            <h3>SongList</h3>
            <button onClick={handleClick}>MyMusic</button>
        </div>
    )
}

路由传参

路径传参

  • 入口
<NavLink className='nav-link' to='/findmusic/ranking/bsb'>
    飙升榜
</NavLink>
  • 出口
<Route path='/findmusic/ranking/:id' component={RankingList} />
  • 获取
// props.match.params.id 或者通过 useParams()
import { useParams } from 'react-router-dom'

export default function RankingList(props) {
    const params = useParams()
    return (
        <>
            <span className='me-3'>props 获取参数: {props.match.params.id}</span>
            <span>useParams 获取参数: {params.id}</span>
        </>
    )
}

query 传参

  • 入口
<NavLink className='nav-link' to='/findmusic/ranking/?id=bsb'>
    飙升榜
</NavLink>
  • 出口
<Route path='/findmusic/ranking' component={RankingList} />
  • 获取
import qs from 'qs'
export default function RankingList(props) {
    const { id } = qs.parse(props.location.search.slice(1))
    return (
        <>
            <span className='me-3'>props 获取参数: {id}</span>
        </>
    )
}

补充

  1. 封装一个睡眠函数, 能够在 ms 时间后, 再返回一个成功的 Promise

image.png

  1. hash 事件的绑定与解绑

这里绑定的事件是当网页 hash 改变的时候, 执行函数

  • 绑定 window.addEventListener('hashchange', hashChange)
  • 解绑 window.removeEventListener('hashchange', hashChange)
  1. qs 包, 能够将 query 传参从 ?id=123 转化为对象的形式

注意

  1. 目前使用的 redux-thunk 和 redux-promise 相当于都是在做 dispatch 功能的增强, 在使用 dispatch 的时候, 不只是能传入一个对象, 使用 redux-thunk 中间件后, 能够传入一个函数, 继续导入 redux-promise 中间件后, 在函数中能够使用特定的异步代码语法

  2. 选项卡和路由跳转的一个区别在于, 路由跳转能够在刷新后保持页面状态, 选项卡会在每次刷新后显示默认内容 (也就是说刷新不会改变地址栏的信息)

  3. exact 可以在 navLink 或Route 中书写

  • 在 navLink 中, 如果不加的话, 点击搜索的时候, 首页按钮也会有 active 类名( exact 在 NavLink 中解决了点击高亮的 bug)
<li>
    <NavLink to='/' exact >首页</NavLink>
</li>
<li>
    <NavLink to='/search'>搜索</NavLink>
</li>
<li>
    <NavLink to='/comment'>评论</NavLink>
</li>
  • 在 Route 中, 如果不加的话, 点击搜索的的时候, 就只会匹配到'/'
<Switch>
    <Route path='/' component={FindMusic} exact />
    <Route path='/mymusic' component={MyMusic} />
    <Route path='/myfollow' component={MyFollow} />
    <Route component={NotFound}></Route>
</Switch>
  1. 一般来说只要跳转路径为 '/', 都需要加 exact

  2. 当一级路由存在二级路由的时候, 不要在一级路由上加 exact, 否则路由匹配不能通过一级路由进入到二级路由匹配

  3. 重定向 <Redirect /> 一般都要加上 exact