这一章我们正式从组件开始,通过这一章,基本可以靠自己写出一些基础组件,如展示型的页面,简单表单页面等,文章中所有出现的源码都在GitHub上,共需要的童鞋下载。统一的,源码链接还是放在了文章的最后面。还有一点如果觉得文章中贴的代码太长影响了阅读可以快速扫一遍,直接阅读文字内容。跑工程源代码的时候再细看。先来看这一章的思维导图
唉?怎么木有组件的生命周期呢。莫慌!这个是有的,生命周期比较重要,所以打算重点介绍,单独领出来放到下一章了。
1.什么是组件
你知道飞机✈️的结构吗?机头,机身,机翼和机尾。机头是专门给机长准备的,机身是给乘客的,机翼机尾是用来保持平衡和飞翔的,每个部分都各司其职。组件就好比这些飞机的一部分,飞机就是页面。一个页面往往需要多个组件共同完成,每个组件负责不同的功能,这些组件还能像飞机的各个部分如机翼,不光这个飞机能用,装到其他飞机(同型号)也可以。组件也是这样,具有一定的独立性。有人会问那HTML是什么?他是构成组件的一部分,组件构成不光有HTML还有逻辑js负责,样式css负责。所以我的理解是组件是完成某一特定功能的含有逻辑、样式、结构的独立集合体,且具有一定通用性。
2.组件的创建方式
第一种:ES6语法格式使用class进行创建,也是目前最流行的方式。通过继承React.Compoent来实现创建组件。里面必须实现render方法。我们之后文章中所有出现的组件都是这种方式创建!
第二种:通过React.createClass实现,这个已经在react16.0.0版本抛弃了,解决方案是使用create-react-class依赖包。
第三种:纯函数方式,无状态组件。
三种方式我写在了同一个文件中,需要显示哪一个组件则导出哪一个,代码如下(可以直接运行源码):
import React, { Component } from 'react';
import CreateReactClass from 'create-react-class';
/*
* 组件概念
* 1.组件的创建方式
* 2.无状态组件
* */
// ES6 现在主流创建组件方式
class ComponentConcept extends Component{
constructor(props){
super(props);
}
render(){
const { text } = this.props;
return(
<div>Hello Component</div>
)
}
}
// 16.0.0之前是react官方的创建组件的方式React.createClass,16.0.0之后弃用了
// 如今想在16.0.0及以上版本使用这种方式可使用Create-React-Class包
const ComponentConcept1 = CreateReactClass({
getDefaultProps(){
return {
text:'随便命名',
}
},
render(){
const { text } = this.props;
return (
<div>{text}</div>
)
}
})
// 纯函数,无状态组件,只负责展示,只接收一个props和context作为参数
function ComponentConcept2({text = "hello 纯函数"}) {
return (
<div>{text}</div>
)
}
// export default ComponentConcept;
// export default ComponentConcept1;
export default ComponentConcept2;
render方法,是用来绘制页面的,即你要展示的页面内容都在这里进行编写,有以下几点需要注意
(1).必须有render函数,且必须有return,如果木有return浏览器会报错。
(2).纯函数则相当于render函数,同样必须return。
(3).return返回值必须都包裹在一个标签内。即最外层不可出现两个及以上的同级标签,return后面的小括号不写没关系,但是建议必须写,规范。
return (
<div></div>
<div></div>
)// 错误,包含了两个同级标签。其他返回这种HTML模板的函数都是一样的规则。
这种写法是对的
return (
<div>
<div></div>
</div>
)// 正确,最外层只有一个标签。
3.无状态组件
纯函数概念:
给定相同的输入,她总是返回相同的输出;过程没有副作用;没有额外的状态依赖。
上面第三个组件就是用纯函数生成,他没有生命周期,也就没有了状态依赖。下一章介绍生命周期,大白话说就是他创建的组件就像一个相框,相框的外形固定,你放什么相片他就展示什么相片,没有自己的更新机制。所以多用来做纯展示型组件。
4.事件系统
最好先了解一下原生DOM事件,这里有一个传送门DOM事件
在react开发中,事件是必须的,从最实际的开始,现在我们开发中最常用的方法绑定方式“箭头函数”
handleBind(e,value){
console.log('handleBind',e,value);
}
// 添加点击事件,仔细体会下面另种方式
<button className='btnStyle' onClick={this.handleBind}>ErrorBind</button>
<button className='btnStyle' onClick={(e)=>this.handleBind(e,'这里可以传值')}>ErrorBind</button>
上面是一个点击事件,其他事件跟原生DOM事件是一样的,比如onChange,onBlur,onFocus等等,唯一区别就是react事件事件名属于驼峰命名。
除上述绑定方式外还有另外一种方式
<button className='btnStyle' className='btnStyle' onClick={this.handleBind.bind(this,'ddddddd')}>Bind</button>
<button className='btnStyle' onClick={::this.handleBind}>SimpleBind</button>
第一行使用.bind,第二行则是第一行的简写形式使用双冒号::进行绑定。
最后说一下React合成事件的实现原理。
事件委派,react事件并不是把事件直接绑定到真实的dom上(如果想用原生事件,则必须绑定到真实dom上,这就需要生命周期函数,前期在没有生成真实dom之前原生事件绑定是无效的),而是把事件绑定到了结构的最外层,并使用统一的事件监听器。这个事件监听器上有一个映射,保存了组件中的所有事件监听和处理函数。当触发某一个事件时则会根据映射找到对应的处理函数执行。当组件卸载或挂载时,就会在映射表中删除或添加新的事件监听映射记录。
自动绑定,在react中,每个方法的上下文都是指向该组件的实例,即自动绑定this为当前组件。
react事件只实现了冒泡,可以像使用原生事件一样使用stopPropagation进行阻止冒泡。这主要是因为捕获阶段很少使用,同时react中是支持使用原生事件的通过ref。下面是整理事件的几种方式,这需要多动手多体会。如果看代码太长可以直接跳读下面的内容,跑源代码的时候再看这些代码!整个工程源码在最下方有链接。
import React, { Component } from 'react';
import '../../style/analysisComponent/analysisStyle.scss';
/*、
* react 事件系统
* 1.事件原理
* 2.绑定方式
* 3.原生事件
* */
class ComponentAndEvent extends Component{
constructor(props){
super(props);
this.state = {
name:"事件",
}
}
// 1.bind
handleBind(e,value){
console.log('handleBind',e,value);
}
// 2.arrow
handleArrow = (e,value) => {
console.log('handleArrow',e,value);
}
// 外层
onClickFather = () => {
console.log(1);
}
// 内层1
onClickSon = () => {
console.log(2);
}
// 内层2
onClickGrandson = (e) => {
console.log(3);
e.stopPropagation();
}
componentDidMount(){
let isCatch = false;
// 是否是在事件分发阶段进行捕获处理,false代表冒泡阶段处理,true代表分发阶段处理
this.refs.grandson.addEventListener('click',e => {
console.log('grandson');
e.stopPropagation();
},isCatch)
this.refs.son.addEventListener('click',e => {
console.log('son');
},isCatch)
this.refs.father.addEventListener('click',e => {
console.log('father');
},isCatch)
}
render(){
return (
<div className='analysisStyle'>
<button className='btnStyle' className='btnStyle' onClick={this.handleBind.bind(this,'ddddddd')}>Bind</button>
<button className='btnStyle' onClick={::this.handleBind}>SimpleBind</button>
<button className='btnStyle' onClick={this.handleBind}>ErrorBind</button>
<button className='btnStyle' onClick={this.handleArrow}>=>方式1</button>
<button className='btnStyle' onClick={(e) => this.handleArrow(e,'ddddddddd')}>=>方式2</button>
<button className='btnStyle' onClick={this.handleArrow()}>Error=></button>
<div className='fatherBox' onClick={this.onClickFather}>
<div className='sonBox' onClick={this.onClickSon}>
<div className='grandsonBox' onClick={this.onClickGrandson}>
冒泡
</div>
</div>
</div>
<div className='fatherBox' ref='father'>
<div className='sonBox' ref='son'>
<div className='grandsonBox' ref='grandson'>
冒泡
</div>
</div>
</div>
</div>
)
}
}
export default ComponentAndEvent;
5.props
流向:react数据流是自上而下的单向数据流。媒介:props属性
props是在react中编写组件时常用的属性之一,数据的流动靠的就是它。是组件之间相互联系的一种机制!我们所创建的每一组件都会有一个自带的props属性。它主要承载了来自父级的数据,比如
// 子组件Son的render函数
render(){
const { name } = this.state;
const { hello, whoUse } = this.props;
return (
<div>
<div>{`姓名state: ${name}`}</div>
<div>我是Son</div>
<div>{whoUse}</div>
<div>{hello}</div>
</div>
)
}
这是一个子组件(子组件名字叫Son)的render函数,我们从this.props中取出两个属性值,hello,whoUse两个。这两个值从哪里来的?注意这两个值就是调用该组件的时候在组件中传入的。开下面的调用处的代码:
// 父组件Father的render函数
render(){
const { name, isShowChild, isAdd } = this.state;
console.log('father',this.props);
return (
<div>
<div>{`姓名state: ${name}`}</div>
<div>下面是一个子组件:</div>
<Son whoUse={'father component'} hello={'hello word'}>
你好我是夹在子组件标签中的内容,显示了吗?
</Son>
</div>
)
}
结果
最后两行显示的值正式在父组件中调用Son这个组件时传入了whoUse和hello这两个属性值。如果不传则在子组件中拿不到该值。
在仔细看发现“你好我是夹在子组件标签中的内容,显示了吗?”这句话没有显示。实际上,除了刚才说的以属性方式放在组件中的值会被放置到props上,其标签内部的所有内容都则会被放进children中即this.props.children。所以当子组件标签中还包含了内容时,可以在子组件的render函数中跟取出标签属性一样,取出children。如下修改:
// 子组件Son修改后的render函数
render(){
const { name } = this.state;
const { hello, whoUse, children } = this.props;
return (
<div>
<div>{`姓名state: ${name}`}</div>
<div>我是Son</div>
{children/*显示子组件标签内部的嵌套内容*/}
<div>{whoUse}</div>
<div>{hello}</div>
</div>
)
}
所有从父组件中传入的值都会绑定到props这个属性上,可以是任意值,布尔,数字,对象数组等等。
6.state
props是承载力父级组件传入的数据,组件本身的数据(需要展示带有状态的数据)应该放在哪?当然是state来解决。
(1)初始化state,建议在constructor函数中初始化。
constructor(props){
super(props);
this.state = {
name:"Father Component",
isShowChild: true,
isAdd:false,
}
}
也可以在class类内部创建,不需要this关键字,在其他方法中使用时使用this.state.
class Father extends Component{
constructor(props){
super(props);
}
state = {
name:"Father Component",
isShowChild: true,
isAdd:false,
}
...// 省略下面代码
(2) state是一个对象,state中任何一个数据发生变化都会触发render函数刷新页面。所以需要刷新的页面数据可以定义在state中。
(3)更新state中的值必须使用setState方法,this.state.xxx = xxx;这种是不行的。setState方法接收两个传参数,第一个是对象,即用来更新stage中属性值得,第二个是匿名函数,是setState更新完值之后调用的(因为setState是异步方法)。
看下面的代码(组件Son代码片段,工程源码在文章最后有GitHub链接,可下载):
// 组件Son代码片段,运行源码看打印值。
changeBaseNum = () => {
this.baseNum2++;
this.setState({result:this.baseNum2*this.baseNum1},()=>{
console.log(2,this.state.result);// result更新之后调用,打印值是更新后的值
});// 会触发刷新
// this.state.result = this.baseNum2*this.baseNum1;// 错误,不会触发刷新
console.log(1,this.state.result);// setState方法是异步的,会先打印这个,且result值为改变。
}
7.受控组件和非受控组件
大白话讲就是受到控制的组件和不受到控制的组件,受到谁的控制,当然是state了。受控和非受控主要指表单是否受控。
1.如果含有表单的组件是受控的,即表单的值变化是根据state变化的,state也是跟着表单值变化的;
2.非受控则是state可以跟着表单值变化,但是表单值并不会跟着state变化,即不受state控制。
我们写两个含有表单的组件,一个是受控的,一个是非受控的。
如何体现受控和非受控:
1.表单都含有两个输入值姓名和地址;
2.state中都各自定义好nameValue和addressValue两个属性值;
3.并给两个input都添加onChange事件。
4.addressValue变化时,我们都在onChange事件中设置值为“我任性,地址就是泰坦星”,观察受控和非受控页面显示情况;
不同点:
1.我们在render函数中受控组件nameValue和addressValue是给了value,非受控组件给了defaultValue。
具体代码如下:
/*
* 受控组件
* */
class ComponentControl extends Component{
constructor(props){
super(props);
this.state = {
componentName:'ComponentControl',
nameValue:"",
addressValue:"",
}
}
handleNameChange = (event) => {
this.setState({nameValue:event.target.value});// 获取输入值并更新到state的nameValue值上
// 受控组件,注释掉上面一行则页面上输入框无法输入,因为输入框value值受state控制,state初始值是空,无变化,则页面显示一直是空。
}
handleAddressChange = (event) => {
this.setState({addressValue:"我任性,地址就是泰坦星"});
}
render(){
const { nameValue, addressValue } = this.state;
return(
<div className='analysisStyle'>
<h2>受控组件</h2>
<input className='inputStyle' type='text' placeholder='输入姓名' value={nameValue} onChange={this.handleNameChange}/>
<br/>
<input className='inputStyle' type='text' placeholder='输入地址' value={addressValue} onChange={this.handleAddressChange}/>
<br/>
<div>姓名:{nameValue}</div>
<div>地址:{addressValue}</div>
{/*调用一下非受控组件,显示在一起便于观察*/}
<ComponentUncontrol />
</div>
)
}
}
非受控组件
class ComponentUncontrol extends Component {
constructor(props){
super(props);
this.state = {
componentName: 'ComponentUncontrol',
nameValue:"",
addressValue:"",
}
}
handleNameChange = (event) => {
// 非受控组件同样对state的nameValue值进行更新,看input输入框显示和下面显示是不一样,因为input输入框的值不受控state控制
this.setState({nameValue:'你好,我是谁?'},() => {
console.log(this.state.nameValue);
});
}
handleAddressChange = (event) => {
this.setState({addressValue:"我任性,地址就是泰坦星"});
}
render(){
const { nameValue, addressValue } = this.state;
return(
<div className='analysisStyle'>
<h2>非受控组件</h2>
<input className='inputStyle' type='text' ref='nameValue' onChange={this.handleNameChange} defaultValue={nameValue} placeholder='输入姓名' />
<input className='inputStyle' type='text' ref='addressValue' onChange={this.handleAddressChange} defaultValue={addressValue} placeholder='输入地址' />
<div>姓名:{nameValue}</div>
<div>地址:{addressValue}</div>
</div>
)
}
}
然后我们对这两个值都实时在页面上输出。
结语:通过这一章,你一已经可以写一些简单的组件了。知道什么是组件,如何创建组件,如何给事件添加事件监听,react合成事件的原理,如何刷新页面,知道什么是受控组件和非受控组件。下一章介绍【组件的生命周期】