React 基础

什么是 React

React 是一个用于构建用户界面的 JavaScript

用户界面:HTML页面(前端)

React 主要用来写HTML页面,或构建Web应用

如果从 MVC 的角度来看,React 仅仅是视图层(V),也就是只负责视图的渲染,而并非提供了 完整的 M 和 C 的功能。

React 起源于 Facebook 的内部项目,后又用来架设 Instagram 的网站,并于 2013 年 5 月开源

React 的特点

  • 声明式

  • 基于组件

  • 学习一次,随处使用

1、声明式

你只需要描述 UI(HTML)看起来是什么样,就跟写HTML一样

React 负责渲染 UI,并在数据变化时更新 UI

const jsx = <div className="app"> 
  <h1>Hello React! 动态变化数据:{count}</h1> </div> 

2、基于组件

组件是 React 最重要的内容

  • 组件表示页面中的部分内容
  • 组合、复用多个组件,可以实现完整的页面功能

3、学习一次,随处使用

使用 React 可以开发 Web 应用

  • 使用 React 可以开发移动端原生应用(react-native)
  • 使用 React 可以开发 VR(虚拟现实)应用(react 360)

React 的基本使用

React 的安装

安装命令:

#安装两个包 React 和 React-DOM
npm i react react-dom 
  • react 包是核心,提供创建元素、组件等功能
  • react-dom 包提供 DOM 相关功能等

React 的使用

1、引入 react 和 react-dom 两个 js 文件

<script src="./node_modules/react/umd/react.development.js"></script> 
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>

2、创建 React 元素

<div id="root"></div> 
<script>
    // (标签名 便签属性  标签中包含的内容 
    //第三个参数以后都是子节点,也可以创建其它标签元素
  const title = React.createElement('h1', null, 'Hello React') 
</script>

3、渲染 React 元素到页面中

<script>  
  //渲染对象 (对象名 渲染到根标签
  ReactDOM.render(title, document.getElementById('root')) 
</script> 

注意:使用页面预览需要在服务器端打开

方法说明

React.createElement() 说明(知道)

// 返回值:React元素 
// 第一个参数:要创建的React元素名称 
// 第二个参数:该React元素的属性 
// 第三个及其以后的参数:该React元素的子节点 
const el = React.createElement('h1', { title: '标题' }, 'Hello React') 

注意:特殊属性名称要根据报错提示使用,例如:class写成className

ReactDOM.render() 说明

// 第一个参数:要渲染的React元素 
// 第二个参数:DOM对象,用于指定渲染到页面中的位置
ReactDOM.render(el, document.getElementById('root')) 

React 脚手架的使用

React 脚手架意义

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

2.充分利用 Webpack、Babel、ESLint 等工具辅助项目开发。

3.零配置,无需手动配置繁琐的工具即可使用。

4.关注业务,而不是工具配置。

使用 React 脚手架初始化项目

  1. 初始化项目,命令:

    npx create-react-app my-app
    

  2. 启动项目,在项目根目录执行命令:

    npm start
    

npx 命令介绍

npm v5.2.0 引入的一条命令

  • 目的:提升包内提供的命令行工具的使用体验
  • 原来:先安装脚手架包,再使用这个包中提供的命令
  • 现在:无需安装脚手架包,就可以直接使用这个包提供的命令

补充说明

1.推荐使用:npx create-react-app my-app

2.npm init react-app my-app

3.yarn create react-app my-app

yarn 是 Facebook 发布的包管理器,可以看做是 npm 的替代品,功能与 npm 相同

  • yarn 具有快速、可靠和安全的特点
  • 初始化新项目:yarn init
  • 安装包: yarn add 包名称
  • 安装项目依赖项: yarn
  • 其他命令,请参考yarn文档

在脚手架中使用 React

  1. 导入 react 和 react-dom 两个包。
import React from 'react' 
import ReactDOM from 'react-dom' 
  1. 调用 React.createElement() 方法创建 react 元素。

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

JSX 的基本使用

createElement() 的问题

1.繁琐不简洁。

2.不直观,无法一眼看出所描述的结构。

3.不优雅,用户体验不爽。

JSX 简介

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

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

JSX 是 React 的核心内容。

使用步骤

  1. 使用 JSX 语法创建 react 元素

    // 使用 JSX 语法,创建 react 元素: 
    const title = <h1>Hello JSX</h1> 
    
  2. 使用 ReactDOM.render() 方法渲染 react 元素到页面中

    // 渲染创建好的React元素 
    ReactDOM.render(title, root) 
    

