React基础(一)

本文详细介绍了React的基础知识,包括React的声明式编程、组件化、JSX使用、组件生命周期、状态管理和事件处理。通过学习,你可以掌握React的基本概念和实践技巧,包括如何创建元素、使用React脚手架、理解和应用JSX、处理组件状态以及管理组件间的通信。文章还探讨了React的渲染优化,如虚拟DOM和Diff算法,帮助开发者理解React的工作原理。

React 学习

基本介绍

A JavaScript library for building user interfaces —— React 是一个 JS 库,用来构建用户界面(再简单点就是写 HTML,构建 web 应用,这是指 PC 端,react 也可以用于很多别的地方) ,从 MVC 的角度来看,相当于 视图层 V(View) 的内容。

特点

  • 声明式(Declarative)
    • 你只需要描述 UI(HTML)看起来是什么样,就跟写 HTML 一样
    • React 负责渲染 UI,并在数据变化时更新 UI
  • 基于组件(Component-Based)
  • 一次学习,随处使用(Learn Once, Write Anywhere) —— 可以用于手机端开发,也可以用于 VR

基本使用

  1. 安装包
npm i react react-dom

react 包是核心,提供创建元素、组件等功能,react-dom 包提供 DOM 相关功能等,同样的 react 还可以用于移动端,这时候就需要安装一个 react-native 这个包可以把创建的元素 渲染成移动端识别的元素

  1. 引入包

如果是直接在 html 页面中,那么就直接如下引入就行了(注意:引入顺序,react 在前,react-dom 在后)

<!-- 引入安装的包 注意引入包的顺序 -->
<script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>

如果是在脚手架中,可以模块化的方式引入

import React from "react";
import ReactDOM from "react-dom";
  1. 创建元素
// 创建 react 元素,最简单的一个元素
const h1 = React.createElement("h1");
// createElement方法有三个参数
// 参数1:表示要创建什么元素,就是 HTML 标签名称
// 参数2:表示元素自身属性,如果没有就传 null,如果要指定元素自身的属性,就传递一个对象({}) class ==> className  for ==> htmlFor
// 参数3:表示元素的子节点(文本、元素节点)
//  如果是文本节点,就直接传递 字符串。
//  如果是元素节点,就继续调用 React.createElement() 方法,创建新的React元素节点
const h1 = React.createElement("div", null, "Hello React");
const h1 = React.createElement(
  "h1",
  {
    id: "title",
    className: "cls",
    htmlFor: "d"
  },
  "Hello React",
  "test 文本节点",
  React.createElement("span", null, "这是一个span")
);
  1. 渲染元素
// 渲染 react 元素
// 参数1:表示要渲染哪个元素
// 参数2:元素渲染到哪个位置去,这里可以使用各种获取DOM的方式
// 比如: document.querySelector("#root")
ReactDOM.render(h1, document.getElementById("root"));

Tips:上述这种方法很明显的在创建元素上显得很麻烦,因此这个我们作为了解就行了,后面我们会使用 react 的脚手架,在脚手架中使用 JSX 来创建 react 元素

React 脚手架

脚手架的意义

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

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

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

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

脚手架的使用

  1. 安装包
# npm create-react-app 项目名称
npx create-react-app my-app

npx 命令介绍 —— 简化使用脚手架初始化项目的流程

npm v5.2.0 引入的一条命令

目的:提升包内提供的命令行工具的使用体验
原来:先安装脚手架包,再使用这个包中提供的命令
现在:无需安装脚手架包,就可以直接使用这个包提供的命令 (npx 命令执行的时候回临时帮我们把包下载到一个位置,然后执行包中的命令来帮我们创建脚手架,创建完成后会把这个包删除)

接下来的步骤就和上面的一样了,都是导入包 —> 创建 react 元素 —> 渲染 react 元素

JSX

JavaScript XML(HTML),也就是在 JS 中书写 HTMl 格式的代码

之前也说了用 react.createElement()创建 react 元素的方式很繁琐、不直观并且效率不高,因此我们就可以用 JSX 来创建 react 元素,JSX 这语法浏览器是不识别的,是经过编译后才让浏览器运行的,中间经过了 @babel/preset-react这个包的编译。

JSX 的基本使用

  • 1 导入 react 和 react-dom

  • 2 使用 JSX 语法创建 React 元素

  • JSX 就跟写 HTML 一样

  • 3 渲染创建好的 React 元素

import React from "react";
import ReactDOM from "react-dom";

const h1 = <h1>hello react</h1>;

ReactDOM.render(h1, document.getElementById("root"));

JSX 语法的注意点

  • JSX 元素的属性名推荐使用:驼峰命名法
    • class ===> className
  • 如果元素没有子节点,可以使用 单标签 方式来结束
    • 比如:<div />
  • 推荐使用 () 来包裹 JSX,从而避免 JS 中自动插入分号机制

JSX 中使用 JavaScript 表达式

数据存储在 JS 中

语法:{ JavaScript 表达式 }

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

import React from "react";
import ReactDOM from "react-dom";

const name = "react";

const h1 = <h1>hello {name}</h1>;

ReactDOM.render(h1, document.getElementById("root"));

注意

  • 单大括号中可以使用任意的 JavaScript 表达式
  • JSX 自身也是 JS 表达式
  • 注意:JS 中的对象是一个例外,一般只会出现在 style 属性中
  • 注意:不能在{}中出现语句(比如:if/for 等)
const span = <span>react</span>;
const h1 = <h1>hello {span}</h1>;
ReactDOM.render(h1, document.getElementById("root"));

条件渲染

  1. 使用 if/esle 来实现
const loadData = () => {
  if (isLoading) {
    return <div>loading...</div>;
  }

  return <div>加载完成后的列表结构</div>;
};

const h1 = <div>{loadData()}</div>;
  1. 使用三元表达式
