【基础】React 官网—核心概念

118 阅读14分钟

React 18.2.0 已经发布很久了,这几天抽空重新完整的过了一遍 React的官网,并结合当时学习React 16.8 的总结,将笔记和案例记录下来,方便以后查阅迭代

总的来说react的语法不算太多,打算按照 React 官网 文档的教程顺序,从 安装 -> 核心概念 -> 高级指引 -> hook 开始。

本文档会结合一些案例(基于react18),方便大家阅读和实践,以备 开箱即用

仓库地址:PantherVkin/react-note

Hello World

最简易的 React 示例如下:

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<h1>Hello, world!</h1>);

它将在页面上展示一个 “Hello, world!” 的标题。

在 CodePen 上试试

JSX 简介

JSX

  1. 什么是JSX?
  • Facebook起草的JS扩展语法

  • 本质是一个JS对象,会被babel编译,最终会被转换为React.createElement

  • 每个JSX表达式,有且仅有一个根节点

  • 每个JSX元素必须结束(XML规范)

  1. 当根节点为空时

真实语法:React.Fragment

// const d1 = (<>
//     <h1>Hello</h1>
//     <p>world</p>
// </>)

const d1 = (<React.Fragment>
    <h1>Hello</h1>
    <p>world</p>
</React.Fragment>)

JSX中嵌入表达式

  1. {} 使用表达式
import React from 'react';
import ReactDOM from 'react-dom';

const a = 100
const b = 9
const d1 = (<div>
    {a} * {b} = {a * b}
</div>)
 
// const d1 = React.createElement('div', {}, `${a} * ${b} = ${a * b}`)
const root = ReactDOM.createRoot(document.querySelector('#app')); 
// 把react元素(虚拟dom) 变成真实的dom对象。 
root.render(d1);

react-note/1.JSX 中嵌入表达式.html at master · PantherVkin/react-note (github.com)

  1. 在JSX中使用注释

image.png

  1. 将表达式作为内容的一部分

null、undefined、false 不会显示。

普通对象,不可以作为子元素

可以放置React元素对象

  1. 可以放置数组
import React from 'react';
import ReactDOM from 'react-dom';

const obj = <p>React Obj</p>
const arr = new Array(10)
arr.fill(obj)
const d1 = (<div>
    {arr}
</div>)
 
const root = ReactDOM.createRoot(document.querySelector('#app')); 
// 把react元素(虚拟dom) 变成真实的dom对象。 
root.render(d1);

react-note/2.表达式放置数组.html at master · PantherVkin/react-note (github.com)

  1. 将表达式作为元素属性,属性使用小驼峰命名法

你可以通过使用引号,来将属性值指定为字符串字面量

const element = <a href="https://www.reactjs.org"> link </a>;

也可以使用大括号,来在属性值中插入一个 JavaScript 表达式

const element = <img src={user.avatarUrl} className={c1}></img>;

防止注入攻击

  1. 自动编码

React自我保护机制、底层Innertext赋值,保证内容为纯为本。

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

const content = "<h1>Hello<p>world</p></h1>"
const d1 = <div>
    {content}
</div>

const root = ReactDOM.createRoot(document.querySelector('#app')); 
// 把react元素(虚拟dom) 变成真实的dom对象。 
root.render(d1);

image.png

react-note/3.放置注入攻击.html at master · PantherVkin/react-note (github.com)

  1. 想作为元素结构时,配置dangerouslySetInnerHTML
import React from 'react';
import ReactDOM from 'react-dom';

const content = "<h1>Hello<p>world</p></h1>"
const d1 = <div dangerouslySetInnerHTML={{
    __html: content
}}>
</div>

const root = ReactDOM.createRoot(document.querySelector('#app')); 
// 把react元素(虚拟dom) 变成真实的dom对象。 
root.render(d1);

react-note/4.dangerouslySetInnerHTML.html at master · PantherVkin/react-note (github.com)

元素的不可变性

  1. 虽然JSX元素是一个对象,但是该对象中的所有属性不可更改

底层使用 Object.freeze() 冻结一个对象。

  1. 如果确实需要更改元素的属性,需要重新创建JSX元素

JSX 表示对象

  1. JSX -> React.createElement()

Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。

以下两种示例代码完全等效:

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);
  1. React 元素(虚拟DOM)