思考

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

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

  2. 需要使用 babel 编译处理后,才能在浏览器环境中使用。

  3. create-react-app 脚手架中已经默认有该配置,无需手动配置。

  4. 编译 JSX 语法的包为:@babel/preset-react 。

注意点

1.React元素的属性名使用驼峰命名法

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

3.没有子节点的React元素可以用 /> 结束 。

4.推荐:使用小括号包裹 JSX ,从而避免 JS 中的自动插入分号陷阱。

// 使用小括号包裹
JSX const dv = ( 
  <div>Helo JSX</div> ) 

嵌入 JS 表达式

  • 数据存储在JS中

  • 语法:**{ JavaScript表达式 }**

    表达式:所有有返回值的代码都是表达式

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

const name = 'Jack' const dv = ( 
  <div>你好,我叫:???</div> ) 
const name = 'Jack' const dv = ( 
  <div>你好,我叫:{name}</div> ) 

注意点

单大括号中可以使用任意的 JavaScript 表达式

JSX 自身也是 JS 表达式

注意:JS 中的对象是一个例外,一般只会出现在 style 属性中 ,不能在{}中出现语句(比如:if/for 等)

const h1 = <h1>我是JSX</h1> 
const dv = ( 
  <div>嵌入表达式:{h1}</div> ) 

JSX 的条件渲染

  • 场景:loading效果

  • 条件渲染:根据条件渲染特定的 JSX 结构

  • 可以使用if/else或三元运算符或逻辑与运算符来实现

    条件运算符

const loadData = () => { 
  if (isLoading) { 
    return <div>数据加载中,请稍后...</div> 
  } 
  return ( 
    <div>数据加载完成,此处显示加载后的数据</div> 
  ) 
} 

​ 三元运算符

let fn = ()=> {
    return true ? <div>1</div> : <div>没有数据</div>
}

​ 与或运算符

//用于控制是否渲染
let fn = ()=> {
    return true && <div>1</div>
}

JSX 的列表渲染

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

const songs = [ 
  {id: 1, name: '痴心绝对'}, 
  {id: 2, name: '像我这样的人'}, 
  {id: 3, name: '南山南'}
] 
const list = ( 
  <ul> 
    // { } 是JavaScript表达式   
    { songs.map(item => <li>{item.name}</li>) } 
  </ul> 
) 

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

注意:渲染列表时应该添加 key 属性,key 属性的值要保证唯一

  • 原则:map() 遍历谁,就给谁添加 key 属性
  • 注意:尽量避免使用索引号作为 key
const songs = [ 
  {id: 1, name: '痴心绝对'}, 
  {id: 2, name: '像我这样的人'}, 
  {id: 3, name: '南山南'}, ] 
const list = ( 
  <ul> 
    {songs.map(item => <li key={item.id}>{item.name}</li>)} 
  </ul> ) 

JSX 的样式处理

  1. 行内样式 —— style

    <h1 style={{ color: 'red', backgroundColor: 'skyblue' }}> 
      JSX的样式处理 </h1> 
    
  2. 类名 —— className(推荐)

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

总结

JSX

1.JSX 是React 的核心内容。

2.JSX 表示在JS代码中写HTML结构,是React声明式的体现。

3.使用 JSX 配合嵌入的 JS 表达式、条件渲染、列表渲染,可以描述任意 UI 结构。

4.推荐使用 className 的方式给JSX添加样式。

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

React组件基础

React 组件介绍

  • 组件是 React 的一等公民,使用 React 就是在用组件
  • 组件表示页面中的部分功能
  • 组合多个组件实现完整的页面功能
  • 特点:可复用、独立、可组合

React 组件的两种创建方式

  1. 使用函数创建组件
  2. 使用类创建组件

使用函数创建组件

  • 函数组件:使用 JS 的函数(或箭头函数)创建的组件
  • 约定1:函数名称必须以大写字母开头
  • 约定2:函数组件必须有返回值,表示该组件的结构
  • 如果返回值为 null,表示不渲染任何内容
function Hello() {
    return (
       <div>这是我的第一个函数组件!</div>
    )
}
  • 渲染函数组件:用函数名作为组件标签名
  • 组件标签可以是单标签也可以是双标签
function Hello() {
    return (
      <div>这是我的第一个函数组件!</div>
    )
}
ReactDOM.render(<Hello />, root)