const loadData = () => {
  return isLoading ? <div>loading...</div> : <div>加载完成后的列表结构</div>;
};
  1. 逻辑运算符 &&
const loadData = () => {
  return isLoading && <div>loading...</div>;
};

列表渲染

  • 使用数组的 map 方法来进行列表渲染
  • 需要给被遍历生成的元素添加 key 属性,key 应该是唯一的。尽量避免使用 index 作为索引号。
  • 剩下的就是 JS 中 map 方法的使用了。

因为在 { } 中只能放表达式,因此 for 是不能使用的,因为这些是语句

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

这里为什么用一个 key 是为了让渲染的性能更好,如果使用过 Vue 的 v-for 指令的,应该可以理解这一块,两边是一样的。

添加样式

  • 行内样式(style) 不推荐
const h1 = (
  <h1 style={{ color: "red", fontSize: 30, backgroundColor: "hotpink" }}>
    我变大了
  </h1>
);
  • className 类名 — 推荐!!!
const h1 = <h1 className="pink">我变大了</h1>;

组件基础

函数组件

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

function Hello() {
  return <h1>这是一个函数组件</h1>;
}
ReactDOM.render(<Hello />, document.getElementById("root"));

注意点:

  • 函数必须有返回值
    • 返回值可以为 null,表示不渲染任何内容
    • 如果想要渲染内容,一般就是返回 JSX(比如上面)
  • 组件名称必须以大写字母开头
    • 用来区分普通的 react 元素 和 react 组件 (默认小写字母开头的都是 react 元素)
  • 使用函数名称作为组件的标签名称来渲染
    • ReactDOM.render(<Hello />, root)

类组件(class)

通过 ES6 中的 class 创建的组件,叫做 类组件

函数组件中的注意点都适用于类组件,另外还有如下注意点

  • 类组件必须得继承自 React.Component 父类
  • 类组件中必须提供一个 render 方法,通过 render 方法的返回值来指定要渲染的内容
class Hello extends React.Component {
  render() {
    return <h1>这是一个类组件</h1>;
  }
}

ReactDOM.render(<Hello />, document.getElementById("root"));

在将组件抽离到一个单独的 js 文件中的时候,不管是函数组件还是类组件,或者使用 JSX ,都需要导入 React!!!

import React from "react";

// 创建 class 组件
// class Hello1 extends React.Component {
//   render() {
//     return <div>这是一个独立的组件</div>
//   }
// }

// 创建函数组件
// JSX -> React.createElement()
const Hello1 = () => <div>这是一个独立的组件</div>;

// 导出组件
export default Hello1;

哪里需要使用到这个组价可以直接导入就行

import Hello1 from 文件路径

事件处理

  1. React 事件绑定语法与 DOM 事件语法相似
  2. 语法:on+事件名称={事件处理程序},比如:onClick={() => {}}
  3. 注意:React 事件采用驼峰命名法,比如:onMouseEnter、onFocus

函数组件中使用

在函数组件中使用事件对象,可以在事件处理程序中用 一个形参 e 来接受

// function handleClick() {
//     console.log('click')
// }

const handleClick = () => console.log('click')
// const handleClick = (e) => console.log(e)

function Hello() {
    return (
        <button onClick={handleClick}>点我呀</button>
		<button onClick={() => console.log('click')}>点我呀</button>
    )
}

类组件中使用

在类组件中使用事件对象,可以在事件处理程序中用 一个形参 e 来接受

class Hello extends React.Component {
  handleClick(e) {
    console.log("click");
    console.log(e);
  }
  render() {
    return <button onClick={this.handleClick}>点我呀</button>;
  }
}

事件对象注意点

  • React 中的事件对象是一个 合成事件。
    • 合成事件:兼容所有浏览器,无需担心跨浏览器兼容性问题
  • 使用方式,与原生 DOM 中的使用方式相同。

有状态组件和无状态组件

  • 有状态组件:class(类)组件
    • 职责(什么时候使用):负责更新 UI(页面),也就是如果页面中的内容,需要变化
    • 状态(state)即数据
  • 无状态组件:函数组件
    • 职责:负责展示内容

State 和 setState

状态(state)即数据,是组件内部的私有数据,只能在组件内部使用,state 的值是对象,表示一个组件中可以有多个数据

初始化状态

class Hello extends React.Component {
  constructor() {
    super();
    // 状态初始化
    this.state = {
      count: 0
    };
  }

  // 简化语法:
  // state = {
  //   count: 66
  // }

  // 在 JSX 中使用状态
  render() {
    return <div>计数器:{this.state.count}</div>;
  }
}

setState()修改状态

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

下面是一个点击按钮后让数字 +1 的例子

class Hello extends React.Component {
  state = {
    count: 0
  };
  render() {
    return (
      <div>
        <span>{this.state.count}</span>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          点我呀
        </button>
      </div>
    );
  }
}

说明: 可能这里有人会想到用 this.state.count++这样的操作,但是上面说过了,不能直接去修改 state 中的值,因为 ++ 会直接修改原值

对于上面的代码,简单的情况可以这样写,但是当逻辑复杂后,代码这样写就会难以理解阅读,因此我们会把函数处理程度单独抽离出来

class Hello extends React.Component {
  state = {
    count: 0
  };
  handleClick() {
    console.log(this);
  }
  render() {
    return (
      <div>
        <span>{this.state.count}</span>
        <button onClick={this.handleClick}>点我呀</button>
      </div>
    );
  }
}

我们把代码改写成这样后,会发现this 居然是一个 undefined 这里我们就需要来讲一下 react 中 this 指向的问题了, 因为你必须谨慎对待 JSX 回调函数中的 this,在 JavaScript 中,class 的方法默认不会绑定 this (这是官网原话),下面这篇文章解释可以参考一下

解决上述 this 指向的问题,有如下三种方法

方法一:

使用一个箭头函数,然后在这个箭头函数中调用一下函数处理程序,因为箭头函数时没有 this 指向的,箭头函数的 this 是指向外部环境(函数)的 this

