React学习笔记(二)—— JSX、组件与生命周期

一、JSX

1.1、什么是JSX?

JSX = JavaScript XML,这是React官方发明的一种JS语法(糖)

概念:JSX是 JavaScript XML(HTML)的缩写,表示在 JS 代码中书写 HTML 结构

设想如下变量声明:

const element = <h1>Hello, world!</h1>;

这个有趣的标签语法既不是字符串也不是 HTML。

它被称为 JSX,是一个 JavaScript 的语法扩展。我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模板语言,但它具有 JavaScript 的全部功能。

JSX 可以生成 React “元素”。

1.2、为什么使用 JSX?

React 认为渲染逻辑本质上与其他 UI 逻辑内在耦合,比如,在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。

React 并没有采用将标记与逻辑分离到不同文件这种人为的分离方式,而是通过将二者共同存放在称之为“组件”的松散耦合单元之中,来实现关注点分离。我们将在后面章节中深入学习组件。如果你还没有适应在 JS 中使用标记语言,这个会议讨论应该可以说服你。

React 不强制要求使用 JSX,但是大多数人发现,在 JavaScript 代码中将 JSX 和 UI 放在一起时,会在视觉上有辅助作用。它还可以使 React 显示更多有用的错误和警告消息。

浏览器默认是不支持JSX的,所以jsx语法必须使用@babel/preset-react进行编译,编译的结果React.createElement()这种Api的代码。

示例:<div>Hello</div> ==>@babel/preset-react==> React.createElement('div',{},'Hello')

在React开发中,JSX语法是可选的(也就是你可以不使用JSX)。如果不使用JSX语法,React组件代码将变得特别麻烦(难以维护)。所以几乎所有React开发都用的是JSX语法。

JSX是Javascript的一种语法拓展

JSX是JavaScript XML简写,表示在JavaScript中编写XML格式代码(也就是HTML格式)

优势:

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

注意:JSX 并不是标准的 JS 语法,是 JS 的语法扩展,浏览器默认是不识别的,脚手架中内置的 @babel/plugin-transform-react-jsx 包,用来解析该语法

1.3、JSX中使用js表达式

目标任务: 能够在JSX中使用表达式

语法

{ JS 表达式 }

const name = '张三'

<h1>你好,我叫{name}</h1>   //    <h1>你好,我叫张三</h1> 

可以使用的表达式

  1. 字符串、数值、布尔值、null、undefined、object( [] / {} )
  2. 1 + 2、'abc'.split('')、['a', 'b'].join('-')
  3. 内置函数,自定义函数

特别注意

​ if 语句/ switch-case 语句/ 变量声明语句,这些叫做语句,不是表达式,不能出现在 {} 中。

可以使用?:与&&替代if的功能

在下面的示例中,我们将调用 JavaScript 函数 formatName(user) 的结果,并将结果嵌入到 <h1> 元素中。

function formatName(user) {
  return user.firstName + ' ' + user.lastName;
}

const user = {
  firstName: 'Harper',
  lastName: 'Perez'
};

const element = (
  <h1>
    Hello, {formatName(user)}!  </h1>
);

为了便于阅读,我们会将 JSX 拆分为多行。同时,我们建议将内容包裹在括号中,虽然这样做不是强制要求的,但是这可以避免遇到自动插入分号陷阱。

1.4. JSX列表渲染

1.4.1、map函数

map()方法定义在JavaScript的Array中,它返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
注意:

    • map()不会对空数组进行检测
    • map()不会改变原始数组
array.map(function(currentValue, index, arr), thisValue)

参数说明:

  • function(currentValue, index, arr):必须。为一个函数,数组中的每个元素都会执行这个函数。其中函数参数:
  1. currentValue:必须。当前元素的的值。
  2. index:可选。当前元素的索引。
  3. arr:可选。当前元素属于的数组对象。
  • thisValue:可选。对象作为该执行回调时使用,传递给函数,用作"this"的值。
//返回由原数组中每个元素的平方组成的新数组:

let array = [1, 2, 3, 4, 5];

let newArray = array.map((item) => {
    return item * item;
})

console.log(newArray)  // [1, 4, 9, 16, 25]

 

1.4.2、JSX列表渲染

JSX 表达式必须具有一个父元素。没有父元素时请使用<></>

目标任务: 能够在JSX中实现列表渲染

页面的构建离不开重复的列表结构,比如歌曲列表,商品列表等,我们知道vue中用的是v-for,react这边如何实现呢?

实现:使用数组的map 方法

案例:

// 列表
const songs = [
  { id: 1, name: '痴心绝对' },
  { id: 2, name: '像我这样的人' },
  { id: 3, name: '南山南' }
]

function App() {
  return (
    <div className="App">
      <ul>
        {
          songs.map(item => <li>{item.name}</li>)
        }
      </ul>
    </div>
  )
}

export default App

注意点:需要为遍历项添加 key 属性

  1. key 在 HTML 结构中是看不到的,是 React 内部用来进行性能优化时使用
  2. key 在当前列表中要唯一的字符串或者数值(String/Number)
  3. 如果列表中有像 id 这种的唯一值,就用 id 来作为 key 值
  4. 如果列表中没有像 id 这种的唯一值,就可以使用 index(下标)来作为 key 值

1.5、JSX条件渲染

目标任务: 能够在JSX中实现条件渲染

作用:根据是否满足条件生成HTML结构,比如Loading效果

实现:可以使用 三元运算符 或 逻辑与(&&)运算符

案例:

// 来个布尔值
const flag = true
function App() {
  return (
    <div className="App">
      {/* 条件渲染字符串 */}
      {flag ? 'react真有趣' : 'vue真有趣'}
      {/* 条件渲染标签/组件 */}
      {flag ? <span>this is span</span> : null}
    </div>
  )
}
export default App

1.6. JSX样式处理

目标任务: 能够在JSX中实现css样式处理

  • 行内样式 - style

    import ReactDOM from "react-dom/client";

    //1、创建根节点
    let root = ReactDOM.createRoot(document.getElementById("root"));

    let songs = ["好汉歌", "南山南", "滴答"];

    //3、将Vnode渲染到根结点上
    root.render(
      <div>
        <h2 style={{ color: "red", backgroundColor: "yellow" }}>歌曲列表</h2>
        <ul>
          {songs.map((item) => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      </div>
    );
  • 行内样式 - style - 更优写法

    const styleObj = {
        color:red
    }
    
    function App() {
      return (
        <div className="App">
          <div style={ styleObj }>this is a div</div>
        </div>
      )
    }
    
    export default App
    
  • 类名 - className(推荐)

    app.css

    .title {
      font-size: 30px;
      color: blue;
    }
    

    app.js

    import './app.css'
    
    function App() {
      return (
        <div className="App">
          <div className='title'>this is a div</div>
        </div>
      )
    }
    export default App
    
  • 类名 - className - 动态类名控制

    import './app.css'
    const showTitle = true
    function App() {
      return (
        <div className="App">
          <div className={ showTitle ? 'title' : ''}>this is a div</div>
        </div>
      )
    }
    export default App

1.7、注意事项

  1. JSX必须有一个根节点,如果没有根节点,可以使用<></>(幽灵节点)替代
  2. 所有标签必须形成闭合,成对闭合或者自闭合都可以
  3. JSX中的语法更加贴近JS语法,属性名采用驼峰命名法 class -> className for -> htmlFor
  4. JSX支持多行(换行),如果需要换行,需使用() 包裹,防止bug出现

1.8、JSX 也是一个表达式

在编译之后,JSX 表达式会被转为普通 JavaScript 函数调用,并且对其取值后得到 JavaScript 对象。

也就是说,你可以在 if 语句和 for 循环的代码块中使用 JSX,将 JSX 赋值给变量,把 JSX 当作参数传入,以及从函数中返回 JSX:

function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;  }
  return <h1>Hello, Stranger.</h1>;}

