前端开发那些事儿

快来跟我一起学 React(Day5)

2021-03-22  本文已影响0人  vv_小虫虫

简介

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

知识点

后面这几节都比较轻松,因为我们基本上把 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 = (
    <div className="root">
        {/* 核心概念 */}
        <MainConcepts/>
    </div>
);
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 (
        <div>
            {/* 组件与属性 */}
            <ComponentsAndProps/>
        </div>
    );
};
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 (
        <React.Fragment>
            {/* 类组件 */}
            <WelcomeCom/>
            {/* 函数式组件 */}
            <WelcomeFunc/>
        </React.Fragment>
    );
};
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<Prop> {
    static propTypes = {
        name: PropTypes.string,
    };
    static defaultProps = {
        name: "小虫"
    };

    render() {
        return <h1>我是类组件,Hello, {this.props.name}</h1>;
    }
}
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 <h1>我是函数式组件,Hello, {props.name}</h1>;
}
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 (
        <React.Fragment>
            {/* 类组件 */}
            <WelcomeCom/>
            {/* 函数式组件 */}
            <WelcomeFunc/>
        </React.Fragment>
    );
};
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<Prop> {
    static propTypes = {
        name: PropTypes.string,
    };
    static defaultProps = {
        name: "小虫"
    };

    render() {
        console.log(Object.isFrozen(this.props));
        this.props.name = "小虫虫";
        return <h1>我是类组件,Hello, {this.props.name}</h1>;
    }
}

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 (
        <React.Fragment>
            {/* 类组件带 state */}
            <StateComponent/>
            {/* 函数组件带 state */}
            <StateFunc/>
        </React.Fragment>
    );
};
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<any, State> {
    state = {
        status: true
    }

    render() {
        return (
            <div
                onClick={this.onToggle.bind(this)}
            >
                我是类组件:{this.state.status ? "on" : "off"}
            </div>
        );
    }

    /**
     * 切换状态
     */
    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<boolean>(true);

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

    return (
        <div
            onClick={onToggle}
        >
            我是函数组件:{status ? "on" : "off"}
        </div>
    );
}

export default StateFunc;

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

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

1-4.png

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

正确地使用 State

生命周期

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

1-5.png

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

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

事件处理

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

例如,传统的 HTML:

<button onclick="activateLasers()">
  Activate Lasers
</button>

在 React 中略微不同:

<button onClick={activateLasers}>  
  Activate Lasers
</button>

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

<a href="#" onclick="console.log('The link was clicked.'); return false">
  Click me
</a>

在 React 中,可能是这样的:

function ActionLink() {
  function handleClick(e) {    
        e.preventDefault();    
        console.log('The link was clicked.');  
    }
  return (
    <a href="#" onClick={handleClick}>      
      Click me
    </a>
  );
}

因为 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 (
        <React.Fragment>
            {/* 函数式组件带条件渲染 */}
            <ConditionFunc/>
        </React.Fragment>
    );
};
export default stateAndLifecycle;

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

import React, {useState} from "react";

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

    function handleLogin() {
        setLogged(true);
    }

    function handleLogout() {
        setLogged(false);
    }

    let button = null;
    if (isLoggedIn) {
        button = (
            <button onClick={handleLogout}>退出登录</button>
        );
    } else {
        button = (
            <button onClick={handleLogin}>去登录</button>
        );
    }

    return (
        <div>
            {isLoggedIn && "恭喜,登录成功!"}
            {button}
        </div>
    );
}

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<boolean>(true);

    function handleLogin() {
        setLogged(true);
    }

    function handleLogout() {
        setLogged(false);
    }

    return (
        <div>
            {isLoggedIn && "恭喜,登录成功!"}
            {isLoggedIn && (<button onClick={handleLogout}>退出登录</button>)}
            {!isLoggedIn && ( <button onClick={handleLogin}>去登录</button>)}
        </div>
    );
}

export default ConditionFunc;

