React基础
一、react元素
TIP
JSX其实只是一种语法糖,最终会通过babeljs转译成createElement语法,最新版本转化为 Object(r.jsx)("h1",{children:"hello"}
1 特点
- React元素是构成React应用的最小单位
- React元素用来描述你在屏幕上看到的内容
- React元素事实上是普通的JS对象
- ReactDOM来确保浏览器中的DOM数据和React元素保持一致
2 jsx转化react元素
简单版本
// jsx
let element = <h1>hello</h1>;
// react元素
let element = React.createElement("h1", null, "Hello");
console.log(JSON.stringify(element, null, 4));
/**
{
"type": "h1",
"props": {
"children": "Hello"
}
}
*/
复杂版本
// jsx
let element = (
<h1 className="title" style={{color:'red'}}>
<span>hello</span>world
</h1>
);
// react元素
/**
* 参数1 标签的类型 h1 span div
* 参数2 属性的JS对象
* 参数3往后的都是儿子们
*/
let element = React.createElement("h1", {
className: "title",
style: {
color: 'red'
}
}, React.createElement("span", null, "hello"), "world");
console.log(JSON.stringify(element, null, 4));
/**
{
"type": "h1",
"props": {
"className": "title",
"style": {
"color": "red"
},
"children": [
{
"type": "span",
"props": {
"children": "hello"
}
},
"world"
]
}
}
*/
3 实现createElement
/**
* @param {*} type 元素的类型
* @param {*} config 配置的属性对象
* @param {*} children 第一个儿子
*/
function createElement(type, config, children) {
// 删除多余对象
if (config) {
delete config._owner;
delete config._store;
}
let props = {...config};
// 截取儿子数据
if (arguments.length > 3) {
children = Array.prototype.slice.call(arguments, 2);
}
// children可能是数组(多于1个儿子)
// 也可能是一个字符串或者数字,也可能是一个null,也可能是一个react元素
props.children = children;
// 返回React元素,也就是虚拟DOM
return {
type, // 元素的类型
props // 元素的属性
}
}
let React = {
createElement
}
export default React;
二、组件基础
1 函数组件
- 函数组件的props参数可以为任意值
- 函数组件接收一个单一的props对象并返回了一个React元素
import React from 'react'; // react核心库
import ReactDOM from 'react-dom'; // react的dom渲染库
/**
* 定义一个函数组件 名字首字母必须要大写
* 函数内部的jsx语法被转化为react.createElement元素
* @param { } props 属性对象
*/
function Welcome(props){
return <h1>Hello, {props.name}</h1>;
}
/**
function Welcome(props) {
return React.createElement("h1", null, "Hello, ", props.name);
}
*/
// 调用组件,组件名称和属性会被当成参数传入到react.createElement中
let element = <Welcome name="world"/>
/**
var element = React.createElement(Welcome, {
name: "world"
});
*/
ReactDOM.render(
element,
document.getElementById('root')
);
2 类组件
import React from 'react'; // react核心库
import ReactDOM from 'react-dom'; // react的dom渲染库
/**
* 定义一个类组件
*/
class Welcome extends React.Component{
// this.props = {name:'world'}
render(){
return <h1>hello,{this.props.name}</h1>;
}
}
// 调用组件,组件名称和属性会被当成参数传入到react.createElement中
let element = <Welcome name="world"/>
ReactDOM.render(
element,
document.getElementById('root')
);
3 总结
- React元素可能是字符串(原生DOM类型),也可能一个函数(函数组件),也可能是一个类(类组件)
- 在定义组件元素的时候,会把JSX所有的属性封装成一个props对象传递给组件
- 组件的名称一定要首字母大写,React是通过首字母来区分原生还是自定义组件
- 组件要先定义,再使用
- 组件要返回并且只能返回一个React根元素,否则会报错:JSX expressions must have one parent element
三、状态
- 组件的数据来源有两个地方,分别是属性对象和状态对象
- 属性是父组件传递过来的(默认属性,属性校验),不能修改
- 状态是内部产生的,可以改,状态只能用在类组件里
- 唯一能给this.state赋值的地方就是构造函数,只能初始值
- 其它地方要想改变状态只能调用setState()方法
- 每当你调用setState方法的时候就会引起组件的刷新,组件会重新调用一次render方法,得到新虚拟DOM,进行DOM更新
- 属性和状态的变化都会影响视图更新
1 使用
import React from 'react'; // react核心库
import ReactDOM from 'react-dom'; // react的dom渲染库
// 定时器,每秒更新+1
class Clock extends React.Component {
constructor(props) {
super(props);
// 初始化状态
this.state = {
date:new Date()
};
setInterval(this.tick, 1000);
}
tick=()=>{
// Do not mutate state directly. Use setState()
// 通过setState()修改状态
this.setState({date:new Date()});
}
render(){
return (
<div>
<h1>Hello</h1>
<h2>当前的时间:{this.state.date.toLocaleTimeString()}</h2>
</div>
)
}
}
let element = <Clock/>
ReactDOM.render(
element,
document.getElementById('root')
);
2 异步更新
- 状态是封闭的,只有组件自己能访问和修改
- 出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用,进行批量执⾏, 多key一次执⾏,相同key合并
- 可能是异步的,在事件处理函数中或生命周期函数中批量更新是异步的,其它地方都是直接同步更新的
import React from 'react'; // react核心库
import ReactDOM from 'react-dom'; // react的dom渲染库
class Counter extends React.Component {
state = { number: 0 }
/**
* 在事件处理函数中,
* setState 的调用会批量执行
* setState 不会修改this.state,需要等实际处理函数结束之后,再进行更新
*/
handleClick = () => {
// setState第一个参数为对象的情况
this.setState({ number: this.state.number + 1 });
console.log(this.state.number); // 0
this.setState({ number: this.state.number + 1 });
console.log(this.state.number); // 0
}
// 点击第一次 打印两个0 最后状态变为1
// 点击第二次 打印两个1 最后状态变为2
render(){
return (
<div>
<p>number:{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter />, document.getElementById('root'));
setState第一个参数为函数的情况
state = { number: 0 }
handleClick = () => {
// 函数进行修改,第一个函数的状态返回值为第二个函数的参数
this.setState((state) => ({ number: state.number + 1 }));
console.log(this.state.number); // 0
this.setState((state) => ({ number: state.number + 1 }));
console.log(this.state.number); // 0
}
// 点击第一次 打印两个0 最后状态变为2
// 点击第二次 打印两个2 最后状态变为4
// 原理如下:
let queue = [];
queue.push((state) => ({ number: state.number + 1 }));
queue.push((state) => ({ number: state.number + 1 }));
let state = { number: 0 };
let result = queue.reduce((newState, action) => {
return action(newState);
}, state);
console.log(result); // 2
setState的第二个参数可以获取立刻更新的值
state = { number: 0 }
handleClick = () => {
// setState第二个参数为回调函数,里面可以获取每次更新的最新值
this.setState({ number: this.state.number + 1 }, () => {
console.log('callback1', this.state.number); // 1
});
console.log(this.state.number); // 0
this.setState({ number: this.state.number + 1 }, () => {
console.log('callback2', this.state.number); // 1
});
// 点击第一次,因为批量执行,最后再回调函数中打印的是1
// 点击第二次,因为批量执行,最后再回调函数中打印的是2
}
- React能管控的地方,就是批量的、异步的,比如事件处理函数中、生命周期中
- React不能管控的地方,就是非批量的、同步的,比如setInterval setTimeout 原生DOM事件等
- 如果要同步获取最新状态值,三种⽅方式:
- 传递回调函数给setState
- 写在定时器中
- 写在JS的原生事件中
state = { number: 0 }
handleClick = () => {
//在其它react不能管控的地方,就是同步执行的
Promise.resolve().then(() => {
this.setState({ number: this.state.number + 1 }, () => {
console.log('callback3', this.state.number); // 1
});
console.log(this.state.number); // 1
this.setState({ number: this.state.number + 1 }, () => {
console.log('callback4', this.state.number); // 2
});
console.log(this.state.number); // 2
});
setTimeout(() => {
this.setState((state) => ({ number: state.number + 1 }));
console.log(this.state.number); // 3
this.setState((state) => ({ number: state.number + 1 }));
console.log(this.state.number); // 4
});
}
原生事件中如果要组件刷新,需要调用forceUpdate
/**
* 组件强制更新方法
* 1.获取老的虚拟DOM =》 React元素
* 2.根据最新的属生和状态计算新的虚拟DOM
* 然后进行比较,查找差异,然后把这些差异同步到真实DOM上
*/
forceUpdate(){
console.log('updateComponent');
let oldRenderVdom = this.oldRenderVdom;//老的虚拟DOM
//根据老的虚拟DOM查到老的真实DOM
let oldDOM = findDOM(oldRenderVdom);
let newRenderVdom = this.render();//计算新的虚拟DOM
compareTwoVdom(oldDOM.parentNode,oldRenderVdom,newRenderVdom);//比较差异,把更新同步到真实DOM上
this.oldRenderVdom=newRenderVdom;
}
3 批量更新器
// 更新器
class Updater {
constructor(){
// 初始状态
this.state={name:'test', number:0};
this.queue = [];
}
// 更新状态方法
setState(newState){
this.queue.push(newState);
}
// 批量更新
flush(){
for(let i=0;i<this.queue.length;i++){
let update = this.queue[i];
if(typeof update === 'function'){
this.state = {...this.state,...update(this.state)};
}else{
this.state = {...this.state,...update};
}
}
}
}
let updater = new Updater();
updater.setState({number: 1});
updater.setState((previousState)=>({number: previousState.number+1}));
updater.setState({number: 2});
updater.setState({number: 3});
updater.flush();
console.log(updater.state); // { name: 'test', number: 3 }
4 同步更新器
// 更新器
class Updater {
constructor(){
// 初始状态
this.state={name:'test', number:0};
this.queue = [];
}
// 更新状态方法
setState(update){
if(typeof update === 'function'){
this.state = {...this.state,...update(this.state)};
}else{
this.state = {...this.state,...update};
}
}
}
let updater = new Updater();
updater.setState({number: 1});
console.log(updater.state); // { name: 'test', number: 1 }
updater.setState((previousState)=>({number: previousState.number+1}));
console.log(updater.state); // { name: 'test', number: 2 }
updater.setState({number: 3});
console.log(updater.state); // { name: 'test', number: 3 }
updater.setState({number: 4});
console.log(updater.state); // { name: 'test', number: 4 }
四、事件处理
- React使用的是自定义的合成事件,合成事件的优点:
- 进行浏览器兼容,实现更好的跨平台
- 避免垃圾回收,16以前利用事件池(数组存储事件),17已经移除事件池
- 方便事件统一管理和事务机制
1 react事件基础
- React 事件的命名采用小驼峰式(camelCase),而不是纯小写
- 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
- 你不能通过返回 false 的方式阻止默认行为。你必须显式的使用preventDefault
import React from 'react';
import ReactDOM from 'react-dom';
class Counter extends React.Component {
// 事件处理函数
handleClick(e) {
// 阻止默认事件
e.preventDefault();
alert('The link was clicked.');
}
render() {
return (
<a href="http://www.baidu.com" onClick={this.handleClick}>
Click me
</a>
);
}
}
ReactDOM.render(
<Counter/>,
document.getElementById('root')
);
2 事件原理
- 在dom节点上挂一个对象,存储各种监听函数,最后将整个事件委托到document节点上
- 在点击事件的时候,会从事件源开始依次向上模拟冒泡,执行事件函数
import { updateQueue } from './Component';
// 调用
function updateProps(dom, oldProps, newProps) {
for (let key in newProps) {
if (key === 'children') { }
if (key === 'style') {
} else if (key.startsWith('on')) {//onClick
//dom[key.toLocaleLowerCase()]=newProps[key];//dom.onclick=handleClick
addEvent(dom, key.toLocaleLowerCase(), newProps[key]);
}
}
}
/**
* 实现的事件委托,把所有的事件都绑定到document上
* @param {*} dom dom 真实DOM
* @param {*} eventType 事件类型onclick
* @param {*} handler 合成事件监听函数
*/
export function addEvent(dom, eventType, handler) {
// 先给dom绑定一个store属性,然后在里面绑定各种事件回调函数handler
let store = dom.store || (dom.store = {});
store[eventType] = handler; // store.onclick = handler;
//如果有很多个元素都绑定 click事件,往document持的时候只挂一次
// 事件委托,不管你给哪个DOM元素上绑事件,最后都统一代理到document上去了,而且如果有多次绑定,也只挂一次
if (!document[eventType]) {
document[eventType] = dispatchEvent;
}
// 等价于:document.addEventListener('click',dispatchEvent);
}
function dispatchEvent(event) {
// target事件源=button,那个DOM元素 类型type=click
let { target, type } = event;
let eventType = `on${type}`; // onclick
// 把队列设置为批量更新模式
updateQueue.isBatchingUpdate = true;
let syntheticEvent = createSyntheticEvent(event);
//模拟事件冒泡的过程
while (target) {
let { store } = target;
let handler = store && store[eventType];
handler && handler.call(target, syntheticEvent);
target = target.parentNode;
}
// 事件处理完毕之后,改为false,然后批量更新状态
updateQueue.isBatchingUpdate = false;
updateQueue.batchUpdate();
}
//在源码里此处做了一些浏览器兼容性的适配
function createSyntheticEvent(event) {
let syntheticEvent = {};
for (let key in event) {
syntheticEvent[key] = event[key];
}
return syntheticEvent;
}
3 this指向
- 事件函数写成箭头函数
- 事件函数为普通函数,调用用匿名函数
- bind改变this,在构造函数、render中、调用方法中都可以bind
- 如果要传参数,只能使用匿名函数或者箭头函数,本质都一样的
import React from 'react';
import ReactDOM from 'react-dom';
class Counter extends React.Component {
constructor(props){
super(props);
// this.handleClick = this.handleClick.bind(this);
}
handleClick1 = ()=>{
console.log('this is:', this);
}
handleClick2() {
console.log('this is:', this);
}
handleClick3 = (amount)=>{
console.log('this is:', this, amount);
}
render() {
// this.handleClick = this.handleClick.bind(this);
return (
<div>
{/* 1 事件函数写成箭头函数(公共属性),this用于指向类的实例 */}
<button onClick={this.handleClick1}>+</button>
{/* 2 事件函数为普通函数,调用用匿名函数 */}
<button onClick={() => this.handleClick2()}>-</button>
{/* 3 bind改变this指向 */}
<button onClick={this.handleClick2.bind(this)}>bind-</button>
{/* 第一种:通过 bind 绑定 this 传参 */}
<button onClick={this.handleClick3.bind(this,3)}>bind传参</button>
{/* 第二种:通过箭头函数绑定 this 传参 */}
<button onClick={() => this.handleClick3(3)}>箭头传参</button>
</div>
);
}
}
ReactDOM.render(<Counter/>, document.getElementById('root'));
4 区别
原生事件 | React 合成事件 | |
---|---|---|
命名方式 | 名称全部小写 onclick、onblur | 名称采用小驼峰 onClick、onBlur |
函数语法 | 字符串 | 函数 |
阻止默认行为方式 | 事件返回 false | 使用 e.preventDefault() 方法 |
/* 1 事件名称命名方式不同 */
// 原生事件 - 全小写
<button onclick="handleClick()">原生事件</button>
// React 合成事件 - 小驼峰
const button = <button onClick={handleClick}>react合成事件</button>
/* 2 事件处理函数写法不同 */
// 原生事件 - 字符串
<button onclick="handleClick()">原生事件</button>
// React 合成事件 - 函数
const button = <button onClick={handleClick}>react合成事件</button>
/* 2 阻止默认行为方式不同 */
// 原生事件 - return false
<a href="" onclick="console.log('阻止原生事件'); return false">原生事件</a>
// React 合成事件 - e.preventDefault();
const handleClick = e => {
e.preventDefault();
console.log('阻止原生事件');
}
const clickElement = <a href="" onClick={handleClick}>react合成事件</a>
5 执行顺序
- React把事件委托到document对象上
- 当真实DOM元素触发事件,先处理原生事件,然后会冒泡到 document 对象后,再处理 React 事件
- React事件绑定的时刻是在reconciliation调和阶段,会在原生事件的绑定前执行
- 目的和优势
- 进行浏览器兼容,React 采用的是顶层事件代理机制,能够保证冒泡一致性
- 事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象(React17中被废弃)
React 17以前,16版本的事件顺序
import React from 'react';
import ReactDOM from 'react-dom';
class Counter extends React.Component {
parentRef = React.createRef();
childRef = React.createRef();
componentDidMount() {
this.parentRef.current.addEventListener("click", () => {
console.log("父元素原生捕获");
}, true);
this.parentRef.current.addEventListener("click", () => {
console.log("父元素原生冒泡");
});
this.childRef.current.addEventListener("click", () => {
console.log("子元素原生捕获");
}, true);
this.childRef.current.addEventListener("click", () => {
console.log("子元素原生冒泡");
});
document.addEventListener('click', () => {
console.log("document原生捕获");
}, true);
document.addEventListener('click', () => {
console.log("document原生冒泡");
});
}
parentCapture = () => {
console.log("父元素React事件捕获");
};
parentBubble = () => {
console.log("父元素React事件冒泡");
};
childCapture = () => {
console.log("子元素React事件捕获");
};
childBubble = () => {
console.log("子元素React事件冒泡");
};
render() {
return (
<div
ref={this.parentRef}
onClick={this.parentBubble}
onClickCapture={this.parentCapture}>
<p
ref={this.childRef}
onClick={this.childBubble}
onClickCapture={this.childCapture}>
事件执行顺序
</p>
</div>
);
}
}
ReactDOM.render(<Counter />, document.getElementById('root'));
/**
document原生捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
父元素React事件捕获
子元素React事件捕获
子元素React事件冒泡
父元素React事件冒泡
document原生冒泡
*/
实现原理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="parent">
<p id="child">事件执行</p>
</div>
<script>
function dispatchEvent(event) {
let paths = [];
let current = event.target;
while (current) { // 从事件源,一直查找到最顶层的事件
paths.push(current);
current = current.parentNode;
}
console.log(paths); // [p#child, div#parent, body, html, document]
//模拟捕获和冒泡,其实在这个时候,原生的捕获阶段已经结束 了
// 先倒序执行,模拟捕获
for (let i = paths.length - 1; i >= 0; i--) {
let handler = paths[i].onClickCapture;
handler && handler();
}
// 然后正序执行,模拟冒泡
for (let i = 0; i < paths.length; i++) {
let handler = paths[i].onClick;
handler && handler();
}
}
//注册React事件的事件委托
document.addEventListener('click', dispatchEvent);
let parent = document.getElementById('parent');
let child = document.getElementById('child');
parent.addEventListener("click", () => {
console.log("父元素原生捕获");
}, true);
parent.addEventListener("click", () => {
console.log("父元素原生冒泡");
});
child.addEventListener("click", () => {
console.log("子元素原生捕获");
}, true);
child.addEventListener("click", () => {
console.log("子元素原生冒泡");
});
document.addEventListener('click', () => {
console.log("document原生捕获");
}, true);
//React会执行一个 document.addEventListener('click',dispatchEvent);
//这个注册是在React注册这后注册的,所以后执行
document.addEventListener('click', () => {
console.log("document原生冒泡");
});
parent.onClickCapture = function () { console.log('父元素React事件捕获') }
parent.onClick = function () { console.log('父元素React事件冒泡') }
child.onClickCapture = function () { console.log('子元素React事件捕获') }
child.onClick = function () { console.log('子元素React事件冒泡') }
/**
document原生捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
父元素React事件捕获
子元素React事件捕获
子元素React事件冒泡
父元素React事件冒泡
document原生冒泡
*/
</script>
</body>
</html>
React 17以后的事件顺序,恢复为跟浏览器正常顺序一致了,因为事件代理到了挂载容器上,目的是为了一个页面可以有多个react版本
import React from 'react';
import ReactDOM from 'react-dom';
class Counter extends React.Component {
parentRef = React.createRef();
childRef = React.createRef();
componentDidMount() {
this.parentRef.current.addEventListener("click", () => {
console.log("父元素原生捕获");
}, true);
this.parentRef.current.addEventListener("click", () => {
console.log("父元素原生冒泡");
});
this.childRef.current.addEventListener("click", () => {
console.log("子元素原生捕获");
}, true);
this.childRef.current.addEventListener("click", () => {
console.log("子元素原生冒泡");
});
document.addEventListener('click', () => {
console.log("document原生捕获");
}, true);
document.addEventListener('click', () => {
console.log("document原生冒泡");
});
}
parentCapture = () => {
console.log("父元素React事件捕获");
};
parentBubble = () => {
console.log("父元素React事件冒泡");
};
childCapture = () => {
console.log("子元素React事件捕获");
};
childBubble = () => {
console.log("子元素React事件冒泡");
};
render() {
return (
<div
ref={this.parentRef}
onClick={this.parentBubble}
onClickCapture={this.parentCapture}>
<p
ref={this.childRef}
onClick={this.childBubble}
onClickCapture={this.childCapture}>
事件执行顺序
</p>
</div>
);
}
}
ReactDOM.render(<Counter />, document.getElementById('root'));
/**
document原生捕获
父元素React事件捕获
子元素React事件捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
子元素React事件冒泡
父元素React事件冒泡
document原生冒泡
*/
实现原理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root">
<div id="parent">
<p id="child">事件执行</p>
</div>
</div>
<script>
function dispatchEvent(event, useCapture) {
let paths = [];
let current = event.target;
while (current) {
paths.push(current);
current = current.parentNode;
}
console.log(paths); // [p#child, div#parent, body, html, document]
//模拟捕获和冒泡,其实在这个时候,原生的捕获阶段已经结束 了
if (useCapture) {
// 倒序执行,模拟捕获
for (let i = paths.length - 1; i >= 0; i--) {
let handler = paths[i].onClickCapture;
handler && handler();
}
} else {
// 正序执行,模拟冒泡
for (let i = 0; i < paths.length; i++) {
let handler = paths[i].onClick;
handler && handler();
}
}
}
//注册React事件的事件委托,它是先注册的,它要先执行
let root = document.getElementById('root');
root.addEventListener('click', (event) => dispatchEvent(event, true), true); // 捕获
root.addEventListener('click', (event) => dispatchEvent(event, false));//冒泡
let parent = document.getElementById('parent');
let child = document.getElementById('child');
parent.addEventListener("click", () => {
console.log("父元素原生捕获");
}, true);
parent.addEventListener("click", () => {
console.log("父元素原生冒泡");
});
child.addEventListener("click", () => {
console.log("子元素原生捕获");
}, true);
child.addEventListener("click", () => {
console.log("子元素原生冒泡");
});
document.addEventListener('click', () => {
console.log("document捕获");
}, true);
document.addEventListener('click', () => {
console.log("document冒泡");
});
parent.onClickCapture = function () { console.log('父元素React事件捕获') }
parent.onClick = function () { console.log('父元素React事件冒泡') }
child.onClickCapture = function () { console.log('子元素React事件捕获') }
child.onClick = function () { console.log('子元素React事件冒泡') }
/**
document原生捕获
父元素React事件捕获
子元素React事件捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
子元素React事件冒泡
父元素React事件冒泡
document原生冒泡
*/
</script>
</body>
</html>
6 React17 事件系统变更
- 更改事件委托: 首先第一个修改点就是更改了事件委托绑定节点,在16版本中,React都会把事件绑定到页面的document元素上,这在多个React版本共存的情况下就会导致另外一个React版本上绑定的事件没有被阻止触发,所以在17版本中会把事件绑定到render函数的节点上
- 去除事件池: 17版本中移除了事件对象池,这是因为 React 在旧浏览器中重用了不同事件的事件对象,以提高性能,并将所有事件字段在它们之前设置为 null。在 React 16 及更早版本中,使用者必须调用event.persist() 才能正确的使用该事件,或者正确读取需要的属性
7 冒泡函数区别
document.addEventListener('click',(event)=>{
//event.stopPropagation(); // 不再向上冒泡了,但是本元素剩下的监听函数还是会执行
event.stopImmediatePropagation(); // 不但不向上冒泡了,本级剩下的监听函数也不执行
console.log(1);
});
document.addEventListener('click',(event)=>{
console.log(2);
});
五、Ref
- Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素
1 原生使用Ref
- 可以使用 ref 去存储 DOM 节点的引用
- 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
- ref的本质就是一个对象,默认key给current,值为null
import React from 'react';
import ReactDOM from 'react-dom';
class Counter extends React.Component {
constructor(props) {
super(props);
// 创建Ref
this.numberA = React.createRef(); // {current:null}
this.numberB = React.createRef();
this.result = React.createRef();
}
handleClick = (event) => {
let numberA = this.numberA.current.value;
let numberB = this.numberB.current.value;
this.result.current.value = parseFloat(numberA) + parseFloat(numberB);
}
render() {
return (
<>
{/* 原生DOM使用Ref */}
<input ref={this.numberA} />
<input ref={this.numberB} />
<button onClick={this.handleClick}>+</button>
<input ref={this.result} />
</>
);
}
}
ReactDOM.render( <Counter />, document.getElementById('root'));
2 类组件使用Ref
- 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
import React from 'react';
import ReactDOM from 'react-dom';
class TextInput extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
getTextInputFocus = () => {
this.inputRef.current.focus();
}
render() {
return <input ref={this.inputRef} />
}
}
class Counter extends React.Component {
constructor(props) {
super(props);
this.textInputRef = React.createRef();
}
getFormFocus = () => {
// this.textInputRef.current就会指向TextInput类组件的实例
this.textInputRef.current.getTextInputFocus();
}
render() {
return (
<>
{/* 类组件使用Ref */}
<TextInput ref={this.textInputRef} />
<button onClick={this.getFormFocus}>获得焦点</button>
</>
);
}
}
ReactDOM.render(<Counter />, document.getElementById('root'));
实现原理
function createRef(){
return {current:null};
}
function createElement(type,config,children){
let ref;//是用来获取虚拟DOM实例的
if(config){
ref = config.ref;
delete config.ref;
}
return {
type,
props,
ref,
key
}
}
function createDOM(vdom) {
let { type, props, ref } = vdom;
let dom;//获取 真实DOM元素
// TODO 。。。其他逻辑
//让虚拟DOM的dom属生指向它的真实DOM
vdom.dom = dom;
if (ref) ref.current = dom;//让ref.current属性指向真实DOM的实例
return dom;
}
3 函数组件使用Ref
- 不能在函数组件上使用 ref 属性,因为他们没有实例
- 但是可以通过Ref转发,将函数组件进行包裹,将其Ref传递下去
import React from 'react';
import ReactDOM from 'react-dom';
function TextInput(props, ref) {
return <input ref={ref} />
}
// forwardRef 包裹函数组件
const ForwardedTextInput = React.forwardRef(TextInput);
class Counter extends React.Component {
constructor(props) {
super(props);
this.textInputRef = React.createRef();
}
getFormFocus = () => {
// this.textInputRef.current就会指向TextInput类组件的实例
this.textInputRef.current.focus();
}
render() {
return (
<>
{/* 函数组件使用Ref */}
<ForwardedTextInput ref={this.textInputRef} />
<button onClick={this.getFormFocus}>获得焦点</button>
</>
);
}
}
ReactDOM.render(<Counter />, document.getElementById('root'));
实现原理: 给forwardRef返回的对象上加上ref标识,然后在虚拟DOM转为真实DOM的时候,解析并判断类型,最后执行函数组件的render方法,传入ref,最后挂载到父组件的ref.current属性上
const REACT_FORWARD_REF_TYPE = Symbol('react.forward_ref');
// 1 加上标识,返回一个对象
function forwardRef(render) {
return {
$$typeof: REACT_FORWARD_REF_TYPE,
render // 原来那个函数件
}
}
// 2 在虚拟DOM转成真实DOM的时候,判断类型,然后进行特殊处理
function createDOM(vdom) {
let { type, props, ref } = vdom;
let dom;//获取 真实DOM元素
//如果type.$$typeof属性是REACT_FORWARD_REF_TYPE值
if (type && type.$$typeof === REACT_FORWARD_REF_TYPE) {
return mountForwardComponent(vdom)
}
// TODO 。。。其他逻辑
//让虚拟DOM的dom属生指向它的真实DOM
vdom.dom = dom;
if (ref) ref.current = dom;//让ref.current属性指向真实DOM的实例
return dom;
}
function mountForwardComponent(vdom) {
let { type, props, ref } = vdom;
// 执行函数组件的render方法
let renderVdom = type.render(props, ref);
vdom.oldRenderVdom = renderVdom;
return createDOM(renderVdom);
}
六、生命周期
1 旧版生命周期
初始化 》 挂载 》 更新 》 卸载
- 状态更新生命周期变化
import React from 'react';
import ReactDOM from 'react-dom';
class Counter extends React.Component {
static defaultProps = {
name: '计算器'
}
constructor(props) {
super(props);
this.state = {number: 0};
console.log("Counter 1.constructor 初始化属性和状态");
}
handleClick = () => {
this.setState({number: this.state.number + 1});
}
componentWillMount() {
console.log("Counter 2.componentWillMount 组件将要挂载");
}
componentDidMount() {
console.log("Counter 4.componentWillMount 组件挂载完成");
}
// setState会引起状态的变化,父组件更新的时候,会让子组件的属性发生变化
// 当属性或者状态发生改变的话,会走此方法来决定 是否要渲染更新
shouldComponentUpdate(nextProps, nextState) {
console.log('Counter 5.shouldComponentUpdate 决定组件是否需要更新?');
// 偶数为true 需要更新,奇数为false 不需要更新
return nextState.number % 2 === 0;
}
componentWillUpdate() {
console.log('Counter 6.componentWillUpdate 组件将要更新');
}
componentDidUpdate() {
console.log('Counter 7.componentDidUpdate 组件更新完成');
}
render() {
console.log("Counter 3.render 挂载");
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter />, document.getElementById('root'))
// 初始化挂载
Counter 1.constructor 初始化属性和状态
Counter 2.componentWillMount 组件将要挂载
Counter 3.render 挂载
Counter 4.componentWillMount 组件挂载完成
// 点击加号,返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?
// 点击加号,返回true
Counter 5.shouldComponentUpdate 决定组件是否需要更新?
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
Counter 7.componentDidUpdate 组件更新完成
- 状态和属性一起变化的生命周期
import React from 'react';
import ReactDOM from 'react-dom';
class ChildCounter extends React.Component {
static defaultProps = {// 1.设置默认属性
name: 'ChildCounter'
}
componentWillMount() {
console.log('ChildCounter 1.componentWillMount 组件将要挂载');
}
render() {
console.log('ChildCounter 2.render');
return (<div>{this.props.count}</div>)
}
componentDidMount() {
console.log('ChildCounter 3.componentDidMount 组件挂载完成');
}
componentWillReceiveProps(newProps) {
console.log('ChildCounter 4.componentWillReceiveProps 组件将要接收到新的属性');
}
shouldComponentUpdate(nextProps, nextState) {
console.log('ChildCounter 5.shouldComponentUpdate 决定组件是否需要更新?');
// 3的倍数就更新,否则就不更新
return nextProps.count % 3 === 0;
}
componentWillUpdate() {
console.log('ChildCounter 7.componentWillUpdate 组件将要更新');
}
componentDidUpdate() {
console.log('ChildCounter 8.componentDidUpdate 组件更新完成');
}
componentWillUnmount() {
console.log('ChildCounter 9.componentWillUnmount 组件将要卸载');
}
}
class Counter extends React.Component {
static defaultProps = {
name: 'Counter' // 设值默认属性
}
constructor(props) {
super(props);
this.state = { number: 0 }; // 设值默认状态
console.log("Counter 1.constructor 初始化属性和状态");
}
handleClick = () => {
this.setState({ number: this.state.number + 1 });
}
componentWillMount() {
console.log("Counter 2.componentWillMount 组件将要挂载");
}
componentDidMount() {
console.log("Counter 4.componentWillMount 组件挂载完成");
}
shouldComponentUpdate(nextProps, nextState) {
console.log('Counter 5.shouldComponentUpdate 决定组件是否需要更新?');
// 偶数为true 需要更新,奇数为false 不需要更新
return nextState.number % 2 === 0;
}
componentWillUpdate() {
console.log('Counter 6.componentWillUpdate 组件将要更新');
}
componentDidUpdate() {
console.log('Counter 7.componentDidUpdate 组件更新完成');
}
render() {
console.log("Counter 3.render 挂载");
return (
<div>
<p>{this.state.number}</p>
{this.state.number === 4 ? null : <ChildCounter count={this.state.number} />}
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter />, document.getElementById('root'))
// 初始化挂载
Counter 1.constructor 初始化属性和状态
Counter 2.componentWillMount 组件将要挂载
Counter 3.render 挂载
ChildCounter 1.componentWillMount 组件将要挂载
ChildCounter 2.render
ChildCounter 3.componentDidMount 组件挂载完成
Counter 4.componentWillMount 组件挂载完成
// 点击加号,state = 1,奇数返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?
// 点击加号,state = 2,偶数返回true
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
ChildCounter 4.componentWillReceiveProps 组件将要接收到新的属性
ChildCounter 5.shouldComponentUpdate 决定组件是否需要更新?
Counter 7.componentDidUpdate 组件更新完成
// 点击加号,state = 3,奇数返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?
// 点击加号,state = 4,偶数返回true,子组件卸载
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
ChildCounter 9.componentWillUnmount 组件将要卸载
Counter 7.componentDidUpdate 组件更新完成
// 点击加号,state = 5,奇数返回false
Counter 5.shouldComponentUpdate 决定组件是否需要更新?
// 点击加号,state = 6,偶数返回true,子组件重新渲染
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
ChildCounter 1.componentWillMount 组件将要挂载
ChildCounter 2.render
ChildCounter 3.componentDidMount 组件挂载完成
Counter 7.componentDidUpdate 组件更新完成
// ...... 一直点击加号到12
// 点击加号,state = 12,偶数返回true,子组件更新
Counter 5.shouldComponentUpdate 决定组件是否需要更新?
Counter 6.componentWillUpdate 组件将要更新
Counter 3.render 挂载
ChildCounter 4.componentWillReceiveProps 组件将要接收到新的属性
ChildCounter 5.shouldComponentUpdate 决定组件是否需要更新?
ChildCounter 7.componentWillUpdate 组件将要更新
ChildCounter 2.render
ChildCounter 8.componentDidUpdate 组件更新完成
Counter 7.componentDidUpdate 组件更新完成
2 新版生命周期
创建时 》 更新时 》 卸载时
- V17可能会废弃的三个⽣命周期函数⽤getDerivedStateFromProps替代,⽬前使⽤的话加上UNSAFE_:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
- 引⼊两个新的⽣命周期函数:
- static getDerivedStateFromProps
- getSnapshotBeforeUpdate
- 变更缘由: 原来(React v16.0前)的⽣命周期在React v16推出的Fiber之后就不合适了,因为如果要开启async rendering,在render函数之前的所有函数,都有可能被执⾏多次
import React from 'react';
import ReactDOM from 'react-dom';
class Counter extends React.Component {
static defaultProps = {// 1.设置默认属性
name: 'ChildCounter'
}
constructor(props) {
super(props);
this.state = { number: 0 }; // 设值默认状态
console.log("Counter 1.constructor 初始化属性和状态");
this.tmpRef = React.createRef();
}
/**
* 替换旧的componentWillReceiveProps
* 从组件的新属性中映射出一个状态
* 设计为静态(static)方法的原因:
* 1 WillReceiveProps里面调用setState 可能会让父组件刷新,父组件一刷新,子组件也会刷新,会出现死循环
* 2 因为static中没有this,就不能调用this.setState方法,就可以避免死循环
*/
static getDerivedStateFromProps(nextProps, prevState) {
console.log("Counter 2.getDerivedStateFromProps", prevState);
const { number } = prevState;
if (number % 2 === 0) {
return { number: number * 2 };
} else if (number % 3 === 0) {
return { number: number * 3 }; // 如果返回的分状态,会跟自己的state进行合并
}
return null; // 如果返回null,表示不修改状态
}
shouldComponentUpdate(nextProps, nextState) {
console.log('Counter 4.shouldComponentUpdate 决定组件是否需要更新?', nextState);
// 3的倍数就更新,否则就不更新
return nextState.number % 4 === 0;
}
handleClick = () => {
this.setState({ number: this.state.number + 1 });
}
render() {
console.log("Counter 3.render");
return (
<div ref={this.tmpRef}>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
</div>
)
}
componentDidMount() {
console.log("Counter 4.componentWillMount 挂载完成");
}
// 在DOM更新前执行,可以用来获取更新前的一些DOM信息
getSnapshotBeforeUpdate() {
console.log('Counter 6.getSnapshotBeforeUpdate 获取DOM更新前的信息');
return this.tmpRef.current.scrollHeight;
}
componentDidUpdate(pervProps, pervState, scrollHeight) {
//当前向上卷去的高度加上增加的内容高度
console.log('Counter 7.componentDidUpdate 更新完成');
console.log('打印高度:', this.tmpRef.current.scrollHeight, scrollHeight);
}
componentWillUnmount() {//清除定时器
console.log('Counter 8.componentWillUnmount 卸载');
}
}
ReactDOM.render(<Counter />, document.getElementById('root'))
// 初始化
Counter 1.constructor 初始化属性和状态
Counter 2.getDerivedStateFromProps {number: 0}
Counter 3.render
Counter 4.componentWillMount 挂载完成
// 点击加号,state = 1
Counter 2.getDerivedStateFromProps {number: 1}
Counter 4.shouldComponentUpdate 决定组件是否需要更新? {number: 1}
// 点击加号,state = 2
Counter 2.getDerivedStateFromProps {number: 2}
Counter 4.shouldComponentUpdate 决定组件是否需要更新? {number: 4}
Counter 3.render
Counter 6.getSnapshotBeforeUpdate 获取DOM更新前的信息
Counter 7.componentDidUpdate 更新完成
打印高度:60 60
七、组件高级
1 组件通信
- Props属性传递可⽤于⽗⼦组件相互通信
- Context跨层级组件之间通信
- Redux⽆明显关系的组件间通信
2 Context
- React中使⽤Context实现祖代组件向后代组件跨层级传值。Vue中的provide & inject来源于Context,在Context模式下有两个⻆⾊:
- Provider:外层提供数据的组件
- Consumer:内层获取数据的组件
- 使用Context: 创建Context => 获取Provider和Consumer => Provider提供值 => Consumer消费值
- React Context
3 高阶组件HOC
- 高阶组件就是一个工厂函数,传给它一个组件,它返回一个新的组件
- 高阶组件的作用其实就是为了组件之间的代码复用,主要用途为 属性代理 和反向继承
- 高级组件来自于高阶函数
属性代理:对原有属性进行扩展
import React from 'react';
import ReactDOM from 'react-dom';
const withLoading = (OldComponent) => {
return class extends React.Component {
show = () => {
let loading = document.createElement('div');
loading.innerHTML = `<p id="loading" style="position:absolute;top:100px;left:50%;z-index:10;">loading</p>`;
document.body.appendChild(loading);
}
hide = () => {
document.getElementById('loading').remove();
}
render() {
// 属性代理:对原有属性进行扩展
return <OldComponent {...this.props} show={this.show} hide={this.hide} />
}
}
}
class Panel extends React.Component {
render() {
return (
<div>
{this.props.title}
<button onClick={this.props.show}>显示</button>
<button onClick={this.props.hide}>隐藏</button>
</div>
)
}
}
// 写法一:直接包装使用
let LoadingPanel = withLoading(Panel);
ReactDOM.render(<LoadingPanel title="这是标题" />, document.getElementById('root'));
- 装饰器写法
// 1. 安装
npm i react-app-rewired customize-cra @babel/plugin-proposal-decorators -D
// 2. 修改package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
}
// 3. 编写config-overrides.js
const { override, addBabelPlugin } = require('customize-cra');
module.exports = override(
addBabelPlugin(
[
"@babel/plugin-proposal-decorators", { "legacy": true }
]
)
);
// 4. 编写jsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true
}
}
// 5. 修改高阶组件代码,使用@进行扩展
const withLoading = (OldComponent) => {}
@withLoading
class Panel extends React.Component {
....
}
ReactDOM.render(<Panel title="这是标题" />, document.getElementById('root'));
反向继承:拦截生命周期、state、渲染过程
// 一个场景:对第三方组件进行扩展
import React from 'react';
import ReactDOM from 'react-dom';
// 第三方组件,不能修改里面的代码,只能进行扩展
class AntDesignButton extends React.Component {
state = { name: '张三' }
componentWillMount() {
console.log('AntDesignButton componentWillMount');
}
componentDidMount() {
console.log('AntDesignButton componentDidMount');
}
render() {
console.log('AntDesignButton render');
return <button name={this.state.name} title={this.props.title} />
}
}
// 高阶组件:反向继承
const wrapper = OldComponent => {
return class extends OldComponent {
state = { number: 0 }
componentWillMount() {
console.log('wrapper componentWillMount');
super.componentWillMount();
}
componentDidMount() {
console.log('wrapper componentDidMount');
super.componentDidMount();
}
handleClick = () => {
this.setState({ number: this.state.number + 1 });
}
render() {
console.log('wrapper render');
// 获取父组件的虚拟dom
let renderElement = super.render();
// 扩展新属性
let newProps = {
...renderElement.props,
onClick: this.handleClick
}
// 老元素 新属性 新儿子
return React.cloneElement(renderElement, newProps, this.state.number);
}
}
}
let WrappedButton = wrapper(AntDesignButton);
ReactDOM.render(<WrappedButton title="这是标题" />, document.getElementById('root'));
// 初始状态,先子后父
wrapper componentWillMount
AntDesignButton componentWillMount
wrapper render
AntDesignButton render
wrapper componentDidMount
AntDesignButton componentDidMount
// 点击按钮
wrapper render
AntDesignButton render
React.cloneElement原理
/**
* 根据一个老的元素,克隆出一个新的元素
* @param {*} oldElement 老元素
* @param {*} newProps 新属性
* @param {*} children 新的儿子们
*/
function cloneElement(oldElement, newProps, children) {
if (arguments.length > 3) {
children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
} else {
children = wrapToVdom(children);
}
// 新属性覆盖老属性 新儿子覆盖老儿子
let props = { ...oldElement.props, ...newProps, children };
return { ...oldElement, props };
}
// 不管原来是什么样的元素,都转成对象的形式,方便后续的DOM-DIFF
export function wrapToVdom(element) {
if (typeof element === 'string' || typeof element === 'number') {
//返回的也是React元素,也是虚拟DOM
return { type: REACT_TEXT, props: { content: element } };//虚拟DOM.props.content就是此文件的内容
} else {
return element;
}
}
4 Render Props
- render-props
- render prop 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
- 具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑
- render prop 是一个用于告知组件需要渲染什么内容的函数 prop,这也是逻辑复用的一种方式
// 移动鼠标获取坐标的案例
import React from 'react';
import ReactDOM from 'react-dom';
// 组件
class MouseTracker extends React.Component {
state = { x: 0, y: 0 }
handleMouseMove = event => {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{/*
<h1>移动鼠标</h1>
<p>当前的鼠标位置是x={props.x}, y={props.y}</p>
*/}
{this.props.render(this.state)}
</div>
)
}
}
ReactDOM.render(
// render属性返回一个函数,props传入到子组件,子组件调用props.render函数传入参数
<MouseTracker render={
props => (
<div>
<h1>移动鼠标</h1>
<p>当前的鼠标位置是x={props.x}, y={props.y}</p>
</div>
)
} />
, document.getElementById('root'));
5 React.PureComponent
- PureComponent表示一个纯组件,可以用来优化React程序,减少render函数执行的次数,从而提高组件的性能
- PureComponent会重写并自动执行 shouldComponentUpdate方法,对属性和状态都做浅比较
- PureComponent的优点:当组件更新时,如果组件的props或者state都没有改变,render函数就不会触发。省去虚拟DOM的生成和对比过程,达到提升性能的目的
// 实现原理
export class PureComponent extends React.Component {
// 重写shouldComponentUpdate方法
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState)
}
}
// 浅比较两个对象
export function shallowEqual(obj1 = {}, obj2 = {}) {
if (obj1 === obj2) { // 两个对象一样
return true;
}
// 不是对象为false
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
return false;
}
let keys1 = Object.keys(obj1);
let keys2 = Object.keys(obj2);
// 长度不一致
if (keys1.length !== keys2.length) {
return false;
}
// 遍历,逐个判断key和val
for (let key of keys1) {
if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
return false;
}
}
return true;
}
6 React.memo
- React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo只能用于函数组
- React.memo 默认是浅比较,可以传入第二个参数进行自定义深比较
// 使用案例
import React from 'react';
import ReactDOM from 'react-dom';
function SubCounter(props) {
return <div>{props.count}</div>
}
// 自定义深比较
// let MemoSubCounter = React.memo(SubCounter, (prevProps, nextProps) => {
// return JSON.stringify(prevProps) === JSON.stringify(nextProps);
// });
// 默认浅比较
let MemoSubCounter = React.memo(SubCounter);
class Counter extends React.Component {
state = { number: 0 }
handleClick = (event) => {
this.setState({ number: this.state.number + 1 });
}
render() {
console.log('Counter render');
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
{/* 使用memo组件 */}
<MemoSubCounter count={this.state.number} />
</div>
)
}
}
ReactDOM.render(<Counter />, document.getElementById('root'));
实现原理:React.memo返回的是一个对象
// memo封装了PureComponent
function memo(OldComponent) {
return class extends React.PureComponent {
render() {
return <OldComponent {...this.props} />
}
}
}
// react中定义具体的虚拟dom对象,在react-dom中domdiff的时候判断类型比做出具体渲染
export const REACT_MEMO = Symbol('react.memo');
function memo(type, compare = shallowEqual) {
return {
$$typeof: REACT_MEMO,
type,//原来那个真正的函数组件
compare
}
}