React.createElement() 会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:

// 注意:这是简化过的结构
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!'
  }
};

这些对象被称为 React 元素。它们描述了你希望在屏幕上看到的内容。React 通过读取这些对象,然后使用它们来构建 DOM 以及保持随时更新。

元素渲染

元素渲染 – React (reactjs.org)

组件 & Props

  1. 组件

包含内容、样式和功能的UI单元

组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素

  1. 组件的名称首字母必须大写

使用组件,生成的,仍然是一个React元素,变化的,只是type值;React 靠首字母区分普通的react元素和组件元素

  • 小写:普通的react元素(内置组件);

  • 大写:组件元素

函数组件

返回一个React元素。

  1. 当做函数使用
import React from 'react';
import ReactDOM from 'react-dom';

function MyFuncComp() {
    return <h1>组件内容</h1>
}

const root = ReactDOM.createRoot(document.querySelector('#app')); 
// 把react元素(虚拟dom) 变成真实的dom对象。 
root.render(<div>
    {MyFuncComp()}
</div>);
  • 缺点:没有构建出组件结构。

react开发者工具查看组件结构。

image.png

  1. 当做React元素使用
import React from 'react';
import ReactDOM from 'react-dom';

function MyFuncComp() {
    return <h1>组件内容</h1>
}

const root = ReactDOM.createRoot(document.querySelector('#app')); 
// 把react元素(虚拟dom) 变成真实的dom对象。 
root.render(<div>
    <MyFuncComp/>
</div>);

react开发者工具查看组件结构。

image.png

  1. 完整案例

react-note/1.函数组件.html at master · PantherVkin/react-note (github.com)

class 组件

必须继承React.Component

必须提供render函数,用于渲染组件,返回React元素;

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

class MyClassComp extends React.Component {
    render() {
        // 该方法必须返回React元素
        return <h1>类组件内容</h1>
    }
}

const root = ReactDOM.createRoot(document.querySelector('#app')); 
// 把react元素(虚拟dom) 变成真实的dom对象。 
root.render(<div>
    <MyClassComp/>
</div>);

Props 的只读性

给组件传递的数据。

  1. 函数组件

对于函数组件,属性会作为一个对象的属性,传递给函数的参数

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

function MyFuncComp(props) {
    return <h1>组件属性:{props.name}</h1>
}

const root = ReactDOM.createRoot(document.querySelector('#app')); 
// 把react元素(虚拟dom) 变成真实的dom对象。 
root.render(<MyFuncComp name="Panther"/>);
  1. 类组件

对于类组件,属性会作为一个对象的属性,传递给构造函数的参数

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

class MyClassComp extends React.Component {
    // 自动完成
    // constructor(props) {
    //     super(props); // this.props = props;
    //     console.log(props, this.props, props === this.props);
    // }
    
    render() {
        // 该方法必须返回React元素
        return <h1>类组件属性:{this.props.name}</h1>
    }
}

const root = ReactDOM.createRoot(document.querySelector('#app')); 
// 把react元素(虚拟dom) 变成真实的dom对象。 
root.render(<MyClassComp name="Panther"/>);
  1. 传递一个对象时
import React from 'react';
import ReactDOM from 'react-dom';

function MyFuncComp(props) {
    return <h1>
        姓名:{props.name}
        地址:{props.address}
    </h1>
}

const obj = {
    name: 'Panther',
    address: 'SZ'
}

const root = ReactDOM.createRoot(document.querySelector('#app')); 
// 把react元素(虚拟dom) 变成真实的dom对象。 
root.render(<div>
    <MyFuncComp {...obj}></MyFuncComp>
    {/* 相当于 name={obj.name} address={obj.address}*/}
</div>);

image.png

react-note/2.函数组件Props.html at master · PantherVkin/react-note (github.com)

  1. 注意

组件无法改变自身的属性。

之前学习的React元素,本质上,就是一个组件(内置组件) 首字母小写。

React中的哲学:数据属于谁,谁才有权力改动。

React中的数据,自顶而下流动。

State & 生命周期

State

  1. 组件状态

组件可以自行维护的数据;

组件状态仅在类组件中有效;

状态(state),本质上是类组件的一个属性,是一个对象。

  1. 状态初始化