1.9、JSX 中指定属性

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

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

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

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

在属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。你应该仅使用引号(对于字符串值)或大括号(对于表达式)中的一个,对于同一属性不能同时使用这两种符号。

警告:

因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。

例如,JSX 里的 class 变成了 className,而 tabindex 则变为 tabIndex

属性也可以是一个箭头函数:

import ReactDOM from "react-dom/client";
import React from "react";
import "./App.css";

//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));

let counter = 0;
const element = (
  <div>
    <h2>计算器</h2>
    <button
      onClick={() => {
        counter++;
        console.log(counter);
      }}
    >
      点击加1
    </button>
  </div>
);

//3、将Vnode渲染到根结点上
root.render(element);

也可以是一个普通函数:

import ReactDOM from "react-dom/client";
import React from "react";
import "./App.css";

//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));

let counter = 0;
const element = (
  <div>
    <h2>计算器</h2>
    <button onClick={increment}>点击加1</button>
  </div>
);

function increment() {
  counter++;
  console.log(counter);
}

//3、将Vnode渲染到根结点上
root.render(element);

1.10、使用 JSX 指定子元素

假如一个标签里面没有内容,你可以使用 /> 来闭合标签,就像 XML 语法一样:

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

JSX 标签里能够包含很多子元素:

const element = (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here.</h2>
  </div>
);

1.11、JSX 防止注入攻击

你可以安全地在 JSX 当中插入用户输入内容:

const title = response.potentiallyMaliciousInput;
// 直接使用是安全的:
const element = <h1>{title}</h1>;

React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。

1.12、JSX 表示对象

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

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

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

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

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

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

import ReactDOM from "react-dom/client";
import React from "react";
import "./App.css";

//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));

const element = React.createElement(
  "h1",
  { className: "blueBg" },
  "Hello, world!"
);

//3、将Vnode渲染到根结点上
root.render(element);

二、组件 Component

如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展,但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。如果我们将一个个功能块拆分后,就可以像搭建积木一下来搭建我们的项目。
 
 

2.1、SPA

SPA指的是Single Page Application,就是只有一张Web页面的应用。单页应用程序 (SPA) 是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。 浏览器一开始会加载必需的HTML、CSS和JavaScript,所有的操作都在这张页面上完成,都由JavaScript来控制。因此,对单页应用来说模块化的开发和设计显得相当重要。

单页Web应用,顾名思义,就是只有一张Web页面的应用。浏览器一开始会加载必需的HTML、CSS和JavaScript,之后所有的操作都在这张页面上完成,这一切都由JavaScript来控制。因此,单页Web应用会包含大量的JavaScript代码,复杂度可想而知,模块化开发和设计的重要性不言而喻。

速度:更好的用户体验,让用户在web app感受native app的速度和流畅

MVVM:经典MVVM开发模式,前后端各负其责

ajax:重前端,业务逻辑全部在本地操作,数据都需要通过AJAX同步、提交

路由:在URL中采用#号来作为当前视图的地址,改变#号后的参数,页面并不会重载

优点:

1.分离前后端关注点,前端负责View,后端负责Model,各司其职;
2.服务器只接口提供数据,不用展示逻辑和页面合成,提高性能;
3.同一套后端程序代码,不用修改兼容Web界面、手机;
4.用户体验好、快,内容的改变不需要重新加载整个页面
5.可以缓存较多数据,减少服务器压力
6.单页应用像网络一样,几乎随处可以访问—不像大多数的桌面应用,用户可以通过任务网络连接和适当的浏览器访问单页应用。如今,这一名单包括智能手机、平板电脑、电视、笔记本电脑和台式计算机。

缺点:

1.SEO问题
2.刚开始的时候加载可能慢很多
3.用户操作需要写逻辑,前进、后退等
4.页面复杂度提高很多,复杂逻辑难度成倍

2.2、什么是组件?

组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。

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

组件它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:

组件树

2.3、组件定义

组件是 React的核心慨念,定 React应用程序的基石。组件将应用的UI拆分成独立的、可复用的模块,React 用任厅止定田一个一个组件搭建而成的。

定义一个组件有两种方式,便用ES 6 class(类组件)和使用函数(函数组件)。我们先介绍使用class定义组件方式。

2.3.1、使用class定义组件

使用class定义组件需要满足两个条件:

(1)class继承自 React.Component。

(2) class内部必须定义 render方法,render方法返回代表该组件UI的React元素。

使用create-react-app新建一个简易BBS项目,在这个项目中定义一个组件PostList,用于展示BBS 的帖子列表。

PostList的定义如下:

import React, { Component } from "react";

class PostList extends Component {
  render() {
    return (
      <div>
        <h2>帖子列表:</h2>
        <ul>
          <li>男子吃霸王餐还教育老板和气生财</li>
          <li>莫斯科险遭无人机炸弹攻击</li>
          <li>武汉官方把房地产归为困难行业</li>
        </ul>
      </div>
    );
  }
}

export default PostList;

index.js

import ReactDOM from "react-dom/client";
import React from "react";
import PostList from "./PostList";

//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));

let vNode = <PostList />;

//3、将Vnode渲染到根结点上
root.render(vNode);

运行效果:

type MyProps = { ... };

type MyState = { value: string };

class App extends React.Component<MyProps, MyState> { ... }

2.3.2、使用函数定义组件

定义组件最简单的方式就是编写 JavaScript 函数:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。

index.js内容如下:

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

//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));

//2、定义函数组件
function Welcome(props) {
  return <h2>Hello {props.name}!</h2>;
}

//3、使用组件
let vNode = <Welcome name="zhangguo" />;

//4、将Vnode渲染到根结点上
root.render(vNode);

运行结果:

 

约定说明

  1. 组件的名称必须首字母大写,react内部会根据这个来判断是组件还是普通的HTML标签

  2. 函数组件必须有返回值,表示该组件的 UI 结构;如果不需要渲染任何内容,则返回 null

  3. 组件就像 HTML 标签一样可以被渲染到页面中。组件表示的是一段结构内容,对于函数组件来说,渲染的内容是函数的返回值就是对应的内容

  4. 使用函数名称作为组件标签名称,可以成对出现也可以自闭合

2.4、组件的props

2.3.1中的PostList 中的每一个帖子都使用一个标签直接包裹,但一个帖子不仅包含能子的标题,还会包含帖子的创建人、帖子创建时间等信息,这时候标签下的结构就会变得复杂。而且每一个帖子都需要重写一次这 个复杂的结构,PostList 的结构将会变成类似这样的形式:

import React, { Component } from "react";

class PostList extends Component {
  render() {
    return (
      <div>
        <h2>帖子列表:</h2>
        <ul>
          <li>
            <div>男子吃霸王餐还教育老板和气生财</div>
            <div>
              创建人:<span>小明</span>
            </div>
            <div>
              创建时间:<span>2023-02-03 18:19:22</span>
            </div>
          </li>
          <li>
            <div>莫斯科险遭无人机炸弹攻击</div>
            <div>
              创建人:<span>小军</span>
            </div>
            <div>
              创建时间:<span>2023-01-22 21:22:35</span>
            </div>
          </li>
          <li>
            <div>武汉官方把房地产归为困难行业</div>
            <div>
              创建人:<span>小华</span>
            </div>
            <div>
              创建时间:<span>2022-12-22 15:14:56</span>
            </div>
          </li>
        </ul>
      </div>
    );
  }
}

export default PostList;

但是,帖子列表的数括依然存在于 PostList中,如何将数据传递给每个 PostItem 组件呢?这时候就需要用到组件的props属性。组件的 props用于把父组件中的数据或方法传递给子组件,供子组件使用。

props是一个简单结构的对象,它包含的属性正是由组件作为JSX标签使用时的属性组成。

例如下面是一个使用User组

<User name='React' age='4' address=' America'>

此时User组件的 props结构如下:

props ={
name: 'React',
age: '4',
address: 'America'
}

利用props定义PostItem组件,PostItem.js如下所示:

import React, { Component } from "react";

class PostItem extends Component {
  render() {
    const { title, author, date } = this.props;
    return (
      <li>
        <div>{title}</div>
        <div>
          创建人:<span>{author}</span>
        </div>
        <div>
          创建时间:<span>{date}</span>
        </div>
      </li>
    );
  }
}

export default PostItem;

src/PostList.js

import React, { Component } from "react";
import PostItem from "./PostItem";

const data = [
  {
    title: "男子吃霸王餐还教育老板和气生财",
    author: "小明",
    date: "2023-02-03 18:19:22",
  },
  {
    title: "莫斯科险遭无人机炸弹攻击",
    author: "小军",
    date: "2023-01-22 21:22:35",
  },
  {
    title: "武汉官方把房地产归为困难行业",
    author: "小华",
    date: "2022-12-22 15:14:56",
  },
];

class PostList extends Component {
  render() {
    return (
      <div>
        <h2>帖子列表:</h2>
        <ul>
          {data.map((item) => (
            <PostItem
              title={item.title}
              author={item.author}
              date={item.date}
            />
          ))}
        </ul>
      </div>
    );
  }
}

export default PostList;

index.js

import ReactDOM from "react-dom/client";
import React from "react";
import PostList from "./PostList";

//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));

let vNode = <PostList />;

//3、将Vnode渲染到根结点上
root.render(vNode);

运行结果:

2.5、组件的state

组件的 state是组件内部的状态,state的变化最终将反映到组件UI的上。我们在组件的构造方法constructor中通过this.state定义组件的初始状态,并通过调用 this.setState 方法改变组件状态(也是改变组件状态的唯一方式),进而组件UI也会随之重新渲染。

示例:Counter.js

import React, { Component } from "react";

export default class Counter extends Component {
  //构造函数,用于初始化当前组件
  constructor(props) {
    super(props); //初始化属性
    //定义当前组件的状态对象
    this.state = {
      n: 100,
    };
    console.log(this);
  }

  clickHandler = () => {
    //this.state对象是只读的,不允许直接修改,使用setState修改
    //当调用setState时会更新UI,render()被调用
    //this.state.n++;

    //更新状态,设置新的状态对象
    this.setState({ n: this.state.n + 1 });
  };

  render() {
    console.log("render");
    return (
      <div>
        <h2>计数器</h2>
        <button
          onClick={() => {
            this.setState({ n: this.state.n + 1 });
          }}
        >
          {this.state.n}
        </button>
      </div>
    );
  }
}

结果:

 

 

下面来改造下BBS项目。我们为每一个帖子增加一个“点赞”按钮每点击一次,该帖子的点赞数增加1。点赞数是会发生变化的,它的变化也会影响到组件UI,因此我们将点赞数vote
作为Postltem的一个状态定义到它的state内。

修改后的PostItem:

import React, { Component } from "react";

class PostItem extends Component {
  constructor(props) {
    super(props); //调用Component的构造方法,用于完成React组件的初始化工作
    this.state = {
      vote: 0,
    };
  }

  //处理点击逻辑
  handleClick() {
    let vote = this.state.vote;
    vote++;
    this.setState({ vote: vote });
  }

  render() {
    const { title, author, date } = this.props;
    return (
      <li>
        <div>{title}</div>
        <div>
          创建人:<span>{author}</span>
        </div>
        <div>
          创建时间:<span>{date}</span>
        </div>
        <div>
          <button
            onClick={() => {
              this.handleClick();
            }}
          >
            点赞 {this.state.vote}
          </button>
        </div>
      </li>
    );
  }
}

export default PostItem;

运行结果:

 

React组件正是由props和state两种类型的数据驱动渲染出组件 UI。props是组件对外的接口,组件通过 props接收外部传入的数据(包括方法); state是组件对内的接口,组件内部状态的变化通过state反映。另外,props是只读的,你不能在组内部修改 props; state是可变的,组件状态的变化通过修改state来实现。

2.5.1、Function.bind函数

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

语法

fun.bind(thisArg[, arg1[, arg2[, ...]]])

参数

thisArg当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用new操作符调用绑定函数时,该参数无效。

arg1, arg2, ...当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。

返回值

返回由指定的this值和初始化参数改造的原函数拷贝

描述

bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。当新函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。

新函数具有下列内部属性:

  • [BoundTargetFunction] - 封装的函数对象;
  • [BoundThis] - 在调用包装函数时作为值传递的值。
  • [BoundArguments] - 元素被用作对包装函数的调用的第一个参数的值列表。
  • [call] - 执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个this值和一个包含通过调用表达式传递给函数的参数的列表。

当调用绑定函数时,它调用[BoundTargetFunction]上的内部方法[Call ],并使用以下参数Call(boundThis args)。其中,boundThis[BoundThis]args[BoundArguments]后跟函数调用传递的参数。

绑定函数也可以使用new运算符来构造:这样做就好像目标函数已被构建一样。所提供的this值将被忽略,同时为仿真函数提供前置参数。

示例

创建绑定函数

bind() 最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的 this 值。JavaScript新手经常犯的一个错误是将一个方法从对象中拿出来,然后再调用,希望方法中的 this 是原来的对象。(比如在回调中传入这个方法。)如果不做特殊处理的话,一般会丢失原来的对象。从原来的函数和原来的对象创建一个绑定函数,则能很漂亮地解决这个问题:

this.x = 9;    // this refers to global "window" object here in the browser
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX(); // 81

var retrieveX = module.getX;
retrieveX();   
// returns 9 - The function gets invoked at the global scope

// Create a new function with 'this' bound to module
// New programmers might confuse the
// global var x with module's property x
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      const obj1 = {
        x: 111,
        getX() {
          console.log(this.x);
        },
      };

      const obj2 = {
        x: 222,
      };

      const obj3 = {
        x: 333,
      };

      x = 444;

      obj1.getX();

      const getX1 = obj1.getX;
      getX1();

      const getX2 = obj1.getX.bind(obj2);
      getX2();

      const getX3 = obj1.getX.bind(obj3);
      getX3();
    </script>
  </body>
</html>

2.5.2、组件中指定this

下面的示例中this指向undefined

import React, { Component } from "react";

export default class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick() {
    console.log(this);
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <h2>计算器</h2>
        <button onClick={this.handleClick}>{this.state.count}</button>
      </div>
    );
  }
}

运行时报错:

 解决方法:

1、使用Function.bind指定this

import React, { Component } from "react";

export default class Counter extends Component {
  //构造函数,用于初始化当前组件
  constructor(props) {
    super(props); //初始化属性
    //定义当前组件的状态对象
    this.state = {
      n: 100,
    };
    //指定clickHandler函数中的this为当前组件,并生成新的函数
    this.clickHandler = this.clickHandler.bind(this);
    debugger;
  }

  clickHandler() {
    this.setState({ n: this.state.n + 1 });
  }

  render() {
    console.log("render");
    return (
      <div>
        <h2>计数器</h2>
        <button onClick={this.clickHandler}>{this.state.n}</button>
      </div>
    );
  }
}

 

 

 可以看出this此时指向了Component这个对象

2、方法2,使用箭头函数,箭头函数中的this始终指向组件对象

import React, { Component } from "react";

export default class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick = () => {
    console.log(this);
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <h2>计算器</h2>
        <button onClick={this.handleClick}>{this.state.count}</button>
      </div>
    );
  }
}

简化

import React, { Component } from "react";

export default class Counter2 extends Component {
  //使用state属性初始化状态对象
  state = { n: 999 };

  //箭头函数中的this始终指向组件本身
  clickHandler = () => {
    this.setState({ n: this.state.n + 1 });
  };

  render() {
    return (
      <div>
        <h2>计数器</h2>
        <button onClick={this.clickHandler}>{this.state.n}</button>
      </div>
    );
  }
}

