陌小北的头像

前端开发:React入门(一)

单集封面

前端开发:React入门(一)

2023-11-12
371 次观看
3 讨论
陌小北的头像
陌小北
粉丝:9
主题:19
描述:32
例子:21
类比:2
其他:13
字数:12034
陌小北的头像
陌小北
粉丝:9

概述

React的定位

React 是一个用于构建用户界面的库,React 不是一个框架

React主要目的

React 的主要目标是最大程度地减少开发人员构建 UI 时发生的错误。它通过使用组件——描述部分用户界面的、自包含的逻辑代码段——来实现此目的。

React使用JavaScript
const heading = <h1>Mozilla Developer Network</h1>;
创建React应用
npx create-react-app moz-todo-react

创建了一个名为 moz-todo-react 的文件夹, 并在此文件夹里做了如下工作: 1、为应用程序安装了一些 npm 包; 2、写入 react 应用启动所需要的脚本文件; 3、创建一系列结构化的子文件夹和文件,奠定应用程序的基础架构; 4、如果安装了 git 的话,顺便把 git 仓库也建好。

React用途

渲染特定环境 React用途

它的应用甚至不局限于 Web 开发,它可以与其他库一起使用以渲染到特定环境。

渲染Native环境 渲染特定环境

例如,React Native 可用于构建移动应用程序;

渲染虚拟现实 渲染特定环境

React 360 可用于构建虚拟现实应用程序……

构建WEB应用 React用途

为了构建 Web 应用,开发人员将 React 与 ReactDOM 结合使用。React 和 ReactDOM 通常被与其他真正的 Web 开发框架相提并论,并用于解决相同的问题。

解释 React与框架

当我们将 React 称为“框架”时,就是在进行口语化的理解。

元素

元素 React要素

元素是构成 React 应用的最小模块。

元素的作用

元素描述了你在屏幕上想看到的内容。

元素
const element = <h1>Hello, world</h1>;

元素渲染

根DOM节点

假设你的 HTML 文件某处有一个 <div>,我们将其称为“根” DOM 节点,因为该节点内的所有内容都将由 React DOM 管理。

根DOM节点
<div id="root"></div>

仅使用 React 构建的应用通常只有单一的根 DOM 节点。

集成现有应用

如果你在将 React 集成进一个已有应用,那么你可以在应用中包含任意多的独立根 DOM 节点。

元素渲染到根DOM

想要将一个 React 元素渲染到根 DOM 节点中,只需把它们一起传入 ReactDOM.createRoot():

将一个元素渲染为根DOM
const root = ReactDOM.createRoot(
  document.getElementById('root')
);
const element = <h1>Hello, world</h1>;
root.render(element);

更新渲染

元素不可变

React 元素是不可变对象。一旦被创建,你就无法更改它的子元素或者属性。

元素 电影单帧

一个元素就像电影的单帧:它代表了某个特定时刻的 UI。

回顾 更新元素

根据我们已有的知识,更新 UI 唯一的方式是创建一个全新的元素,并将其传入 root.render()。

计时器 更新元素
const root = ReactDOM.createRoot(
  document.getElementById('root')
);

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  root.render(element);}

setInterval(tick, 1000);

这个例子会在 setInterval() 回调函数,每秒都调用 root.render()。

实际 只调一次render

在实践中,大多数 React 应用只会调用一次 root.render()。

组件 & Props

组件的作用

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

组件 JavaScript函数

组件,从概念上类似于 JavaScript 函数。

组件的输入输出

它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。

函数组件

函数组件

定义组件最简单的方式就是编写 JavaScript 函数, 它本质上就是 JavaScript 函数。

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

class组件

class组件

同时还可以使用 ES6 的 class 来定义组件

class组件
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

与函数组件的例子是等效的

渲染组件

回顾 元素是标签

之前,我们遇到的 React 元素都只是 DOM 标签

const element = <div />;
元素是组件

不过,React 元素也可以是用户自定义的组件:

const element = <Welcome name="Sara" />;
props

当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称之为 “props”。

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

const root = ReactDOM.createRoot(document.getElementById('root'));
const element = <Welcome name="Sara" />;
root.render(element);

