React知识总结✨

199 阅读41分钟

“携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情

React知识总结✨

🚀标志为在 vscode 书写的快捷提示

❗ 标志为易错点和注意点

✨✅表示重点

React基础

React概述

React 是一个用于构建用户界面的 JavaScript 库。React 主要用来写HTML页面,或构建Web应用

React 的特点

  • 1 声明式

    • 程序员只需写标签,React 负责渲染 UI,并在数据变化时更新 UI
  • 2 基于组件

  • 3 学习一次,随处使用

React 的基本使用

  • 安装 npm i react react-dom
  • 使用
    • 1 引入 react 和 react-dom 两个 js 文件
    • 2 创建 React(虚拟DOM) 元素
      • React.createElement('h1',null,'Hello React'(文本节点),React.createElement('span',null,'Hello React')(元素节点)
        • 返回值:React元素
        • 第一个参数:要创建的React元素名称
        • 第二个参数:该React元素的属性 eg:{ className:'box' }
        • 第三个及其以后的参数:该React元素的子节点
    • 3 渲染 React 元素到页面中
      • ReactDOM.render(el, document.getElementById('root'))
        • 第一个参数:要渲染的React元素
        • 第二个参数:DOM对象,用于指定渲染到页面中的位置(挂载点)

React 脚手架的使用

脚手架是开发 现代Web 应用的必备。

使用 React 脚手架初始化项目

  • 初始化项目,命令:npx create-react-app my-app

  • 启动项目,在项目根目录执行命令:npm start

npx命令

目的:提升包内提供的命令行工具的使用体验

现在:无需安装脚手架包,就可以直接使用这个包提供的命令

在脚手架中使用 React

1 导入 react 和 react-dom 两个包。

  • import xx from 'react'
  • import xx from 'react-dom'
    • ✨ React18 写法,多了 'react-dom/client'

2 调用 React.createElement() 方法创建 react 元素。❌

3 调用 ReactDOM.render() 方法渲染 react 元素到页面中。

  • ✨React18写法:ReactDOM.createRoot(document.getElementById('root')).render()
    • 渲染函数式组件,注意render 里面写成函数 如:render(App())

JSX

JSX 是 JavaScript XML 的简写,表示在 JavaScript 代码中写 XML(HTML) 格式的代码。

优势:声明式语法更加直观、与HTML结构相同,降低了学习成本、提升开发效率

语法糖=> 通过 babel 插件 => 转化成 JS 能识别的语法 React.createElement()

JSX 是 React 的核心内容。

使用步骤:

1 :star:推荐使用 JSX 语法创建 react(虚拟DOM) 元素

const title = <h1>Hello JSX</h1>

2 使用 ReactDOM.render() 方法渲染 react 元素到页面中

ReactDOM.render(title, root)

为什么脚手架中可以使用 JSX 语法?

  • JSX 不是标准的 ECMAScript 语法,它是 ECMAScript 的语法扩展。

  • reate-react-app 脚手架中已经默认有babel,无需手动配置。

JSX中对象无法直接显示,需转换成字符串

<h2> { JSON.stringfy(obj) } </h2>

String{false}

JSX 中使用 JavaScript 表达式

嵌入 JS 表达式
  • 数据存储在JS中

  • 语法:{ JavaScript表达式 }

  • 注意:语法中是单大括号,不是双大括号!

注意点
  • 单大括号中可以使用任意的 JavaScript 表达式 * + - / 三元表达式...
  • 能写 js 对象,一般只出现在 style 属性里
  • JSX 自身也是 JS 表达式 {JSX}
  • 能在{}中出现语句(比如:if/for 等)
JavaScript表达式(有值)
  • (1). a
  • (2). a+b
  • (3). demo(1)
  • (4). arr.map()
  • (5). function test () {}
语句(代码)
  • (1).if(){}
  • (2).for(){}
  • (3).switch(){case:xxxx}

JSX 的条件渲染

if/else 三元表达式 逻辑与运算符

JSX 的列表渲染

如果要渲染一组数组,应该使用数组的 map() 方法

注意:

  • 渲染列表时应该添加 key 属性,key 属性的值要保证唯一
  • 原则:map() 遍历谁,就给谁添加 key 属性
  • 尽量避免使用索引号作为 key

{songs.map(item => <li key={item.id}>{item.name}</li>)}

❗ 属性值写法:属性名={ 值 },与 Vue 不一样

JSX 的样式处理

行内样式 —— style

12px 可省略成 12

<h1 style={{ color: 'red', backgroundColor: 'skyblue',fontSize:30 }}></h1>

:star:类名 —— className

<h1 className="title">JSX的样式处理</h1>

总结:

React 完全利用 JS 语言自身的能力来编写UI,而不是造轮子增强 HTML 功 能

:heavy_exclamation_mark: 注意点

1、 属性名使用 驼峰命名法

2、 特殊属性名:class->className for->htmlFor tabindex->tabIndex

3、 没有子节点的React 元素可以使用 /> ,必须有结束标签

  • eg:<span/>

4、 推荐使用 ()包裹 JSX 避免 JS 中的自动插入分号陷阱。

  • const dv = <div> hello </div>

5、 标签中混入JS表达式时要用{}

6、 只有一个根标签,必须要有一个根节点:<></> <React.Fragment></React.Fragment>

7、 内联样式: 要用style={{ key:value }}的形式去写

8、 标签首字母

​ (1).若写字母开头,则将该标签转为html中同名元素,若html中无该标签对应的同名元素,则报错。

​ (2).若写字母开头,react就去渲染对应的组件,若组件没有定义,则报错。

9、❗布尔类型、null、undefined 在 JSX 中不会显示在页面中(Vue 模板会转化成字符串再显示) String(true)

组件基础

使用React 就是使用组件,组件表示页面中的部分功能,组合多个组件实现完整的页面功能

特点:可复用、独立、可组合

React 组件的两种创建方式

1 使用函数创建组件

使用 JS 的函数(或箭头函数)创建的组件

  • 函数名称必须以大写字母开头
  • 函数组件必须有返回值,表示该组件的结构
    • 如果返回值为 null,表示不渲染任何内容
  • this 是undefined,因为babel编译后开启了严格模式
  • 没有 state 和 refs 属性
  • 可以使用 props
    • 函数传参的方式使用

渲染组件:

  • 用函数名作为组件标签名
  • 组件标签可以是单标签也可以是双标签
function Hello() {
    return (
    <div>这是我的第一个函数组件!</div>
    ) 
}
ReactDOM.render(<Hello />, root)

执行了ReactDOM.render(.......之后,发生了什么?

1.React解析组件标签,找到了MyComponent组件。

2.发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。
2 使用类创建组件

使用 ES6 的 class 创建的组件

  • 类名称也必须以写字母开头

  • 类组件应该继承 React.Component 父类,从而可以使用父类中提供的方法或属性

  • 类组件必须提供 render() 方法

  • render() 方法必须有返回值,表示该组件的结构

class Hello extends React.Component {
    render() {
        return <div>Hello Class Component!</div>
    } 
}
ReactDOM.render(<Hello />, root)
//render是放在哪里的?—— MyComponent的原型对象上,供实例使用。

//render中的this是谁?—— MyComponent的实例对象 <=> MyComponent组件实例对象。

执行了ReactDOM.render(.......之后,发生了什么?

1.React解析组件标签,找到了MyComponent组件。

2.发现组件是使用类定义的,随后 new 出来该类的实例,并通过该实例调用到原型上的render方法。

3.将render返回的 虚拟DOM 转为真实DOM,随后呈现在页面中。
3 抽离为独立 JS 文件

组件作为一个独立的个体,一般都会放到一个单独的 JS 文件中

步骤:

创建

  1. 创建MyFooter.js

  2. 在 MyFooter.js 中导入React

  • import React from 'react'
  1. 创建组件(函数 或 类)

    // 类 src/componnets/MyFooter.js
    export default class MyFooter extends React.Component {
    render() {
    return <div>Hello Class Component!</div>
    } }
    
    // 函数 src/componnets/MyHeader.js
     export default () => {
      return (
        <header>
          <h1>页面头部文件</h1>
        </header>
      )
    }
    
  2. 在 MyFooter.js 中导出该组件

    • export default MyFooter

使用:

  1. 在 index.js 中导入 MyFooter组件

    • import MyFooterfrom './MyFooter'
  2. 渲染组件

    • ReactDOM.render(<Hello />, root)
// index.js

import React from 'react'
import ReactDOM from 'react-dom'
import MyFooter from './components/MyFooter'
import MyHeader from './components/MyHeader'

class App extends React.Component {
  render() {
    return (
      <>
        <MyHeader />
        <h1>Hello React</h1>
        <MyFooter />
      </>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

React 事件处理

事件绑定

语法:

on+事件名称={事件处理程序},比如:onClick={() => {}}/{handelClick} ❗注意不要写括号调用

React 事件采用驼峰命名法

事件对象

可以通过事件处理程序的参数获取到事件对象 event

React 中的事件对象叫做:合成事件(对象)

(1).通过onXxx属性指定事件处理函数(注意大小写)

    a.React使用的是自定义(合成)事件, 而不是使用的原生DOM事件 —————— `为了更好的兼容性`
    
    b.React中的事件是通过事件委托方式处理的(委托给组件最外层的元素) ————————`为了的高效`
    
(2).通过event.target得到发生事件的DOM元素对象 ——————————`避免过度使用 ref`

有状态组件和无状态组件

函数组件又叫做无状态(数据)组件,类组件又叫做有状态组件

函数组件

  • 没有自己的状态,只负责数据展示(静)

类组件

  • 有自己的状态,负责更新 UI,让页面“动” 起来

组件(类组件)中的 statesetState

state的基本使用

状态(state)即数据,是组件内部的私有数据,只能在组件内部使用

state 的值是对象,表示一个组件中可以有多个数据

  • 获取状态:this.state
  • ❗模板中使用 { this.state } 只有一个大括号

语法:

class Hello extends React.Component {
    // 简化语法
    state= {
        count: 0
    }
    render() {
        return (
<button onClick="this.setState({count:this.state.count + 1
})"></button>
	// 错误 this.state.count += 1
        )
	}
}

🔔 React 重要思想:单向数据流,this.state 只负责访问,更新需要用 this.setState()

不要直接修改 state 中的值,这是错误的!!!

setState()修改状态
  • 状态是可变的
  • 语法:this.setState({ 要修改的数据 })
  • ❌注意:要直接修改 tate 中的值,这是错误的!!!
  • setState() 作用: 1. 修改 state 2. 更新UI (调用了 render 方法)
  • 思想:数据驱动视图

可以修改 state,后面使用 setState({})方法可以使render方法重新执行,进而更新数据

❌常见错误:通过赋值的方式修改 state,render 方法不会运行,视图不会更新(数据和视图不同步)

从 JSX 中抽离事件处理程序

原因:事件处理程序中 this 的值为 undefined

  • 由于changeWeather是作为onClick的回调,所以不是通过实例调用的,是直接调用。类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefined

希望:this 指向组件实例(render方法中的 this 即为组件实例)

自定义方法————要用赋值语句的形式+箭头函数

事件绑定 this 指向

React 事件处理 语法:

on事件类型={ 事件处理函数 } ❗ 花括号内绑定的是事件处理函数,而不是函数的调用

eg:onClick={this.handleClick}

1 箭头函数

利用箭头函数自身不绑定this的特点

render() 方法中的 this 为组件实例,可以获取到 setState()

事件传参 语法:on事件类型={ () => this.handleXxx(参数) }

<button onClick={() => this.onIncrement(1)}></button>

2 Function.prototype.bind()

利用ES5中的bind方法,将事件处理程序中的this与组件实例绑定到一起

class Hello extends React.Component {
    constructor() {
    	super()
    	this.onIncrement = this.onIncrement.bind(this) 
        this.state = { count:0 }
    }
    // ...省略 onIncrement
    render() {
    	return (
    	<button onClick={this.onIncrement}></button>
    	)
	}
}
3 :star:class 的实例方法

利用箭头函数形式的class实例方法

注意:该语法是实验性语法,但是,由于 babel 的存在可以直接使用

onIncrement = () => {
	this.setState({ … })
}
onIncrement 放在 实例对象上

由于 onIncrement 是作为onClick的回调,所以不是通过实例调用的,是直接调用

//类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefined
补充注意点
  • React 组件自带的结构体,this 指向底层做过处理,如 render 结构指向为组件实例
  • ❗ 如果是自定义的函数,需要写成箭头函数写法,否则 this 指向为 undefined

表单处理 & ref

受控组件
  1. React 将 state 与表单元素值 value (相当于 v-model 双向绑定)绑定到一起,由 state 的值来控制表单元素的值
  2. React 对表单元素的 onChange 事件进行封装,统一使用 onChange 获取表单值即可

步骤:

1 在 state 中添加一个状态,作为表单元素的value值(控制表单元素值的来源)

  • state = { txt: '' }
  • <input type="text" value={this.state.txt}/>

2 给表单元素绑定 change 事件,将 表单元素的值 设置为 state 的值(控制表单元素值的变化)

  • <input type="text" value={this.state.txt} onChange={e => this.setState({ txt: e.target.value })} />

❗示例总结:

1 文本框、富文本框、下拉框 操作value属性

2 复选框 操作checked属性

handleChecked = e =>{
    this.setState({ isChecked: e.target.value })
}
state = { isChecked:'' }
<input type:'checkbox' checkd={ this.state.isChecked } onChange={ this.handleChecked }/>

✨✨多表单元素优化

1 给表单元素添加name属性,名称与 state 相同

2 根据表单元素类型获取对应值

3 在 change 事件处理程序中通过对象名称的插值符号 [] 来修改对应的state

<input type="text" name="txt" value={this.state.txt} onChange={this.handleForm}/>
    
handelFrom = e =>{
     // 🎈利用解构语法简化代码
    const { name,type,checked,value } = e.target
    this.setDate({[name]:type === 'checkbox' ? checked : value})
}
非受控组件(DOM方式)❌

借助于 ref,使用原生 DOM 方式来获取表单元素值

场景:

  1. 获取 DOM 元素,和 document.querySelector() 获取无差别
  2. 获取组件,ref 价值所在✨

使用步骤

1 调用 React.createRef() 方法创建一个 ref 对象

constructor() {
	super()
	this.txtRef = React.createRef()
}

2 将创建好的 ref 对象添加到文本框中

<input type="text" ref={this.txtRef} />

3 通过 ref 对象获取到文本框的值

Console.log(this.txtRef.current.value)

组件进阶

组件通讯介绍

打破组件的独立封闭性,让其与外界沟通。这个过程就是组件通讯

组件的 props

作用:接收传递给组件的数据

1 传递数据:给组件标签添加属性

  • <Hello name="jack" age={19} />
    • 字符串 " "
    • 变量 {}

2 接收数据:函数组件通过参数props接收数据,组件通过 this.props 接收数据

class式:

class Hello extends React.Component {
    constructor(props) {
		// 推荐将props传递给父类构造函数
		super(props) 
    }
    render() {
        // 
        return (
         <div>接收到的数据:{ this.props.age }</div>
        )  // props 是一个对象
 	} 
}

函数式:

const Hello = props => {
	return (
	<div>接收到数据:{ props.name }</div>
	) 
}

const person = {name:'老刘',age:18,sex:'女'}
ReactDOM.render(<Person {...p}/>,document.getElementById('test3'))

特点:

  1. 可以给组件传递任意类型的数据
  2. props 是只读的对象,只能读取属性的值,无法修改对象
  3. 注意:使用组件时,如果写了构造函数,应该将 props 传递给 super(),否则,无法在构造函数中获取到 props!
    • 推荐将props传递给父类构造函数
    • 构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props

组件通讯的三种方式

1 父组件 -> 子组件

  1. 父组件提供要传递的state数据

    • state = { lastName: '王' }
  2. 给子组件标签添加属性,值为 state 中的数据

    • <Child name={ this.state.lastName } />
  3. 子组件中通过 props 接收父组件中传递的数据

    • 函数组件
      • function Child(props) { return <div>子组件接收到数据:{ props.name }</div> }
      • ✨函数组件的参数就是 props,可直接解构出父组件传过来的数据
    • 类组件
      • { this.props.name }
      • { ()=>this.props.HandleTilte(this.state.title) }

2 子组件 -> 父组件

利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数。

  1. 父组件提供一个回调函数(用于接收数据)

    • getChildMsg = data=> {this.setState({parentMsg:data})}
  2. 将该函数作为属性的值,传递给子组件

    • <Child getMsg={ this.getChildMsg } />
  3. 子组件通过 props 调用回调函数

    • handleClick=()=>{ this.props.getMsg(this.state.childMsg) }
  4. 将子组件的数据作为参数传递给回调函数

3 兄弟组件

将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态

公共父组件职责:1. 提供共享状态 2. 提供操作共享状态的方法

要通讯的子组件只需通过 props 接收状态或操作状态的方法

❗注意点:

  1. 可以给组件传递任意类型的数据
    • 包括虚拟DOM结构、函数(用来子向父传递数据)(比 Vue 更灵活)
  2. props 是只读的,不允许修改 props 的数据( Vue 的 props 也是只读的)

Context

App => Child 组件

  • 使用 Context,,实现跨组件传递数据(比如:主题、语言等)

使用步骤:

1 调用 React. createContext() 创建 Provider(提供数据) 和 Consumer(消费数据) 两个组件。

const { Provider, Consumer } = React.createContext()

2 使用 Provider 组件作为父节点

<Provider> <div className="App"><Child1 /></div> </Provider>

3 设置 value 属性,表示要传递的数据。

<Provider value="pink">

4 子组件调用 Consumer 组件接收数据。

<Consumer> { data => <span> data参数表示接收到的数据 -- {data}</span>}</Consumer>

5 孩子的孩子接收数据:

<Consumer> { data => <span> data参数表示接收到的数据 -- {data}</span>}</Consumer>

注:如果传过来的数据是对象,用解构,或者JSON.stringfy(date)

<Consumer>
  {({ a, b }) => {
    return (
      <div>
        {a}---{b}
      </div>
    )
  }}
</Consumer>

<Consumer>
  {value => {
    return <div>{JSON.stringify(value)}</div>
  }}
</Consumer>

❗✨注意事项:

  1. Consumer 内部嵌套的函数会自动执行,要注意函数的返回值需符合 模板数据展示语法要求

  2. 分拆的单文件组件如何处理?

    • 解决方案:确保 Provider 和 Consumer 是由同一个 Context 对象解构出来的
    // src/store.js
    import React from 'react'
    // 创建一个上下文对象,Provider 负责提供数据 ,Consumer 负责消费数据
    export const { Provider, Consumer } = React.createContext()
    
    // src/Test.js
    import { Consumer } from './store'
    // ...书写子组件的业务代码
    
    // src/index.js
    import { Provider } from './store'
    import Son from './Test'
    // 书写父组件的业务代码
    

props 深入

children 属性---组件的插槽

表示组件标签的子节点。当组件标签有子节点时,props 就会有该属性

值可以是任意值(文本、React元素、组件,甚至是函数)

React 插槽和 Vue 插槽写法区别:

Vue插槽 : <slot></slot>

React插槽: { this.props.children }

const Hello= props => {
	return (
        <div>
        组件的子节点:{props.children}
        </div>
	) 
}
<Hello>我是子节点</Hello>

这里的“我是子节点”是 props.children

子组件里使用:

{/* 渲染子节点 文本、组件、JSX节点 */}
{this.props.children}
{/* {this.props.children()} 子节点为函数时 */}

props 校验

允许在创建组件的时候,就指定 props 的类型、格式等,捕获使用组件时因为props导致的错误,给出明确的错误提示,增加组件的健壮性

使用步骤
  1. 导入 prop-types 包 (无需下载,React自带的) 🚀impt

  2. 使用组件名.propTypes = {colors: PropTypes.array} 来给组件的props添加校验规则 (写在组件定义之后)

  3. 也可用以下方法定义在组件内:(类组件✨)

    static propTypes = {
        title:PropTypes.string
    }
    
  4. 校验规则通过 PropTypes 对象来指定

约束规则
  1. 常见类型:array、bool、func、number、object、string

  2. React元素类型:element

  3. 必填项:isRequired

  4. ✨特定结构的对象:shape({ })

// 常见类型
potional: PropTypes.number,
// 必选
requiredFunc: PropTypes.func.isRequired,
// 特定结构的对象
optionalObjectWithShape: PropTypes.shape({
    color: PropTypes.string,
    fontSize: PropTypes.number
})

官网文档更多阅读

props 的默认值

场景:分页组件 -> 每页显示条数

作用:给 props 设置默认值,在未传入 props 时生效

App.defaultProps = { pageSize: 10 }

类组件✨

也可在组件内写:static defaultProps = { pageSize:10 }

传:<App pageSize={20}/>

函数组件✨

function List({ pagesize=10 }){return (...)}
知识补充:

类的静态成员:

class Person {
    // 类的实例属性
    name = 'zs'
   // 等价于 Person.age = 18 ,是添加给了 Person 类的成员,而非 new 出来的实例
    static age = 18
   // 原型实例方法
    sayHi() {
      console.log('哈哈')
    }
    static goodBye() {
      console.log('byebye')
    }
  }

请添加图片描述

组件的生命周期

组件的生命周期概述

意义:

  • 组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等

组件的生命周期:

  • 组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程,生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数

钩子函数的作用:为开发人员在不同阶段操作组件提供了时机。

只有 类组件 才有生命周期。

生命周期的三个阶段

请添加图片描述

1 创建时

执行时机 :组件创建时(页面加载时)

钩子函数的执行顺序: constructor -> render -> componentDidMount

钩子 函数触发时机作用
constructor创建组件时,最先执行,初始化的时候只执行一次1. 初始化state 2. 创建 Ref 3. 使用 bind 解决 this 指向问题等
render每次组件渲染都会触发渲染UI(注意: 不能在里面调用setState()
componentDidMount组件挂载(完成DOM渲染)后执行,初始化的时候执行一次1. 发送网络请求 2.DOM操作

✨开发经验:

  1. construcor()平常很少用,一般用简写的方式初始化 state ,即:

    • state = { msg:'Hello' }
  2. componentDidMount 🚀cdm 可快速生成结构

    • 组件挂载时可获取本地存储,如:

      componentDidMount(){
        this.setState({list:JSON.parse(localStorage.getItem('todos'))|| []})
      }
      
2 更新时

执行时机 : 1 setState() 2 forceUpdate() 3 组件接收到新的 props

钩子函数的执行顺序 : render -> componentDidUpdate🚀cdu

钩子函数触发时机作用
render每次组件渲染都会触发渲染UI(与 挂载阶段 是同一个render)
componentDidUpdate组件更新后(DOM渲染完毕)DOM操作,可以获取到更新后的DOM内容,不要直接调用setState

render生命周期注意点:

  1. setState 会导致递归更新!所以不能在 render 直接调用
  2. 不要在 render 内写定时器,会累加

✨React 可通过 componentDidUpdate 钩子实现类似 Vue 中的 watch 功能

componentDidUpdate(prevProps,prevState){
    // count 新值和旧值不一样,说明 count 变化了(Vue的watch)
    if(this.state.count !== prevState.count){
        console.log('count变化了,实现Vue的watch')
    }
}

也可在componentDidUpdate中设置本地存储

3 卸载时

执行时机 : 组件从页面中消失 🚀cwu

钩子函数触发时机作用
componentWillUnmount组件卸载(从页面中消失)执行清理工作(比如:清理定时器等)

render-props高阶组件

React组件复用概述

复用什么?
  1. state 2. 操作state的方法 (组件状态逻辑 )
两种方式:

​ 1. render props模式 2. 高阶组件(HOC)

注意:这两种方式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式(写法)

render props 模式

使用步骤

1 创建Mouse组件,在组件中提供复用的状态逻辑代码(1. 状态 2. 操作状态的方法

2 将要复用的状态作为 props.render(state) 方法的参数,暴露到组件外部

render() { return this.props.render(this.state) }

3 使用 props.render() 的返回值作为要渲染的内容

<Mouse render={(mouse) => <p>鼠标当前位置 {mouse.x},{mouse.y}</p>}/>

上面的 render 属性可以使用任意名

函数要是没有返回值,就是直接调用,然后返回了一个 undefined

children代替render属性

1 组件内部:

render(){ return this.props.children(this.state)}

2 使用组件:

<Mouse> <p>{({x,y} => <p>鼠标的位置:{x} {y})}</p> </Mouse>

代码优化

1 给 render porps 添加 props 校验

2 在组件卸载时解除 mousemove 事件绑定

Mouse.propTypes = { children: PropTypes.func.isRequired }

componentWillUnmount(){
   window.removeEventListener('mousemove',this.handleMouseMove)
}

高阶函数

如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。

1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。

2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。

常见的高阶函数有:Promise、setTimeout、arr.map()等等

可以实现函数的复用

//保存表单数据到状态中
    saveFormData = (dataType)=>{
        return (event)=>{
            this.setState({[dataType]:event.target.value})
        }
    }

用户名:<input onChange={this.saveFormData('username')} type="text" name="username"/>
						密码:<input onChange={this.saveFormData('password')} type="password" name="password"/>

不用函数柯里化的方法

saveFormData = (dataType,event)=>{
    this.setState({[dataType]:event.target.value})
}
<input onChange={ e => this.saveFormData('username',e)}
<input onChange={ e => this.saveFormData('password',e)}

函数的柯里化

通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。

  function sum(a){
    return(b)=>{
    return (c)=>{
    return a+b+c
         }
     }
  }

高阶组件

高阶组件(HOC,Higher-Order Component)是一个函数,接收要包装的组件,返回增强后的组件

高阶组件内部创建一个组件,在这个类组件中提供复用的状态逻辑代码,通过props将复用的状态传递给被包装组件 WrappedComponent

使用步骤:

1 创建一个函数,约定以 with 开头

2 指定函数参数,参数应该以大写字母开头(作为要渲染的组件)

3 在函数内部创建一个类组件,提供复用状态逻辑代码,并返回

4 在该组件中 渲染参数组件,同时将状态通过 props 传递给参数组件

function withMouse(WrappedComponent) {
    class Mouse extends React.Component {
        render(){
return <WrappedComponent {...this.state}  {...this.props}/>
        }
    }
    return Mouse
}

5 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中

// 创建组件
const MousePosition = withMouse(Position)
// 渲染组件
<MousePosition />

6 设置displayName

  • 使用高阶组件存在的问题:

    • 得到的两个组件名称相同
  • 原因:

    • 默认情况下,React使用组件名称作为 displayName
  • displayName的作用:

    • 用于设置调试信息(React Developer Tools信息)
  • 设置方式:

    Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
    
    function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component' }
    

7 传递props

问题:

  • props丢失

原因:

  • 高阶组件没有往下传递props

解决方式:

<WrappedComponent {...this.state} {...this.props} />

React原理

✨setState() 的说明 --- 🔔面试题-setState深入

更新数据

setState() 是异步更新数据的

❗后面的 setState() 不要依赖于前面的 setState()

对象写法:覆盖运行,多次调用 setState ,只会触发最后一次
count:1 

this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 2 })

{/* 🔔 视图结果为:3 */}
<h2>count:{this.state.count}</h2>
函数写法:所有的 count 更新会叠加运行
count:1

this.setState((state) => {
  return { count: state.count + 1 }
})

this.setState((state) => {
  return { count: state.count + 2 }
})

{/* 🔔 视图结果为:4 */}
<h2>count:{this.state.count}</h2>

推荐语法:

setState((state, props) => {})

参数state:表示最新的state 参数props:表示最新的props

第二个参数

DOM 更新后才触发的回调函数,相当于 Vue 的 this.$nextTick(()=>{ })

setState(updater[, callback])

this.setState(
    (state, props) => {},
    () => {
    document.title = '更新state后的标题:' + this.state.count
    }
)

知识点补充---事件的覆盖和叠加

 // 会覆盖前面的 onclick
  btn.onclick = function () {
    console.log('1')
  }
  btn.onclick = function () {
    console.log('2')
  }

// 不覆盖前面添加的 click 事件,全部叠加运行
  btn.addEventListener('click', () => {
    console.log(1)
  })
  btn.addEventListener('click', () => {
    console.log(2)
  })

JSX 语法的转化过程

请添加图片描述

JSX 仅仅是 createElement() 方法的语法糖(简化语法) JSX 语法被 @babel/preset-react 插件编译为 createElement() 方法 React 元素:是一个对象,用来描述你希望在屏幕上看到的内容

请添加图片描述

组件更新机制

setState() 的两个作用:

  1. 修改 state
  2. 更新组件(UI)

过程:

父组件重新渲染时,也会重新渲染子组件,但只会渲染当前组件子树(当前组件及其所有子组件)

官网原话:大部分情况下,你应该遵循默认行为。

✨组件性能优化

减轻 state

  1. 只存储跟组件相关的数据(比如:count/ 列表数据 /loading
  2. 不要做渲染的数据不要放在 state 中,比如定时器 id 等
    • 对于需要在多个方法中用到的数据,应该放在 this 中(这样不会加重 state)

      // 与 state 同级:
      timerId = -1
       componentDidMount() {
          this.timerId = setInterval(() => {
            console.log('定时器运行中')
          }, 1000)
        }
      
        componentWillUnmount() {
          clearInterval(this.timerId)
        }
      

避免不必要的重新渲染

由于 组件更新机制:

父组件更新会引起子组件也被更新,那么子组件没有任何变化时也会重新渲染

解决:使用钩子函数 shouldComponentUpdate(nextProps, nextState)🚀scu
  • 作用:通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染

  • 触发时机:更新阶段的钩子函数,组件重新渲染前执行 (shouldComponentUpdate --> render)

class Hello extends Component{
    shouldComponentUpdate(nextProps, nextState){
        // 根据条件,决定是否重新渲染组件
        return boolean(eg: nextState.number !== this.state.number)
    }
    render(){...}
}

纯组件

PureComponent 与 React.Component 功能相似

✨区别:PureComponent 内部自动实现了 shouldComponentUpdate 钩子,不需要手动比较

PureComponent 底层源码揭秘:纯组件内部通过分别 对比 前后两次 propsstate 的值,如果没有改变,返回 false 以告知 React 可以跳过更新。

class Hello extends React.PureComponent{
    render(){
        return (
          <div>纯组件</div>
        )
    }
}
纯组件内部的对比是 shallow compare(浅层对比)
  • 对于值类型来说:比较两个值是否相同(直接赋值即可,没有坑)

    最新的state.number === 上一次的state.number // false,重新渲染组件
    
  • ❗对于引用类型来说:只比较对象的引用(地址)是否相同,而不是❌比较值

    const obj = { number: 0 }
    ❌const newObj = obj(不要直接赋值)
    newObj.number = 2
    // ❌错误做法
    state.obj.number = 2
    最新的state.obj === 上一次的state.obj // true,不重新渲染组件
    
  • ❗state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据!

    ✅对象

    const newObj = { ...state.obj,number:2 }
    
    setState({ obj:newObj })
    

    ✅数组

    • ❌不要用数组的push / unshift 等直接修改当前数组的的方法
    • ✅应该用 concat 或 slice 等这些返回新数组的方法
    this.setState({
        list:[...this.state.list, {新数据}]
    })
    

虚拟 DOM 和 Diff 算法(实现部分更新)

虚拟 DOM本质是保存 DOM 关键信息的 JS 对象(state + JSX)

好处

提高DOM更新的性能,不频繁的操作真实Dom,在内存中找到变化的部分,再更新真实DOM

执行过程

  1. 初次渲染时,React 会根据初始state(Model),创建一个虚拟 DOM 对象(树)。
  2. 根据虚拟 DOM 生成真正的 DOM,渲染到页面中。
  3. 当数据变化后(setState()),重新根据新的数据,创建新的虚拟DOM对象(树)。
  4. 与上一次得到的虚拟 DOM 对象,使用 Diff 算法 对比(找不同),得到需要更新的内容。
  5. 最终,React 只将变化的内容更新(patch)到 DOM 中,重新渲染到页面。

React路由基础---V5

路由基本使用

使用React路由简单来说,就是配置 路径和组件(配对)

使用步骤:

  1. 安装:yarn add react-router-dom

  2. 导入路由的三个核心组件:Router / Route / Link

    import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
    
  3. ✨使用 Router 组件包裹整个应用(重要)

    • 一个 React应用只需用一次
    <Router>
        <div className="App">
        // … 省略页面内容
        </div>
    </Router>
    
  4. 使用 Link 组件作为导航菜单(路由入口)

    <Link to="/first">页面一</Link>
    
  5. 使用 Route 组件配置路由规则和要展示的组件(路由出口)

    const First = () => <p>页面一的页面内容</p>
    <Router>
        <div className="App">
            <Link to="/first">页面一</Link>
            <Route path="/first" component={First}></Route>
        </div>
    </Router>
    

常用组件

Router 组件:

包裹整个应用,一个 React 应用只需要使用一次

两种常用 Router:HashRouter 和 BrowserRouter

HashRouter:使用 URL 的哈希值实现(localhost:3000/#/first)

类似 Vue 的 new VueRouter()

✨**BrowserRouter**:使用 H5 的 history API 实现(localhost:3000/first)

✨✅ ❗开发经验:导入 HashRouter / BrowserRouter 组件后建议通过 as 改名为 Router

import { BrowserRouter as Router } from 'react-router-dom'
Link/NavLink 组件:

用于指定导航链接(a 标签),类似 Vue 的 <router-link> 组件

Route(OBJECT) 组件:

指定路由展示组件相关信息,相当于 Vue 的 <router-view> + 路由配置

  • path属性:路由规则

  • component属性:展示的组件

  • 推荐写法:通过 component 属性绑定组件,会通过 props 传递路由相关信息给子组件,工作常用

    <Route path='/find' component={Find} />

  • 以下写法只负责展示组件,没有传递路由相关 props 给子组件

    <Route path='/my'> <My/> </Route >

  • Route组件写在哪,渲染出来的组件就展示在哪

Switch 组件 --- 404错误处理提示处理

通常,我们会把Route包裹在一个Switch组件中

Switch组件中,不管有多少个路由规则匹配到了,都只会渲染第一个匹配的组件

✅通过Switch组件内,最后一个 Route 组件可用于实现404错误页面的提示

<Switch>
    <Route path='/find' component={Find} />
    <Route path='/my' component={My} />
    {/* 处理 404 情况需要写 Switch 的最后 */}
    <Route component={NotFound}></Route>
</Switch>
❗注意事项:

Link/NavLink 组件需要嵌套到 HashRouter / BrowserRouter 组件内

NavLink 组件如果路径匹配的话,会自动添加 class="active" 的类名

  • 样式要引入 index.js 中

❗记得 import 再用

React路由 V5 的基本使用
import { BrowserRouter as Router, Route, Link, NavLink } from 'react-router-dom'
<Router>
  <h1>仿网易云音乐路由 - 公共的头部</h1>
  <Link to="/find">发现音乐</Link>
  <br />
  <NavLink to="/my">我的音乐</NavLink>
  <Route path="/find" component={Find}></Route>
  <Route path="/my" component={My}></Route>
</Router>

路由执行过程

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

路由嵌套

src/views/Find.js

<NavLink to='/find/toplist'>排行榜</NavLink>

{/* React的 Route 组件:相当于 Vue 的 <router-view> + 路由配置 */}

<Route path='/find/toplist' component={Toplist} />

编程式导航

通过 JS 代码来实现页面跳转

  • history 是 React 路由提供的,用于获取浏览器历史记录的相关信息
  • push(path):跳转到某个页面,参数 path 表示要跳转的路径
  • go(n): 前进或后退到某个页面,参数 n 表示前进或后退页面数量(比如:-1 表示后退到上一页)
this.props.history.push('/my') 
// vue 的写法 this.$router.push('/my')

默认路由

表示进入页面时就会匹配的路由,默认路由path为:/

<Route path='/' component={Home} />

匹配模式

默认情况下,React 路由是模糊匹配模式

模糊匹配模式

模糊匹配规则:只要 pathname 以 path 开头就会匹配成功

  • path 代表 Route 组件的 path 属性
  • pathname 代表 Link 组件的 to 属性(也就是 location.pathname

精确匹配

避免默认路由在任何情况下都会展示

给 Route 组件添加 exact 属性,让其变为精确匹配模式

// 此时,该组件只能匹配 pathname=“/” 这一种情况
<Route exact path="/" component=... />

✨推荐:给默认路由添加 exact 属性。

动态路由和路由参数

动态路由参数,通过 /:xxx 占位

<Route path='/playlist/:id' component={PlayListDetail} />

获取动态路由参数

React: this.props.match.params.xxx Vue: this.$route.params.xx

React路由基础---V6

❗只写与 V5不同的点

核心组件

Routes

提供一个路由出口,满足条件的路由组件会渲染到组件内部,定义 path 和组件之间的关系

语法说明:
<Routes>
    <Route/>
    <Route/>
</Routes>    

Route

用于指定导航链接,完成路由匹配

<Route path="/about" element={ <About/> }

path 指定匹配的路径地址 element 指定要渲染的组件

编程式导航

语法:

navigage('/about,{ replace:true }')

  1. 导入useNavigate钩子函数

  2. 执行钩子函数得到跳转函数

  3. 执行跳转函数完成跳转

✨注:如果在跳转时不想加历史记录,可以添加额外参数 replace 为 true

跳转携带参数

1 searchParams传参

传参:navigage('/about/?id=100')

取参:

let [params] = useSearchParams()
let id = params.get('id')
2 params传参(path配合:/:id)

传参:navigage('/about/1001')

取参:

let [params] = useParams()
let id = params.get('id')

嵌套路由

1. App.js: 定义嵌套路由声明

<Routes>
    // 定义嵌套关系
    <Route path='/' element={ <Layout /> }>
        <Route path='/board' element={ <Board /> } />
    <Route/> 
</Routes>            

2. Layout.js: 使用 <Outlet/>指定二级路由出口

import { Outlet } from 'react-router-dom'

function Layout (){
    return (
        <div>
          // 二级路由出口
          <Outlet />
        </div>
    )
}

默认二级路由

场景:首先渲染完毕就要显示的二级路由

步骤:

  1. 给默认的路由标记 index
  2. 去掉跳转路径 path
<Routes>
    <Route path='/' element={ <Layout /> }>
        <Route index element={ <Board /> } />
        <Route path='/article' element={ <Board /> } />
    <Route/> 
</Routes> 

404路由配置

场景:当所有的路径都没有匹配成功的时候显示

语法说明:在各级路由的最后添加 *号路由 作为兜底

<Routes>
    <Route path='/' element={ <Layout /> }>
        <Route index element={ <Board /> } />
        <Route path='/article' element={ <Article /> } />
            <Route path='*' element={ <NotFound /> } />
    <Route/> 
</Routes> 

✨Hooks基础

什么是hooks

Hooks的本质:一套能够使 函数组件 更强大,更灵活的“钩子”,react v16.8开始

❗注意点:

  1. 有了hooks之后,为了兼容老版本,class类组件并没有被移除,俩者都可以使用
  2. 有了hooks之后,不能在把函数成为无状态组件了,因为hooks为函数组件提供了状态
  3. hooks只能在函数组件中使用

Hooks解决了什么问题

1 组件的状态逻辑复用

mixin混入的数据来源不清晰,HOC高阶组件的嵌套问题

2 class组件自身的问题

学习成本高,比如各种生命周期,this指向问题等

useState

替换之前的 state 写法

语法:

const [ 访问state的变量名, 设置state的函数名 ] = useState(初始值)

❗注意事项:

  1. useState 需要主动导入
  2. 函数组件没有 this ( this的值为 undefined )

state={ count:0 } ==> const [count,seCount] = useState(0)

this.setState({ count:this.state.count +1 })==> setCount(count + 1)

函数组件声明多个状态

useState() 可以调用多次,声明多个状态

为函数组件添加多个状态,再调用 useState 即可
const [isShow,setIsShow] = useState(true)

<button onClick={()=> setIsShow(!isShow)}></button>

useState 注意事项

1 只能出现在函数组件中
2 不能嵌套在if/for/其它函数中(react按照hooks的调用顺序识别每一个hook)
  • let num = 1
    function List(){
      num++
      if(num / 2 === 0){
         const [name, setName] = useState('cp') 
      }
      const [list,setList] = useState([])
    }
    // 俩个hook的顺序不是固定的,这是不可以的!!!
    
3 可以通过开发者工具查看hooks状态

请添加图片描述

useEffect - 副作用

主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用比如:

官方定义: 数据获取,设置订阅,localstorage操作以及手动更改 React 组件中的 DOM 都属于副作用。

官方提示:

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

1. 组件挂载时会执行一次 2. 每次组件更新时会执行 3. useEffect 内部返回的回调函数,会在组件被卸载时自动执行

基本使用

1 导入 useEffect 函数

2 调用 useEffect 函数,并传入回调函数

3 在回调函数中编写副作用处理(dom操作)

4 修改数据状态

5 检测副作用是否生效

useEffect - 传入和不传入第二个可选参数区别

没传第二个参数:componentDidMount + componentDidUpdate
  1. 组件挂载时会执行一次
  2. 每次组件更新(所有的state, props)时会执行
  3. useEffect 内部返回的回调函数,会在组件被卸载时自动执行
useEffect(()=>{
    console.log(11111, '没有传入第二个参数', count)
})
传递了第二个参数:componentDidUpdate + 内部判断参数变化

用法1:添加空数组

组件只在首次渲染时执行一次

// componentDidMount()
  useEffect(() => {
    console.log('组件挂载时,运行一次~~')
  }, [])

用法2:添加特定依赖项

类似 Vue 的立刻执行的 watch,在首次渲染时执行,在依赖项发生变化时重新执行

// componentDidUpdate()
  useEffect(() => {
    console.log('count更新时', count)
  }, [count])

✨Hooks进阶

useState - 回调函数的参数

使用场景

参数只会在初次渲染中起作用,初始 state 需要通过计算才能获得,则可以传入一个函数,返回计算后的初始值,供初始渲染的时候用

语法:

const [name,setName] = useState(()=>{
    // 编写计算逻辑    return '计算之后的初始值'
})

❗语法规则:

  1. 回调函数return出去的值将作为初始值
  2. 回调函数中的逻辑只会在组件初始化的时候执行一次

应用:

如何实现一个自增按钮,可以由使用的时候以传参的方式决定递增的初始值?

import { useState } from 'react'

export default function App() {
  return <Count initCount={10} />
}
function Count({ initCount }) {
  const [count, setCount] = useState(() => initCount)

  return (
    <div>
      <h2>count:{count}</h2>
      <button onClick={() => setCount(count + 1)}> +</button>
    </div>
  )
}

useEffect清理操作

语法:

useEffect() 内部返回的回调函数,会在组件卸载时自动执行(相当于类组件的 componentWillUnmount)

作用:

执行清除操作(如:清理定时器,清理全局注册的事件,清理订阅等)

useEffect(() => {
    // 挂载时运行的代码
    console.log('组件挂载!!!')
    window.addEventListener('resize', fn)

    // ✅ 卸载时运行的代码
    return () => {
      console.log('组件卸载了~~~')
      window.removeEventListener('resize', fn)
    }
  }, [])

useEffect - 发送请求获取数据

❗开发注意事项:

❌1 不要给 useEffect 第一级函数添加 async

  • 因为异步会导致清理函数无法立即返回

❌常见错误写法: useEffect(async () => {}, [])

✅正确写法:

 const [channels, setChannels] = useState([])
useEffect(() => {
   async function loadDate(){
       // 使用 axios 请求数据
        const res = await request({
        url: '/v1_0/channels',
        method: 'get',
      })
      setChannels(res.data.channels)
   }
   loadDate()
 }, [])

2 ✅useEffect 回调函数中用到的数据就是依赖数据,就应该出现在依赖项数组中,如果不添加依赖项就会有bug出现

useRef

可以在函数组件中获取真实的dom元素对象或者是组件对象

使用步骤:

  1. 导入 useRef 函数
  2. 执行 useRef 函数并传入null,返回值为一个对象 内部有一个current属性存放拿到的dom对象(组件实例)
  3. 通过 ref 绑定 要获取的元素或者组件

获取dom

import { useEffect, useRef } from 'react'
function App() {  
    const h1Ref = useRef(null)  
    useEffect(() => {    
        console.log(h1Ref)  
    },[])  
    return (    
        <div>      
            <h1 ref={ h1Ref }>this is h1</h1>    
        </div>  
    )
}
export default App

获取组件实例

函数组件由于没有实例,不能使用ref获取,如果想获取组件实例,必须是类组件

Foo.js

class Foo extends React.Component {  
    sayHi = () => {    
        console.log('say hi')  
    }  
    render(){    
        return <div>Foo</div>  
    }
}
    
export default Foo

App.js

import { useEffect, useRef } from 'react'
import Foo from './Foo'
function App() {  
    const h1Foo = useRef(null)  
    useEffect(() => {    
        console.log(h1Foo)  
    }, [])  
    return (    
        <div> <Foo ref={ h1Foo } /></div>  
    )
}
export default App

useContext

实现步骤

  1. 使用createContext 创建Context对象
  2. 在顶层组件通过Provider 提供数据
  3. 在底层组件通过useContext函数获取数据

代码实现

import { createContext, useContext } from 'react'
// 创建Context对象
const Context = createContext()

function Foo() {  
    return <div>Foo <Bar/></div>
}

function Bar() {  
    // 底层组件通过useContext函数获取数据  
    const name = useContext(Context)  
    return <div>Bar {name}</div>
}

function App() {  
    return (    
        // 顶层组件通过Provider 提供数据    
        <Context.Provider value={'this is name'}>     
            <div><Foo/></div>    
        </Context.Provider>  
    )
}

export default App

杂记

TodoList案例重难点分析 --- 类组件

❗不要直接改 state 的数据

如:id:list.length++ 这样其实就是直接改了 list ,给它直接加了一个 empty 值

控制显示隐藏:

style={{ display: list.length === 0 ? 'none' : '' }}

✨调用父组件的方法步骤:

  1. 父组件准备方法
  2. 传递给子组件
  3. 子组件解构出来,判断是否是带参函数
    • 是,() => addList(参数)
    • 不是,addList()

删除任务实现:

  1. 传 id

  2. 用数组过滤方法

  3. 更新 list 值

    delTodo = id => {
        this.setState({
          list: this.state.list.filter(item => item.id !== id)
        })
      }
    

✨❗更新 单选框状态

changeChecked = id => {
    const newList = this.state.list.map(item => {
      if (item.id === id) {
        // 需要更新 done
        return {
          ...item,
          done: !item.done
        }
      } else {
        return item
      }
    })
    this.setState({
      list: newList
    })
  }

✨更新全选状态

// 1. allChecked 由反选框计算出来
const allChecked = list.length !== 0 && list.every(item => item.done)

// 2. 改 allChecked 即更改反选框的值
changeAll = allChecked => {
    const newList = this.state.list.map(item => {
      return { ...item, done: allChecked }
    })
    this.setState({
      list: newList
    })
  }

✨添加任务用拓展运算符

addList = options => {
    this.setState({
      list: [...this.state.list, options]
    })
  }

计算完成任务总量的两种方法

1 filter
const count = list.filter(item => item.done).length
✅2 reduce 适合结合价格、数量等复杂的算法
const count = list.reduce((sum, item) => {
      if (item.done) return ++sum
      else return sum
    }, 0)

✨本地存储

组件挂载时获取本地存储:
componentDidMount() {
    this.setState({
      list: JSON.parse(localStorage.getItem('todos')) || [],
      type: JSON.parse(localStorage.getItem('todos-type')) || 'all'
    })
  }
组件(state,props)更新时,设置本地存储
  componentDidUpdate(prevProps, prevState) {
    localStorage.setItem('todos', JSON.stringify(this.state.list))
localStorage.setItem('todos-type', JSON.stringify(this.state.type))
  }
函数组件的存储:
// 组件挂载时 - 获取本地存储数据
  useEffect(() => {
    console.log('组件挂载时 - 获取本地存储数据')
    setList(JSON.parse(localStorage.getItem('todos')))
    setActive(localStorage.getItem('todos-type'))
  }, [])

  // 数据更新时 - 设置本地存储数据
  useEffect(() => {
    console.log('数据更新时 - 设置本地存储数据')
    localStorage.setItem('todos', JSON.stringify(list))
    localStorage.setItem('todos-type', active)
  }, [list, active])

❗类组件中自己写的函数记得要写成箭头函数!!

✅React 进阶

状态逻辑复用

高阶函数

什么是高阶组件?

高阶组件本质是一个函数,类比高阶组件,入参为组件,返回值也为组件,可以在函数体内给入参组件携带更多参数

如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。

1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数

2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。

常见的高阶函数有:PromisesetTimeoutarr.map()等等

高阶组件的代码实现

实现两个组件:1 在页面中显示鼠标在页面中的位置 2 在页面中实现span标签跟随鼠标移动

HocWrapper.js

import React from 'react'
const withMouse =(BaseComponent) => {
  class Wrapper extends React.Component {
        // 状态和逻辑公共部分
    state = {
        x: 0,
        y: 0,
    }
     moveHandler = (e) => {
      this.setState({
        x: e.pageX,
        y: e.pageY,
      })
    }
    }
    componentDidMount() {
    document.addEventListener('mousemove', this.moveHandler)
    }
    componentWillUnmount() {
    document.removeEventListener('mousemove', this.moveHandler)
    }
    render() {
      const { x, y } = this.state
      // 数据状态x,y 想怎么用怎么用
      return <BaseComponent x={x} y={y} />
    }
  }
	return Wrapper
}
export { withMouse }

使用高阶组件

import React from 'react'
import { withMouse } from './HocWrapper'

function AComponent({ x, y }) {
  return (
    <div>
      x:{x},y:{y}
    </div>
  )
}
// 调用高阶组件包裹源组件
const WrapperA = withMouse(AComponent)

class App extends React.Component {
  render() {
    return (
      <div>
        <WrapperA />
      </div>
    )
  }
}

export default App

render props

render props本质是一个组件,将组件的children属性设计为一个函数,在调用函数的同时传入额外参数

render props定义 renderProps.jsx

// render Props
import React from 'react'

class WithMouse extends React.Component {
  state = {
    x: 0,
    y: 0,
  }
  moveHandler = (e) => {
    this.setState({
      x: e.pageX,
      y: e.pageY,
    })
  }
  componentDidMount() {
    document.addEventListener('mousemove', this.moveHandler)
  }
  componentWillUnmount() {
    document.removeEventListener('mousemove', this.moveHandler)
  }
  render() {
    return <div>{this.props.children(this.state)}</div>
  }
}

export default WithMouse

使用 render props

import React from 'react'
import WithMouse from './renderProps'

function AComponent({ x, y }) {
  return (
    <div>
      x:{x},y:{y}
    </div>
  )
}

class App extends React.Component {
  render() {
    return (
      <div>
        <WithMouse>{(state) => <AComponent {...state} />}</WithMouse>
      </div>
    )
  }
}

export default App

hooks

天生为组合而生

hook实现

import { useState, useEffect } from 'react'

function useMouse () {
  const [p, setP] = useState({
    x: 0,
    y: 0
  })
  function moveHandler (e) {
    setP({
      x: e.pageX,
      y: e.pageY
    })
  }
  useEffect(() => {
    document.addEventListener('mousemove', moveHandler)
    return () => {
      document.removeEventListener('mousemove', moveHandler)
    }
  }, [])
  return [p.x, p.y]
}
export { useMouse }

hook使用

import { useMouse } from './withMouse'
function AComponent() {
  const [x, y] = useMouse()
  return (
    <div>
      x:{x},y:{y}
    </div>
  )
}

性能优化

优化的方向:减少没必要的组件更新

组件更新机制:

父组件更新会引起子组件也被更新,那么子组件没有任何变化时也会重新渲染

React.memo

React.memo是一个高阶组件,其实就是一个函数,通过比较**props的变化**决定是否重新渲染,如果通过内部的对比发现props并没有变化,则不会重新渲染,从而达到提高渲染性能的目的

基础使用

1)props不变不重新渲染

import React, { useState } from 'react'
// 通过React.memo包裹子组件
const Son = React.memo(() => {
  console.log('我是son组件,我更新了')
  return <div>this is son </div>
})

function App() {
  const [count, setCount] = useState(0)
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      <Son />
    </div>
  )
}
export default App

再次运行发现,子组件只会在首次渲染时执行一次,后续App组件的更新不再影响它

2)props变化重新渲染

import React, { useState } from 'react'

const Son = React.memo(() => {
  console.log('我是son组件,我更新了')
  return <div>this is son </div>
})

function App() {
  const [count, setCount] = useState(0)
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      <Son count={count} />
    </div>
  )
}

export default App

再次运行发现,子组件会随着count的变化而变化

复杂类型的Props

由于react内部对于复杂类型数据进行的是浅对比,只对比引用,如果引用不同,则代表俩次的props是不一致的,如果不一致,就重新渲染

import React, { useState } from 'react'

const Son = React.memo(() => {
  console.log('我是son组件,我更新了')
  return <div>this is son </div>
})

function App() {
  const [count, setCount] = useState(0)
  // 这里我们每次 修改count进行App组件更新 list都会生成一个不同的引用 所以造成子组件又重新渲染了
  const list = [1, 2, 3]
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      <Son list={list} />
    </div>
  )
}

export default App

自定义props对比

如果不想使用React.memo内置的对比方式,而是想自己做判断,也是可以的,它为我们开放了对比函数,只需要在memo函数中传入第二个参数即可

import React, { useState } from 'react'

const Son = React.memo(
  () => {
    console.log('我是son组件,我更新了')
    return <div>this is son </div>
  },
  (prev, next) => {
    // 自定义对比关系 决定是否要重新渲染
    // 如果返回false 代表俩次props发生了改变 组件重新渲染
    // 如果返回true  代表来此props没有发生变化 组件不会重新渲染
    return prev.list.length === next.list.length
  }
)

function App() {
  const [count, setCount] = useState(0)
  const list = [1, 2, 3]
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      <Son list={list} />
    </div>
  )
}