2.5.3、componentDidMount

挂载

当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方

这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

你可以在 componentDidMount() 里直接调用 setState()。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。请谨慎使用该模式,因为它会导致性能问题。通常,你应该在 constructor() 中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modals 和 tooltips 等情况下,你可以使用此方式处理

2.5.4、componentWillUnmount

componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。

componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

Timer.js

import React, { Component } from "react";

export default class Timer extends Component {
  constructor(props) {
    super(props);
    this.timer = null;
    this.state = {
      time: new Date().toLocaleString(),
    };
  }

  componentDidMount() {
    this.timer = setInterval(() => {
      this.setState({ time: new Date().toLocaleString() });
      console.log(this.state.time);
    }, 1000);
  }

  render() {
    return <div>{this.state.time}</div>;
  }
}

Counter.js

import React, { Component } from "react";

import Timer from "./Timer";

export default class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isShow: true };
  }

  handleClick = () => {
    console.log(this);
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <button
          onClick={() => {
            this.setState({ isShow: !this.state.isShow });
          }}
        >
          显示/隐藏
        </button>
        <div>{this.state.isShow && <Timer />}</div>
      </div>
    );
  }
}

结果:

可以显示时钟,但隐藏时控制台依然输出,修改Timer.js文件,当组件卸载时清除时钟

import React, { Component } from "react";

export default class Timer extends Component {
  constructor(props) {
    super(props);
    this.timer = null;
    this.state = {
      time: new Date().toLocaleString(),
    };
  }

  componentDidMount() {
    this.timer = setInterval(() => {
      this.setState({ time: new Date().toLocaleString() });
      console.log(this.state.time);
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.timer);
  }

  render() {
    return <div>{this.state.time}</div>;
  }
}

结果:

2.6、有状态组件和无状态组件

是不是每个组件内部都需要定义state呢?当然不是。state用来反映组件内部状态变化,如果一个组件的内部状态是不变的,当然就用不到state,这样的组件称之为无状态组件,例如PostList。
反之,一个组件的内部状态会发生变化,就需要使用state 来保存变化,这种组件称为有状态组件,例如PostItem。
定义无状态组件除了使用 ES 6 class的方式外,还可以使用函数定义,一个函数组件接收props作为参数,返回代表这个组件的UI React 元素结构。
例如,下面是一个简单的函数组件:

function Welcome (props) {
    return <h1>Hello, {props.name }</hl>;
}

可以看出,函数组件的写法比类组件的写法要简洁很多,在使用无状态组件时,应该尽量将其定义成函数组件

在开发React应用时,一定要先认真思考哪些组件应该设计成有状态组件,哪些组件应该设计成无状态组件。并且,应该尽可能多地使用无状态组件,无状态组件不用关心状态的变化,只聚焦于UI的展示,因而更容易被复用。React应用组件设计的一般思路是,通过定义少数的有状态组件管理整个应用的状态变化,并且将状态通过props传递给其余的无状态组件,由无状态组件完成页面绝大部分UI的渲染工作。总之,有状态组件主要关注处理状态变化的业务逻辑,无状态组件主要关注组件UI的渲染。

下面让我们回过头来看一下BBS项目的组件设计。当前的组件设计并不合适,主要体现在:

(1)帖子列表通过一个常量data保存在组件之外,但帖子列表的数据增加或原有帖子的删除都会导致帖子列表数据的变化。

(2)每一个 PostItem都维持个 vote状态,但除了vote以外,帖子其他的信息(如标题、创建人等)都保存在PostList中,这显然也是不合理的。

我们对这两个组件进行重新设计,将PostList 设计为有状态组件,负责帖子列表数据的获取以及点赞行为的处理,将PostItem设计为无状态组件,只负责每一个帖子的
展示。

修改后的PostList.js:

import React, { Component } from "react";
import PostItem from "./PostItem";

class PostList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      posts: [],
    };
    this.timer = null;
    //改变handleVote中的this指向
    this.handleVote = this.handleVote.bind(this);
  }

  data = [
    {
      id: 1,
      title: "男子吃霸王餐还教育老板和气生财",
      author: "小明",
      date: "2023-02-03 18:19:22",
      vote: 0,
    },
    {
      id: 2,
      title: "莫斯科险遭无人机炸弹攻击",
      author: "小军",
      date: "2023-01-22 21:22:35",
      vote: 0,
    },
    {
      id: 3,
      title: "武汉官方把房地产归为困难行业",
      author: "小华",
      date: "2022-12-22 15:14:56",
      vote: 0,
    },
  ];

  //组件挂载到DOM后
  componentDidMount() {
    this.timer = setTimeout(() => {
      this.setState({
        posts: this.data,
      });
    }, 1000);
  }

  //组件从DOM中卸载
  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  handleVote(id) {
    //遍历所有的贴子,如果编号相同,则增加点赞后返回
    const posts = this.state.posts.map((item) =>
      item.id === id ? { ...item, vote: ++item.vote } : item
    );
    //重新设置状态
    this.setState({
      posts,
    });
  }

  render() {
    return (
      <div>
        <h2>帖子列表:</h2>
        <ul>
          {this.state.posts.map((item) => (
            <PostItem post={item} onVote={this.handleVote} />
          ))}
        </ul>
      </div>
    );
  }
}

export default PostList;

修改后的PostItem.js:

import React from "react";

function PostItem(props) {
  const { post, onVote } = props;
  const handleVote = () => {
    onVote(post.id);
  };

  return (
    <li>
      <div>{post.title}</div>
      <div>
        创建人:<span>{post.author}</span>
      </div>
      <div>
        创建时间:<span>{post.date}</span>
      </div>
      <div>
        <button onClick={handleVote}>点赞 {post.vote}</button>
      </div>
    </li>
  );
}

export default PostItem;

运行效果:

 

2.7、属性校验和默认属性

2.7.1.组件特殊属性——propTypes

对Component设置propTypes属性,可以为Component的props属性进行类型检查。

import React, { Component } from "react";
import PropTypes from "prop-types";

class Hello extends Component {
  render() {
    return <h2>Hello {this.props.name}!</h2>;
  }
}
//设置组件的props对象的类型信息
Hello.propTypes = {
  //要求name为string类型
  name: PropTypes.string,
};

export default Hello;

调用:

import ReactDOM from "react-dom/client";
import React from "react";
import Hello from "./Hello";

//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));

let vNode = <Hello name={99} />;

//3、将Vnode渲染到根结点上
root.render(vNode);

PropTypes提供了许多验证工具,用来帮助你确定props数据的有效性。在上面这个例子中,我们使用了PropTypes.stirng。意思是:name的值类型应该是string。 当Component的props接收到一个无效的值时,浏览器控制台就会输出一个警告。因此,<Hello name={99}/> 控制台会出现如下警告:

处于性能原因,类型检查仅在开发模式下进行。

2.7.2.PropTypes的更多验证器

import React from 'react';
import PropTypes from 'prop-types';

class MyComponent extends React.Component {
  render() {
    // 利用属性做更多得事
   }
}