让我们来回顾一下这个例子中发生了什么: 1.我们调用 root.render() 函数,并传入 <Welcome name="Sara" /> 作为参数。 2.React 调用 Welcome 组件,并将 {name: 'Sara'} 作为 props 传入。 3.Welcome 组件将 <h1>Hello, Sara</h1> 元素作为返回值。 4.React DOM 将 DOM 高效地更新为 <h1>Hello, Sara</h1>。

tips 组件名必须大写开头

注意: 组件名称必须以大写字母开头。 React 会将以小写字母开头的组件视为原生 DOM 标签。例如,<div /> 代表 HTML 的 div 标签,而 <Welcome /> 则代表一个组件,并且需在作用域内使用 Welcome。

组合组件

引题 组件引用

组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。

组件的表现形式

按钮,表单,对话框,甚至整个屏幕的内容:在 React 应用程序中,这些通常都会以组件的形式表示。

组合组件

创建一个可以多次渲染 Welcome 组件的 App 组件

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

function App() {
  return (
    <div>
      <Welcome name="Sara" />      
      <Welcome name="Cahal" />      
      <Welcome name="Edite" />    
     </div>
  );
}
顶层组件

通常来说,每个新的 React 应用程序的顶层组件都是 App 组件。

组件集成

但是,如果你将 React 集成到现有的应用程序中,你可能需要使用像 Button 这样的小组件,并自下而上地将这类组件逐步应用到视图层的每一处。

提取组件

提取组件定义

将组件拆分为更小的组件。

提取组件

提取前:

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

提取后:

function Avatar(props) {
  return (
    <img className="Avatar"      
    src={props.user.avatarUrl}      
    alt={props.user.name}    />  );
}

function UserInfo(props) {
  return (
    <div className="UserInfo">      
        <Avatar user={props.user} />      
        <div className="UserInfo-name">        
            {props.user.name}     
         </div>    
     </div>  );
}

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />      
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}
总结 提取组件的必要性

最初看上去,提取组件可能是一件繁重的工作,但是,在大型应用中,构建可复用组件库是完全值得的。根据经验来看,如果 UI 中有一部分被多次使用(Button,Panel,Avatar),或者组件本身就足够复杂(App,FeedStory,Comment),那么它就是一个可提取出独立组件的候选项。

Props 的只读性

不能修改自身

组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。

纯函数

“纯函数”不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。

纯函数
function sum(a, b) {
  return a + b;
}
不是纯函数
function withdraw(account, amount) {
  account.total -= amount;
}
不允许改props React规则

React 非常灵活,但它也有一个严格的规则: 所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

铺垫 允许动态更改

当然,应用程序的 UI 是动态的,并会伴随着时间的推移而变化。在下一章节中,我们将介绍一种新的概念,称之为 “state”。 在不违反上述规则的情况下,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。

State & 生命周期

回顾 渲染元素

请参考前一章节中时钟的例子。在元素渲染章节中,我们只了解了一种更新 UI 界面的方法。 通过调用 root.render() 来修改我们想要渲染的元素:

const root = ReactDOM.createRoot(document.getElementById('root'));
  
function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  root.render(element);}

setInterval(tick, 1000);
概括 封装组件

在本章节中,我们将学习如何封装真正可复用的 Clock 组件。它将设置自己的计时器并每秒更新一次。

组件转换

函数组件转换class组件

通过以下五步将 Clock 的函数组件转成 class 组件:

  1. 创建一个同名的 ES6 class,并且继承于 React.Component。

  2. 添加一个空的 render() 方法。

  3. 将函数体移动到 render() 方法之中。

  4. 在 render() 方法中使用 this.props 替换 props。

  5. 删除剩余的空函数声明。

转换实例
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
结果 组件转化为class

现在 Clock 组件被定义为 class,而不是函数。

class实例创建时机

每次组件更新时 render 方法都会被调用,但只要在相同的 DOM 节点中渲染 <Clock /> ,就仅有一个 Clock 组件的 class 实例被创建使用。 这就使得我们可以使用如 state 或生命周期方法等很多其他特性。

添加局部state

class组件添加局部state

我们通过以下三步将 date 从 props 移动到 state 中:

1.把 render() 方法中的 this.props.date 替换成 this.state.date

class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>      </div>
    );
  }
}

2.添加一个 class 构造函数,然后在该函数中为 this.state 赋初值:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

通过以下方式将 props 传递到父类的构造函数中:

  constructor(props) {
    super(props);    this.state = {date: new Date()};
  }

