05.组件化

组件化开发

组件化开发的优势

  • 利于团队协作开发
  • 利于组件复用
  • 利于SPA单页面应用开发
  • ……

主流组件的对比

Vue中的组件化开发:

http://fivedodo.com/upload/html/vuejs3/guide/migration/functional-components.html

  1. 全局组件和局部组件
  2. 函数组件(functional)和类组件(Vue3不具备functional函数组件」)

React中的组件化开发:

没有明确全局和局部的概念(可以理解为都是局部组件,不过可以把组件注册到React上,这样每个组件中只要导入React即可使用)

  1. 函数组件
  2. 类组件
  3. Hooks组件:在函数组件中使用React Hooks函数

React函数组件

创建

两个情况都是创建了一个函数组件:

  1. 在SRC目录中,创建一个 xxx.jsx 的文件,就创建一个组件了;
  2. 我们在此文件中,创建一个函数,让函数返回JSX视图(或者JSX元素、virtualDOM虚拟DOM对象)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// views/FunctionComponent.jsx
const FunctionComponent = function FunctionComponent() {
return <div>
我是函数组件
</div>;
};
export default FunctionComponent;

// index.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import FunctionComponent from '@/views/FunctionComponent';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<>
<FunctionComponent/>
</>
);

调用

在文件中,可以基于ES6Module规范导入创建的组件(导入时可以忽略.jsx后缀名),然后像写标签一样调用。

1
2
3
4
5
//两种调用方式
<Component/> //单闭合调用

<Component> ... </Component> //双闭合调用
//双闭合调用内可以放子节点,在传递函数的props时,会有一个children属性来存储这些子节点。

传递属性的规则:

  • 调用组件时,可以给被调用的组件设置(传递)各种各样的属性,例如:

    <DemoOne title="我是标题" x={10} data={[100, 200]} className="box" style={{ fontSize: '20px' }} />

  • 若设置的属性值不是字符串格式,则需要基于{}语法进行嵌套。

命名

组件名,一般都采用PascalCase(大驼峰命名法)

渲染过程

由上两章可知组件的渲染过程是

  1. 基于babel-preset-react-app把调用的组件转换为createElement格式
1
2
3
4
5
6
7
8
9
React.createElement(DemoOne, {
title: "zono",
x: 10,
data: [100, 200],
className: "box",
style: {
fontSize: "20px",
},
});
  1. 执行createElement方法创建一个virtualDOM对象
1
2
3
4
5
6
7
{
$$typeof: Symbol(react.element),
key: null,
props: {title: '我是标题', x: 10, data: 数组, className: 'box', style: {fontSize: '20px'}}, //如果有子节点「双闭合调用」,则也包含children!!
ref: null,
type: DemoOne
}
  1. 基于root.rendervirtualDOM变为真实的DOM

type值不再是一个字符串,而是一个函数了,此时:

  • 函数执行→DemoOne()
  • virtualDOM中的props,作为实参传递给函数 DemoOne()DemoOne(props)
  • 接收函数执行的返回结果(也就是当前组件的virtualDOM对象)
  • 最后由render函数把组件返回的虚拟DOM变为真实DOM,插入到id=root的容器中!!

Props的处理

  • 调用组件,传递进来的属性(Props)是“只读”的,原理是:props对象被冻结了Object.freeze()(见下文解释)
1
2
3
4
//测试
Object.isFrozen(props) //output:true
$ 获取:props.xxx
$ 修改:props.xxx=xxx //output:=>报错

回顾js知识点:关于对象的规则设置(了解可跳过)

阮老师单推人😘

标准库 - 属性描述对象 - 《阮一峰 JavaScript 教程》 - 书栈网 · BookStack

控制对象状态的一节,讲述了以下内容:

  • 扩展(弱):

    Object.*preventExtensions*():使得一个对象无法再添加新的属性
    Object.isExtensible(obj):检测是否可扩展(返回bool值,下面都是)

  • 密封(较强)
    Object.seal(obj):密封对象,使得一个对象既无法添加新属性,也无法删除旧属性。
    Object.isSealed(obj):检测是否被密封

    • 被密封的对象:可以修改成员的值,但也不能删、不能新增、不能劫持Object.defineProperty
  • 冻结(强)
    Object.freeze(obj):冻结对象,对象实际上变成了常量。
    Object.isFrozen(obj) 检测是否被冻结

    • 被冻结的对象:不能修改成员值、不能新增成员、不能删除现有成员、不能给成员做劫持Object.defineProperty
  • 局限性:可以通过改变原型对象,来为对象增加属性。(详见阮一峰教程)

  • 劫持:(也在”属性描述对象’’同一章中)

    Object.defineProperty()方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象()

👏tips:

  • 被设置不可扩展的对象:除了不能新增成员、其余的操作都可以处理!!
  • 被密封的对象,也是不可扩展的!!
  • 同理,被冻结的对象,即是不可扩展的,也是密封的!!
  • 通过三个方法的互相调用可以部分解封,但如果要加入新的属性那就只能创建一个新对象了