MyComponent.propTypes = {
//你可以定义一个属性是特定的JS类型(Array,Boolean,Function,Number,Object,String,Symbol)。默认情况下,这些都是可选的。

optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,

//指定类型为:任何可以被渲染的元素,包括数字,字符串,react 元素,数组,fragment。
optionalNode: PropTypes.node,

// 指定类型为:一个react 元素
optionalElement: PropTypes.element,

//你可以类型为某个类的实例,这里使用JS的instanceOf操作符实现
optionalMessage: PropTypes.instanceOf(Message),


//指定枚举类型:你可以把属性限制在某些特定值之内
optionalEnum: PropTypes.oneOf(['News', 'Photos']),

// 指定多个类型:你也可以把属性类型限制在某些指定的类型范围内
optionalUnion: PropTypes.oneOfType([
  PropTypes.string,
  PropTypes.number,
  PropTypes.instanceOf(Message)
]),

// 指定某个类型的数组
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

// 指定类型为对象,且对象属性值是特定的类型
optionalObjectOf: PropTypes.objectOf(PropTypes.number),


//指定类型为对象,且可以规定哪些属性必须有,哪些属性可以没有
optionalObjectWithShape: PropTypes.shape({
  optionalProperty: PropTypes.string,
  requiredProperty: PropTypes.number.isRequired
}),

// 指定类型为对象,且可以指定对象的哪些属性必须有,哪些属性可以没有。如果出现没有定义的属性,会出现警告。
//下面的代码optionalObjectWithStrictShape的属性值为对象,但是对象的属性最多有两个,optionalProperty 和 requiredProperty。
//出现第三个属性,控制台出现警告。
optionalObjectWithStrictShape: PropTypes.exact({
  optionalProperty: PropTypes.string,
  requiredProperty: PropTypes.number.isRequired
}),

//加上isReqired限制,可以指定某个属性必须提供,如果没有出现警告。
requiredFunc: PropTypes.func.isRequired,
requiredAny: PropTypes.any.isRequired,

// 你也可以指定一个自定义的验证器。如果验证不通过,它应该返回Error对象,而不是`console.warn `或抛出错误。`oneOfType`中不起作用。
customProp: function(props, propName, componentName) {
  if (!/matchme/.test(props[propName])) {
    return new Error(
      'Invalid prop `' + propName + '` supplied to' +
      ' `' + componentName + '`. Validation failed.'
    );
  }
},

//你也可以提供一个自定义的验证器 arrayOf和objectOf。如果验证失败,它应该返回一个Error对象。
//验证器用来验证数组或对象的每个值。验证器的前两个参数是数组或对象本身,还有对应的key。
customArrayProp: PropTypes.arrayOf(function(propValue, key,     componentName, location, propFullName) {
  if (!/matchme/.test(propValue[key])) {
    return new Error(
      'Invalid prop `' + propFullName + '` supplied to' +
      ' `' + componentName + '`. Validation failed.'
    );
  }
})

示例:

Hello.js

import React, { Component } from "react";
import PropTypes from "prop-types";

class Hello extends Component {
  render() {
    return <h2>Hello {this.props.name}!</h2>;
  }
}
//设置组件的props对象的类型信息
Hello.propTypes = {
  //要求name为string类型
  name: PropTypes.string,
  sex: PropTypes.oneOf(["男", "女"]),
  //props 属性对象 ,propName 当前属性名,componentName组件名
  phone: function (props, propName, componentName) {
    if (!/\d{13}/.test(props[propName])) {
      return new Error("手机号码必须是13位的数字");
    }
  },
};

export default Hello;

index.js

import ReactDOM from "react-dom/client";
import React from "react";
import PostList from "./PostList";
import Hello from "./Hello";

//1、创建根节点
let root = ReactDOM.createRoot(document.getElementById("root"));

let vNode = <Hello name={99} sex="未知" phone="abc" />;

//3、将Vnode渲染到根结点上
root.render(vNode);

结果:

2.7.3. 必填

        static propTypes={

                属性:PropTypes.类型.(是否必填,不能为空)

        }
  //city必须是string类型且必须填写
  city: PropTypes.string.isRequired,

2.7.4. 子元素

限制单个子元素

使用 PropTypes.element 你可以指定只有一个子元素可以被传递给组件。

 //将children限制为单个子元素。
Greeting.propTypes = {
  name: PropTypes.string,
  children: PropTypes.element.isRequired
};

如果如下方式引用Greeting组件:

//传了两个子元素给组件Greeting那么,控制台会出现警告

 //传了两个子元素给组件Greeting那么,控制台会出现警告
<Greeting name={'world'}>
      <span>子元素1</span>
      <span>子元素2</span>
 </Greeting>

警告如图:

限制其它子元素

children: PropTypes.number,

获取子元素:

使用children

class Hello extends Component {
  render() {
    return (
      <h2>
        Hello {this.props.name}! {this.props.children}
      </h2>
    );
  }
}

使用属性

// index.tsx

import React from 'react'
import NavBar from './NavBar'

const ReactSlot = () => {
  return (
    <div>
      <NavBar 
        leftSlot={<div>left---这里内容可以随意填充</div>}
        centerSlot={<div>center---这里内容可以随意填充</div>}
        rightSlot={<div>right---这里内容可以随意填充</div>}
      ></NavBar>
    </div>
  )
}

export default ReactSlot
// NavBar.tsx

import React, { ReactNode } from 'react'
import './navbar.css'

type Props = {
  leftSlot: ReactNode
  centerSlot: ReactNode
  rightSlot: ReactNode
}
const NavBar = (props:Props) => {
  return (
    <div className='navbar-container'>
      <div className='navbar-left'>
        {props.leftSlot}
      </div>
      <div className='navbar-center'>
        {props.centerSlot}
      </div>
      <div className='navbar-right'>
        {props.rightSlot}
      </div>
    </div>
  )
}

export default NavBar

Layout.js

import PropTypes from "prop-types";
import React, { Component } from "react";
import "./layout.css";

export default class Layout extends Component {
  render() {
    return (
      <div className="container">
        <div className="item">{this.props.left}</div>
        <div className="item">{this.props.center}</div>
        <div className="item">{this.props.right}</div>
      </div>
    );
  }
}

layout.css

.container{
    display: flex;
}

.item{
    flex: auto;
    min-height: 200px;
    border: 1px solid #999;
    margin: 20px;
}

index.js

import React from "react";
import ReactDOM from "react-dom/client";
import Layout from "./Layout";

//虚拟DOM,JSX
const vnode = (
  <div>
    <Layout
      left={<h1>侧边栏1</h1>}
      center={<h1>中间区域2</h1>}
      right={<h1>右侧内容3</h1>}
    ></Layout>

    <Layout
      left={<h1>侧边栏a</h1>}
      center={<h1>中间区域b</h1>}
      right={<h1>右侧内容c</h1>}
    ></Layout>

    <Layout
      left={<h1>侧边栏d</h1>}
      center={<h1>中间区域e</h1>}
      right={<h1>右侧内容f</h1>}
    ></Layout>
  </div>
);

//2、创建根节点
const root = ReactDOM.createRoot(document.querySelector("#root"));

//3、将虚拟DOM节点挂载到根节点上
root.render(vnode);

效果:

2.7.5.指定默认属性值

你可以给组件分配一个特殊的defaultProps属性。

//给Greeting属性中的name值指定默认值。当组件引用的时候,没有传入name属性时,会使用默认值。
Greeting.defaultProps = {
  name: 'Stranger'
};

// ES6可以这样写

class Greeting extends React.Component {
  static defaultProps = {
    name: 'stranger'
  }
  render() {
    return (
      <div>Hello, {this.props.name}</div>
    )
  }
}

Hello.js

import React, { Component } from "react";
import PropTypes from "prop-types";

class Hello extends Component {
  render() {
    return (
      <h2>
        Hello {this.props.name}! {this.props.children} {this.props.city}
      </h2>
    );
  }
}
//设置组件的props对象的类型信息
Hello.propTypes = {
  //要求name为string类型
  name: PropTypes.string,
  sex: PropTypes.oneOf(["男", "女"]),
  //props 属性对象 ,propName 当前属性名,componentName组件名
  phone: function (props, propName, componentName) {
    if (!/\d{13}/.test(props[propName])) {
      return new Error("手机号码必须是13位的数字");
    }
  },
  children: PropTypes.number,
  //city必须是string类型且必须填写
  city: PropTypes.string.isRequired,
};

Hello.defaultProps = {
  city: "珠海",
};

export default Hello;

如果有默认值就变成了非必填了。

 2.8、React-组件样式的两种方式

与传统使用CSS给HTML添加样式的方式相比,React在给元素添加样式的时候方式有所不同。React的核心哲学之一就是让可视化的组件自包含,并且可复用。这就是为什么HTML元素和Javascript放在一起组成了(组件)。本节内容我们将介绍React定义样式的方式。

2.8.1、第一种方式:选择器样式

首先创建Person.css,定义我们所需要的样式,具体样式代码定义为如下形式:

.Person{width:60%;margin:16pxauto;border:1pxsolid#eee;box-shadow:2px3px#ccc;padding:16px;text-align:center;}

input{width:200px;border:1pxsolid#eee;outline:none;border-radius:10px;padding:16px;}

如果要使用样式,需要在Person.js组件中引入:

import'./Person.css'

这时可以查看页面变化了。

2.8.2、第二种方式:内联样式

React推崇的是内联的方式定义样式。这样做的目的就在于让你的组件更加的容易复用。下面给按钮添加一个内联样式:

来到App.js文件,将button按钮定义为如下形式:

style={{backgroundColor:'white',border:'1px solid blue',padding:'8px',cursor:'pointer'}}>

更改状态值

需要注意的是,JSX中使用样式对象定义内联样式,复合样式使用驼峰命名法,对象属性直接使用逗号隔开。

// 让数组中的每一项变成双倍
const numbers = [2,2,4,5];
const doubles = numbers.map((item,index) => {
  return <li style={{ color: 'red', fontWeight: 200 }} key={index}> {item * 2}}</li>
})

使用图片

import React from "react";

import like from "./assets/images/like.png";

function PostItem(props) {
  const { post, onVote } = props;
  const handleVote = () => {
    onVote(post.id);
  };

  return (
    <li>
      <div>{post.title}</div>
      <div>
        创建人:<span>{post.author}</span>
      </div>
      <div>
        创建时间:<span>{post.date}</span>
      </div>
      <div>
        <span>
          <img src={like} onClick={handleVote} />
        </span>
        <span>{post.vote}</span>
      </div>
    </li>
  );
}

export default PostItem;

注意图片不要放到src目录以外。

 

2.9、组件的生命周期

其实React组件并不是真正的DOM, 而是会生成JS对象的虚拟DOM, 虚拟DOM会经历创建,更新,删除的过程

这一个完整的过程就构成了组件的生命周期,React提供了钩子函数让我们可以在组件生命周期的不同阶段添加操作

在 React v17版本删除componentWillMount()componentWillReceiveProps()componentWillUpdate() 这三个函数,保留使用 UNSAFE_componentWillMount()UNSAFE_componentWillReceiveProps()UNSAFE_componentWillUpdate()

这张图是从 react生命周期链接里找的,里面有可以根据react不同版本查看对应的生命周期函数。

2.9.1、生命周期总览

常用的:

 

 不常用的:

react的生命周期大概分为

  • 组件装载(Mount)组件第一次渲染到Dom树
  • 组件更新(update)组件state,props变化引发的重新渲染
  • 组件卸载(Unmount)组件从Dom树删除

2.9.2、构造方法 constructor 

说明:

  1. 如果不需要初始化state,或者不进行方法的绑定,则不需要实现constructor构造函数
  2. 在组件挂载前调用构造函数,如果继承React.Component,则必须调用super(props)
  3. constructor通常用于处理了state初始化和为事件处理函数绑定this实例
  4. 尽量避免将props外部数据赋值给组件内部state状态

注意: constructor 构造函数只在初始化化的时候执行一次

示例代码如下:

import React, { Component } from "react";

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log(this);
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <h2>计算器</h2>
        <button onClick={this.handleClick}>点击加1</button>
        <span> {this.state.count} </span>
      </div>
    );
  }
}

