目录
主要内容:函数式组件,类式组件,组件复用(数据抽取/传入),组件状态state,组件交互(绑定事件,获取事件对象,原生DOM对象,获取原生DOM对象),状态更新setstate,props与state区别,组件通信与数据流,key的唯一性。
1.组件类型
web
组件就是对 web
中的数据、结构、方法等进行封装,复用,与 JavaScript
中功能函数封装类似,但更关注的是对 web
元素(标签)的封装与扩展(扩展:webComponent
)
React
提供了两种组件构建方式
- 函数式组件
- 类式组件
1.1函数式组件
在 React.js 中,定义一个组件的最简单的方式就是 函数。
如果当前文件中用到了JSX,就必须导入react。
function Kaikeba() {
return (
<div>
<h2>开课吧!</h2>
</div>
);
}
ReactDOM.render(
<Kaikeba />,
document.getElementById('app')
);
- 函数的名称就是组件的名称
- 函数的返回值就是组件要渲染的内容
**函数的名称(组件的名称),必须是首字母大写
1.2类式组件
我们还可以通过 类(class)类定义组件。
无论类式组件还是函数式组件,最后都要渲染内容。
class Miaov extends React.Component {
render() {
return (
<div>
<h2>妙味!</h2>
</div>
);
}
}
- 组件类必须继承父类 React.Component
- 组件类必须有 render 方法
- render 方法的返回值就是组件要渲染的内容
**类的名称(组件的名称),也必须是首字母大写
2.创建FriendList组件——案例
不从实际案例开始的知识点就是耍 LM
样式 :
.friend-list {
border: 1px solid #000000;
width: 200px;
}
.friend-group dt {
padding: 10px;
background-color: rgb(64, 158, 255);
font-weight: bold;
}
.friend-group dd {
padding: 10px;
display: none;
}
.friend-group.expanded dd {
display: block;
}
.friend-group dd.checked {
background: green;
}
创建 FriendList 组件:
import React from 'react';
import './FriendList.css'
function FriendList(){
return (
<div className="friend-list">
<div className="friend-group">
<dt>家人</dt>
<dd>爸爸</dd>
<dd>妈妈</dd>
</div>
<div className="friend-group">
<dt>朋友</dt>
<dd>张三</dd>
<dd>李四</dd>
<dd>王五</dd>
</div>
<div className="friend-group">
<dt>客户</dt>
<dd>阿里</dd>
<dd>腾讯</dd>
<dd>头条</dd>
</div>
</div>
);
}
export default FriendList;
报错 'React' must be in scope when using JSX react/react-in-jsx-scope
解决:组建中也使用到了JSX,所以必须引入react。import React from 'react';
3.组件复用——数据抽取
为了提高组件的复用性,通常会把组件中的一些可变数据提取出来
let datas = {
family: {
title: '家人',
list: [
{name: '爸爸'},
{name: '妈妈'}
]
},
friend: {
title: '朋友',
list: [
{name: '张三'},
{name: '李四'},
{name: '王五'}
]
},
customer: {
title: '客户',
list: [
{name: '阿里'},
{name: '腾讯'},
{name: '头条'}
]
}
};
export default datas;
import React from 'react';
import './FriendList.css';
import datas from './datas/friends1';
function FriendList(){
return (
<div className="friend-list">
<div className="friend-group">
{Object.keys(datas).map((key, index) => (
<div className="friend-group" key={key}>
<dt>{datas[key].title}</dt>
{datas[key].list.map(list => <dd key={list.name}>{list.name}</dd>)}
</div>
))}
</div>
</div>
);
}
export default FriendList;
4.组件复用——数据传入
数据虽然分离了,但是为了降低组件与数据之前的依赖,我们应该尽量避免在组件内部直接访问数据,通过传参的方法来进行解耦。
4.1property
使用组件的时候通过 标签属性-property 的方式传入数据,在组件内部通过构造函数参数(如果是函数组件,则通过函数参数)来接收传入的数据 。
<组件名称 属性名称="值" />
// 使用表达式
<组件名称 属性名称={表达式} />
ReactDOM.render(
<FriendList datas={datas} />,
document.getElementById('app')
);
4.2接收参数——props
在组件对应的函数或类中通过:
- 函数式组件:会传递给函数的第一个参数来接收
- 类式组件:会传递给类的props属性(这个属性在父类React.Component中就已经存在),再通过构造函数第一个参数来接收
无论是函数式组件还是类式组件,都会把传入的参数组装成一个对象:
<组件名称 属性名称1="值1" 属性名称二={表达式二} />
// 函数式组件
function 组件名称(参数) {
// 参数的结构为
参数 = {
属性名称1: "值1",
属性名称二: 表达式二的值
}
return <div>组件结构</div>
}
// 类式组件
class 组件名称 extends React.Component {
constructor(参数) {
super(参数);
this.props = {
属性名称1: "值1",
属性名称2: 表达式二的值
}
}
render() {
return <div>组件结构</div>
}
}
在类式组件中,需要注意:
当子类重写 constructor ,则必须调用父类 super - 类的知识点
把接收到的参数传入父类构造函数,父类构造函数中会创建一个对象属性:props 类存储传入的参数数据
最后示例代码:
函数式组件:
Friends_func.js数据:
export default {
jser: {
title: 'js爱好者',
list: [
{name: '宝哥'},
{name: '莫涛'},
{name: '大海'}
]
},
tser: {
title: 'ts爱好者',
list: [
{name: '雨轩'},
{name: '小姐姐'}
]
}
};
friendlist_func.js函数式组件代码:
// 注意写组件时只要用到了JSX都需要引入react
import React from 'react';
import '../css/FriendList.css';
//函数式组件外部传入数据是通过函数参数进行接收
function FriendListFunc(props){
// 将外部传入的数据进行结构
let {data} = props;
//注意一个组件中只能有一个return
return (
<div className="friend-list">
{
Object.keys(data).map(key=>(
<div className="friend-group" key={key}>
<dt>{data[key].title}</dt>
{
// 注意map()括号中由于只有一个参数所以可以直接跟返回值,而JSX代码不止一行需要使用()括号括起来方便代码查看,不括起来也不会出错
data[key].list.map(dd=>(
<dd key={dd.name}>{dd.name}</dd>
))
}
</div>
))
}
</div>
);
}
export default FriendListFunc;
类式组件数据:
export default {
family: {
title: '家人',
list: [
{name: '爸爸'},
{name: '妈妈'}
]
},
friend: {
title: '朋友',
list: [
{name: '张三'},
{name: '李四'},
{name: '王五'}
]
},
customer: {
title: '客户',
list: [
{name: '阿里'},
{name: '腾讯'},
{name: '头条'}
]
}
};
类式组件代码:
import React from "react";
class FriendListClass extends React.Component{
//类式组件外部传入的数据,需要通过构造函数的参数进行接收
constructor(props){
// 当子类重写constructor ,则必须调用父类super类的知识点
super(props);
// 把接收到的参数传入父类构造函数,父类构造函数中会创建一个对象属性:<u>props</u> 类存储传入的参数数据
this.props = props;
}
//注意render(){ return () }结构不能写错
render(){
let {data} = this.props;
return (
<div className="friend-list">
{
Object.keys(data).map(key => (
<div className="friend-group" key={key}>
<dt>{data[key].title}</dt>
{
data[key].list.map(item=>(
<dd key={item.name}>{item.name}</dd>
))
}
</div>
))
}
</div>
);
}
}
//不管是函数组件还是类式组件都需要导出
export default FriendListClass;
类式组件和函数式组件最终调用及外部数据传入的不同调用:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import FriendListFunc from './components/friendlist_func';
import FriendListClass from './components/friendlist_class';
//通过属性将外部数据传入组件
import FriendFuncData from './datas/friends_Func'
//通过属性将外部数据传入组件
import FriendClassData from './datas/friends_class'
function App() {
return (
<div className="App">
<FriendListFunc data={FriendFuncData}/>
<hr/>
<FriendListClass data={FriendClassData}/>
</div>
);
}
export default App;
4.3通过参数动态渲染组件结构
函数式组件:
APP.js
import React from 'react';
// import FriendList from './compenents/FriendList';
import FriendListProps from './compenents/FriendListProps';
import datas from './compenents/datas/friends1';
function App() {
return (
<div className="App">
<h1>hi, react!</h1>
{/* <FriendList /> */}
<FriendListProps datas={datas} name='列表渲染'/>
</div>
);
}
export default App;
FriendsListProp.js:
import React from 'react';
import './FriendList.css';
// import datas from './datas/friends1';
function FriendListProps(props){
// 函数式组件:通过函数的第一个参数来接收
console.log(props);
let {datas} = props;
return (
<div className="friend-list">
<div className="friend-group">
{Object.keys(datas).map((key, index) => (
<div className="friend-group" key={key}>
<dt>{datas[key].title}</dt>
{datas[key].list.map(list => <dd key={list.name}>{list.name}</dd>)}
</div>
))}
</div>
</div>
);
}
export default FriendListProps;
类式组件:
friends.js:
let datas = {
jser: {
title: 'JS爱好者',
list: [
{name: '姗凤'},
{name: '姐大'},
{name: '冬冬'}
]
},
tser: {
title: 'TS爱好者',
list: [
{name: 'lili'},
{name: '君君'},
{name: '香儿'}
]
}
};
export default datas;
APP.js
import React from 'react';
// import FriendList from './compenents/FriendList';
// import FriendListProps from './compenents/FriendListProps';
import FriendList2 from './compenents/FriendList2';
// import datas from './compenents/datas/friends1';
import datas from './compenents/datas/friends2';
function App() {
return (
<div className="App">
<h1>hi, react!</h1>
{/* <FriendList /> */}
{/* <FriendListProps datas={datas} name='列表渲染'/> */}
<FriendList2 datas={datas} name='列表渲染'/>
</div>
);
}
export default App;
FriendList2.js:
import React from 'react';
import './FriendList.css';
class FriendList2 extends React.Component {
// 类式组件:通过类的构造函数第一个参数来接收
constructor(props){
super(props);
this.props = props;
}
render(){
let {datas} = this.props;
return (
<div className="friend-list">
<div className="friend-group">
{Object.keys(datas).map((key, index) => (
<div className="friend-group" key={key}>
<dt>{datas[key].title}</dt>
{datas[key].list.map(list => <dd key={list.name}>{list.name}</dd>)}
</div>
))}
</div>
</div>
);
}
}
export default FriendList2;
4.4子组件提取
组件与类一样,是一个不断提取的过程,当我们发现某个部分可复用或者结构复杂的时候,我们可以对它再次进行提取 。
class FriendGroup extends React.Component {
constructor(props) {
super(props);
}
render() {
let {data} = this.props;
return (
<div className="friend-group">
<dt>{data.title}</dt>
{data.list.map((list, index) => {
<dd key={index}>{list}</dd>
})}
</div>
);
}
}
class FriendList extends React.Component {
constructor(props) {
super(props);
}
render() {
let {datas} = this.props;
return (
<div className="friend-list">
{Object.keys(datas).map((key, index) => (
<FriendGroup data={datas[key]} key={key} />
))}
</div>
);
}
}
5.组件状态state
有的时候,一个组件除了可以接收外部传入的数据,还会有属于自己的内部私有数据,同时组件私有数据的更新还会导致组件的重新渲染,比如上面的例子中的每一个好友面板会有一个内部私有数据来控制面板的展开与收缩,我们又把这种数据称为组件状态 。
props和state区别:
- props:由外部控制传入数据;
- state:组件自己控制和维护内容私有数据;
state:存储组件私有状态(数据)的对象。当state状态发生改变时,组件UI也会相应进行重新渲染,如果是自定义属性不会进行重新渲染。
函数式组件没有状态state(最新hooks除外)。函数式组件只提供了外部传入组件props方式,而没有内容私有数据控制的state。因为有状态有可能会导致不可控性,一般能写无状态的函数式组件就写这个。现在流行的也是函数式组件。
示例:通过类式组件的state,有内部传入数据控制组件内部收缩展开功能。
import React from 'react';
import '../css/FriendList.css';
/**
* 用户手动控制好友列表展开/收缩
*/
class FriendList extends React.Component {
constructor(props) {
super(props);
this.state = {
//此处手动控制expanded为true/false时,好友列表会展开/收缩
expanded : true
};
}
render() {
//注意解构后的变量名一定要和对象props中已存在的变量名一致
let { datas } = this.props;
//通过解构获得expanded标识
let {expanded} = this.state;
return (
<div className="friend-list">
{
Object.keys(datas).map(item => (
// 此处使用JSX中数组输出方式设置多个className的值
<div className={["friend-group",expanded && "expanded"].join(' ')} key={item}>
<dt>{datas[item].title}</dt>
{
datas[item].list.map(key=><dd key={key.name}>{key.name}</dd>)
}
</div>
))
}
</div>
);
}
}
export default FriendList;
示例解析:
- 通过class样式以语法形式控制:
className={[
"friend-group",
expanded && "expanded"
].join(' ')}
- JSX表达式{}中数据类型为数组转为字符串会以逗号隔开,所以需要使用join(' ')将class样式以空格隔开,且只有当expanded为true时,才会展开有expanded样式,所以使用expanded && "expanded",也可以使用三目运算符。
- expanded && "expanded"原本应该是this.state.expanded && "expanded",可以通过let {expanded} = this.state进行解构,当使用时即可直接使用expanded && "expanded"即可。
6.组件交互
一个组件除了有结构、样式,有的时候还会有交互,如上面的例子:组件初始化时是收缩的,现在我们希望当用户点击组件面板 title 的时候,面板组件的展开收缩状态就会发生改变 。
6.1绑定事件
import React from 'react';
import '../css/FriendList.css';
/**
* 实现用户交互:点击列表后实现展开收缩(事件绑定)
*/
class FriendList extends React.Component {
constructor(props) {
super(props);
this.state = {
//此处手动控制expanded为true/false时,好友列表会展开/收缩
expanded : true
};
//将改变this绑定挂载到实例上,在onClick时就可以直接使用this.expand了
this.expand = this.expand.bind(this)
}
//事件绑定:用户点击列表
expand(){
//如果直接将this.state.expanded = !this.state.expanded;取反,会报错,因为react中时间绑定中,this默认指向undefined,所以需要调用时绑定this指向为这个实例
//React的state控制私有数据,但是需要调用setState()方法才能进行状态更新
this.setState({
expanded : !this.state.expanded
})
}
render() {
//注意解构后的变量名一定要和对象props中已存在的变量名一致
let { datas } = this.props;
//通过解构获得expanded标识
let {expanded} = this.state;
return (
<div className="friend-list">
{
Object.keys(datas).map(item => (
// 此处使用JSX中数组输出方式设置多个className的值
<div className={["friend-group",expanded && "expanded"].join(' ')} key={item}>
<dt onClick={this.expand}>{datas[item].title}</dt>
{
datas[item].list.map(key=><dd key={key.name}>{key.name}</dd>)
}
</div>
))
}
</div>
);
}
}
export default FriendList;
React.js 的事件绑定需要注意:
事件名称是驼峰命名的
事件绑定函数的 this 指向
通过 bind 改变 this 指向,为了能够在方法中调用组件对象的属性和其它方法,我们需要把 this 指向组件
通过箭头函数处理 this
注意:
事件不要写成行间样式,尽量单独写成方法,再在onClick事件中调用该方法,调用时不能写括号。
事件中,直接使用this.state.expanded = !this.state.expanded会报错。因为在react中,事件绑定的函数中的this(并不是所有react中的this都是指向undefined)默认指向undefined。在使用时需要通过bind()方法改变react中原有的this指向,onClick={this.expand.bind(this)},这样就会将this指向组件本身。为了增加复用性会在类初始化构造函数中,直接使用this.expand = this.expand.bind(this);而在html行间onClick={this.expand}。
6.2获取事件对象
事件绑定函数的第一个参数是事件对象
6.3原生DOM对象
通过事件对象可以获取到原生 DOM 对象
6.4获取原生DOM对象
有的时候我们也是需要操作原生 DOM 对象的,除了可以通过事件源来获取,还可以通过 ref 的方式来获取
class MyComponent extends React.Component {
render() {
return (
<div>
<input ref={el=>this.el=el} />
<button onClick={this.todo.bind(this)}>Button</button>
</div>
);
}
todo() {
console.log(this.el.value);
}
}
7.状态更新
当我们希望更新组件状态的时候,不要直接修改 state 的值,而是需要调用 setState 方法来进行更新。
在ES6中通过Object.defineProperty() 和Proxy来进行动态渲染更新(也可以自定义状态更新如setObj()但是涉及到生命周期一般不建议)。但在react中只能使用特定的setState方法进行更新。
ES6中Object.defineProperty()和Object.defineProperties():
Object.defineProperty()功能:
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。如果不指定configurable, writable, enumerable ,则这些属性默认值为false,如果不指定value, get, set,则这些属性默认值为undefined
语法: Object.defineProperty(obj, prop, descriptor)
obj: 需要被操作的目标对象
prop: 目标对象需要定义或修改的属性的名称
descriptor: 将被定义或修改的属性的描述符
var obj = new Object();
Object.defineProperty(obj, 'name', {
configurable: false,
writable: true,
enumerable: true,
value: '张三'
})
console.log(obj.name) //张三
Object.defineProperties()
功能:
方法直接在一个对象上定义一个或多个新的属性或修改现有属性,并返回该对象。
语法: Object.defineProperties(obj, props)
obj: 将要被添加属性或修改属性的对象
props: 该对象的一个或多个键值对定义了将要为对象添加或修改的属性的具体配置
var obj = new Object();
Object.defineProperties(obj, {
name: {
value: '张三',
configurable: false,
writable: true,
enumerable: true
},
age: {
value: 18,
configurable: true
}
})
console.log(obj.name, obj.age) // 张三, 18
7.1setState方法
setState 方法由父类 React.Component 提供,当该方法被调用的时候,组件的 state 会被更新,同时会重新调用组件的 render 方法对组件进行渲染
不要直接修改 state,而是调用 setState 方法来更新,组件构造函数是唯一可以对 state 直接赋值(初始化)的位置
this.setState({key:val})
7.1.1更新异步
出于性能考虑,setState 方法的修改并不是立即生效的(异步)。
会在多次调用setState()方法后统一进行更新
// this.state.val = 0
this.setState({
val: this.state.val + 1
});
// this.state.val 的值并不是 1
console.log(this.state.val);
7.1.2更新合并
React 会把多个 setState 合并成一个调用
// this.state.val = 0
this.setState({
val: this.state.val + 1
});
this.setState({ // 因为异步的问题,this.state.val 的值在这里还是0
val: this.state.val + 1
});
如下:setState()采用的是Object.assign()方法进行更新,每次更新都会覆盖上一次的state值,所以即使多次调用this.state.val值都为2。即使经过多次调用this.setState({}),val值仍然只会执行最后一次调用。
Object.assign(
previousState,
{val: state.val + 1},
{val: state.val + 1},
...
)
7.1.3setState的回调函数
如果,有多个 setState 的调用,后一次的修改如果需要依赖上一次修改的值,那么可以使用函数的方式
// this.state.val = 0
this.setState((state, props) => {
return {val: state.val + 1}
});
this.setState((state, props) => {
return {val: state.val + 1}
});
7.1.4子组件抽离
需求:从组件中抽离出子组件,收缩和展开功能点击时,只控制当前分组。将每单个组件写成一个子组件,有子组件控制收缩展开状态。
注意:
- 子组件的数据也是有父组件从外部传入的。
- 引入子组件时此处也是在map循环中,所以也需要唯一key;
父组件:
import React from 'react';
import '../css/FriendList.css';
import Item from './Item';
/**
* 子组件抽离:原本的state组件交互,只能控制整个列表的同时展开/收缩。
* 现在需要实现点击某个分组控制自己展开/收缩
* 状态由子组件控制
*/
class FriendList extends React.Component {
constructor(props) {
super(props);
}
render() {
let { datas } = this.props;
return (
<div className="friend-list">
{
Object.keys(datas).map(item => (
//注意此处Item组件也在map循环中也需要唯一key
<Item data={datas[item]} key={datas[item].title}/>
))
}
</div>
);
}
}
export default FriendList;
子组件:所有状态控制和事件绑定都在子组件中实现
import React from 'react';
class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
//此处手动控制expanded为true/false时,好友列表会展开/收缩
expanded : true
};
//将改变this绑定挂载到实例上,在onClick时就可以直接使用this.expand了
this.expand = this.expand.bind(this)
}
//事件绑定:用户点击列表
expand(){
//如果直接将this.state.expanded = !this.state.expanded;取反,会报错,因为react中时间绑定中,this默认指向undefined,所以需要调用时绑定this指向为这个实例
//React的state控制私有数据,但是需要调用setState()方法才能进行状态更新
this.setState({
expanded : !this.state.expanded
})
}
render() {
let { data } = this.props;
//通过解构获得expanded标识
let {expanded} = this.state;
return (
// 此处使用JSX中数组输出方式设置多个className的值
<div className={["friend-group", expanded && "expanded"].join(' ')} key={data.title}>
<dt onClick={this.expand}>{data.title}</dt>
{
data.list.map(key => <dd key={key.name}>{key.name}</dd>)
}
</div>
);
}
}
export default Item;
8.props和state的区别
state 的主要作用是用于组件保存、控制、修改自己的可变状态,在组件内部进行初始化,也可以在组件内部进行修改,但是组件外部不能修改组件的 state
props 的主要作用是让使用该组件的父组件可以传入参数来配置该组件,它是外部传进来的配置参数,组件内部无法控制也无法修改
state 和 props 都可以决定组件的外观和显示状态。通常,props 做为不变数据或者初始化数据传递给组件,可变状态使用 state
使用 props 就不要使用 state
8.1无状态式组件
没有 state 的组件,我们称为无状态组件,因为状态会带来复杂性,所以,通常我们推荐使用无状态组件,也鼓励编写无状态组建 。
8.2函数式组件
函数式组件没有 state ,所以通常我们编写使用函数式组件来编写无状态组件
9.组件通信与数据流
在上面的案例中,我们实现了一个多选式的好友列表(每个独立的面板都有自己独立的状态,互不影响)。下面我们再来看另外一种情况:单选式的好友列表(多组列表同时只能有一个为展开状态,也就是多个面板之间的状态是互相影响的)
9.1状态提升
需求:实现多个分组,同时只有一个分组展开其他都收缩
通过分析可以发现,面板的展开状态不再是组件内部私有状态了,多个组件都会受到这个状态的影响,也就是它们共享了一个状态。为了能够让多个不同组件共享同一个状态,这个时候,我们把这个状态进行提升,交给这些组件最近的公共父组件进行管理 。
- 状态由父组件进行控制,子组件不再控制状态state的改变;
- 由于父级不再维护一个分组的展开与否所以不能再使用expanded = false,而需要使用index控制所有分组;
- 父组件会将index值传递给子组件(let {index} = this.state <Item index={index} n={n}/>)(n为对象map遍历生成列表时的索引),子组件接收父组件传递过来的index,判断index与子组件下标相同(index===n)时再展开/收缩;
- 父组件状态值发生变化时,子组件会重新进行渲染;
- 子组件不要直接修改父组件传入的props。因为父组件传入的prop不一定只有当前一个子组件共享。可以通过事件通知或事件回调通知父组件进行修改。
- 在父组件中的展开收缩方法onExpand()方法中通过this.setState({ index:n}) 改变需要更改的子组件索引状态,然后通过props属性onExpand = {this.onExpand.bind(this)}将更改后的值传递给子组件 ;在子组件自己的expand方法中,通过调用父组件的展开收缩方法this.props.onExpand && this.props.onExpand(this.props.n)将子组件需要的修改的n通知给父组件;从而达到子组件的状态修改由父组件控制的目的。
父组件:
import React from 'react';
import '../css/FriendList.css';
import Item from './Item_state_improve';
/**
* 状态提升:
* 所有分组同时只有一个分组展开其他分组都收缩
* 面板的展开状态不再是组件内部私有状态了,多个组件都会受到这个状态的影响,也就是它们共享了一个状态。
* 为了能够让多个不同组件共享同一个状态,这个时候,我们把这个状态进行提升,交给这些组件最近的公共父组件进行管理
*
* 由父组件管理state状态,并通过父组件将分组index和当前点击分组的索引n传递给子组件
* 子组件接收父组件传递的index和num,通过展开方法回调给父组件进行更新状态
*/
class FriendList extends React.Component {
constructor(props) {
super(props);
//状态提升时,将子组件的数据改变回调给父组件进行更改,因为此时不再是控制某个分组展开/收缩,所以需要记录所有分组的index
this.state = {
index : 1
};
this.onExpand = this.onExpand.bind(this)
}
//事件绑定:用户点击列表
onExpand(num){
this.setState({
index:num
})
}
render() {
let { datas } = this.props;
return (
<div className="friend-list">
{
Object.keys(datas).map((item,num) => (
//将所有分组的index,当前分组的索引num,供子组件回调的展开方法onExpand() 传递给子组件
<Item data={datas[item]} key={datas[item].title} index={this.state.index} num={num} onExpand={this.onExpand}/>
))
}
</div>
);
}
}
export default FriendList;
子组件:将状态的更改回调给父组件进行控制
import React from 'react';
/**
* 状态提升子组件
*/
class Item extends React.Component {
constructor(props) {
super(props);
//子组件的this指向绑定到子组件上
this.expand = this.expand.bind(this);
}
//子组件自身展开/收缩方法
expand(e){
//子组件自身不能更改状态,而是回调给父组件统一控制,所以需要调用父组件的展开/收缩方法(父组件通过props属性传递过来)
//**this.props.num是父级传递过来的分组索引,通过子组件点击后将当前索引回调给父组件控制展开
this.props.onExpand && this.props.onExpand(this.props.num);
//通过classList切换展开和收缩
e.target.parentNode.classList.toggle("expanded");
}
render() {
let { data,index,num,flag } = this.props;
return (
// 只有当分组索引index和当前点击分组num一致时才展开
<div className={["friend-group", (index===num)? "expanded":""].join(' ')} key={data.title}>
<dt onClick={this.expand}>{data.title}</dt>
{
data.list.map(key => <dd key={key.name}>{key.name}</dd>)
}
</div>
);
}
}
export default Item;
示例2(方法二):
class FriendGroup extends React.Component {
constructor(props) {
super(props);
this.changeExpand = this.changeExpand.bind(this);
}
changeExpand() {
typeof this.props.onExpand === 'function' && this.props.onExpand(this.props.index);
}
render() {
let {data, expandedIndex, index} = this.props;
return (
<div onClick={this.changeExpand} className={[
"friend-group",
expandedIndex === index && "expanded"
].join(' ')}>
<dt>{data.title}</dt>
{data.list.map((list, index) => {
<dd key={index}>{list}</dd>
})}
</div>
);
}
}
class FriendList extends React.Component {
constructor(props) {
super(props);
this.state = {
expandedIndex: 0
}
this.changeExpandedIndex = this.changeExpandedIndex.bind(this);
}
changeExpandedIndex(expandedIndex) {
this.setState({
expandedIndex
});
}
render() {
let {datas} = this.props;
return (
<div onExpand={this.changeExpandedIndex} className="friend-list">
{Object.keys(datas).map((key, index) => (
<FriendGroup data={datas[key]} index={index} expandedIndex={expandedIndex} key={key} />
))}
</div>
);
}
}
延展练习:点击完后,再次点击仍然可以展开收缩如果实现?
9.2数据流
在 React.js 中,数据是从上自下流动(传递)的,也就是一个父组件可以把它的 state / props 通过 props 传递给它的子组件,但是子组件不能修改 props - React.js 是单向数据流,如果子组件需要修改父组件状态(数据),是通过回调函数方式来完成的。
9.2.1props函数
父组件在调用子组件的时候,传入一个函数类型的 props
<FriendGroup data={datas[key]} index={index} expandedIndex={expandedIndex} key={key} />
9.2.2更新父组件
在父组件的代码中,通过 setState 重新渲染父组件,父组件的更新会重新渲染子组件 。
10.key的唯一性
import React from 'react';
class KeyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
arr: ['html', 'css', 'javascript']
}
this.sort = this.sort.bind(this);
}
sort() {
this.setState({
arr: this.state.arr.sort( _ => Math.random() - .5 )
});
}
render() {
let {arr} = this.state;
return(
<div>
<ul>
{arr.map( (v, k) => (
//在map循环中加上key={v}即可
<li key={v}>
<input type="checkbox"/>
<span>{v}</span>
</li>
) )}
</ul>
<button onClick={this.sort}>乱序</button>
</div>
);
}
}
export default KeyComponent;
通过控制台查看结构变化
问题:发现本来复选框选中的是html选项,但是点击乱序后,复选框选中的是JavaScript。
问题原因:因为在react中考虑到性能问题,当数据发生变化时,不是根据数据重新创建结构再覆盖掉原有虚拟DOM结构,而是会比较虚拟DOM结构与数据之间是否有发生变化,如果没有变化就不会进行删除创建结构等,而是复用原有结构。这种在列表循环时就会造成列表结构和数据内容没有相关联,所以需要使用唯一key(一般为id)对虚拟DOM结构和数据内容进行相关联。不要使用数组下标和时间戳作为唯一key。
10.1渲染优化
- 用JS对象模拟DOM树 : Virtual DOM
- 通过 DOM 操作 把 Virtual DOM 生成真实的 DOM
- 后期的更新,会比较 新老 Virtual DOM ,通过 diff 算法优化更新操作
10.1.1Virtual DOM
原生 DOM 对象结构太复杂,同时存在许多情况下用不到的一些特性
Virtual DOM 本质上与 原生 DOM 没有区别,都是 JavaScript 对象,只是它的结构更加简洁
10.1.2Diffing 算法
React.js 不只是模拟了一个 DOM,同时在针对 DOM 进行渲染的时候