因为在 render 函数中是有 this 的,指向的是当前实例,因此箭头函数中的 this 就指向了当前实例,这也就导致 handleClick 中的 this 指向的是调用这个函数的 this ,也就是 this.handleClick() 的 this ,经过这一系列的 this 传递,最后我们就可以在 handleClick 中获取到 this,然后就可以去调用 setState()方法来修改值

<button onClick={() => this.handleClick()}>点我呀</button>

方法二:

使用 bind() 方法来改变 this 的指向 ,让 事件处理程序一开始就指向了当前实例

constructor 比 render 先执行,因此我们可以在 constructor 中使用 bind() 把 this 指向绑定了,并且把调用 bind() 后返回的新函数赋值给 this.handleClick111, 这样 我们就可以直接在 onClick 中使用 this.handleClick111,注意,onClick 绑定的函数是赋了新值的,这种方法同样解决了 this 的指向问题

class Hello extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 0
    };
    this.handleClick111 = this.handleClick.bind(this);
  }
  handleClick() {
    console.log(this);
  }
  render() {
    return (
      <div>
        <span>{this.state.count}</span>
        <button onClick={this.handleClick111}>点我呀</button>
      </div>
    );
  }
}

方法三:

利用箭头函数形式的 class 实例方法(推荐)

class Hello extends React.Component {
  state = {
    count: 0
  };
  handleClick = () => {
    console.log(this);
  };
  render() {
    return (
      <div>
        <span>{this.state.count}</span>
        <button onClick={this.handleClick}>点我呀</button>
      </div>
    );
  }
}

表单处理

受控组件

其值收到 react 控制的表单元素,叫做受控组件

  • HTML 中的表单元素是可输入的,也就是有自己的可变状态
  • 而,React 中可变状态通常保存在 state 中,并且只能通过 setState() 方法来修改
  • React 将 state 与表单元素值 value 绑定到一起,由 state 的值来控制表单元素的值
  • 受控组件:其值受到 React 控制的表单元素

步骤:

  1. 在 state 中添加一个状态,作为表单元素的 value 值(控制表单元素值的来源)
state = { txt: '' }

<input type="text" value={this.state.txt} />
  1. 给表单元素绑定 change 事件,将 表单元素的值 设置为 state 的值(控制表单元素值的变化)
<input
  type="text"
  value={this.state.txt}
  onChange={e => this.setState({ txt: e.target.value })}
/>

如果有多个表单元素就需要提供多个表单处理函数,这样会很繁琐

受控组件优化

  handleChange = e => {
    const target = e.target
    // 针对于表单元素进行处理:
    const value = target.type === 'checkbox' ? target.checked : target.value
    const name = target.name

    this.setState({
      [name]: value
    })
  }
  1. 在 state 中添加表单元素的状态

  2. 将 state 设置为每个表单元素的 value 值

  3. 给 表单元素 绑定 change 事件

  4. 创建 handleChange 事件处理程序,用来统一处理表单项的值

  5. 给每一个表单元素添加 name 属性,name 属性的值为:当前对应的状态名称

  6. 在 handleChange 这个统一的事件处理程序中,通过 e.target.name 来获取到当前要更新的状态名称

  7. 使用 ES6 中的属性名表达式,来更新状态即可

注.意:因为 checkbox 复选框,操作的是 checked 属性(也就是是否选中),所以,需要对 checkbox 进行特殊的处理。

非受控组件

  • 说明:借助于 ref,使用原生 DOM 方式来获取表单元素值
  • ref 的作用:获取 DOM 或组件

使用方式

// 类 组件
class Hello extends React.Component {
    constructor() {
        super()
        //  1、调用 React.createRef() 方法创建一个 ref 对象
        this.txtRef = React.createRef()
    }

    handleClick = () => {
        // 3、通过 ref 对象获取到文本框的值
        console.log(this.txtRef.current.value)
    }
    render() {
        return (
            <div>
            	// 2、将创建好的 ref 对象添加到文本框中
            	<input type="text" ref={this.txtRef} />
                <button onClick={this.handleClick}>click</button>
            </div>
        )
    }
}

// 创建函数组件
// 注意:不要在函数组件中使用 this,直接按照 普通函数的方式来使用即可
const Hello = () => {
  // 1 创建ref
  const txtRef = React.createRef()

  // 事件处理程序:
  const handleClick = () => {
    // 3 通过 ref 来获取文本框的值
    console.log(txtRef.current.value)
  }

  return (
    <div>
      {/* 2 将 ref 和 文本框绑定到一起 */}
      <input type="text" ref={txtRef} />

      <button onClick={handleClick}>获取文本框的值</button>
    </div>
  )
}

组件通讯(传值)

组件是独立封闭的单元,默认情况下,只能使用组件自己的数据。也就是说,当前组件中的state只能在当前组件中使用,但是不可避免的组件间要共享某些数据。

注意:一般数据属于哪个组件,那么就在哪个组件中修改该数据

props

作用:接收到传递给组件中的属性

  • 在函数组件中如何获取到 props? 通过函数的参数
  • 在 类组件 中如何获取到 props? 通过 this.props 来获取
  • props 是一个对象!!!
  • 特点:只读!!!( 只能读取 props 对象中的属性,而不能修改增加 props 对象中的属性 )
  • 可以给组件传递任何类型的数据。
// 类 组件
class Hello extends React.Component {
    render() {
        return (
            <div>
                props中的name: {this.props.name}
            </div>
        )
    }
}

// 函数组件
const Hello = (props) => {
    return (
        <div>
            props中的name: {props.name}
        </div>
    )
}

ReactDOM.render(<Hello name="felix" />, document.getElementById('root'))
  • 注意:如果在 class 组件中,手动添加了 constructor ,那么,就应该通过参数获取到 props, 然后传递给 super,这样,才能够在 constructor 中,获取到 props!!!
