快来跟我一起学 React(Day5)

简介

上一节我们完成了从 0 开始搭建一个企业级 React 项目的全部内容,项目是有了,但是我们一直都没有近距离接触过 React,所以接下来我们就快速撸一遍 React 官方文档内容,弄清楚一些概念性的东西,为后面的源码分析章节做铺垫。

知识点

  • 项目搭建
  • 核心概念
  • 高级指引
  • API 指引
  • hook 指引

后面这几节都比较轻松,因为我们基本上把 React 官网:https://reactjs.org/ 的内容跑一遍。

让我们开始吧!

项目搭建

我们直接 clone 一个前面我们搭建的基础项目,然后取名字为 react-demo-day5

git clone https://gitee.com/vv_bug/cus-react-demo.git react-demo-day5

接着我们打开 react-demo-day5 目录,并且安装 npm 依赖:

cd react-demo-day5 && npm install --registry https://registry.npm.taobao.org

然后我们在 react-demo-day5 目录下执行 npm start 命令启动项目:

npm start

启动项目后,浏览器会自动打开我们项目的入口页面:

1-1.png

到这,我们的准备工作就算是完成了。

组件 & Props

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

React 中有 “函数式” 与 ”类组件“ 之分,下面我们就通过 Demo 来演示一下。

在开始之前,我们先修改一下当前项目结构。

首先修改一下 src/main.tsx 文件:

import React from "react";
import ReactDOM from "react-dom";
import "./main.scss";
import MainConcepts from "./main-concepts";
// App 组件
const App = (
    
{/* 核心概念 */}
); ReactDOM.render( App, document.getElementById("root") );

可以看到,我们抽离了一个 App 组件实例,然后在 App 中引入了 MainConcepts 组件。

接下来我们在 src 目录中创建一个 main-concepts 目录,然后在 src/main-concepts 目录下创建一个 index.tsx 文件:

mkdir ./src/main-concepts && touch ./src/main-concepts/index.tsx

然后将以下内容写入到 src/main-concepts/index.tsx 文件:

import ComponentsAndProps from "./components-and-props";

/**
 * 核心概念列表组件
 */
function mainConcepts() {
    return (
        
{/* 组件与属性 */}
); }; export default mainConcepts;

接着在 src/main-concepts 目录下创建一个 components-and-props 目录,并在 components-and-props 目录下创建一个 index.tsx 文件:

mkdir ./src/main-concepts/components-and-props && touch ./src/main-concepts/components-and-props/index.tsx

然后将以下内容写入到 src/main-concepts/components-and-props/index.tsx 文件:

import React from "react";
import WelcomeCom from "./welcome.com";
import WelcomeFunc from "./welcome.func";

function componentsAndProps() {
    return (
        
            {/* 类组件 */}
            
            {/* 函数式组件 */}
            
        
    );
};
export default componentsAndProps;

类组件

继续在 src/main-concepts/components-and-props 下创建一个 welcome.com.tsx 文件作为类组件:

touch ./src/main-concepts/components-and-props/welcome.com.tsx

然后将以下内容写入到 src/main-concepts/components-and-props/welcome.com.tsx 组件:

import React from "react";
import PropTypes from "prop-types";
type Prop = {
    name: string, // 姓名
};
class Welcome extends React.Component {
    static propTypes = {
        name: PropTypes.string,
    };
    static defaultProps = {
        name: "小虫"
    };

    render() {
        return 

我是类组件,Hello, {this.props.name}

; } } export default Welcome;

可以看到,我们用类组件方式定义了一个 Welcome 组件,然后在 Welcome 组件中定义了一个 name 属性,并且利用 tsprop-types 对属性进行了校验,一个简单的 “React 类组件” 就创建完成了。

函数式组件

同样在src/main-concepts/components-and-props 下创建一个 welcome.func.tsx 文件作为函数式组件:

touch ./src/main-concepts/components-and-props/welcome.func.tsx

然后将以下内容写入到 src/main-concepts/components-and-props/welcome.func.tsx 组件:

import PropTypes from "prop-types";
type Prop = {
    name: string, // 姓名
};
function Welcome(props: Prop) {
    return 

我是函数式组件,Hello, {props.name}

; } Welcome.propTypes={ name: PropTypes.string }; Welcome.defaultProps = { name: "小虫" }; export default Welcome;