export default Counter;

运行结果:

组件state也可以不定义在constructor构造函数中,事件函数也可以通过箭头函数处理this问题

因此如果不想使用constructor 也可以将两者移出

示例代码如下

import React, { Component } from "react";

class Counter extends Component {
  //初始化组件状态
  state = {
    count: 0,
  };

  //箭头函数处理this问题
  handleClick = () => {
    console.log(this);
    this.setState(() => ({ count: this.state.count + 1 }));
  };

  render() {
    return (
      <div>
        <h2>计算器</h2>
        <button onClick={this.handleClick}>点击加1</button>
        <span> {this.state.count} </span>
      </div>
    );
  }
}

export default Counter;

输出结果:

 箭头函数中的this指向了当前组件实例,而普通函数则指向undefined。箭头函数this是词法作用域,由上下文确定。

2.9.3、组件装载(Mount)

  • constructor: 在此初始化state,绑定成员函数this环境,props本地化,constructor 构造函数只在初始化化的时候执行一次
  • getDerivedStateFromProps:在创建时或更新时的render之前执行,让 props 能更新到组件内部 state中,必须是静态的。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。
  • render: 渲染函数,唯一的一定不能省略的函数,必须有返回值,返回null或false表示不渲染任何DOM元素。它是一个仅仅用于渲染的纯函数,返回值完全取决于this.state和this.props,不能在函数中任何修改props、state、拉取数据等具有副作用的操作。render函数返回的是JSX的对象,该函数并不因为这渲染到DOM树,何时进行真正的渲染是有React库决定的。
  • componentDidMount: 挂载成功函数。该函数不会再render函数调用完成之后立即调用,因为render函数仅仅是返回了JSX的对象,并没有立即挂载到DOM树上,而componentDidMount是在组件被渲染到DOM树之后被调用的。另外,componentDidMount函数在进行服务器端渲染时不会被调用。
import React, { Component } from "react";

class Hi extends Component {
  constructor(props) {
    super(props);
    console.log("constructor()函数被调用,构造函数");
    this.state = {
      n: 100,
    };
  }
  render() {
    console.log("render()函数被调用,返回UI描述");
    return <h2>Hello {this.props.name}!</h2>;
  }

  static getDerivedStateFromProps() {
    console.log(
      "getDerivedStateFromProps()函数被调用,让 props 能更新到组件内部 state中"
    );
    return null;
  }

  componentDidMount() {
    console.log("componentDidMount()函数被调用,组件被挂载到DOM后");
  }
}

export default Hi;

运行结果:

修改案例,查看this

import React, { Component } from "react";

class Hi extends Component {
  constructor(props) {
    super(props);
    console.log("constructor()函数被调用,构造函数", this);
    this.state = {
      n: 100,
    };
  }
  render() {
    console.log("render()函数被调用,返回UI描述", this);
    return <h2>Hello {this.props.name}!</h2>;
  }

  static getDerivedStateFromProps() {
    console.log(
      "getDerivedStateFromProps()函数被调用,让 props 能更新到组件内部 state中",
      this
    );
    return null;
  }

  componentDidMount() {
    console.log("componentDidMount()函数被调用,组件被挂载到DOM后", this);
  }
}

export default Hi;
View Code

2.9.4、getDerivedStateFromProps

getDerivedStateFromProps作用是为了让 props 能更新到组件内部 state中。他会在render方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。但注意在没有内容更新的情况下也一定要返回一个null值。不然会报错。

这个生命周期的意思就是从props中获取state,这个生命周期替换了原有的生命周期函数componentWillReceiveProps,getDerivedStateFromProps,它
是一个静态函数,也就是说不能通过this来访问class的属性,也不推荐直接访问属性。而是通过参数提供的nextPros以及prevState来进行判断,根据新传入的props来映射state
注意:如果传入的props不影响state,则必须返回一个null,一般尽量写在末尾。

counter3.js

import React, { Component } from "react";

export default class Counter3 extends Component {
  //构造器
  constructor(props) {
    super(props);
    this.state = { n: props.a };
    this.clickHandler = this.clickHandler.bind(this);

    console.log("constructor,构造器", this);
  }

  //获得状态从属性中
  static getDerivedStateFromProps(nextProps, state) {
    console.log("getDerivedStateFromProps,获得状态从属性中", this);
    console.log(nextProps, state);
    if (nextProps.a !== state.n) {
      return { n: nextProps.a };
    }
    return null;
  }

