React组件基础
React的元素与组件
React的元素与组件本质上是同一个东西,不过出现的形式不一样。
React元素:直接用const div = React.createElement创建的虚拟DOM
React组件:使用函数的形式返回React元素,const Div = ()=>React.createElement。注意组件名第一个字母要大写,这是开发者的约定俗成。
组件分类
-
类组件
-
以class的模式声明的React组件,叫做类组件。
class myComponent extends React.Component{ constructor(){ } render(){ return (<h1> this is myComponent</h1>) } } -
类组件的使用方法
首先要确保引入了组件。
// 直接使用 ReactDOM.render(<myComponent />, document.querySeletor('#app')) // 组件嵌套 function App() { return ( <div className="App"> 这是爸爸,下面有一个儿子和一个孙子 <Son /> </div> ); } class Son extends React.Component { render() { return ( <div className="Son"> 这是儿子,下面还有一个孙子 <Grandson /> </div> ); } }
-
-
函数组件
-
以函数的方式声明的React组件,在使用上比类组件更简洁,所以更受欢迎
function myComponent(){ return (<h1> this is myComponent</h1>) } -
函数组件的使用方法
和类组件的方法相同。
-
-
React.createElement的逻辑
-
<div /><myComponent />会被翻译成什么呢?<div />会被翻译成React.createElement('div')<myComponent />则会被翻译成React.createElement(myComponent)可以通过 babel翻译查询 来查看
-
React.createElement的逻辑
- 如果传入的是一个字符串,则会创建一个该字符串对应的标签的React元素,如
React.createElement('div') - 如果传入的是一个函数,则会调用该函数,获取其返回值。如:函数组件。
- 如果传入的是一个类,则会通过new创建一个该类的实例(这会导致执行constructor)作为组件对象,然后调用对象的render方法,获取返回值。
- 如果传入的是一个字符串,则会创建一个该字符串对应的标签的React元素,如
-
-
组件的外部数据props
-
怎么传递外部数据?
在使用组件的时候直接传递。
<Son messageForSon="儿子你好" /> // 完整实例 function App() { return ( <div className="App"> 爸爸 <Son messageForSon="儿子你好" /> </div> ); } -
类组件中使用props
通过**
this.props.[props名]**获取外部传入的数据,注意React中JS语句要使用{}包裹。class Son extends React.Component { render() { return ( <div className="Son"> 我是儿子,我爸对我说「{this.props.messageForSon}」; <Grandson messageForGrandson="孙子你好" /> </div> ); } } -
函数组件中使用props
函数组件在声明的时候第一个参数默认就是props外部数据组成的对象,所以直接
[第一个参数名].[props名]即可调用到外部参数。但是我们一般都把这第一个参数名写成props,更加语义化。function Grandson(haha){ // 这里使用haha作为形参,一般会叫props。 return ( <div className="Grandson"> 我是孙子,我爸对我说「{haha.messageForGrandson}」 </div> ) }
-
-
内部数据state
-
类组件中的state
class Son extends React.Component { constructor() { super(); // 声明和初始化state this.state = { n: 0, m: 0 }; } add() { // 修改state // 思考:this.state.n += 1 为什么不行 this.setState({ n: this.state.n + 1 }); } render() { return ( <div className="Son"> // 直接在模板html中渲染state 儿子 n: {this.state.n} <button onClick={() => this.add()}>+1</button> <Grandson /> </div> ); } }-
声明和直接使用state
使用类组件时,在声明组件的过程中,
constructor里可以通过this.state = { ...}初始化内部数据,并且可以直接通过{this.state.[stateName]}渲染这些数据。 -
修改state
-
新手写法:类组件中的state需用通过类组件对象的内置方法
this.setState({产生变化的数据}/函数)来修改,如:this.setState({n:this.n + 1})。这个API调用时,会新创建一个对象来覆盖掉原来的state对象。这个新创建的对象中变化的数据使用变化后的值,而没有变化的数据沿用原来的值,但是我们使用该API时只需要写变化的数据。 -
经验写法:
this.setState({产生变化的数据}/函数)这个方法是异步的,如果直接使用:this.setState({n:this.n + 1})来更新数据的话,那么我们不能马上更新 n ,必须等同步任务结束后才会更新 n ,忽视这点容易出现获取的数据都是旧的数据的问题。因此有经验的开发者会这样写:this.setState( (state) =>{ // 函数的第一个参数就是state对象 const n = state.n + 1 ; // ↓之间的部分是对新数据的操作内容 console.log(n); // ↑ return {n} } )这样实际上我们将更新和操作新数据的步骤一起执行了,就不会出现旧数据的问题。
-
错误写法一:不使用
this.setState,直接this.state.xxx = ...。React并不像Vue一样监听了所有的数据,直接this.state.xxx = ...是不会触发界面UI的更新的,因为React根本不知道数据发生了改变。 -
错误写法二:没有新创建一个对象传递给
this.setState,如:this.state.n += 1 setState(this.state)注意:React强烈反对直接修改state的数据,而是推荐创建一个新的对象覆盖之前的state,因为后续的操作可能会覆盖前面的修改。
-
-
-
函数组件中的state
-
const Grandson = () => { // 声明state const [n, setN] = React.useState(0); return ( <div className="Grandson"> // 直接使用state 孙子 n:{n} <button onClick={() => setN(n + 1)}>+1</button> </div> ); }; -
简单的声明和直接使用state
函数组件的state声明过程很独特:
const Div = ()=>{ const [n,setN] = React.useState(0);return (..)};我们先声明一个数组,[n,setN]这就是我们state中的其中一个数据的2个接口,n是这个数据的读接口,setN是这个数据的写接口,React.useState(0)这里0就是这个数据的初始化的值。 -
复杂的声明
如果state中有多个数据如何声明呢?
// 方法一:多次声明 const Div1 = ()=>{ const [n,setN] = React.useState(0) const [m,setM] = React.useState(0) } // 方法二:一次性声明(这里不再有读写接口的概念) // state:包含所有state数据的对象 // setState:能够更新所有state的接口,和类组件的this.setState类似 // state、setState只是个名字可以自定义,但是最好还是使用这两个 const Div2 = ()=>{ const [state,setState] = React.useState({n:0,m:0}) } -
读写state
函数组件中所有的数据都有读接口和写接口。
-
简单声明下使用state:
// 读:直接使用读接口 // 写:使用如:setN( n+1 ) const Div1 = ()=>{ const [n,setN] = React.useState(0) const add = ()=>{ // setN的参数是一句简单的JS语句 setN(n+1) // 这既用到了写接口,又用到了读接口 } } -
复杂声明下使用state:
const Div2 = ()=>{ const [state,setState] = React.useState({n:0,m:0}) const add = ()=>{ // 这里的参数是一个新的state的对象了 setState({ // 特别注意这个操作 ...state; // state.n读取n的数据 n:state.n+1 }) } }复杂声明下,读取数据需要通过对象的形式
state.[stateName],而更新state不再是简单声明下的直接修改,而是和类组件一样通过传递一个新的对象来覆盖旧的state,但是这里又有一个非常大的区别:注意...state这个操作。函数组件的‘setState’并没有像类组件一样对没有变化的数据沿用旧值这一操作,即传入的参数对象就是最新的state,如果新对象中没有但旧对象中有的数据,值都为undefined。所以
...state这个操作或者Object.assign把之前的state中的所有数据拷贝到新的对象中,防止数据的丢失。
-
-
类组件与函数组件关于操作state的对比以及注意点
- 类组件中的大多操作都涉及
this,所以要清楚this的指向,而函数组件则很少涉及到this。 - 类组件的
this.setState对没有变化的属性会自动沿用旧值,而函数组件的setState则会完全覆盖旧state对象,所以要记得...state或者Object.assign。 - 类组件的沿用旧值的功能只在第一层数据生效。
constructor(){ super() this.state = { // obj是第一层数据,而obj的值则是第二层数据 obj:{ name:'jack', age:18 } } }在这个例子下,
this.setState({obj:{ name:'ben' }})会改变this.state.obj.name,this.state.obj.age没有改变,但是由于沿用旧值的功能只在第一层数据生效,this.state.obj.age并不会沿用旧值而是变成了undefined,整个this.state.obj的值就是{ name:'ben' }。所以非常不建议依赖React的旧值沿用的功能,而是统统使用
...语法来保证数据的稳定。 - 类组件中的大多操作都涉及
-
-
以数据为基础比较React与Vue

-
事件绑定
-
函数组件事件绑定
const Div = ()=>{ const [n,setN] = React.useState(0) // 设置处理函数 const add = ()=>{ setN( n+1 ) } return ( <div> {n} // 函数名绑定 <button onClick={add}> +1 </button> // 匿名函数绑定 <button onClick={()=>{setN(n-1)}}> -1 </button> </div> ) }函数组件的事件绑定比较简单,可以直接使用匿名函数绑定事件,也可以通过事先声明的函数绑定事件。
-
类组件的事件绑定
React对事件函数的调用是
button.onclick.addN.call(null,...)进行调用的,所以要注意this的指向。这里我们探究类组件的事件绑定的写法演进以及注意点
-
正常写法以及拓展
class Son extends React.Component { constructor() { super(); this.add = ()=>{...} } render() { return ( <div className="Son"> <button onClick={() => this.add()}>+1</button> </div> ); } }在
constructor中声明处理函数,并以onClick={() => this.add()}的匿名函数的方式传递给<button>,那么如果我们直接onClick={this.add}可以吗?我们知道函数中的this指向的是当前函数运行的环境。
onClick={() => this.add()}这里我们在匿名的箭头函数中调用事件函数,此时this.add()的this指向的是组件的实例,即:组件实例.add()所以add函数调用时其内部的this指向这个实例。而
onClick={this.add}中的this指向的是组件实例这点没有问题,但是之后就会出现2种情况:①执行函数以 function(){} 的形式 定义在constructor中;②执行函数以箭头函数的形式定义在constructor中。①如果执行函数以 function(){} 的形式 定义在constructor中,那么
onClick={this.add}事件触发时相当于是直接执行这个function,所以函数内部的this指向全局变量window。②如果执行函数以箭头函数的形式定义在constructor中,那么这里就涉及到箭头函数的一个特性:箭头函数的this指向在声明的时候就已经确定。所以其内部的this的指向就是组件实例。
根据上面的推导,我们运行下列的代码可以发现
onClick={this.addM}会报错,原因是Cannot read property 'm' of undefined这是因为this指向了window。解决方式:onClick={this.addM.bind(this)},但是这个函数名过于复杂,差评!class Son extends React.Component { constructor() { super(); this.state = {n: 0,m: 0}; this.addN = ()=>{ this.setState({n:this.state.n + 1}) } this.addM = function () { this.setState({m:this.state.m + 1}) } } render() { return ( <div className="Son"> // 成功运行 <button onClick={this.addN}>n+1</button> // 运行报错 <button onClick={this.addM}>m+1</button> </div> ); } } -
最佳写法以及拓展
其实最佳写法在上面的推演中已经出现了:
class Son extends React.Component { constructor() { super(); this.state = {n: 0}; this.addN = ()=>{ this.setState({n:this.state.n + 1}) } } render() { return ( <div className="Son"> <button onClick={this.addN}>n+1</button> </div> ); } }上面这种就是类组件绑定的事件的最佳方式:我们可以直接通过声明的函数名进行绑定,不会出现this的指向错误,以及过于复杂的函数名(this.addM.bind(this))。很多开发者在这个基础上还希望不要将函数放到constructor中,为此React特地提供了一个语法糖:
class Son extends React.Component { constructor() {...} // 语法糖 addN = ()=>{...} render() { return ( <div className="Son"> <button onClick={this.addN}>n+1</button> </div> ); } }这个语法糖的效果就和声明在constructor中的效果一样,不过省略了this,以及不需要放在constructor中。注意:是
addN = ()=>{...}的形式,而不是addN(){...}的形式。addN = ()=>{...}和addN(){...}的区别在于,前者是语法糖,实际上addN是声明在constructor中的,是组件实例的自有属性;而后者则是属于这个组件class的原型上的函数,而且函数的定义其实相当于addN:function(){...}的形式,其内部的this也指向的window。 -
总结:
用函数组件吧。。。
-
-
-