使用JS中的函数创建的组件叫做:函数组件

  • 函数组件必须有返回值
  • 组件名称必须以大写字母开头, React 据此区分 组件 和 普通的
  • React 元素
  • 使用函数名作为组件标签名
function Hello() {
    return (
       <div>这是我的第一个函数组件!</div>
    )
}
ReactDOM.render(<Hello />, root)

使用类创建组件

类组件:使用 ES6 的 class 创建的组件 约定1:类名称也必须以大写字母开头 约定2:类组件应该继承 React.Component 父类,从而可以使用父类中提供的方法或属性 约定3:类组件必须提供 render() 方法 约定4:render() 方法必须有返回值,表示该组件的结构

class Hello extends React.Component {
    render() {
        return <div>Hello Class Component!</div>
    }
}
ReactDOM.render(<Hello />, root)

抽离为独立 JS 文件

思考:项目中的组件多了之后,该如何组织这些组件呢?

选择一:将所有组件放在同一个JS文件中

选择二:将每个组件放到单独的JS文件中

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

  1. 创建Hello.js
  2. 在 Hello.js 中导入React
  3. 创建组件(函数 或 类)
  4. 在 Hello.js 中导出该组件
  5. 在 index.js 中导入 Hello 组件
  6. 渲染组件
// Hello.js
import React from 'react'
class Hello extends React.Component {
    render() {
        return <div>Hello Class Component!</div>
    }
}
// 导出Hello组件
export default Hello
// index.js
import Hello from './Hello'
// 渲染导入的Hello组件
ReactDOM.render(<Hello />, root)

React 事件处理

  1. 事件绑定
  2. 事件对象

事件绑定

React 事件绑定语法与 DOM 事件语法相似

语法:on+事件名称={事件处理程序},比如:onClick={() => {}}

注意:React 事件采用驼峰命名法,比如:onMouseEnter、onFocus

在函数组件中绑定事件:

class App extends React.Component {
    handleClick() {
        console.log('单击事件触发了')
    }
    render() {
        return (
            <button onClick={this.handleClick}></button>
        )
    }
}
function App() {
    function handleClick() {
        console.log('单击事件触发了')
    }
    return (
        <button onClick={handleClick}>点我</button>
    )
}

事件对象

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

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

合成事件:兼容所有浏览器,无需担心跨浏览器兼容性问题

function handleClick(e) {
    e.preventDefault()
    console.log('事件对象', e)
}
<a onClick={handleClick}>点我,不会跳转页面</a>

带有参数如何获取事件对象

function handleClick(msg,e) {
    e.preventDefault()
    console.log('事件对象', e)
    console.log('数据', msg)
}
<a onClick={ (e) => {
    handleClick('实参',e)
}}>点我,不会跳转页面</a>

有状态组件和无状态组件

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

状态(state)即数据

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

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

比如计数器案例中,点击按钮让数值加 1 。0 和 1 就是不同时刻的状态,而由 0 变为 1 就表示状态发生了变化。状态变化后,UI 也要相应的更新。React 中想要实现该功能,就要使用有状态组件来完成。

组件中的 state 和 setState

  1. state的基本使用

  2. setState()修改状态

state的基本使用

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

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

class Hello extends React.Component { 
  constructor() { 
    super() 
    // 初始化state 
    this.state = { 
      count: 0 
    } 
  } 
  render() { 
    return ( 
      <div>有状态组件</div> 
    ) 
  } } 
class Hello extends React.Component { 
  // 简化语法 
  state= { 
    count: 0 
  } 
  render() { 
    return ( 
      <div>有状态组件</div> 
    ) 
  } 
} 

获取状态:this.state

class Hello extends React.Component { 
  // 简化语法 
  state= { 
    count: 0 
  } 
  render() { 
    return ( 
      <div>有状态组件,{this.state.count}</div> 
    ) 
  } } 

状态即数据

状态是私有的,只能在组件内部使用

通过 this.state 来获取状态

class Hello extends React.Component { 
  // 简化语法 
  state= { 
    count: 0 
  } 
  render() { 
    return ( 
      <div>有状态组件,{this.state.count}</div> 
    ) 
  }
 } 

setState()修改状态

状态是可变的

语法:this.setState({ 要修改的数据 })

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

setState() 作用:1. 修改 state 2. 更新UI 

思想:数据驱动视图

从 JSX 中抽离事件处理程序