import React, { Component } from 'react'

export default class Tick extends Component {
    //初始化状态,JS Next 语法,目前处于实验阶段
    state = {
        left: this.props.number,
        n: 123
    }

    constructor(props) {
        super(props);
        //初始化状态
        // this.state = {
        //     left: this.props.number
        // };

    }

    render() {
        return (
            <h1>
                倒计时剩余时间:{this.state.left}
            </h1>
        )
    }
}
  1. 状态的变化

不能直接改变状态:因为React无法监控到状态发生了变化;

必须使用this.setState({})改变状态(同名属性的值会被替换);

一旦调用了this.setState,会导致当前组件重新渲染;

组件中的数据

  1. props

该数据是由组件的使用者传递的数据,所有权不属于组件自身,因此组件无法改变该数组。

  1. state

该数据是由类组件自身创建的,所有权属于类组件自身,因此类组件有权改变该数据。

深入setState

不要直接修改 State

// Wrong
this.state.comment = 'Hello';

而是应该使用 setState():

// Correct
this.setState({comment: 'Hello'});

构造函数是唯一可以给 this.state 赋值的地方。

setState可能是异步的

  1. 改变状态的代码处于某个HTML元素的事件中,则其是异步的,否则是同步
import React from 'react'
export class CompSetstate extends React.Component {
    state = {
        n: 0
    }

    handleClick = () => {
        this.setState({
            n: this.state.n + 1
        })
        // 改变状态的代码处于HTML事件中,是异步的;
        // n 打印在render 之前,说明此时状态任然没有改变
        console.log(this.state.n)
    }

    render() {
        console.log('render')
        return <div>
            <h1>
                {this.state.n}
            </h1>
            <button onClick={this.handleClick}>
                点击+1
            </button>
        </div>
    }
}

image.png

react-note/1.setState 是异步的.html at master · PantherVkin/react-note (github.com)

  1. 第二个参数

状态完成改变之后触发,该回调运行在render之后。

import React from 'react'
export class CompSetstate extends React.Component {
    state = {
        n: 0
    }

    handleClick = () => {
        this.setState({
            n: this.state.n + 1
        }, () => {
            // 状态完成改变之后触发,该回调运行在render之后
            console.log(this.state.n)
        })
        
    }

    render() {
        console.log('render')
        return <div>
            <h1>
                {this.state.n}
            </h1>
            <button onClick={this.handleClick}>
                点击+1
            </button>
        </div>
    }
}

image.png

react-note/2.第二个参数.html at master · PantherVkin/react-note (github.com)

  1. 第一个参数是函数的方式

如果遇到某个事件中,需要同步调用多次用到之前的状态,第一个参数使用函数的方式得到最新状态。

import React from 'react'
export class CompSetstate extends React.Component {
    state = {
        n: 0
    }

    handleClick = () => {
        // 第一个参数是函数的情况
        // cur 表示当前的状态,会混合(覆盖)掉之前的状态
        // 该函数是异步执行
      this.setState((cur) => {
        console.log('cur', cur);
        return {
          n: cur.n + 1
        };
      });
      this.setState((cur) => {
        console.log('cur', cur);
        return {
          n: cur.n + 1
        };
      });
      this.setState((cur) => {
        console.log('cur', cur);
        return {
          n: cur.n + 1
        };
      });
    }

    render() {
        console.log('render')
        return <div>
            <h1>
                {this.state.n}
            </h1>
            <button onClick={this.handleClick}>
                点击+1
            </button>
        </div>
    }
}

image.png

react-note/3.第一个参数是函数的方式.html at master · PantherVkin/react-note (github.com)

最佳实践

  1. 把所有的setState当作是异步的

  2. 永远不要信任setState调用之后的状态

  3. 如果要使用改变之后的状态,需要使用回调函数(setState的第二个参数)

  4. 如果新的状态要根据之前的状态进行运算,使用函数的方式改变状态(setState的第一个函数)

  5. React会对异步的setState进行优化,将多次setState进行合并(将多次状态改变完成后,再统一对state进行改变,然后触发render)。

import React from 'react'
export class CompSetstate extends React.Component {
    state = {
        n: 0
    }