可以看到,我们用函数式组件方式定义了一个 Welcome 组件,然后在 Welcome 组件中定义了一个 name 属性,并且利用 tsprop-types 对属性进行了校验,一个简单的 “React 函数式组件” 就创建完成了。

运行

react-demo-day5 项目根目录下执行 npm start 命令重新启动项目:

npm start
1-2.png

可以看到,两个组件都正常显示到了页面。

组合组件

组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。按钮,表单,对话框,甚至整个屏幕的内容:在 React 应用程序中,这些通常都会以组件的形式表示。

例如,我们的 src/main-concepts/components-and-props/index.tsx 组件:

import React from "react";
import WelcomeCom from "./welcome.com";
import WelcomeFunc from "./welcome.func";

function componentsAndProps() {
    return (
        
            {/* 类组件 */}
            
            {/* 函数式组件 */}
            
        
    );
};
export default componentsAndProps;

我们把 “函数式组件” 跟 “类组件” 组合到了一个组件中。

Props 的只读性

React 中,组件决不能修改自身的 props。

React 非常灵活,但它也有一个严格的规则:

所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

我们可以试一下,比如我们修改一下 src/main-concepts/components-and-props/welcome.com.tsx 文件:

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

type Prop = {
    name: string, // 姓名
};

class Welcome extends React.Component {
    static propTypes = {
        name: PropTypes.string,
    };
    static defaultProps = {
        name: "小虫"
    };

    render() {
        console.log(Object.isFrozen(this.props));
        this.props.name = "小虫虫";
        return 

我是类组件,Hello, {this.props.name}

; } } export default Welcome;

可以看到,我们在 render 方法中试着去修改 name 属性值,并且我们打印了 this.props 是否是 Object.freeze 类型:

console.log(Object.isFrozen(this.props));
this.props.name = "小虫虫";

我们保存文件等自动编译完成:


1-3.png

可以看到,三处报错了:

  1. Webpack 编译直接报错了,说 “我们不能修改只读属性”。
  2. IDE 也报错了,主要是 Eslint 的配置。
  3. 浏览器也报错了,说 “遇到了未知异常”。
  4. Object.isFrozen(this.props) 返回了 true

从上面可以看出,我们利用了 TypeScriptEslint 等规则在写代码的时候就已经成功避免了这类错误的出现,最后 ReactJs 还会直接渲染报错,因为我们对一个 Object.freeze 类型的对象进行了修改操作。

当然,即使有各种条件的限制,但是我们还是可以变相的去修改 props的值,比如我们把一个属性定义为 object 类型,我们还是可以在子组件中修改这个属性的某些值,虽然我们可以这样做,但是在开发的时候千万不要这么干哈,因为在某些大项目中,当进行变量追踪的时候,你压根就不知道是谁修改了这个属性的内容,这样就很容易出错了, 我就不演示了。

State & 生命周期

State

State 相当于 MVVM 模式中的 ViewModel,通过监听对比 ViewModel 的变化,最后实现页面的更新,每个组件都可以定义自己的 state

我们在 src/main-concepts 目录下创建一个 state-and-lifecycle 目录:

mkdir ./src/main-concepts/state-and-lifecycle

然后在 /src/main-concepts/state-and-lifecycle 中创建一个 index.tsx 文件:

import React from "react";
import StateComponent from "./state.com";
import StateFunc from "./state.func";

function stateAndLifecycle() {
    return (
        
            {/* 类组件带 state */}
            
            {/* 函数组件带 state */}
            
        
    );
};
export default stateAndLifecycle;

类组件带 State

/src/main-concepts/state-and-lifecycle 中创建一个 state.com.tsx 文件:

import React from "react";

type State = {
    status: boolean
};

class StateComponent extends React.Component {
    state = {
        status: true
    }