JSX 中掺杂过多 JS 逻辑代码,会显得非常混乱

推荐:将逻辑抽离到单独的方法中,保证 JSX 结构清晰

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

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

事件绑定 this 指向

1. 箭头函数

2. Function.prototype.bind()

3. class 的实例方法

箭头函数

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

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

class Hello extends React.Component { 
  onIncrement() { 
    this.setState({ … }) 
} 
  render() { 
    // 箭头函数中的this指向外部环境,此处为:render()方法 
    return ( 
      <button onClick={() => this.onIncrement()}></button> 
    ) 
  }
}

Function.prototype.bind()

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

class Hello extends React.Component { 
  constructor() { 
    super() 
    this.onIncrement = this.onIncrement.bind(this) 
  } 
  // ...省略 onIncrement 
  render() { 
    return ( 
      <button onClick={this.onIncrement}></button> 
    ) 
  } 
} 

class 的实例方法

利用箭头函数形式的class实例方法 ,是直接将方法直接存储在实例

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

class Hello extends React.Component { 
  onIncrement = () => { 
    this.setState({ … }) 
  } 
  render() { 
    return ( 
      <button onClick={this.onIncrement}></button> 
    ) 
  }
 } 

总结

1.推荐:使用class的实例方法

2.箭头函数

3.bind

//推荐使用的方法
class Hello extends React.Component { 
  onIncrement = () => { 
    this.setState({ … }) 
  } 
  render() { 
    return ( 
      <button onClick={this.onIncrement}></button> 
    ) 
  }
 } 

表单处理

1. 受控组件

2. 非受控组件(DOM方式)

受控组件

HTML 中的表单元素是可输入的,也就是有自己的可变状态

而,React 中可变状态通常保存在 state 中,并且只能通过 setState() 方法来修改

HTML 中的表单元素是可输入的,也就是有自己的可变状态

而,React 中可变状态通常保存在 state 中,并且只能通过 setState() 方法来修改

React将 state 与表单元素值value绑定到一起,由 state 的值来控制表单元素的值

受控组件:其值受到 React 控制的表单元素

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

步骤:

  1. 在 state 中添加一个状态,作为表单元素的value值(控制表单元素值的来源)
  2. 给表单元素绑定 change 事件,将 表单元素的值 设置为 state 的值(控制表单元素值的变化)
state = { txt: '' } 
<input type="text" value={this.state.txt}
    onChange={e => this.setState({ txt: e.target.value })}
/> 

示例:

  1. 文本框、文本域、下拉框

  2. 复选框

示例总结:

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

  2. 复选框 操作checked属性

多表单元素优化:

问题:每个表单元素都有一个单独的事件处理程序处理太繁琐

优化:使用一个事件处理程序同时处理多个表单元素

多表单元素优化步骤:

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

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

3.在 change 事件处理程序中通过 [name] 来修改对应的state

<input  
  type="text" 
  name="txt" 
  value={this.state.txt}  
  onChange={this.handleForm} /> 
// 根据表单元素类型获取值 
const value = target.type === 'checkbox'  
    ? target.checked  
    : target.value 

// 根据name设置对应state this.setState({ 
  [name]: value }) 

非受控组件

ref获取 DOM 或组件

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

ref 的作用:获取 DOM 或组件

使用步骤:

  1. 调用 React.createRef() 方法创建一个 ref 对象 ,该对象不可复用
//标准形式
class App extends React.Component{
    constructor() { 
      super() 
      this.txtRef = React.createRef() 
    } 
}
//简写形式
class App extends React.Component{
    txtRef = React.createRef()
}
  1. 将创建好的 ref 对象添加到文本框中
<input type="text" ref={this.txtRef} /> 
  1. 通过 ref 对象获取到文本框的值
Console.log(this.txtRef.current.value) 

组件通讯

组件通讯简介

组件是独立且封闭的单元,默认情况下,只能使用组件自己的数据。在组件化过程中,我们将一个完整的功能 拆分成多个组件,以更好的完成整个应用的功能。而在这个过程中,多个组件之间不可避免的要共享某些数据 。为了实现这些功能,就需要打破组件的独立封闭性,让其与外界沟通。这个过程就是组件通讯。

组件的 props

组件是封闭的,要接收外部数据应该通过 props 来实现 props的作用:接收传递给组件的数据 传递数据:给组件标签添加属性 接收数据:函数组件通过参数props接收数据,类组件通过 this.props 接收数据