Class 组件应该始终使用 props 参数来调用父类的构造函数。

3.移除 <Clock /> 元素中的 date 属性:

root.render(<Clock />);

我们之后会将计时器相关的代码添加到组件中。

整体代码如下:

class Clock extends React.Component {
  constructor(props) {    
      super(props);    
      this.state = {date: new Date()};  
   }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>      
      </div>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);
过渡 引出后续每秒更新计时器

接下来,我们会设置 Clock 的计时器并每秒更新它。

添加生命周期方法

释放资源

在具有许多组件的应用程序中,当组件被销毁时释放所占用的资源是非常重要的。

挂载

当 Clock 组件第一次被渲染到 DOM 中的时候,就为其设置一个计时器。这在 React 中被称为“挂载(mount)”。

卸载

同时,当 DOM 中 Clock 组件被删除的时候,应该清除计时器。这在 React 中被称为“卸载(unmount)”。

生命周期方法

我们可以为 class 组件声明一些特殊的方法,当组件挂载或卸载时就会去执行这些方法: 这些方法叫做“生命周期方法”。

生命周期方法生命周期
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {  }
  componentWillUnmount() {  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

componentDidMount() 方法会在组件已经被渲染到 DOM 中后运行,所以,最好在这里设置计时器:

  componentDidMount() {
    this.timerID = setInterval(     
     () => this.tick(), 1000    
     );  
   }

接下来把计时器的 ID 保存在 this 之中(this.timerID)。

尽管 this.props 和 this.state 是 React 本身设置的,且都拥有特殊的含义,但是其实你可以向 class 中随意添加不参与数据流(比如计时器 ID)的额外字段。

我们会在 componentWillUnmount() 生命周期方法中清除计时器:

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

最后,我们会实现一个叫 tick() 的方法,Clock 组件每秒都会调用它。 使用 this.setState() 来时刻更新组件 state:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

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

  tick() {    
      this.setState({  
          date: new Date()    
     });  
  }
  
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);

现在时钟每秒都会刷新。

让我们来快速概括一下发生了什么和这些方法的调用顺序:

  1. 当 <Clock /> 被传给 root.render()的时候,React 会调用 Clock 组件的构造函数。因为 Clock 需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化 this.state。我们会在之后更新 state。

  2. 之后 React 会调用组件的 render() 方法。这就是 React 确定该在页面上展示什么的方式。然后 React 更新 DOM 来匹配 Clock 渲染的输出。

  3. 当 Clock 的输出被插入到 DOM 中后,React 就会调用 ComponentDidMount() 生命周期方法。在这个方法中,Clock 组件向浏览器请求设置一个计时器来每秒调用一次组件的 tick() 方法。

  4. 浏览器每秒都会调用一次 tick() 方法。 在这方法之中,Clock 组件会通过调用 setState() 来计划进行一次 UI 更新。得益于 setState() 的调用,React 能够知道 state 已经改变了,然后会重新调用 render() 方法来确定页面上该显示什么。这一次,render() 方法中的 this.state.date 就不一样了,如此一来就会渲染输出更新过的时间。React 也会相应的更新 DOM。

  5. 一旦 Clock 组件从 DOM 中被移除,React 就会调用 componentWillUnmount() 生命周期方法,这样计时器就停止了。

##正确使用

概括 三件事

关于 setState() 你应该了解三件事:

不可直接修改

不要直接修改 State

例如,此代码不会重新渲染组件:

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

而是应该使用 setState():

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

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

更新异步

更新异步

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。 因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。

错误示例 更新异步

例如,此代码可能会无法更新计数器:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});
正确示例 更新异步

要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));
正确示例 更新异步

上面使用了箭头函数,不过使用普通的函数也同样可以:

// Correct
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

合并更新

合并state

当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state。 例如,你的 state 包含几个独立的变量:

  constructor(props) {
    super(props);
    this.state = {
      posts: [],      
      comments: []    
      };
  }

然后你可以分别调用 setState() 来单独地更新它们:

  componentDidMount() {
    fetchPosts().then(
        response => {
          this.setState({
        posts: response.posts      
        });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments      
        });
    });
  }

这里的合并是浅合并,所以 this.setState({comments}) 完整保留了 this.state.posts, 但是完全替换了 this.state.comments。

讨论
随记