三目运算符

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

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

import React, {useState} from "react";

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

    function handleLogin() {
        setLogged(true);
    }

    function handleLogout() {
        setLogged(false);
    }

    return (
        <div>
            {
                isLoggedIn ? "恭喜,登录成功!" : ""
            }
            {
                isLoggedIn ? (
                    <button onClick={handleLogout}>退出登录</button>
                ) : (
                    <button onClick={handleLogin}>去登录</button>
                )
            }
        </div>
    );
}

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 (
        <React.Fragment>
            {/* 函数组件列表渲染 */}
            <ListFunc/>
        </React.Fragment>
    );
};
export default ListAndKey;

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

import React, {useState} from "react";

function ListFunc() {
    let [todos] = useState<Array<string>>(["React", "Vue", "Angular"]);
    let todoElements = todos.map((todo) => (<li>{todo}</li>));
    return (
        <ul>
            {todoElements}
        </ul>
    );
}

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 (
        <div>
            {/* 组件与属性 */}
            <ComponentsAndProps/>
            {/* State & 生命周期 */}
            <StateAndLifecycle/>
            {/* 条件渲染 */}
            <ConditionRender/>
            {/* 列表与 key */}
            <ListAndKey/>
        </div>
    );
};
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<Array<string>>(["React", "Vue", "Angular"]);
    return (
        <ul>
            {todos.map((todo) => (<li>{todo}</li>))}
        </ul>
    );
}

export default ListFunc;

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

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

key

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

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

1-10.png

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

import React, {useState} from "react";

function ListFunc() {
    let [todos] = useState<Array<string>>(["React", "Vue", "Angular"]);
    return (
        <ul>
            {todos.map((todo) => (<li key={todo}>{todo}</li>))}
        </ul>
    );
}

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 (
        <React.Fragment>
            {/* 函数组件之受控组件 */}
            <ControlledFunc/>
        </React.Fragment>
    );
};
export default Form;

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

import React, {useState} from "react";

function ControlledFunc() {
    let [name, setName] = useState<string>("");

    function handleInput(event: any) {
        setName(event.target.value);
    }

    return (
        <div>
            <input value={name} onInput={handleInput}/>
            <div>{name}</div>
        </div>
    );
}

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 (
        <div>
            {/* 组件与属性 */}
            <ComponentsAndProps/>
            {/* State & 生命周期 */}
            <StateAndLifecycle/>
            {/* 条件渲染 */}
            <ConditionRender/>
            {/* 列表与 key */}
            <ListAndKey/>
            {/* 表单-受控组件 */}
            <Form/>
        </div>
    );
};
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 (

    <div>
      {/* 状态提示--价格 */ }
      <StateUpCom title={ "价格:" } value={price} handleInput={ handlePriceInput }/>
      {/* 状态提示--数量 */ }
      <StateUpCom title={ "数量:" } value={count} handleInput={ handleCountInput }/>
      总价:{ total }
    </div>
  );
}
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<Prop> {
  static propTypes = {
    title: PropTypes.string, // 标题
    value: PropTypes.number, // 输入值
    handleInput: PropTypes.func, // 处理输入监听函数
  }
  static defaultProps = {
    title: "",
    value: 0
  }

  render() {
    const {title, value, handleInput} = this.props;
    return (
      <fieldset>
        <legend>{title}</legend>
        <input onInput={handleInput} type="number" value={value}/>
      </fieldset>
    );
  }
}

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 (
        <div>
            {/* 组件与属性 */}
            <ComponentsAndProps/>
            {/* State & 生命周期 */}
            <StateAndLifecycle/>
            {/* 条件渲染 */}
            <ConditionRender/>
            {/* 列表与 key */}
            <ListAndKey/>
            {/* 表单-受控组件 */}
            <Form/>
            {/* 状态提升 */}
            <LiftingStateUp/>
        </div>
    );
};
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/

上一篇下一篇

猜你喜欢

热点阅读