<Hello name="jack" age={19} /> 
function Hello(props) {
    console.log(props)
    return (
        <div>接收到数据:{props.name}</div>
    )
}
// --------------------------------------------------
class Hello extends React.Component {
    render() {
        return (
            <div>接收到的数据:{this.props.age}</div>
        )
    }
}

特点

  1. 可以给组件传递任意类型的数据
  2. props 是只读的对象,只能读取属性的值,无法修改对象
  3. 注意:使用类组件时,如果写了构造函数,应该将 props 传递给 super(),否则,无法在构造函数中获取到 props!
class Hello extends React.Component {
    constructor(props) {
        // 推荐将props传递给父类构造函数
        super(props)
    }
    render() {
        return <div>接收到的数据:{this.props.age}</div>
    }
}

组件通讯的三种方式

组件之间的通讯分为 3 种:

  1. 父组件 -> 子组件
  2. 子组件 -> 父组件
  3. 兄弟组件

父组件传递数据给子组件

  1. 父组件提供要传递的state数据
  2. 给子组件标签添加属性,值为 state 中的数据
  3. 子组件中通过 props 接收父组件中传递的数据
class Parent extends React.Component {
    state = { lastName: '王' }
    render() {
        return (
            <div>
                传递数据给子组件:<Child name={this.state.lastName} />
            </div>
        )
    }
}
function Child(props) {
    return <div>子组件接收到数据:{props.name}</div>
}

子组件传递数据给父组件

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

  1. 父组件提供一个回调函数(用于接收数据)
  2. 将该函数作为属性的值,传递给子组件
  3. 子组件通过 props 调用回调函数
  4. 将子组件的数据作为参数传递给回调函数
class Parent extends React.Component {
    getChildMsg = (msg) => {
        console.log('接收到子组件数据', msg)
    }
    render() {
        return (
            <div>
                子组件:<Child getMsg={this.getChildMsg} />
            </div>
        )
    }
}
class Child extends React.Component {
    state = { childMsg: 'React' }
    handleClick = () => {
        this.props.getMsg(this.state.childMsg)
    }
    return (
        <button onClick={this.handleClick}>点我,给父组件传递数据</button>
    )
}

兄弟组件

将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态 思想:状态提升 公共父组件职责:1. 提供共享状态 2. 提供操作共享状态的方法 要通讯的子组件只需通过 props 接收状态或操作状态的方法

Context

思考:App 组件要传递数据给 Child 组件,该如何处理? 处理方式:使用 props 一层层组件往下传递(繁琐)

思考: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>
    

总结

  1. 如果两个组件是远方亲戚(比如,嵌套多层)可以使用Context实现组件通讯
  2. Context提供了两个组件:Provider 和 Consumer
  3. Provider组件:用来提供数据
  4. Consumer组件:用来消费数据

props 深入

children 属性

children 属性:表示组件标签的子节点。当组件标签有子节点时,props 就会有该属性 children 属性与普通的props一样,值可以是任意值(文本、React元素、组件,甚至是函数)

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

props 校验

对于组件来说,props 是外来的,无法保证组件使用者传入什么格式的数据 如果传入的数据格式不对,可能会导致组件内部报错 关键问题:组件的使用者不知道明确的错误原因

// 小明创建的组件App
function App(props) {
    const arr = props.colors
    const lis = arr.map((item, index) => <li key={index}>{item.name}</li>)
    return (
        <ul>{lis}</ul>
    )
}
// 小红使用组件App
<App colors={19} />

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

App.propTypes = {
    colors: PropTypes.array
}

使用步骤

  1. 安装包 prop-types yarn add prop-types / npm i props-types
  2. 导入 prop-types 包
  3. 使用组件名.propTypes = {} 来给组件的props添加校验规则
  4. 校验规则通过 PropTypes 对象来指定
import PropTypes from 'prop-types'
    function App(props) {
        return (
            <h1>Hi, {props.colors}</h1>
        )
    }
    App.propTypes = {
    // 约定colors属性为array类型
    // 如果类型不对,则报出明确错误,便于分析错误原因
    colors: PropTypes.array
}

约束规则

  1. 常见类型:array、bool、func、number、object、string
  2. React元素类型:element
  3. 必填项:isRequired
  4. 特定结构的对象:shape({ })
// 常见类型
optionalFunc: PropTypes.func,
// 必选
requiredFunc: PropTypes.func.isRequired,
// 特定结构的对象
optionalObjectWithShape: PropTypes.shape({
    color: PropTypes.string,
    fontSize: PropTypes.number
})