export default App

useCallBack

作用:缓存一个函数,只在依赖项变化的时候才会更新引用值,经常与React.memo配合使用

未优化版本

我们上一节刚讲过,通过React.memo虽然可以在一定程度上避免子组件务必要的渲染,但是对于引用类型是无效的,那函数也是一个引用类型,且在父传子的时候也是可以把函数传给子组件的,这个时候就可以利用useCallBack和React.memo配合使用

import React, { useState } from 'react'

const Son = React.memo(() => {
  console.log('Son组件更新了')
  return <div>this is son</div>
})

function App() {
  const [count, setCount] = useState(0)
  const getName = () => {
    return 'this is app'
  }
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      {/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
      <Son getName={getName} />
    </div>
  )
}

export default App

优化版本

import React, { useState, useCallback } from 'react'

const Son = React.memo(() => {
  console.log('Son组件更新了')
  return <div>this is son</div>
})

function App() {
  const [count, setCount] = useState(0)
  // 缓存
  const getName = useCallback(() => {
    return 'this is app'
  }, [])

  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      {/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
      <Son getName={getName} />
    </div>
  )
}

export default App

useMemo

作用:缓存一个函数,该函数只在依赖项变化时才会重新执行此函数,在一些计算量很大的场景中非常有用

错误演示