constructor(props) {
    super(props)
    console.log(props)
}

children属性

  • 作用:获取组件标签的子节点
  • 获取方式: props.children / this.props.children
  • children 与普通的 props 属性相同,可以是任意值。
<Hello>
  我是子节点 -> 这就是 children 属性的内容。
</Hello>

props 校验 (官方文档 --> 高级指引 --> 使用 PropTypes 检查类型

  • 场景:给组件添加 props 校验,来增强组件的健壮性。

    • 约定:封装公共组件的时候,都添加 props 校验
  • 1 安装:yarn add prop-types

  • 2 导入 import PropTypes from 'prop-types'

  • 3 给组件名称添加 propTypes 属性,值是一个对象

  • 4 对象的键就是要校验的 props 名称,值是 PropTypes.array 等,从PropTypes中获取到的校验规则

const Parent = () => { ... }

// 2 给组件添加 props 校验
Parent.propTypes = {
  // 规定 colors 属性的类型为:数组(array),如果将来使用组件的时候,传入的 colors 属性类型不是 array ,就会通过警告来告诉使用者。
  colors: PropTypes.array,

  gender: PropTypes.oneOf(['male', 'female']).isRequired
}

props默认值

可以通过 组件名.defaultProps = {} 来给组件添加 props 的默认值(这个不需要装包,组件自带的属性)

const Parent = () => { ... }

// 添加 props 的默认值:
Parent.defaultProps = {
  gender: 'male'
}

父传子

  • 1 父组件中提供状态(数据)
  • 2 在子组件标签上添加属性,值为 父组件中的状态
  • 3 子组件中通过 props 来接收父组件中传递过来的数据
class Parent extends React.Component {
    state = {
        msg: '这是父组件的msg'
    }
    render() {
        return (
            <div>
                <Child msg={this.state.msg}></Child>
            </div>
        )
    }
}
// 这里也可以用class组件,我这里是为了两者一起使用才这样写的
const Child = (props) => {
    return (
        <div>
            接受到的父组件的数据: {props.msg}
        </div>
    )
}

子传父

思路:父组件提供一个事件(函数),让子组件调用;子组件调用的时候,将数据作为参数的传递,父组件中通过事件(函数)的参数,就拿到子组件中的数据了。

  • 1 父组件提供事件

  • 2 将事件通过props传递给子组件

  • 3 子组件中通过props接收到父组件中传递过来的事件

  • 4 子组件调用该事件,将数据作为参数传递

  • 注意点:父组件提供的方法中 this 执行问题。

    • 为什么会有这个问题?因为这个方法不是父组件自己调用的,是由其他组件调用的,所以,需要处理this指向。(使用下面的箭头函数方式就不会有这个问题)
class Parent extends React.Component {
    getSonMsg = (msg) => {
        console.log(msg, this)
    }
    // 这样会有this指向问题,this指向的props
    // getSonMsg(msg){
    //     console.log(msg, this)
    // }
    render() {
        return (
            <div>
                <Child getSonMsg={this.getSonMsg}></Child>
            </div>
        )
    }
}

const Child = (props) => {
    const state = {
        msg: '这是子组件的msg'
    }
    const sendToParent = () => {
        props.getSonMsg(state.msg)
    }
    return (
        <div>
            <button onClick={sendToParent}>发送msg给父组件</button>
        </div>
    )
}

兄弟组件传值

思路:状态提升,也就是:将两个兄弟组件之间的共享数据,放在父组件中。

  • 父组件的职责:1 提供共享数据(state) 2 提供修改状态的方法
  • 例子:如果 子组件2 要传递数据给 子组件1
  • 子组件1:只要通过 props 接收到父组件中传递过来的数据(父 -> 子)
  • 子组件2:调用父组件中修改状态的方法(子 -> 父)
    • 但是,需要先通过 props 获取到父组件中传递过来的方法

兄弟组件传值其实就是 父传子 和 子传父 的结合

class Parent extends React.Component {
    state = {
        msg: "共享的数据"
    }

    // 父组件提供的修改数据的方法
    modifyMsg = (data) => {
        console.log(data)
        this.setState({
            msg: data
        })
    }

    render() {
        return (
            <div>
                <Child1 msg={this.state.msg} />
                <Child2 modifyMsg={this.modifyMsg} />
            </div>
        )
    }
}


class Child1 extends React.Component {
    render() {
        return (
            <h2>
                {/* 子组件1显示父组件的数据 */}
                {this.props.msg}
            </h2>
        )
    }
}

class Child2 extends React.Component {
    // 子组件2调用父组件传递的修改数据的方法
    handleClick = () => {
        this.props.modifyMsg("子组件2修改了数据")
    }
    render() {
        return (
            <div>
                <button onClick={this.handleClick}>修改数据</button>
            </div>
        )
    }
}

如果学过Vue就会发现,这两者其实方式是一样的,基本上大家都是用一种相同的方式来实现组件通讯,这样就节省了我们的学习成本部

跨组件传值(context)

这部分了解即可,我们之后会有别的方式来实现状态管理类似于Vue中的Vuex

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

Parent 中包含组件 Child1 ,Child1 中包含 Child2 ,Child2 中包含 Child3,要将Parent中的msg传递给Child3

// 顶层父组件中
class Parent extends React.Component {
    state = {
        msg: "嵌套传递的msg"
    }

    render() {
        return (
            <Provider value={this.state.msg}>
                <div>
                    <Child1 />
                </div>
            </Provider>
        )
    }
}

// Child3 
class Child3 extends React.Component {
    render() {
        return (
            <div>
            	{/* Consumer中是一个函数,通过data接受Provider传递的value */}
                <Consumer>{data => <p>接收到的数据为:{data}</p>}</Consumer>
            </div>
        )
    }
}

组件生命周期

整体来看一下生命周期

在这里插入图片描述

注意:只有 class 组件才有生命周期。

总体可以分为三个阶段

  • 挂载阶段
  • 更新阶段
  • 卸载阶段

挂载阶段

钩子函数触发时机作用
constructor创建组件时,最先执行1. 初始化state
2. 为事件处理程序绑定this
render每次组件渲染都会触发渲染UI(注意: 不能调用setState() )
componentDidMount组件挂载(完成DOM渲染)后1. 发送网络请求
2. DOM操作

执行顺序 : constructor --> render — > componentDidMount

为什么render中不能调用setState() ?

setState() 两个作用 1、更新数据 2、更新UI(触发render方法) ,如果在render中调用setState() 就会导致 死循环

class Hello extends React.Component {
    // 最早执行:
    // 1 初始化state
    // 2 给事件处理程序绑定 this
    constructor(props) {
        super(props)
        console.log("constructor 触发了")
    }

    // 作用:渲染 UI,负责将 JSX 渲染到页面中
  	// 注意:不要在 render 方法中调用 setState() 方法,否则,会造成死循环!
    render() {
        console.log("render 触发了")
        return (
            <div>
                <h1>Hello world</h1>
            </div>
        )
    }
  	// 作用1:可以用来在进入页面时(该组件渲染时),发送ajax请求
  	// 作用2:可以操作DOM(因为 render 已经将 JSX 渲染到页面中了)
    componentDidMount() {
        console.log('componentDidMount 触发了')
        document.getElementsByTagName('h1')[0].style.color = 'red'
    }
}

更新阶段

钩子函数触发时机作用
render每次组件渲染都会触发渲染UI(与 挂在阶段 是同一个render)
componentDidUpdate组件更新(完成DOM渲染)后1 发送网络请求
2 DOM操作
注意:如果要setState() 必须放在一个if条件中

render和挂载阶段的render是一样的,在 componentDidUpdate 钩子函数中,其实有两个参数prevProps 和 prevState 表示上一个props和上一个state,一般可以根据现有的一些数据来进行限定执行 setState() ,不过这个方法知道即可

class Hello extends React.Component {
    state = {
        count: 0
    }
    handleClick = () => {
        this.setState({
            count: this.state.count + 1
        })
        // 强制组件更新(知道即可):
        // this.forceUpdate()
    }
    render() {
        console.log("render 触发了")
        return (
            <div>
                <h1>计数器:{this.state.count}</h1>
                <button onClick={this.handleClick}>修改数据</button>
            </div>
        )
    }
    //  比如:可以对比更新前后的 props 是否相同,或者 对比更新前后的 状态是否相同
    componentDidUpdate(prevProps, prevState) {
        console.log("上一次", prevProps, prevState)
        console.log("componentDidUpdate 触发了")
        console.log("当前", this.props, this.state)
    }
}
  • 导致组件更新的三种情况:
    • 1 setState()
    • 2 组件接收到新的props (可以参考兄弟组件传值)
    • 3 forceUpdate()
  • 注意:不管是 函数组件 还是 类组件,只要接收到新的 props ,那么,组件都会重新渲染

卸载阶段

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

  • componentWillUnmount
  • 作用:执行清理工作,比如:清理定时器、给window绑定的事件等,我们手动开启的操作
钩子函数触发时机作用
componentWillUnmount组件卸载(从页面中消失)执行清理工作(比如:清理定时器等)

一般我们自己添加的行为,比如:添加的定时器、添加的一些监听事件等,在组件卸载的时候都需要自己手动进行清理移除

class Hello extends React.Component {
    state = {
        count: 0
    }
    handleClick = () => {
        this.setState({
            count: this.state.count + 1
        })
    }
    render() {
        return (
            <div>
                {
                    this.state.count >= 1 ?
                        <p>组件已经销毁了</p>
                        : <Child count={this.state.count} />
                }
                <button onClick={this.handleClick}>修改数据</button>
            </div>
        )
    }
}

class Child extends React.Component {
    handleResize = () => {
        console.log("窗口变小了")
    }

    componentDidMount() {
        window.addEventListener('resize', this.handleResize)
        this.timeId = setInterval(() => {
            console.log("定时器触发了")
        }, 1000);
    }
    render() {
        console.log("子组件 render 触发了")
        return (
            <h1>计数器:{this.props.count}</h1>
        )
    }
    componentWillUnmount() {
        console.log("componentDidMount 触发了")
        window.removeEventListener('resize', this.handleResize)
        clearInterval(this.timeId)
    }
}

Render Props 模式(重要)

render-props 模式 作用:实现状态逻辑复用

复用组件的职责

  • 提供了state
  • 提供了操作状态的方法

注意:复用组件仅仅负责状态逻辑复用,不会指定要渲染的内容。要渲染什么内容,就在复用组件的标签中通过 render 属性的返回值指定 (这个名字可以随意取,可以叫render也可以叫别的)

把prop是一个函数并且告诉组件要渲染什么内容的技术叫做:render props模式

/**
 * 这个是需要复用的组件
 * 提供state和操作state的方法
 */
class Mouse extends React.Component {
    state = {
        x: 0,
        y: 0
    }
    handleMouseMove = (e) => {
        this.setState({
            x: e.clientX,
            y: e.clientY
        })
    }
    // 在页面挂载的时候添加鼠标移动事件
    componentDidMount() {
        window.addEventListener("mousemove", this.handleMouseMove)
    }
    // 在页面销毁的时候移除添加的事件
    componentWillUnmount() {
        window.removeEventListener("mousemove", this.handleMouseMove)
    }

    render() {
        // 调用标签上添加的render属性的函数,将返回的结果在下一行的return中返回给界面进行渲染
        return this.props.render(this.state)
    }
}

// 渲染什么内容是由下面的render属性对应的函数返回值决定的
// mouse形参接受的实参就是上述的this.state,而state对应的就是鼠标的位置
ReactDOM.render(<Mouse render={mouse => <p>鼠标的位置:{mouse.x}---{mouse.y}</p>} />, document.getElementById('root'))

上面的写法中我们每次都要指定一个 render属性,而且在复用组件中也同样必须使用与指定的属性一样的名字来调用函数,因此我们可以用 children 属性来对上述的代码进行修改

下面我返回的不仅仅只是一个p标签了,我返回了一张图片,因此这里我需要导入一张图片

import Cat from "./images/cat.png"

组件中render方法的返回值修改如下,原本是用自己添加在组件标签上的render方法,现在我们直接使用了props的children属性,我们不用再单独的给组件标签添加额外的属性

// 修改前
return this.props.render(this.state)
// 修改后
return this.props.children(this.state)

页面的渲染方法修改如下,原本是在组件标签上添加一个属性,这个属性的值是一个函数,返回我们想要的页面结构,现在我们把这个函数放到了组件标签的中间,这样就会让组件的props生成一个children属性,因此上面就可以使用this.props.children进行调用

// 修改前
ReactDOM.render(<Mouse render={mouse => <p>鼠标的位置:{mouse.x}---{mouse.y}</p>} />, document.getElementById('root'))

// 修改后
ReactDOM.render(
    <Mouse>
        {
            (mouse) => {
    			// 下面 -64 只是因为让鼠标在图片中心,自行根据需求修改
                return (
                    <img src={Cat} alt="" style={{ position: 'absolute', top: mouse.y - 64, left: mouse.x - 64 }} />
                )
            }
        }
    </Mouse>, document.getElementById('root'))

React中用到的一个动画库 react spring 就是使用了 Render Props 模式

高阶组件(重要)

高阶组件(HOC,Higher-Order Component) 目的:实现 状态逻辑复用

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

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

使用步骤:

  • 函数名称约定以 with 开头
  • 指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
  • 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
  • 在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件
  • 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
// 高阶组件是一个函数,接受一个参数是组件,因此这个参数的名字需要首字母大写
const withMouse = (WrappedComponent) => {
    class Mouse extends React.Component {
        state = {
            x: 0,
            y: 0
        }
        handleMouseMove = (e) => {
            this.setState({
                x: e.clientX,
                y: e.clientY
            })
        }
        // 在页面挂载的时候添加鼠标移动事件
        componentDidMount() {
            window.addEventListener("mousemove", this.handleMouseMove)
        }
        // 在页面销毁的时候移除添加的事件
        componentWillUnmount() {
            window.removeEventListener("mousemove", this.handleMouseMove)
        }
        render() {
            // 其实就是在这里对传递进来的组件进行了包装,在这个demo中所谓的包装就是把鼠标的位置数据传递给组件并return
            // 这个 WrappedComponent 其实就是下面的 Position 组件,因此 通过 {...this.state} 展开在 props 上的属性可以在下面的函数组件中通过形参获取到,这里不是一个解构,而是展开运算
            return <WrappedComponent {...this.state} />
        }
    }
    return Mouse
}

// 需要包装的组件
const Position = (props) => {
    console.log(props)
    return (
        <p>鼠标位置:{props.x}--{props.y}</p>
    )
}

// 调用高阶组件,生成一个新的组件
const PositionWithMouse = withMouse(Position)

// 最后渲染的时候使用新生成的组件
ReactDOM.render(<PositionWithMouse />, document.getElementById('root'))

有时候我们也会需要在 新生成的组件标签上添加属性,但是现在这个情况下我们在 被包装组件中是无法获取到的,因此我们需要做如下修改

ReactDOM.render(<PositionWithMouse name="felix"/>, document.getElementById('root'))

高阶组件函数中组件render方法返回值修改,上面说了,这不是解构,使用不能写一起,{ } 中需要是表示,这里只是执行一个表达式

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

如果有多个组件复用了上面的高阶组件,那么我们会在 React Developer Tools 中看到显示的标签是一样的,这样就不利于我们的调试使用,因此为了区分不同的组件,我们可以给 高阶组件 中返回的组件添加一个 displayName

Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
// 下面这个方法是官方文档中的一个方法,我们就直接这么用就行
function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}