  clickHandler() {
    this.setState({ n: this.state.n + 1 });
  }

  //渲染UI
  render() {
    console.log("render,渲染UI", this);
    return (
      <div style={{ backgroundColor: "lightblue" }}>
        <h2>Counter3计数器</h2>
        <button onClick={this.clickHandler}>{this.state.n}</button>
      </div>
    );
  }

  //挂载完成
  componentDidMount() {
    console.log("componentDidMount,挂载完成", this);
  }
}

Box.js

import React, { Component } from "react";
import Counter3 from "./Counter3";

export default class Box extends Component {
  //是否显示
  state = { isShow: true, n: 100 };

  clickHandle = () => {
    this.setState({ n: this.state.n + 100 });
  };

  render() {
    return (
      <div style={{ backgroundColor: "lightgreen", padding: "20px" }}>
        <h2>Box父组件</h2>
        <Counter3 a={this.state.n} />
        <button onClick={this.clickHandle}>n={this.state.n}</button>
      </div>
    );
  }
}

index.js

import React from "react";
import ReactDOM from "react-dom/client";
import Box from "./Box";


//虚拟DOM,JSX
const vnode = (
  <div>
    <Box />
  </div>
);

//2、创建根节点
const root = ReactDOM.createRoot(document.querySelector("#root"));

//3、将虚拟DOM节点挂载到根节点上
root.render(vnode);

结果

2.9.5、组件更新(update)

当组件挂载到DOM树上之后,props/state被修改会导致组件进行更新操作。更新过程会以此调用如下的生命周期函数:

  • shouldComponentUpdate(nextProps, nextState):是否重新渲染组件 返回bool值,true表示要更新,false表示不更新,使用得当将大大提高React组件的性能,避免不需要的渲染。
  • getSnapshotBeforeUpdate:在render之后,React更新dom之前执行。它使您的组件可以在可能更改之前从DOM捕获一些信息(例如滚动位置),例如在聊天气泡页中用来计算滚动高度。它返回的任何值都将作为参数传递给componentDidUpdate()
  • render: 渲染函数
  • componentDidUpdate: 更新完成函数

2.9.6、shouldComponentUpdate函数

说明:

  1. shouldComponentUpdate函数使用来判读是否更新渲染组件
  2. 函数返回值是一个布尔值,为true,表示继续走其他的生命周期函数,更新渲染组件
  3. 如果返回一个false表示,不在继续向下执行其他的生命周期函数,到此终止,不更新组件渲染
  4. 函数接受两个参数, 第一个参数为props将要更新的数据, 第二个参数为state将要更新的数据
shouldComponentUpdate(nextProps, nextState)

根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染。大部分情况下,你应该遵循默认行为。

当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。首次渲染或使用 forceUpdate() 时不会调用该方法。

此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。你应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()PureComponent 会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。

如果你一定要手动编写此函数,可以将 this.props 与 nextProps 以及 this.state 与nextState 进行比较,并返回 false 以告知 React 可以跳过更新。请注意,返回 false 并不会阻止子组件在 state 更改时重新渲染。

我们不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能。

目前,如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()render() 和 componentDidUpdate()。后续版本,React 可能会将 shouldComponentUpdate 视为提示而不是严格的指令,并且,当返回 false 时,仍可能导致组件重新渲染。

import React, { Component } from "react";

class Counter extends Component {
  constructor(props) {
    super(props);
    console.log("constructor()构造函数", this);
  }

  //初始化组件状态
  state = {
    count: 0,
  };

  //箭头函数处理this问题
  handleClick = () => {
    console.log("调用this.setState更新状态", this);
    this.setState(() => ({ count: this.state.count + 1 }));
  };

  render() {
    console.log("render()渲染UI", this);
    return (
      <div>
        <h2>计算器</h2>
        <button onClick={this.handleClick}>点击加1</button>
        <span> {this.state.count} </span>
      </div>
    );
  }

  static getDerivedStateFromProps() {
    console.log(
      "getDerivedStateFromProps()函数被调用,让 props 能更新到组件内部 state中",
      this
    );
    return null;
  }

  //是否允许组件更新
  shouldComponentUpdate(props, state) {
    console.log("shouldComponentUpdate()函数被调用,", props, state, this);
    return true;
  }

  componentDidMount() {
    console.log("componentDidMount()挂载成功", this);
  }
}

export default Counter;

结果

2.9.7、componentDidUpdate函数

说明:

  1. 组件render执行后,页面渲染完毕了以后执行的函数
  2. 接受两个参数,分别为更新前的props和state
componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法。

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。

componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

你也可以在 componentDidUpdate() 中直接调用 setState(),但请注意它必须被包裹在一个条件语句里,正如上述的例子那样进行处理,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。不要将 props “镜像”给 state,请考虑直接使用 props。 欲了解更多有关内容,请参阅为什么 props 复制给 state 会产生 bug

如果组件实现了 getSnapshotBeforeUpdate() 生命周期(不常用),则它的返回值将作为 componentDidUpdate() 的第三个参数 “snapshot” 参数传递。否则此参数将为 undefined。

注意

如果 shouldComponentUpdate() 返回值为 false,则不会调用 componentDidUpdate()

import React, { Component } from "react";

class Counter extends Component {
  constructor(props) {
    super(props);
    console.log("constructor()构造函数", this);
  }

  //初始化组件状态
  state = {
    count: 0,
  };

  //箭头函数处理this问题
  handleClick = () => {
    console.log("调用this.setState更新状态", this);
    this.setState(() => ({ count: this.state.count + 1 }));
  };

  render() {
    console.log("render()渲染UI", this);
    return (
      <div>
        <h2>计算器</h2>
        <button onClick={this.handleClick}>点击加1</button>
        <span> {this.state.count} </span>
      </div>
    );
  }

  static getDerivedStateFromProps() {
    console.log(
      "getDerivedStateFromProps()函数被调用,让 props 能更新到组件内部 state中",
      this
    );
    return null;
  }

  //是否允许组件更新
  shouldComponentUpdate(props, state) {
    console.log(
      "shouldComponentUpdate()函数被调用,是否允许组件更新",
      props,
      state,
      this
    );
    return true;
  }

  //组件更新后
  componentDidUpdate(prevProps, prevState) {
    console.log(
      "componentDidUpdate()被调用,组件更新后",
      prevProps,
      prevState,
      this
    );
  }

  componentDidMount() {
    console.log("componentDidMount()挂载成功", this);
  }
}

export default Counter;

结果

 注意state是更新前的状态。

2.9.8、React.createRef() 非生命周期函数

获取非受控组件

可以通过React.createRef()创建Refs并通过ref属性联系到React组件。Refs通常当组件被创建时被分配给实例变量,这样它们就能在组件中被引用。

import React, { Component } from "react";

class RefDemoCom extends Component {
  constructor(props) {
    super(props);
    this.txtName = React.createRef();
  }

  clickHandler = () => {
    console.log(this.txtName);
    this.txtName.current.focus();
    this.txtName.current.value = new Date().toLocaleString();
    this.txtName.current.style.color = "blue";
  };

  render() {
    return (
      <div>
        <h2>React.createRef()</h2>
        <input type="text" ref={this.txtName} />
        <button onClick={this.clickHandler}>点击获得按钮</button>
      </div>
    );
  }
}

export default RefDemoCom;

 

2.9.9、getSnapshotBeforeUpdate函数

说明:

  1. getSnapshotBeforeUpdate必须跟componentDidUpdate一起使用,否则就报错
  2. 但是不能与componentWillReceivePropscomponentWillUpdate一起使用
  3. state和props更新都会触发这个函数的执行, 在render函数之后执行
  4. 接受两个参数,更新前的props和当前的state
  5. 函数必须return 返回结果
  6. getSnapshotBeforeUpdate返回的结果将作为参数传递给componentDidUpdate
getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()

此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。

应返回 snapshot 的值(或 null)。

例如:

 

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 我们是否在 list 中添加新的 items ?
    // 捕获滚动​​位置以便我们稍后调整滚动位置。
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
    // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
    //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

在上述示例中,重点是从 getSnapshotBeforeUpdate 读取 scrollHeight 属性,因为 “render” 阶段生命周期(如 render)和 “commit” 阶段生命周期(如 getSnapshotBeforeUpdate 和 componentDidUpdate)之间可能存在延迟。

示例:
import React, { Component } from "react";

class BeforeUpdateDemo extends Component {
  constructor(props) {
    super(props);
    this.state = { name: "tom" };
  }

  componentDidMount() {
    setTimeout(() => {
      this.setState({ name: "jack" });
    }, 1000);
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("更新前的状态:", prevState);
    document.getElementById("prev").innerHTML = "更新前:" + prevState.name;
    return document.getElementById("container").clientHeight;
  }

  componentDidUpdate(prevProps, prevState, preHeight) {
    console.log("更新后的状态:", this.state);
    document.getElementById("next").innerHTML = "更新后:" + this.state.name;
    const height = document.getElementById("container").clientHeight;
    console.log("原高度:" + preHeight);
    console.log("现高度:" + height);
  }

  render() {
    return (
      <div id="container">
        <h2>示例</h2>
        <div id="prev"></div>
        <div id="next"></div>
      </div>
    );
  }
}

export default BeforeUpdateDemo;

运行结果:

示例:

UserList.js

import React, { Component } from "react";

export default class UserList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 100,
      users: ["jack", "rose", "mali"],
    };
    this.div1 = React.createRef();
  }

  clickHandler = () => {
    this.setState({
      count: this.state.count + 1,
      users: [...this.state.users, this.state.count],
    });
  };

  render() {
    console.log("render被调用");
    return (
      <div
        id="div1"
        ref={this.div1}
        style={{ backgroundColor: "yellow", padding: "10px", margin: "10px" }}
      >
        <h2>用户信息</h2>
        <div>
          用户名:<span>{this.props.username}</span>
        </div>
        <div>
          <button onClick={this.clickHandler}>{this.state.count}</button>
        </div>
        <div>
          <ul>
            {this.state.users.map((user) => (
              <li key={user}>{user}</li>
            ))}
          </ul>
        </div>
      </div>
    );
  }

  //更新前获得DOM的快照信息
  getSnapshotBeforeUpdate(prevprops, prevstate) {
    console.log("上次的属性:", prevprops, "当前的属性:", this.props);
    console.log("上次的状态:", prevstate, "当前的状态:", this.state);
    console.log("getSnapshotBeforeUpdate被调用");
    console.log("更新前的DOM:", this.div1.current.innerHTML);
    return this.div1.current.clientHeight;
  }

  //更新完成时执行的函数
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("更新前DOM的高度值:", snapshot);
    console.log("当前DOM的高度值:", this.div1.current.clientHeight);
    console.log("componentDidUpdate被调用");
    console.log("更新后的DOM:", this.div1.current.innerHTML);
  }
}

Box.js

import React, { Component } from "react";
import Counter4 from "./Counter4";
import UserList from "./UserList";

export default class Box extends Component {
  //是否显示
  state = { isShow: true, x: 100, username: "jack" };

  clickHandle = () => {
    this.setState({
      x: this.state.x + 100,
      username: this.state.username + "!",
    });
  };

  render() {
    return (
      <div style={{ backgroundColor: "lightgreen", padding: "20px" }}>
        <h2>Box父组件</h2>
        <UserList username={this.state.username} />
        <button onClick={this.clickHandle}> a={this.state.x} </button>
      </div>
    );
  }
}

运行效果:

2.9.10.组件卸载(Unmount)

  • componentWillUnmount:当React组件要从DOM树上删除前,会调用一次这个函数。这个函数经常用于去除componentDidMount函数带来的副作用,例如清除计时器清除事件监听

示例:

componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。

componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

import React, { Component } from "react";

class HelloComponent extends Component {
  componentWillUnmount() {
    console.log("componentWillUnmount()被调用了!");
  }
  render() {
    return <div>HelloComponent被加载了</div>;
  }
}

class WillUnmountDemo extends Component {
  state = {
    i: 0,
  };

  handleClick = () => {
    this.setState({
      i: ++this.state.i,
    });
  };

  render() {
    return (
      <div>
        <h2>计算器</h2>
        <button onClick={this.handleClick}>i的当前值:{this.state.i}</button>
        {this.state.i % 2 === 0 ? (
          <HelloComponent />
        ) : (
          <h2>HelloComponent未加载</h2>
        )}
      </div>
    );
  }
}

export default WillUnmountDemo;

结果:

2.9.11、React V16.3 新增的生命周期方法

React v16.3虽然是一个小版本升级,但是却对React组件生命周期函数有巨大变化。

新增生命周期如下:

  • getDerivedStateFromProps
  • getSnapshotBeforeUpdate

同时逐渐废弃了以下生命周期:

  • componentWillReceiveProps
  • componentWillMount
  • componentWillUpdate

17 版本删除了 componentWillMount()componentWillReceiveProps()componentWillUpdate() 这三个函数,保留使用 UNSAFE_componentWillMount()UNSAFE_componentWillReceiveProps()UNSAFE_componentWillUpdate()

2.9.12、不能使用setState的生命周期

2.9.13、小结

三、作业

3.0、页面中有一个按钮,点击按钮显示隐藏右边的文字Hello,初始化时显示,点击第一次隐藏,再点击显示。。。

3.1、定义一个组件,当文本框中输入内容时在文本框后显示输入的值,双向绑定。

 

 

3.2、请完成课程中的所有示例。

3.3、请定义一个vue分页组件,可以实现客户端分页功能,接收参数

3.4、定义一个对话框组件,要求显示在body标签内

3.5、定义一个选项卡组件,3个不同的组件,点击卡片名称时动态切换。

3.6、完成如下示例

举例:举个在实际项目中使用此生命周期的例子,在聊天时的气泡页会遇到弹新的消息气泡场景,此时组件重新渲染了,如果不重新给外层包裹的dom计算滚动高度,会导致dom不停下滑。

在线demo: getSnapshotBeforeUpdate例子

动图封面

而使用getSnapshotBeforeUpdate可以优化此场景。在getSnapshotBeforeUpdate生命周期中记录外层dom元素的高度perScrollHeight,在componentDidUpdate中重新计算外层dom的scrollTop。此时,页面滚动一段距离不会出现消息跳动的现象 优化后的例子

this.listRef.current.scrollTop = curScrollTop + (this.listRef.current.scrollHeight - perScrollHeight)

代码: github代码

3.7、定义一个子组件,每隔1秒数字加1,在父组件中定义一个按钮进行显示隐藏子组件,隐藏子组件时要求停止计数,点击显示时从0开始重新计数。

3.8、

使用ant desgin完成一个后台管理界面

搭建一个后台系统 实现商品展示 以及 增删查改 并且点击菜单项的时候通过路由跳转页面,路由跳转组件>=3

跳转页面 1.表格数据 2.Form表单 3.跳转一个组件即可 

如果增删查改都写在表格页面  (另外两个路由随意跳转组件)  

主要目的提前预习路由熟悉路由匹配规则 少让老师操点心

使用react 脚手架 

npm i antd

npm i react-router-dom

npm i axios (也可以使用js对静态数据CRUD)

效果图如下(仅供参考)

四、视频

 https://www.bilibili.com/video/BV1Vb411D7qa/?share_source=copy_web&vd_source=475a31f3c5d6353a782007cd4c638a8a

五、源码

posted @ 2023-02-28 09:29  张果  阅读(1529)  评论(0编辑  收藏  举报
AmazingCounters.com