props 的默认值

场景:分页组件 ->每页显示条数 作用:给 props 设置默认值,在未传入 props 时生效

function App(props) {
    return (
        <div>
            此处展示props的默认值:{props.pageSize}
        </div>
    )
}
// 设置默认值
App.defaultProps = {
    pageSize: 10
}
// 不传入pageSize属性
<App />

组件的生命周期

组件的生命周期概述

意义:组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等 组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程 生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数。 钩子函数的作用:为开发人员在不同阶段操作组件提供了时机。 只有 类组件 才有生命周期。

生命周期的三个阶段

  1. 每个阶段的执行时机
  2. 每个阶段钩子函数的执行顺序
  3. 每个阶段钩子函数的作用

创建时(挂载阶段)

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

更新时(更新阶段)

执行时机:1. setState() 2. forceUpdate() 3. 组件接收到新的props 说明:以上三者任意一种变化,组件就会重新渲染 执行顺序:

注意: forceUpdate() 是强制更新数据的方法

卸载时(卸载阶段)

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

在此期间会清楚用DOM注册的一些事件

钩子函数

创建时(挂载阶段)

  • constructor() 初始化state,给事件处理程序绑定this
  • render() 渲染UI,该阶段不能调用setState()
  • componentDidMount()组件挂载完成,发送请求,操作DOM

更新时(更新阶段)

  • render()
  • componentDidUpdate() 组件更新后执行,发送请求,操作DOM,这里的setState()必须要有结束条件

卸载时(卸载阶段)

  • componentWillUnmount() 组件销毁之后执行

完整版生命周期钩子函数

render-props和高阶组件

React组件复用概述

思考:如果两个组件中的部分功能相似或相同,该如何处理? 处理方式:复用相似的功能(联想函数封装) 复用什么?1. state 2. 操作state的方法 (组件状态逻辑 ) 两种方式:1. render props模式 2. 高阶组件(HOC) 注意:这两种方式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式(写法)

render props 模式

思路分析

思路:将要复用的state和操作state的方法封装到一个组件中 问题1:如何拿到该组件中复用的state? 在使用组件时,添加一个值为函数的prop,通过 函数参数 来获取(需要组件内部实现)

问题2:如何渲染任意的UI? 使用该函数的返回值作为要渲染的UI内容(需要组件内部实现)

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

使用步骤

  1. 创建Mouse组件,在组件中提供复用的状态逻辑代码(1. 状态 2. 操作状态的方法)
  2. 将要复用的状态作为 props.render(state) 方法的参数,暴露到组件外部
  3. 使用 props.render() 的返回值作为要渲染的内容
class Mouse extends React.Component {
    // … 省略state和操作state的方法
    render() {
        return this.props.render(this.state)
    }
}
<Mouse render={(mouse) => <p>鼠标当前位置 {mouse.x},{mouse.y}</p>}/>

演示Mouse组件的复用

Mouse组件负责:封装复用的状态逻辑代码(1. 状态 2. 操作状态的方法) 状态:鼠标坐标(x, y) 操作状态的方法:鼠标移动事件 传入的render prop负责:使用复用的状态来渲染UI结构

class Mouse extends React.Component {
    // … 省略state和操作state的方法
    render() {
        return this.props.render(this.state)
    }
}
<Mouse render={(mouse) => <p>鼠标当前位置 {mouse.x},{mouse.y}</p>}/>

children代替render属性

注意:并不是该模式叫 render props 就必须使用名为render的prop,实际上可以使用任意名称的prop 把prop是一个函数并且告诉组件要渲染什么内容的技术叫做:render props模式 推荐:使用 children 代替 render 属性

<Mouse>
    {({x, y}) => <p>鼠标的位置是 {x},{y}</p> }
</Mouse>
// 组件内部:
this.props.children(this.state)
// Context 中的用法:
<Consumer>
    {data => <span>data参数表示接收到的数据 -- {data}</span>}
</Consumer>

代码优化

  1. 推荐:给 render props 模式添加 props校验
  2. 应该在组件卸载时解除 mousemove 事件绑定
Mouse.propTypes = {
    chidlren: PropTypes.func.isRequired
}
componentWillUnmount() {
    window.removeEventListener('mousemove', this.handleMouseMove)
}

高阶组件

概述