    handleClick = () => {
        // 第一个参数是函数的情况
        // cur 表示当前的状态,会混合(覆盖)掉之前的状态
        // 该函数是异步执行
        this.setState(cur => ({
            n: cur.n + 1
        }), ()=>{
            //所有状态全部更新完成,并且重新渲染后执行
            console.log("state更新完成", this.state.n);
        });
        this.setState(cur => ({
            n: cur.n + 1
        }),  ()=>{
            //所有状态全部更新完成,并且重新渲染后执行
            console.log("state更新完成", this.state.n);
        });
        this.setState(cur => ({
            n: cur.n + 1
        }),  ()=>{
            //所有状态全部更新完成,并且重新渲染后执行
            console.log("state更新完成", this.state.n);
        });
    }

    render() {
        console.log('render')
        return <div>
            <h1>
                {this.state.n}
            </h1>
            <button onClick={this.handleClick}>
                点击+1
            </button>
        </div>
    }
}

image.png

react-note/4.setState 合并.html at master · PantherVkin/react-note (github.com)

生命周期

组件从诞生到销毁会经历一系列的过程,该过程就叫做生命周期。

React在组件的生命周期中提供了一系列的钩子函数(类似于事件),可以让开发者在函数中注入代码,这些代码会在适当的时候运行。

生命周期仅存在于类组件中函数组件每次调用都是重新运行函数,旧的组件即刻被销毁

image.png

  1. constructor
  • 同一个组件对象只会创建一次

  • 不能在第一次挂载到页面之前,调用setState,为了避免问题,构造函数中严禁使用setState

  1. render
  • 返回一个虚拟DOM,会被挂载到虚拟DOM树中,最终渲染到页面的真实DOM中

  • render可能不只运行一次,只要需要重新渲染,就会重新运行

  • 严禁使用setState,因为可能会导致无限递归渲染

  1. componentDidMount
  • 只会执行一次

  • 可以使用setState

  • 通常情况下,会将网络请求、启动计时器等一开始需要的操作,书写到该函数中

组件进入活跃状态

  1. shouldComponentUpdate
  • 指示React是否要重新渲染该组件,通过返回true和false来指定

  • 默认情况下,会直接返回true

  1. componentDidUpdate
  • 往往在该函数中使用dom操作,改变元素
  1. componentWillUnmount
  • 通常在该函数中销毁一些组件依赖的资源,比如计时器
  1. getDerivedStateFromProps
  • 两个参数,通过参数可以获取新的属性和状态

  • 该函数是静态的,不能获取this

  • 该函数的返回值会覆盖掉组件状态

  • 该函数几乎是没有什么用

  1. getSnapshotBeforeUpdate
  • 真实的DOM构建完成,但还未实际渲染到页面中。

  • 在该函数中,通常用于实现一些附加的dom操作(直接操作Dom)

  • 该函数的返回值,会作为componentDidUpdate的第三个参数

事件处理

在React中,组件的事件,本质上就是一个属性。

按照之前React对组件的约定,由于事件本质上是一个属性,因此也需要使用小驼峰命名法。

内置组件的事件

会在合适的时候,自动调用事件 。

import React from 'react'
import ReactDOM from 'react-dom'

const btn = <button onClick={(e) => {
    console.log('点击了', e)
}}>点击</button>

ReactDOM.render(btn, document.getElementById('root'))

自定义组件的事件

不会自动调用事件。

如果没有特殊处理,在事件处理函数中,this指向undefined ?

export class HelloWorldCls extends React.Component {
constructor(props) {
  super(props);
  // this.handleClick = this.handleClick.bind(this);
}
render() {
  return <h1 onClick={this.handleClick}> HelloWorldCls </h1>;
}

handleClick() {
  console.log('点击了', this);
}
}

image.png

  1. 使用bind函数,绑定this
export class HelloWorldCls extends React.Component {
    constructor(props) {
        super(props)
        this.handleClick = this.handleClick.bind(this)
    }
    render() {
        return <h1 onClick={this.handleClick}>
            HelloWorldCls
        </h1>
    }

    handleClick() {
        console.log('点击了', this)
    }
}
export class HelloWorldCls extends React.Component {
    render() {
        return <h1 onClick={this.handleClick.bind(this)}>
            HelloWorldCls
        </h1>
    }

    handleClick() {
        console.log('点击了', this)
    }
}
  1. 使用箭头函数