import React, { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const getNums = () => {
    // 模拟昂贵计算
    const nums = new Array(10000).fill(0).map((item) => item)
    console.log('计算了')
    return nums
  }
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      {getNums()}
    </div>
  )
}

export default App

getNums内部有一个很大的计算,每次只有count发生变化,组件重新执行,计算也跟着重新执行,然后这个计算很明显只计算一次就可以了,这时候,就需要使用useMemo来进行优化

优化版本

import React, { useMemo, useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const getNums = useMemo(() => {
    // 模拟昂贵计算
    const nums = new Array(10000).fill(0).map((item) => item)
    console.log('计算了')
    return nums
  }, [])
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      {getNums}
    </div>
  )
}

export default App

✨虚拟DOM与diff算法

虚拟DOM是什么?

1 本质是Object类型的对象(一般对象)

2 虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。

3 虚拟DOM最终会被React转化为真实DOM,呈现在页面上。

Diff算法

gth } )

function App() { const [count, setCount] = useState(0) const list = [1, 2, 3] return (

<button onClick={() => setCount(count + 1)}>+{count}
) }

export default App


## useCallBack

> 作用:缓存一个函数,只在依赖项变化的时候才会更新引用值,经常与React.memo配合使用

**未优化版本**

我们上一节刚讲过,通过React.memo虽然可以在一定程度上避免子组件务必要的渲染,但是对于引用类型是无效的,那函数也是一个引用类型,且在父传子的时候也是可以把函数传给子组件的,这个时候就可以利用useCallBack和React.memo配合使用