目的:实现状态逻辑复用 采用 包装(装饰)模式 ,比如说:手机壳 手机:获取保护功能 手机壳 :提供保护功能 高阶组件就相当于手机壳,通过包装组件,增强组件功能

思路分析

高阶组件(HOC,Higher-Order Component)是一个函数,接收要包装的组件,返回增强后的组件 高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给 被包装组件 WrappedComponen

const EnhancedComponent = withHOC(WrappedComponent)
// 高阶组件内部创建的类组件:
class Mouse extends React.Component {
    render() {
        return <WrappedComponent {...this.state} />
    }
}

使用步骤

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

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

    function withMouse(WrappedComponent) {}
    
  3. 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回

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

function withMouse(WrappedComponent) {
    class Mouse extends React.Component {}
    return Mouse
}
// Mouse组件的render方法中:
return <WrappedComponent {...this.state} />
  1. 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中

    // 创建组件,Position为需要包装的组件
    const MousePosition = withMouse(Position)
    
    // 渲染组件,渲染包装之后的组件,在没有包装前的组件中里面是有this.props.X接收数据
    <MousePosition />
    

设置displayName

使用高阶组件存在的问题:得到的两个组件名称相同 原因:默认情况下,React使用组件名称作为 displayName 解决方式:为 高阶组件 设置 displayName 便于调试时区分不同的组件 displayName的作用:用于设置调试信息(React Developer Tools信息) 设置方式:

// 在高阶组件包装函数返回值 return Mouse 前,编写下如下代码
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`

function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}

传递props

问题:props丢失 原因:高阶组件没有往下传递props 解决方式:在包装组件里需要再传递一次,渲染 WrappedComponent 时,将 state 和 this.props 一起传递给组件 传递方式:

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

React 原理揭秘

setState() 的说明

更新数据

setState() 是异步更新数据的 注意:使用该语法时,后面的 setState() 不要依赖于前面的 setState() 可以多次调用 setState() ,只会触发一次重新渲染

this.state = { count: 1 }
this.setState({
    count: this.state.count + 1
})
console.log(this.state.count) // 1

推荐语法

推荐:需要依据上一次执行的操作来实现,使用 setState((state, props) => {}) 语法 参数state:表示最新的state 参数props:表示最新的props

this.setState((state, props) => {
return {
    count: state.count + 1
}
})
console.log(this.state.count) // 1

第二个参数

场景:在状态更新(页面完成重新渲染)后立即执行某个操作 语法: setState(updater[, callback])

this.setState(
    (state, props) => {},
    () => {console.log('这个回调函数会在状态更新后立即执行')}
)
this.setState(
    (state, props) => {},
    () => {
        document.title = '更新state后的标题:' + 			this.state.count
    }
)

JSX 语法的转化过程

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

组件更新机制

setState() 的两个作用: 1. 修改 state 2. 更新组件(UI) 过程:父组件重新渲染时,也会重新渲染子组件。但只会渲染当前组件子树(当前组件及其所有子组件)

组件性能优化

减轻 state

减轻 state:只存储跟组件渲染相关的数据(比如:count / 列表数据 / loading 等) 注意:不用做渲染的数据不要放在 state 中,比如定时器 id等 对于这种需要在多个方法中用到的数据,应该放在 this 中

class Hello extends Component {
    componentDidMount() {
    // timerId存储到this中,而不是state中
    this.timerId = setInterval(() => {}, 2000)
}
componentWillUnmount() {
    clearInterval(this.timerId)
}
render() { … }
}

避免不必要的重新渲染

组件更新机制:父组件更新会引起子组件也被更新,这种思路很清晰 问题:子组件没有任何变化时也会重新渲染 如何避免不必要的重新渲染呢? 解决方式:使用钩子函数 shouldComponentUpdate(nextProps, nextState) 作用:通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染 触发时机:更新阶段的钩子函数,组件重新渲染前执行 (shouldComponentUpdate -> render)

class Hello extends Component {
    shouldComponentUpdate(nextProps, nextState) {
    // 根据条件,决定是否重新渲染组件
    return false
}
render() {…}
}

纯组件

纯组件:PureComponent 与 React.Component 功能相似 区别:PureComponent 内部自动实现了 shouldComponentUpdate 钩子,不需要手动比较 原理:纯组件内部通过分别 对比 前后两次 props 和 state 的值,来决定是否重新渲染组件

class Hello extends React.PureComponent {
    render() {
        return (
            <div>纯组件</div>
        )
    }
}

纯组件的浅拷贝问题

简单数据类型

说明:纯组件内部的对比是 shallow compare(浅层对比) 对于值类型来说:比较两个值是否相同(直接赋值即可,没有坑)

let number = 0
let newNumber = number
newNumber = 2
console.log(number === newNumber) // false
state = { number: 0 }
    setState({
        number: Math.floor(Math.random() * 3)
    })
// PureComponent内部对比:
最新的state.number === 上一次的state.number // false,重新渲染组件

复杂数据类型

说明:纯组件内部的对比是 shallow compare(浅层对比) 对于引用类型来说:只比较对象的引用(地址)是否相同 注意:state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据!(示例)

// 正确!创建新数据
const newObj = {...state.obj, number: 2}
setState({ obj: newObj })
// 正确!创建新数据
// 不要用数组的push / unshift 等直接修改当前数组的的方法
// 而应该用 concat 或 slice 等这些返回新数组的方法
this.setState({
    list: [...this.state.list, {新数据}]
})

虚拟 DOM 和 Diff 算法

React 更新视图的思想是:只要 state 变化就重新渲染视图 特点:思路非常清晰 问题:组件中只有一个 DOM 元素需要更新时,也得把整个组件的内容重新渲染到页面中? 理想状态:部分更新,只更新变化的地方。 问题:React 是如何做到部分更新的? 不是 虚拟 DOM 配合 Diff 算法

虚拟 DOM:本质上就是一个 JS 对象,用来描述你希望在屏幕上看到的内容(UI)。

执行过程

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

代码演示

组件 render() 调用后,根据 状态 和 JSX结构 生成虚拟DOM对象 示例中,只更新 p 元素的文本节点内容

{
    type: 'div',
    props: {
        children: [
            { type: 'h1', props: {children: '随机数'} },
            { type: 'p', props: {children: 0} }
        ]
    }
}
// ...省略其他结构
{ type: 'p', props: {children: 2} }

React 路由基础

React路由介绍

现代的前端应用大多都是 SPA(单页应用程序),也就是只有一个 HTML 页面的应用程序。因为它的用户体 验更好、对服务器的压力更小,所以更受欢迎。为了有效的使用单个页面来管理原来多页面的功能,前端路由 应运而生。

  • 前端路由的功能:让用户从一个视图(页面)导航到另一个视图(页面)
  • 前端路由是一套映射规则,在React中,是 URL路径 与 组件 的对应关系
  • 使用React路由简单来说,就是配置 路径和组件(配对)

路由的基本使用

使用步骤

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

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

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

    <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)
  • (推荐)BrowserRouter:使用 H5 的 history API 实现(localhost:3000/first)

Link 组件:用于指定导航链接(a 标签)

// to属性:浏览器地址栏中的pathname(location.pathname)
<Link to="/first">页面一</Link>

Route 组件:指定路由展示组件相关信息

// path属性:路由规则
// component属性:展示的组件
// Route组件写在哪,渲染出来的组件就展示在哪
<Route path="/first" component={First}></Route>

路由的执行过程

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

编程式导航

场景:点击登录按钮,登录成功后,通过代码跳转到后台首页,如何实现?

编程式导航:通过 JS 代码来实现页面跳转

history 是 React 路由提供的,用于获取浏览器历史记录的相关信息

push(path):跳转到某个页面,参数 path 表示要跳转的路径

go(n): 前进或后退到某个页面,参数 n 表示前进或后退页面数量(比如:-1 表示后退到上一页)

class Login extends Component {
    handleLogin = () => {
    // ...
    this.props.history.push('/home')
    }
    render() {...省略其他代码}
}

默认路由

问题:现在的路由都是点击导航菜单后展示的,如何在进入页面的时候就展示呢?

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

默认路由path为:/

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

匹配模式

模糊匹配模式

问题:当 Link组件的 to 属性值为 “/login”时,为什么 默认路由 也被匹配成功?

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

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

<Link to="/login">登录页面</Link>
<Route path="/" component={Home} /> 匹配成功
// path 代表Route组件的path属性
// pathname 代表Link组件的to属性(也就是 location.pathname)
path 能够匹配的pathname
/ 所有 pathname
/first /first 或 /first/a 或 /first/a/b/…

精确匹配

问题:默认路由任何情况下都会展示,如何避免这种问题?

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

精确匹配:只有当 path 和 pathname 完全匹配时才会展示该路由

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

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