    render() {
        return (
            
我是类组件:{this.state.status ? "on" : "off"}
); } /** * 切换状态 */ onToggle() { // 修改 status 状态 this.setState((state) => { return { status: !state.status }; }); } } export default StateComponent;

可以看到,我们在类组件 state.com.tsx 中定义了一个 state,然后给 div 元素添加了一个点击事件,最后在点击事件 onToggle 回调中用 setState 修改了 status 的值。

函数组件带 State

/src/main-concepts/state-and-lifecycle 中创建一个 state.func.tsx 文件:

import React, {useState} from "react";

function StateFunc() {
    let [status, setStatus] = useState(true);

    function onToggle() {
        setStatus(!status);
    }

    return (
        
我是函数组件:{status ? "on" : "off"}
); } export default StateFunc;

可以看到,我们直接利用了 useState 这个 Hook 定义了一个 state,跟上面的类组件一样,在点击事件中修改了 status 的值,之前说函数式组件是 “无状态的”,但是利用了 Hook,我们同样是可以让一个函数式组件也具备 StateHook 的内容我们后面再详细解析。

我们保存等项目重新编译看结果:

1-4.png

当我们点击对应文字区域的时候,页面会进行 onoff 的切换效果,我就不演示了哈,小伙伴自己试试。

正确地使用 State

  • 不要直接修改 State
  • State 的更新可能是异步的
  • State 的更新会被合并

生命周期

先上一张官方提供的 React 的生命周期图:

1-5.png

图片来源:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

生命周期我们后期源码解析的时候再详细讲解。

事件处理

React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。

例如,传统的 HTML:


在 React 中略微不同:


在 React 中另一个不同点是你不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault 。例如,传统的 HTML 中阻止链接默认打开一个新页面,你可以这样写:


  Click me

在 React 中,可能是这样的:

function ActionLink() {
  function handleClick(e) {    
        e.preventDefault();    
        console.log('The link was clicked.');  
    }
  return (
          
      Click me
    
  );
}

因为 eReact 生成的一个合成事件,React 事件与原生事件不完全相同。

上面例子中有演示过的,就不再演示了。

条件渲染

因为 React 中可以使用 JSX 语法,所以我们可以在 JSX 语法中进行条件判断做渲染就可以了。

元素变量

你可以使用变量来储存元素。 它可以帮助你有条件地渲染组件的一部分,而其他的渲染部分并不会因此而改变。

我们还是来演示一下吧。

首先在 src/main-concepts 目录下创建一个 condition-render 目录:

mkdir ./src/main-concepts/condition-render

然后在 src/main-concepts/condition-render 目录下创建一个 index.tsx 文件:

import React from "react";
import ConditionFunc from "./condition.func";

function stateAndLifecycle() {
    return (
        
            {/* 函数式组件带条件渲染 */}
            
        
    );
};
export default stateAndLifecycle;

接着在 src/main-concepts/condition-render 目录下创建一个 condition.func.tsx 文件:

import React, {useState} from "react";

function ConditionFunc() {
    let [isLoggedIn, setLogged] = useState(true);

    function handleLogin() {
        setLogged(true);
    }

    function handleLogout() {
        setLogged(false);
    }

    let button = null;
    if (isLoggedIn) {
        button = (
            
        );
    } else {
        button = (
            
        );
    }

    return (
        
{isLoggedIn && "恭喜,登录成功!"} {button}
); } export default ConditionFunc;

可以看到,我们利用 button 变量充当了一个元素,然后通过 StateisLoggedIn 变量进行条件判断,对 button 变量进行赋值。

最后我们在 src/main-concepts/index.tsx 文件中引入 src/main-concepts/condition-render/index.tsx 组件测试:

1-6.gif

可以看到,页面中根据我们的点击条件渲染了不同的状态。

与运算符 &&

通过花括号包裹代码,你可以在 JSX 中嵌入表达式。这也包括 JavaScript 中的逻辑与 (&&) 运算符。它可以很方便地进行元素的条件渲染。

比如上面的condition.func.tsx 文件,我们用 “运算符 &&” 方式来改造一下:

import React, {useState} from "react";

function ConditionFunc() {
    let [isLoggedIn, setLogged] = useState(true);

    function handleLogin() {
        setLogged(true);
    }

    function handleLogout() {
        setLogged(false);
    }

    return (
        
{isLoggedIn && "恭喜,登录成功!"} {isLoggedIn && ()} {!isLoggedIn && ( )}
); } export default ConditionFunc;

三目运算符

另一种内联条件渲染的方法是使用 JavaScript 中的三目运算符 condition ? true : false

比如上面的condition.func.tsx 文件,我们用 “三目运算符” 方式来改造一下:

import React, {useState} from "react";

function ConditionFunc() {
    let [isLoggedIn, setLogged] = useState(true);

    function handleLogin() {
        setLogged(true);
    }

    function handleLogout() {
        setLogged(false);
    }

    return (
        
{ isLoggedIn ? "恭喜,登录成功!" : "" } { isLoggedIn ? ( ) : ( ) }
); } export default ConditionFunc;

后面两种效果跟第一种一样,我就不演示了。

不过在平时的项目开发中,面对复杂一点的逻辑判断,不建议用后两种内联方式,因为对代码的可读性跟调试都不友好。

列表 & Key

在 React 中,我们只需要把数组转化为元素列表就可以了。

我们来演示一下。

元素变量数组

首先一样的套路,在 src/main-concepts 目录下创建一个 list-and-key 目录:

mkdir ./src/main-concepts/list-and-key

然后在 src/main-concepts/list-and-key 目录下创建一个 index.tsx 文件:

import React from "react";
import ListFunc from "./list.func";

function ListAndKey() {
    return (
        
            {/* 函数组件列表渲染 */}
            
        
    );
};
export default ListAndKey;

接着在 src/main-concepts/list-and-key 下创建一个 list.func.tsx 文件:

import React, {useState} from "react";

function ListFunc() {
    let [todos] = useState>(["React", "Vue", "Angular"]);
    let todoElements = todos.map((todo) => (
  • {todo}
  • )); return (
      {todoElements}
    ); } export default ListFunc;

    可以看到,我们用了一个元素数组 todoElements 变量来承载了我们所有需要渲染的元素,最后利用 JSX 语法渲染。

    最后在 src/main-concepts/index.tsx 中引入 src/main-concepts/list-and-key/index.tsx 组件:

    import ComponentsAndProps from "./components-and-props";
    import StateAndLifecycle from "./state-and-lifecycle";
    import ConditionRender from "./condition-render";
    import ListAndKey from "./list-and-key";
    
    /**
     * 核心概念列表组件
     */
    function mainConcepts() {
        return (
            
    {/* 组件与属性 */} {/* State & 生命周期 */} {/* 条件渲染 */} {/* 列表与 key */}
    ); }; export default mainConcepts;

    我们重新运行项目看效果:

    npm start
    
    1-7.png

    可以看到,页面中正常渲染了我们的 todos 列表。

    在 JSX 中嵌入 map()

    我们可以直接把 map 放在 JSX 语法中。

    比如我们重构一下上面的 list.func.tsx 组件:

    import React, {useState} from "react";
    
    function ListFunc() {
        let [todos] = useState>(["React", "Vue", "Angular"]);
        return (
            
      {todos.map((todo) => (
    • {todo}
    • ))}
    ); } export default ListFunc;

    效果跟上面的一样,我就不演示了。

    不过还是那句话,简单的逻辑可以用 JSX 内联语法操作,复杂的逻辑就不建议用内联了,对调试跟代码的可读性都不友好。

    key

    key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。

    我们现在没有提供 key,在开发模式中会报错:

    1-10.png

    我们需要修改一下 list.func.tsx 组件:

    import React, {useState} from "react";
    
    function ListFunc() {
        let [todos] = useState>(["React", "Vue", "Angular"]);
        return (
            
      {todos.map((todo) => (
    • {todo}
    • ))}
    ); } export default ListFunc;

    可以看到,我们给每一个 li 标签添加了一个 key 属性(数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的)。

    表单

    受控组件

    输入的值始终又 ReactState 控制的组件就叫 “受控组件”。

    我们来演示一下。

    首先在 src/main-concepts 目录下创建一个 form 目录:

    mkdir ./src/main-concepts/form
    

    接着在 src/main-concepts/form 目录中创建一个 index.tsx 文件:

    import React from "react";
    import ControlledFunc from "./controlled.func";
    
    function Form() {
        return (
            
                {/* 函数组件之受控组件 */}
                
            
        );
    };
    export default Form;
    

    然后在 src/main-concepts/form 目录中创建一个 controlled.func.tsx 文件:

    import React, {useState} from "react";
    
    function ControlledFunc() {
        let [name, setName] = useState("");
    
        function handleInput(event: any) {
            setName(event.target.value);
        }
    
        return (
            
    {name}
    ); } export default ControlledFunc;

    可以看到,我们用了一个 Statename 的属性值,通过监听 input 标签的 onInput 事件,然后把输入的值赋给了 name 变量,最后 Statename 变量又控制着 input 的输入值,这样一个受控组件就创建完毕了。

    接着我们在 src/main-concepts/index.tsx 组件中引入 src/main-concepts/form/index.tsx 组件:

    import ComponentsAndProps from "./components-and-props";
    import StateAndLifecycle from "./state-and-lifecycle";
    import ConditionRender from "./condition-render";
    import ListAndKey from "./list-and-key";
    import Form from "./form";
    
    /**
     * 核心概念列表组件
     */
    function mainConcepts() {
        return (
            
    {/* 组件与属性 */} {/* State & 生命周期 */} {/* 条件渲染 */} {/* 列表与 key */} {/* 表单-受控组件 */}
    ); }; export default mainConcepts;

    我们重新运行 npm start 命令开启项目看结果:

    npm start
    
    1-8.gif

    可以看到,当我们输入的时候,State 中的 name 变量实时跟 input 输入的值绑定。

    textareaselect 等其它的 form 标签也可以进行同样的操作,就不一一演示了。

    状态提升

    通常,state 都是首先添加到需要渲染数据的组件中去,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中,这种操作就叫 “状态提升”。

    我们还是通过 Demo 来演示一下吧。

    我们首先在 src/main-concepts 目录下创建一个 lifting-state-up 目录:

    mkdir ./src/main-concepts/lifting-state-up
    

    然后在 src/main-concepts/lifting-state-up 目录下创建一个 index.tsx 文件:

    import React, {useState} from "react";
    import StateUpCom from "./state-up.com";
    
    function LiftingStateUp() {
      let [price, setPrice] = useState(0);
      let [count, setCount] = useState(0);
    
      /**
       * 处理单价
       */
      function handlePriceInput(event: any) {
        setPrice(parseFloat(event.target.value));
      }
    
      /**
       * 处理数量
       */
      function handleCountInput(event: any) {
        setCount(parseFloat(event.target.value));
      }
    
      // 计算总价
      let total = count * price;
      return (
    
        
    {/* 状态提示--价格 */ } {/* 状态提示--数量 */ } 总价:{ total }
    ); } export default LiftingStateUp;

    接着在 src/main-concepts/lifting-state-up 目录下创建一个 state-up.com.tsx 组件:

    import React from "react";
    import PropTypes from "prop-types";
    
    type HandleInputFunc = (event: any) => void;
    type Prop = {
      title: string,
      value: number,
      handleInput: HandleInputFunc,
    };
    
    class StateUpCom extends React.Component {
      static propTypes = {
        title: PropTypes.string, // 标题
        value: PropTypes.number, // 输入值
        handleInput: PropTypes.func, // 处理输入监听函数
      }
      static defaultProps = {
        title: "",
        value: 0
      }
    
      render() {
        const {title, value, handleInput} = this.props;
        return (
          
    {title}
    ); } } export default StateUpCom;

    可以看到,我们把 StateUpCom 组件的 input 输入值通过handleInput 提升到了 “父组件” lifting-state-up/index.tsx

    最后我们在 src/main-concepts/index.tsx 组件中引入 src/main-concepts/lifting-state-up/index.tsx 组件:

    import ComponentsAndProps from "./components-and-props";
    import StateAndLifecycle from "./state-and-lifecycle";
    import ConditionRender from "./condition-render";
    import ListAndKey from "./list-and-key";
    import Form from "./form";
    import LiftingStateUp from "./lifting-state-up";
    
    /**
     * 核心概念列表组件
     */
    function mainConcepts() {
        return (
            
    {/* 组件与属性 */} {/* State & 生命周期 */} {/* 条件渲染 */} {/* 列表与 key */} {/* 表单-受控组件 */} {/* 状态提升 */}
    ); }; export default mainConcepts;

    我们重新运行项目看结果:

    npm start
    
    1-9.gif

    可以看到,子组件中的输入值都提升到了父组件,父组件会根据子组件中的输入值自动算出总价的值。

    总结

    我们照着 React 官网:https://reactjs.org/ 的内容跑了一遍 React 的所有核心概念,虽然有些概念可能很简单,但是搞技术的切勿眼高手低,有些看似很简单的东西,看千遍不如自己敲一遍,弄清这些概念对我们后面分析 React 的源码很有帮助,后面我们还会对 React 的高级特性以及一些 API 做解析。

    ok,这节就先到这了,下节见!

    本节内容的 Demo 项目地址:https://gitee.com/vv_bug/react-demo-day5/tree/dev/

    你可能感兴趣的:(快来跟我一起学 React(Day5))