这样之后我们在 React Developer Tools 看到的就是我们新生成后的组件的名称了

React 原理

setState()

setState() 是异步更新数据

setState({})

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

class Hello extends React.Component {
    state = {
        count: 0
    }

    handleClick = () => {
        console.log("执行setState之前", this.state.count)
        this.setState({
            count: this.state.count + 1
        })
        // 多次执行如下的setState并不会在上一次的基础上修改数据
        this.setState({
            count: this.state.count + 1
        })
        // 输出结果和上一个一样,所以证明setState是异步的
        console.log("执行setState之后", this.state.count)
    }

    render() {
        return (
            <div>
                <h1>计数器:{this.state.count}</h1>
                <button onClick={this.handleClick}>+1</button>
            </div>
        )
    }
}

setState((state, props) =>{}) 推荐使用

使用这个方式可以让多次调用的 setState() 可以依赖前一次的结果

这个方法任然是异步的,但是我们可以像同步那样来写一些代码

class Hello extends React.Component {
    state = {
        count: 0
    }

    handleClick = () => {
        console.log("执行setState之前", this.state.count)
        // 1 state 表示最新的状态
    	// 2 props 表示最新的 props
        this.setState((state, props) => {
            return {
                count: state.count + 1
            }
        })
        // 这里的state 和 props 就是上面执行完setState后的最新值
        this.setState((state, props) => {
            return {
                count: state.count + 1
            }
        })
        console.log("执行setState之后", this.state.count)
    }