💬Ps:

  • 越底层越要看js基础,看来八股并不是没用。
  • 是不是这部分我应该单独开一章🤦‍♂️🤦‍♀️

然后让我们举一个传Props的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src\index.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import DemoOne from '@/views/DemoOne';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<>
<DemoOne title="可以看见成功传入了值" x={10}>
<span>哈哈哈</span>
<span>呵呵呵</span>
</DemoOne>

<DemoOne title="尝试传入不同的值,实现了组件的复用,注意要用<></>包起来" />
</>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src\views\DemoOne.jsx
import PropTypes from 'prop-types';

const DemoOne = function DemoOne(props) {
let { title } = props;//从父组件取值
let x = props.x;
x = 1000;

return <div className="demo-box">
<h2 className="title">{title}</h2>
<span>{x}</span>
</div>;
};
/* 通过把函数当做对象,设置静态的私有属性方法,来给其设置属性的校验规则 */
DemoOne.defaultProps = {
x: 0
};
DemoOne.propTypes = {
//组件属性:校验格式
title: PropTypes.string.isRequired,// 类型是字符串、必传
x: PropTypes.number,// 类型是数字
y: PropTypes.oneOfType([
PropTypes.number,
PropTypes.bool,
])// 多种校验规则中的一个
};

export default DemoOne;

属性的使用逻辑

  • 父组件index.jsx调用子组件DemoOne.jsx的时候,可以基于属性,把不同的信息传递给子组件;
  • 子组件接收相应的属性值,呈现出不同的效果,让组件的复用性更强!!

对于传递进来的属性规则校验

  • 原理:通过把函数当做对象,设置静态的私有属性方法,来给其设置属性的校验规则
  • 可以设置默认值(见上面代码):
1
2
3
4
5
6
7
/* 函数组件.defaultProps = {
x: 0,
......
};*/
DemoOne.defaultProps = {
x: 0
};
  • 设置其它规则,例如:数据值格式、是否必传… (依赖于官方的一个插件:prop-types

  • React 校验库 prop-types 安装与详细使用 - 掘金 (juejin.cn)

  • https://github.com/facebook/prop-types

    这里就是用这个插件来进行类型检验,让原本动态的js能像c++、java这样的静态语言可以不用担心类型转换出现的问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import PropTypes from 'prop-types';
    函数组件.propTypes = {
    title: PropTypes.string.isRequired,// 类型是字符串、必传
    x: PropTypes.number,// 类型是数字
    y: PropTypes.oneOfType([
    PropTypes.number,
    PropTypes.bool,
    ])// 多种校验规则中的一个
    };

    传递进来的属性,首先会经历规则的校验,不管校验成功还是失败,最后都会把属性给形参props,只不过如果不符合设定的规则,控制台会抛出警告错误{不影响属性值的获取}!!

    🤔Tips:

    • 使用 TypeScript(TS)的话,通常不需要使用 prop-types 库。

    • 之前写python也有类似的库,看样子动态语言都得被静态束缚。

    (那python是不是强类型呢😅,与前端无关,可以忽略)

修改props的规则

  • 把props中的某个属性赋值给其他内容(例如:变量、状态…)
  • 我们不直接操作props.xxx=xxx,但是我们可以修改变量/状态值!!

待规划文字(可以先不读,我复习时会把内容规整进入正文)

  • 在JSX元素渲染的时候,如果type是函数,则把函数执行!
    • 会把解析出来的props「含children」,传递给函数
      • 单闭合和双闭合调用的区别「插槽」

      • React.Children

      • props是只读的「被冻结」

    • props的规则设置
      • 设置默认值

      • 设置属性规则「基于prop-types插件」

    • 属性和插槽都可以让组件具备更强的复用性
1
2
3
4
5
6
7
8
9
10
// index.jsx
root.render(
<>
<FunctionComponent x={10} title="你好世界" arr={[10, 20, 30]}>
<div className='slot-box'>
我是插槽信息
</div>
</FunctionComponent>
</>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// views/FunctionComponent.jsx
import React from "react";
import PropTypes from 'prop-types';
const FunctionComponent = function FunctionComponent(props) {
/* 被冻结:不能修改/新增/删除/劫持...「可以赋值给其它状态再进行修改」 */
// console.log(Object.isFrozen(props)); //->true

/* 对props.children进行操作:count/forEach/map/toArray... */
// console.log(React.Children);

return <div>
我是函数组件
</div>;
};
// 赋值属性默认值
FunctionComponent.defaultProps = {
x: 0,
title: '',
arr: [],
num: 0
};
// 设置属性规则
FunctionComponent.propTypes = {
x: PropTypes.number,
title: PropTypes.string.isRequired,
arr: PropTypes.array,
num: PropTypes.any
};
export default FunctionComponent;