组件化开发的优势
- 利于团队协作开发
- 利于组件复用
- 利于SPA单页面应用开发
- ……
Vue中的组件化开发
详情参考:fivedodo.com/upload/html…
- 全局组件和局部组件
- 函数组件(functional)和类组件(Vue3不具备functional函数组件)
React中的组件化开发
React没有明确全局和局部的概念(可以理解为都是局部组件,不过可以把组件注册到React上,这样每个组件中只要导入React即可使用)
- 函数组件
- 类组件
- Hooks组件:在函数组件中使用React Hooks函数
1. 函数组件
创建一个函数返回jsx元素
1.1 属性和插槽
新建文件FunctionComponent.jsx
import React from "react";
const FunctionComponent = function FunctionComponent(props) {
console.log(props);
return <div className="box">
函数组件
</div>;
};
export default FunctionComponent;
在index.jsx中:
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<>
<FunctionComponent x={20} y='30' arr={[10, 20, 30]}>
<div className="slot-box">
额外信息
</div>
</FunctionComponent>
</>
);
render渲染的时候,如果发现type值是一个函数,会将这个函数执行,并且将解析出来的props当作实参传递给函数。
函数组件单闭合调用,不能传递子节点信息(没有children);双闭合调用,可以有children,实现出类似于Vue中插槽的概念(有助于组件的复用性更强)
打印FunctionComponent组件接收的参数props,如图所示:
模拟插槽
React.Children中提供的方法:
通过React.Children.toArray()保证children一定是个数组,这样就可以通过数组的索引去取值。
可以将每一个子节点添加slot属性,这样在函数组件中拿到的变量children内部会含有slot属性,属性值是header的放在头部,是footer的放到尾部,这样就不用通过数组的索引来取值。
index.jsx
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<>
<FunctionComponent x={20} y='30' arr={[10, 20, 30]}>
<div className="slot-box" slot="header">
头部信息
</div>
<div className="slot-box" slot="footer">
尾部信息
</div>
</FunctionComponent>
</>
);
FunctionComponent.jsx
const FunctionComponent = function FunctionComponent(props) {
let children = React.Children.toArray(props.children),
headerSlots = children.filter(item => item.props.slot === 'header'),
footerSlots = children.filter(item => item.props.slot === 'footer');
return <div className="box">
{ headerSlots }
函数组件
{ footerSlots }
</div>;
};
这样就实现了Vue中具名插槽的概念。
我们通过 Object.isFrozen() 来检测当前对象是否被冻结。将props传进去结果为true,即props是被冻结的,这就意味着不能新增、删除、修改、劫持props中的属性/值。
如果直接操作props对象,通过成员访问的形式来修改props某个属性的值,如:props.x = 2000,则会报错:
这些属性都是只读的!
要想实现对传递进来的props的属性值进行修改,可以对props进行解构,解构出 x 相当于定义了一个 x 的变量,将props的x的属性值赋给它,然后再对其更改值。
const FunctionComponent = function FunctionComponent(props) {
let children = React.Children.toArray(props.children),
headerSlots = children.filter(item => item.props.slot === 'header'),
footerSlots = children.filter(item => item.props.slot === 'footer');
let { x } = props;
x = 2000;
return <div className="box">
{ headerSlots }
函数组件 { x }
{ footerSlots }
</div>;
};
渲染后的结果:
给函数组件传进来的属性设置默认值: 因为函数也是一个对象,它里面也有自己的键值对,即静态的私有属性和方法,那我们给这个函数设置静态的属性就可以实现了
// 给props属性设置默认值
FunctionComponent.defaultProps = {
num: 100,
};
这时候,打印props会发现,尽管没有传num,但是结果里还是包含了:
除了设置默认值以外,还想给某些属性设置一些规则校验,这时候需要React官方提供的一个插件,叫作 prop-types。同样,也是给当前函数设置静态私有属性。
// 给属性设置规则校验
FunctionComponent.propTypes = {
x: PropTypes.number.isRequired, // 必须是number类型,必传
y: PropTypes.string,
arr: PropTypes.array
};
如果这时候我们在index.jsx中不给函数组件传递 x 属性,这时候触发了验证器的校验,那么就会报错:
属性和插槽都可以让组件具备更强的复用性
1.2 函数组件的特点
做一个小需求:现有计数器,值为0,每点击一次按钮值就加1
index.jsx,
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<>
<TextOne />
</>
);
新建文件 TestOne.jsx
import React from "react";
const TestOne = function TestOne() {
let num = 0;
return (
<div className="box">
{num}
<br />
<button
onClick={() => {
num++;
console.log(num);
}}
>
新增
</button>
</div>
);
};
export default TestOne;
这个事件是一个合成事件,关于合成事件的原理和知识后面再叙。
在连续对按钮点击了7次后,控制台正确显示了结果
然后视图中的num还是0,没有更新:
原因:第一次调用这个组件,会产生私有上下文,里面有个初始值为0的私有变量num。开始渲染,把返回的JSX元素交给render方法变为真实DOM。当点击按钮的时候,num变量加1,这时候只是把私有变量num进行了修改,但是不会触发视图更新。这时候只有让函数重新执行才能更改视图。
函数组件的视图更新,就是让函数重新执行
ps:如果父组件(调用的组件)更新了,给函数组件(被调用组件)传递了最新的值,也会重新渲染。
总结:函数组件是静态组件
,具有以下特点:
- 不具备状态、生命周期函数、ref等内容
- 第一次渲染完毕,除非父组件控制其重新渲染,否则内容不会再更新
- 优势:渲染速度快
- 弊端:静态组件,无法实现组件动态更新
要想在第一次渲染完成后,通过内部某些操作让其更新视图这个目前是没有办法实现的。后面会叙述的Hooks函数可以让函数组件动态化。
函数组件的使用场景:
一个当面当中包含了很多板块,其中有几个板块我只需要第一次渲染成什么样后面就一直什么样,不会再更新,这种情况下用函数组件会更好一点。真实项目中用函数组件来做的需求还是比较多的。
1.3 Vue中的函数组件
在 template 加上关键字 functional
<template functional>
<div class="box">
{{ num }}
<br />
<button @click="change">新增</button>
</div>
</template>
<script>
export default {
name: "Demo",
data() {
return {
num: 0,
};
},
methods: {
change() {
this.num++:
}
}
};
</script>
在Vue2的函数组件中,第一次渲染后视图也没有发生更新。在Vue3中,函数式组件2.x的性能提升可以忽略不计,官方建议只使用有状态的组件。
2. 类组件
创建一个类
,并继承React.Component/PureComponent
,基于render
返回JSX视图。
2.1 类组件特点
新建文件ClassDemo.jsx:
import React from "react";
class ClassDemo extends React.Component {
render() {
return <div className="boc">
类组件
</div>
}
}
export default ClassDemo;
render渲染的时候,如果发现type值是一个类,会将这个类基于new执行,创建其一个实例。
按照面向对象的思想绘画ClassDemo的原型链:
同样的,在index.jsx中调用ClassDemo的时候,传入两个属性
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<>
<ClassDemo x={10} y={20} />
</>
);
在ClassDemo的构造函数中接收传进来的参数props,new ClassDemo的时候会将constructor执行,
通过babel将类组件转换成 React.creatElement 格式
执行 React.createElement() 方法,将返回结果用一个变量接收并打印后得到:
import React from "react";
class ClassDemo extends React.Component {
constructor(props) {
super(); // 一旦使用了ES6继承,编写constructor函数,则进来需先执行super
}
render() {
return <div className="boc">
类组件
</div>
}
}
export default ClassDemo;
通过 Object.isFrozen 可以知道类组件也被冻结了,那就不能直接对props进行增删改和劫持。同样给类组件做一些属性规则校验和默认值,也要引入PropTypes插件,添加静态私有属性和方法
static defaultProps = {
x: 0,
y: 0,
};
static propTypes = {
x: PropTypes.number,
y: PropTypes.number,
};
类组件还有自己完善的状态管理,只要修改状态,就能控制当前组件重新渲染(类不用重新new,只要render方法重新执行就行了)
// 设置初始状态
state = {
num: 10
}
或者写在 constructor 里
constructor(props) {
super();
this.state = {
num: 10
};
}
如果要将props中的某些值作为初始状态值,这时候将状态写在constructor里更方便点;如果初使状态值是固定的,就直接在外面写即可。
在render方法中找到状态里的num并进行修改,并更新视图。在Vue当中,是给指定的状态做数据劫持,所以通过 this.state.num 的形式就可以,但这在React中没有做任何劫持,只会修改值,视图不会更新。要想视图也会更新,则需要调用该类继承的 React.Component 原型对象上的方法 forceUpdate,控制视图强制更新。
render() {
let { num } = this.state;
return <div className="boc">
{ num }
<br />
<button onClick={() => {
this.state.num = 200;
this.forceUpdate();
}}>按钮</button>
</div>;
}
或者使用 React.Component 原型对象上的方法 setState,它不仅能修改状态值,还能让视图更新。
this.setState({
num: 200
});
总结:
类组件是动态组件
-
具备属性及规则校验
-
具备状态,修改状态可以控制视图更新
- setState
- forceUpdate
-
具备ref可以获取DOM元素或者组件实例
- ref='xxx'
- ref=(x=>this.xxx=x)
- React.createRef
-
具备周期函数
- 严格模式下,一些不安全的周期函数是禁止使用的
2.2 类组件流程
(1)整个类组件从new开始到最后视图呈现出来,所经历的步骤:
- getDefaultProps && 属性规则校验
- 初始化
- constructor执行,将处理好的props传递给constructor
- super()等价于 React.Component.call(this),也就是将父类当作普通方法执行,并将this指向子类实例。会往this上挂载props/refs/context/updater这些属性
- super(props) 这是直接将传递进来的props挂载到实例上 this.props={...}
- 初始化状态 this.state={...}
- constructor执行,将处理好的props传递给constructor
- 初始化结束后,会把props/context这些信息全部挂载到实例上
- 触发一个周期函数 componentWillMount:第一次渲染之前
- 不安全的周期函数(未来要移除掉)
- 用 UNSAFE_componentWillMount 取代 componentWillMount,但不要在严格模式下使用
- 建议放弃对该函数的使用
- 触发 render 周期函数
- 把render执行返回的JSX元素对象(虚拟DOM对象)进行渲染解析
- render必须要有,必须返回JSX
- 触发 componentDidMount 周期函数:第一次渲染完
- 获取真实的DOM元素
- 从服务器获取数据
- 设置定时器or监听器
- ...
查看源码,在 node_modules --> react --> cjs --> react.development.js 中:
由此可知,在super执行后,实例上会挂载四个私有属性
在执行super后,打印this:
因为浏览器控制台展开后看到的永远是堆内存最新的值,所以这时候的this里的props其实是undefined
super执行的时候,将props传进去即可,这时候挂载的this.props就不是undefined了
constructor(props) {
super(props)
}
使用不安全的生命周期函数 componentWillMount,控制台会抛出警告:
componentWillMount() {
console.log('第一次渲染之前');
}
在React18中,开始使用 UNSAFE_componentWillMount 来取代 componentWillMount。但是在 React.StrictMode 严格模式下使用会报错
index.jsx
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<ClassDemo x={10} y={20} />
</React.StrictMode>
);
ClassDemo.jsx
UNSAFE_componentWillMount() {
console.log("第一次渲染之前");
}
(2)基于setState修改状态,通知视图更新,所经历的步骤:
- 触发 shouldComponentUpdate 周期函数:是否允许更新
- 返回true:允许更新,继续执行后续步骤
- 返回false:停止更新,状态/属性值也不会修改、视图也不会更新
- 可以基于这个周期函数做项目性能优化
- 进行到这一步,状态和属性还没有改为最新的值
- 触发 componentWillUpdate 周期函数:不安全的周期函数
- 进行到这一步,状态和属性还没有改为最新的值
- 修改状态/属性为最新的值,基于this.props/state访问,获取的也是最新的值
- 触发 render:让视图按照最新的值进行渲染更新
- 触发 componentDidUpdate 周期函数:视图更新完毕
- 如果 setState 设置了回调函数,则把回调函数触发执行(类似于Vue中的$nextTick)
shouldComponentUpdate(nextProps, nextState) {
console.log(this.props, this.state); // 原有的属性和状态
console.log(nextProps, nextState); // 即将要修改的属性和状态
return true;
}
componentDidUpdate() {
console.log('视图更新完毕');
}
render() {
let { num } = this.state;
return (
<div className="boc">
{num}
<br />
<button
onClick={() => {
this.setState({
num: 200,
}, () => {
console.log('setState回调函数');
});
}}
>
按钮
</button>
</div>
);
}
点击按钮,查看打印结果:
如果 shouldComponentUpdate 中返回的是false,那么setState中的回调函数也会被触发执行。
(3)基于forceUpdate强制让视图更新:
直接跳过shouldComponentUpdate,继续下一步操作(后续操作和setState一致)
(4)父组件重新调用该组件(可能传递新的值进来),组件也需要更新
- 触发 componentWillReceiveProps 周期函数:不安全
- 触发 shouldComponentUpdate 周期函数
- ...
(5)组件销毁:
- 触发 componentWillUnmount 周期函数:销毁之前
- 把自己手动设置的事件、定时器、监听器...手动释放掉,来优化性能
- 对目前组件中的一些信息,做缓存
- ...
- 销毁
2.3 PureComponent的处理机制
创建一个类组件的时候,除了让其继承React.Component,还可以继承React.PureComponent
当点击按钮时,向数组中push数据
export default class PureClassDemo extends React.Component {
state = {
arr: [10, 20]
};
handle = () => {
let { arr } = this.state;
arr.push(30);
this.setState({
arr
});
};
render() {
// console.log('render');
let { arr } = this.state;
// console.log(arr);
return <div>
{ arr }
<br />
<button onClick={ this.handle }>处理</button>
</div>;
}
};
ps:修改状态后的值和之前状态的值的堆内存地址是相同的
这时候点击页面按钮,发现30并没有渲染在页面上,但是控制台发现有打印出 'render',这说明 setState 这个操作确实通知了视图要重新更新,打印arr发现有新增的30,说明在render的时候已经拿到了最新的值了,但是视图没有更新。这其实是 dom diff 的问题,后续文章会提到关于React中dom diff的处理。
那我想在视图中看到渲染最新结果的效果怎么办?将arr变成字符串就可以了
render() {
let { arr } = this.state;
return <div>
{ arr.join('+') }
<br />
<button onClick={ this.handle }>处理</button>
</div>;
}
这时候,我用 React.PureComponent 替换 React.Component,代码其他地方不变。结果发现,在点击按钮后,视图并没有更新,且连render方法都没有触发。
通过React生命周期可以知道当状态改变时,会触发一个钩子 shouldComponentUpdate,它的返回值决定了是否更新。
继承 React.PureComponent,会默认创建 shouldComponentUpdate 周期函数。它默认在这个周期函数中,做了一个“浅比较”:拿最新要修改的属性/状态和原始属性/状态去比较,如果一样,则不用更新。
模拟React.PureComponent的特点:
继承 React.Component,手写一个对象浅比较的方法:
因为 typeof null 的结果也是 object,所以再加一个粗略判断是不是object的方法
const isObject = function isObject(obj) {
return obj !== null && typeof obj === 'object';
}
因为是浅比较,所以只对对象里的第一级做了比较
const shallowEqual = function shallowEqual(obj1, obj2) {
// 1.比较是不是同一个堆内存地址
if (obj1 === obj2) return true;
// 2.保证两个都是对象
if (!isObject(obj1) || !isObject(obj2)) return false;
// 3.比较键值对数量
let key1 = Reflect.ownKeys(obj1),
key2 = Reflect.ownKeys(obj2);
if (key1.length !== key2.length) return false;
// 4.每个属性值是否相同
for (let i = 0; i < key1.length; i++) {
let key = key1[i];
if (obj1[key] !== obj2[key]) return false;
}
return true;
};
因为是React.Component,可以写shouldComponentUpdate生命周期函数内的逻辑
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState);
};
这样就达到了模拟React.PureComponent不能render的效果
那么怎么做到继承了 React.PureComponent,修改前后对象的堆内存不变但页面又能重新渲染呢?
export default class PureClassDemo extends React.PureComponent {
state = {
arr: [10, 20]
};
handle = () => {
let { arr } = this.state;
arr.push(30);
this.setState({
arr: [...arr]; // *
});
};
render() {
let { arr } = this.state;
return <div>
{ arr.join('+') }
<br />
<button onClick={ this.handle }>处理</button>
</div>;
}
};
通过 * 处操作让arr堆内存地址发生改变,这样就可以实现视图更新了。
或者使用 forceUpdate 进行强制更新。它会跳过 shouldComponentUpdate 钩子函数校验
handle = () => {
let { arr } = this.state;
arr.push(30);
this.forceUpdate();
};
但是我们用 forceUpdate 强制更新就没有意义了,因为既然继承了React.PureComponent就是想用shouldComponentUpdate的浅比较规则来进行优化,却还是用 forceUpdate 跳过优化那就毫无必要了。因此这种做法并不推荐。