    render() {
        return (
            <div>
                <h1>计数器:{this.state.count}</h1>
                <button onClick={this.handleClick}>+1</button>
            </div>
        )
    }
}

setState() 方法有两个参数,第一个参数可以是一个 对象 或者 回调函数,第二个参数就是一个回调函数

第一个参数的作用就是setState需要修改状态的数据,第二个回调函数表示执行完修改后立即执行的回调,这里面我们就可以获取到更新后的状态,第二个参数用的很少,因为我们也可以在 生命周期钩子函数 componentDidUpdate 中获取到

// 第一个参数是一个对象 { }
this.setState(
    {
        count: this.state.count + 1
    },
    () => {
        // 当 React 更新状态后,就会立即出发这个回调函数
        // 因为,在这个回调函数中获取到的就是更新后的状态值了。
        // 使用场景:在状态更新后,进行处理
        console.log('更新后立即触发的回调函数:', this.state.count)

        document.title = 'React App' + this.state.count
    }
)

// 第一个参数是回调函数 ,如果 props 用不到可以不写
this.setState(
    state => {
        return {
            count: state.count + 1
        }
    },

    () => {
        console.log('更新后立即触发的回调函数:', this.state.count)
        document.title = 'React App' + this.state.count
    }
)

JSX语法转化