export class HelloWorldCls extends React.Component {
    render() {
        return <h1 onClick={this.handleClick}>
            HelloWorldCls
        </h1>
    }

    handleClick = ()=> {
        console.log('点击了', this)
    }
}

向事件处理程序传递参数

在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id 是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

上述两种方式是等价的,分别通过箭头函数和 Function.prototype.bind来实现。

在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。

如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。

传递元素内容

内置组件

内置组件:div、h1、p

<div>
asdfafasfafasdfasdf
</div>

自定义组件

  1. 通过属性

img

  1. children

如果给自定义组件传递元素内容,则React会将元素内容作为children属性传递过去。

img

  1. 传多个元素内容

列表 & Key

key

  1. key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。
const todoItems = todos.map((todo) =>
  <li key={todo.id}>    {todo.text}
  </li>
);
  1. 当元素没有确定 id 的时候,万不得已你可以使用元素索引 index 作为 key:

如果列表项目的顺序可能会变化,我们不建议使用索引来用作 key 值,因为这样做会导致性能变差,还可能引起组件状态的问题。可以看看 Robin Pokorny 的深度解析使用索引作为 key 的负面影响这一篇文章。

如果你选择不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。

要是你有兴趣了解更多的话,这里有一篇文章深入解析为什么 key 是必须的可以参考。

用 key 提取组件

元素的 key 只有放在就近的数组上下文中才有意义。

比方说,如果你提取出一个 ListItem 组件,你应该把 key 保留在数组中的这个 <ListItem /> 元素上,而不是放在 ListItem 组件中的 <li> 元素上。

  1. 例子:不正确的使用 key 的方式
function ListItem(props) {
  const value = props.value;
  return (
    // 错误!你不需要在这里指定 key:    <li key={value.toString()}>      {value}
    </li>
  );
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // 错误!元素的 key 应该在这里指定:    <ListItem value={number} />  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}
  1. 例子:正确的使用 key 的方式
function ListItem(props) {
  // 正确!这里不需要指定 key:  return <li>{props.value}</li>;}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // 正确!key 应该在数组的上下文中被指定    <ListItem key={number.toString()} value={number} />  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

一个好的经验法则是:在 map() 方法中的元素需要设置 key 属性。

key 值在兄弟节点之间必须唯一

数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。

然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的 key 值。

function Blog(props) {
  const sidebar = (    <ul>
      {props.posts.map((post) =>
        <li key={post.id}>          {post.title}
        </li>
      )}
    </ul>
  );
  const content = props.posts.map((post) =>    <div key={post.id}>      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );
  return (
    <div>
      {sidebar}      <hr />
      {content}    </div>
  );
}

const posts = [
  {id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
  {id: 2, title: 'Installation', content: 'You can install React from npm.'}
];

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Blog posts={posts} />);

key 会传递信息给 React ,但不会传递给你的组件。如果你的组件中需要使用 key 属性的值,请用其他属性名显式传递这个值:

const content = posts.map((post) =>
  <Post
    key={post.id}    id={post.id}    title={post.title} />
);

上面例子中,Post 组件可以读出 props.id,但是不能读出 props.key

表单

受控组件

组件的使用者,有能力完全控制该组件的行为和内容。通常情况下,受控组件往往没有自身的状态,其内容完全受到属性的控制。

非受控组件

组件的使用者,没有能力控制该组件的行为和内容,组件的行为和内容完全自行控制

React 的表单元素

表单组件,默认情况下是非受控组件,一旦设置了表单组件的value属性,则其变为受控组件(单选和多选框需要设置checked)。

  1. value受控组件
import React from 'react'
export class CompForm extends React.Component {    
    constructor(props) {
        super(props)
    }
    state = {
        value: "请输入。。。"
    }

    handleChange = (e) => {
        this.setState({
            value: e.target.value
        }, () => {
            console.log(this.state.value)
        })
    }

    render() {
        return <div>
                {/* 默认是一个非受控组件 */}
                {/* <input type="text" /> */}
                <input type="text" value={this.state.value} onChange={this.handleChange}/>
        </div>
    }
}
  1. checked 受控组件
<input type="checkbox"  checked={this.state.checked} onChange={ e=> {
    this.setState({
        checked: e.target.checked
    })
}}/>