简介
上一节我们完成了从 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
启动项目后,浏览器会自动打开我们项目的入口页面:
到这,我们的准备工作就算是完成了。
组件 & 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
属性,并且利用 ts
跟 prop-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
属性,并且利用 ts
跟 prop-types
对属性进行了校验,一个简单的 “React 函数式组件” 就创建完成了。
运行
在 react-demo-day5
项目根目录下执行 npm start
命令重新启动项目:
npm start
可以看到,两个组件都正常显示到了页面。
组合组件
组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。按钮,表单,对话框,甚至整个屏幕的内容:在 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 = "小虫虫";
我们保存文件等自动编译完成:
可以看到,三处报错了:
-
Webpack
编译直接报错了,说 “我们不能修改只读属性”。 -
IDE
也报错了,主要是Eslint
的配置。 - 浏览器也报错了,说 “遇到了未知异常”。
-
Object.isFrozen(this.props)
返回了true
。
从上面可以看出,我们利用了 TypeScript
、Eslint
等规则在写代码的时候就已经成功避免了这类错误的出现,最后 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
,我们同样是可以让一个函数式组件也具备 State
,Hook
的内容我们后面再详细解析。
我们保存等项目重新编译看结果:
当我们点击对应文字区域的时候,页面会进行 on
与 off
的切换效果,我就不演示了哈,小伙伴自己试试。
正确地使用 State
- 不要直接修改 State
- State 的更新可能是异步的
- State 的更新会被合并
生命周期
先上一张官方提供的 React
的生命周期图:
图片来源: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
);
}
因为 e
是 React
生成的一个合成事件,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
变量充当了一个元素,然后通过 State
的 isLoggedIn
变量进行条件判断,对 button
变量进行赋值。
最后我们在 src/main-concepts/index.tsx
文件中引入 src/main-concepts/condition-render/index.tsx
组件测试:
可以看到,页面中根据我们的点击条件渲染了不同的状态。
与运算符 &&
通过花括号包裹代码,你可以在 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
可以看到,页面中正常渲染了我们的 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
,在开发模式中会报错:
我们需要修改一下 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 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的)。
表单
受控组件
输入的值始终又 React
的 State
控制的组件就叫 “受控组件”。
我们来演示一下。
首先在 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;
可以看到,我们用了一个 State
为 name
的属性值,通过监听 input
标签的 onInput
事件,然后把输入的值赋给了 name
变量,最后 State
的 name
变量又控制着 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
可以看到,当我们输入的时候,State
中的 name
变量实时跟 input
输入的值绑定。
textarea
、select
等其它的 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 (
);
}
}
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
可以看到,子组件中的输入值都提升到了父组件,父组件会根据子组件中的输入值自动算出总价的值。
总结
我们照着 React 官网:https://reactjs.org/ 的内容跑了一遍 React
的所有核心概念,虽然有些概念可能很简单,但是搞技术的切勿眼高手低,有些看似很简单的东西,看千遍不如自己敲一遍,弄清这些概念对我们后面分析 React
的源码很有帮助,后面我们还会对 React
的高级特性以及一些 API
做解析。
ok,这节就先到这了,下节见!