JSX 仅仅是 createElement() 方法的语法糖(简化语法),JSX 语法被 @babel/preset-react 插件编译为 createElement() 方法

React 元素:是一个JS对象,用来描述你希望在屏幕上看到的内容

在这里插入图片描述

组件更新机制及优化

更新机制

上面说过了 setState() 方法有两个功能 1. 更新状态(数据) 2. 更新UI ,当父组件的render触发时,也会导致该父组件的子组件(后代组件)一起触发更新,如下图

在这里插入图片描述
很明显这样的方式在性能上来说是比较浪费的,而且也是没有必要的

组件优化

  1. 减轻 state

只存储跟组件渲染相关的数据(比如:count / 列表数据 / loading 等),一些跟渲染无关的数据可以直接定义在 this 上,比如 定时器的id, 对于这种需要在多个方法中用到的数据,应该放在 this 中

class Hello extends Component {
    componentDidMount() {
        // timerId存储到this中,而不是state中
        this.timerId = setInterval(() => {}, 2000)
	}
    componentWillUnmount() {
        clearInterval(this.timerId)
    }
	render() {}
}
  1. 避免不必要的更新

有时候父组件更新会引起子组件也被更新,这种思路很清晰,但是导致其所有子组件都会更新,这是没有必要的,因此我们有时候需要避免这样的更新

解决方法: 使用生命周期更新阶段的一个钩子函数 shouldComponentUpdate

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

class Hello extends React.Component {
    state = {
        count: 0
    }
    handleClick = () => {
        this.setState({
            count: this.state.count + 1
        })
    }
    render() {
        return (
            <div>
                <Child count={this.state.count}></Child>
                <button onClick={this.handleClick}>+1</button>
            </div>
        )
    }
}

class Child extends React.Component {
    // 奇数渲染,偶数不渲染
    // 第一个参数:表示最新的props值(更新后)
	// 第二个参数:表示最新的state值(更新后)
    // this.props 表示当前的 props(更新前)
    // this.state 表示当前的 state(更新前)
    shouldComponentUpdate(nextProps, nextState) {
        return nextProps.count % 2
    }
    render() {
        return <h1>计数器:{this.props.count}</h1>
    }
}

一般我们可以通过判断 当前的props和state更新后的props和state 对比,来判断是否有改动,如果有改动表示需要重新渲染,如果一致就表示没改动,因此就没必要重新渲染。

我们这样写其实也是比较麻烦的,因此 react 就帮我们简化了操作,我们可以把 继承的 React.Component 换成 React.PureComponent

PureComponent(纯组件) 内部自动实现了 shouldComponentUpdate 钩子,不需要手动比较,纯组件内部通过分别 对比 前后两次 props 和 state 的值,来决定是否重新渲染组件

class Child extends React.PureComponent {
    render() {
        return <h1>计数器:{this.props.count}</h1>
    }
}

纯组件内部原理

  • 原理:内部进行的是浅对比(shallow compare)
  • 对于值类型,直接修改即可,没有坑(上面的计数器就是值类型)
  • 但是,对于引用类型来说:(只比较对象的地址)
    • 1 如果直接修改当前对象中属性的值,那么,在更新状态的时候,即便数据变化了,组件也不会被重新渲染
    • 2 应该创建新的引用类型值,再更新状态

错误示例

对于引用类型的数据来说,如果你只是修改了数据内部的值,而没有改变引用的地址,那么纯组件判断前后的两个state或者props是一样的,这样就会导致明明数据改变了,但是页面不会重新渲染

class Hello extends React.PureComponent {
    state = {
        obj: {
            count: 0
        }
    }
    handleClick = () => {
        // newObj 的引用地址 与 this.state.obj 的地址一致,即两者指向同一个对象
        let newObj = this.state.obj
        // 通过下面修改了对象中的数据
        newObj.count += 1
        this.setState({
            obj: newObj
        })
    }
    render() {
        return (
            <div>
                <h1>计数器:{this.state.obj.count}</h1>
                <button onClick={this.handleClick}>+1</button>
            </div>
        )
    }
}

正确示例

通过新建一个对象,并且把原数据获取到,这样机会有一个新的地址,最后把这个对象赋值给对应的状态,这时候因为state中数据的引用地址发生了改变,这样机会触发纯组件的机制,从而让页面进行重新渲染

handleClick = () => {
    let newObj = { ...this.state.obj }
    // es5 写法
    // let newObj = Object.assign({}, this.state.obj)
    newObj.count += 1
    this.setState({
        obj: newObj
    })
}

Tips: state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据!

虚拟 DOM 和 Diff 算法

React 更新视图的思想是:只要 state 变化就重新渲染视图。

但是如果组件中只有一个 DOM 元素需要更新时,也得把整个组件的内容重新渲染到页面中吗?

显然这样也是没必要的,理想的状态就是 部分更新 ,哪里改了更新哪里

因此就有了 虚拟DOM 和 Diff 算法