```jsx
import React, { useState } from 'react'

const Son = React.memo(() => {
  console.log('Son组件更新了')
  return <div>this is son</div>
})

function App() {
  const [count, setCount] = useState(0)
  const getName = () => {
    return 'this is app'
  }
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      {/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
      <Son getName={getName} />
    </div>
  )
}

export default App

优化版本

import React, { useState, useCallback } from 'react'

const Son = React.memo(() => {
  console.log('Son组件更新了')
  return <div>this is son</div>
})

function App() {
  const [count, setCount] = useState(0)
  // 缓存
  const getName = useCallback(() => {
    return 'this is app'
  }, [])

  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      {/* 传给子组件一个函数引用 每次子组件都会跟着更新 */}
      <Son getName={getName} />
    </div>
  )
}

export default App

useMemo

作用:缓存一个函数,该函数只在依赖项变化时才会重新执行此函数,在一些计算量很大的场景中非常有用

错误演示

import React, { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const getNums = () => {
    // 模拟昂贵计算
    const nums = new Array(10000).fill(0).map((item) => item)
    console.log('计算了')
    return nums
  }
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      {getNums()}
    </div>
  )
}

export default App

getNums内部有一个很大的计算,每次只有count发生变化,组件重新执行,计算也跟着重新执行,然后这个计算很明显只计算一次就可以了,这时候,就需要使用useMemo来进行优化

优化版本

import React, { useMemo, useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const getNums = useMemo(() => {
    // 模拟昂贵计算
    const nums = new Array(10000).fill(0).map((item) => item)
    console.log('计算了')
    return nums
  }, [])
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      {getNums}
    </div>
  )
}

export default App

✨虚拟DOM与diff算法

虚拟DOM是什么?

1 本质是Object类型的对象(一般对象)

2 虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。

3 虚拟DOM最终会被React转化为真实DOM,呈现在页面上。

Diff算法

对比 初始虚拟 DOM 树 和 更新后的虚拟 DOM 树,找到不同之处,最终,只将不同的地方更新到页面中