虚拟DOM:本质上就是一个 JS 对象,用来描述你希望在屏幕上看到的内容(UI)。其实就是我们上面看到的JSX语法转化中的 React元素
在这里插入图片描述

下面我们来看看执行过程是怎么样的?

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

在这里插入图片描述

如何可以看到效果呢?

我们在浏览器中可以在控制台的 Elements 下把标签展开,然后在界面上执行更新操作,就会发现只有更新的数据会闪烁一下

Diff 算法的说明 - 1

  • 如果两棵树的根元素类型不同,React 会销毁旧树,创建新树
// 旧树
<div>
  <Counter />
</div>

// 新树
<span>
  <Counter />
</span>

执行过程:destory Counter -> insert Counter

Diff 算法的说明 - 2

  • 对于类型相同的 React DOM 元素,React 会对比两者的属性是否相同,只更新不同的属性
  • 当处理完这个 DOM 节点,React 就会递归处理子节点。
// 旧
<div className="before" title="stuff"></div>
// 新
<div className="after" title="stuff"></div>
只更新:className 属性

// 旧
<div style={{color: 'red', fontWeight: 'bold'}}></div>
// 新
<div style={{color: 'green', fontWeight: 'bold'}}></div>
只更新:color属性

Diff 算法的说明 - 3

  • 1 当在子节点的后面添加一个节点,这时候两棵树的转化工作执行的很好
// 旧
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// 新
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

执行过程:
React会匹配新旧两个<li>first</li>,匹配两个<li>second</li>,然后添加 <li>third</li> tree
  • 2 但是如果你在开始位置插入一个元素,那么问题就来了:
// 旧
<ul>
  <li>1</li>
  <li>2</li>
</ul>

// 新
<ul>
  <li>3</li>
  <li>1</li>
  <li>2</li>
</ul>

执行过程:
React将改变每一个子节点,而非保持 <li>Duke</li><li>Villanova</li> 不变

key 属性

为了解决以上问题,React 提供了一个 key 属性。当子节点带有 key 属性,React 会通过 key 来匹配原始树和后来的树。

// 旧
<ul>
  <li key="2015">1</li>
  <li key="2016">2</li>
</ul>

// 新
<ul>
  <li key="2014">3</li>
  <li key="2015">1</li>
  <li key="2016">2</li>
</ul>

执行过程:
现在 React 知道带有key '2014' 的元素是新的,对于 '2015''2016' 仅仅移动位置即可
  • 说明:key 属性在 React 内部使用,但不会传递给你的组件
  • 推荐:在遍历数据时,推荐在组件中使用 key 属性:<li key={item.id}>{item.name}</li>
  • 注意:key 只需要保持与他的兄弟节点唯一即可,不需要全局唯一
  • 注意:尽可能的减少数组 index 作为 key,数组中插入元素的等操作时,会使得效率底下

组件的极简模型

  • (state, props) => UI

最后补充说明一下

虚拟DOM的真正价值从来都不是性能。因为虚拟DOM说白了就是一个JS的对象,既然是JS对象,那么只要是能编译JS的环境都可以使用虚拟DOM,这样就可以进行跨平台了,不再局限于浏览器端,也可以用于服务器端(当然环境得是Nodejs),这才是虚拟DOM的真正价值

/div>

// 新树


执行过程:destory Counter -> insert Counter


> Diff 算法的说明 - 2

- 对于类型相同的 React DOM 元素,React 会对比两者的属性是否相同,只更新不同的属性
- 当处理完这个 DOM 节点,React 就会递归处理子节点。

```html
// 旧
<div className="before" title="stuff"></div>
// 新
<div className="after" title="stuff"></div>
只更新:className 属性

// 旧
<div style={{color: 'red', fontWeight: 'bold'}}></div>
// 新
<div style={{color: 'green', fontWeight: 'bold'}}></div>
只更新:color属性

Diff 算法的说明 - 3

  • 1 当在子节点的后面添加一个节点,这时候两棵树的转化工作执行的很好
// 旧
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// 新
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

执行过程:
React会匹配新旧两个<li>first</li>,匹配两个<li>second</li>,然后添加 <li>third</li> tree
  • 2 但是如果你在开始位置插入一个元素,那么问题就来了:
// 旧
<ul>
  <li>1</li>
  <li>2</li>
</ul>

// 新
<ul>
  <li>3</li>
  <li>1</li>
  <li>2</li>
</ul>

执行过程:
React将改变每一个子节点,而非保持 <li>Duke</li><li>Villanova</li> 不变

key 属性

为了解决以上问题,React 提供了一个 key 属性。当子节点带有 key 属性,React 会通过 key 来匹配原始树和后来的树。

// 旧
<ul>
  <li key="2015">1</li>
  <li key="2016">2</li>
</ul>

// 新
<ul>
  <li key="2014">3</li>
  <li key="2015">1</li>
  <li key="2016">2</li>
</ul>

执行过程:
现在 React 知道带有key '2014' 的元素是新的,对于 '2015''2016' 仅仅移动位置即可
  • 说明:key 属性在 React 内部使用,但不会传递给你的组件
  • 推荐:在遍历数据时,推荐在组件中使用 key 属性:<li key={item.id}>{item.name}</li>
  • 注意:key 只需要保持与他的兄弟节点唯一即可,不需要全局唯一
  • 注意:尽可能的减少数组 index 作为 key,数组中插入元素的等操作时,会使得效率底下

组件的极简模型

  • (state, props) => UI

最后补充说明一下

虚拟DOM的真正价值从来都不是性能。因为虚拟DOM说白了就是一个JS的对象,既然是JS对象,那么只要是能编译JS的环境都可以使用虚拟DOM,这样就可以进行跨平台了,不再局限于浏览器端,也可以用于服务器端(当然环境得是Nodejs),这才是虚拟DOM的真正价值

Tips:Vue中的虚拟DOM和Diff 也是差不多的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值