从零开始的react入门教程(二),从react组件说到props/state的联系与区别
壹 ❀ 引
在从零开始的react入门教程(一)一文中,我们搭建了第一个属于自己的react应用,并简单学习了jsx语法。jsx写法上与dom标签高度一致,当然我们也知道,本质上这些react元素都是React.createElement()
的语法糖,通过编译,bable会将其还原成最原始的样子,比如如下代码效果相同:
<div class="echo"></div>
// 等同于
React.createElement(
'div',
{className: 'echo'}
)
至少从书写上,jsx为我们提供了极大便利。在文章结尾,我们也敲到了react元素并非组件,它可能是一个单一的标签,也可能是一个代码块,在react中有专门的方式来创建组件,那么本文就从组件说起。
贰 ❀ 组件
贰 ❀ 壹 函数组件
react中的组件分为函数组件与class组件,两者有一定区别,但都非常好理解。函数组件很简单,比如我们现在想复用一段简单的dom结构,但它的文本内容可能会不同,这种情况我们想到的就是文本内容是一个变量,这样就能做到dom复用的目的了,所以函数组件就是做了这样一件事:
// 这是一个函数组件,它接受一些props,并返回组合后的dom结构
function Echo(props) {
return <div>听风是风又叫{props.name}</div>;
};
ReactDOM.render(<Echo name="听风是风" />, document.getElementById("root"));
需要注意的是,函数组件的函数名是大写的(class组件也是如此),我们在render中使用了组件Echo,并传递了一个name属性,所有在组件上传递的属性都会被包裹在props对象中,所以通过props参数我们能访问到每一个传递给组件的属性。通过打印props可以看到它是一个对象:
function Echo(props) {
console.log(props);
return <div>听风是风又叫{props.name}</div>;
};
传递的数据格式除了字符,数字,它当然也支持对象传递,比如下面这个例子运行结果与上方相同:
const myName = {
name:'echo'
};
function Echo(props) {
console.log(props);
return <div>听风是风又叫{props.name.name}</div>;
};
//
ReactDOM.render(<Echo name= {myName}/>, document.getElementById("root"));
我们来解读下props.name.name
,首先我们是将myName
作为name
的值传递给了组件,所以要访问到myName
得通过props.name
拿到,之后才是name
取到了具体的值。其次需要注意的是传递对象需要使用{}
包裹,如果不加花括号会有错误提示,jsx这里只支持加引号的文本或者表达式,而{myName}
就是一个简单的表达式。
我们在上文中,也有将react元素赋予给一个变量的写法,比如:
const ele = <div>我的名字是听风。</div>
ReactDOM.render(ele, document.getElementById("root"));
其实组件也能像这样赋予给一个变量,所以看到下面这样的写法也不要奇怪:
function Echo(props) {
return <div>我的名字是{props.name}</div>;
};
// 这里将组件赋予给了一个变量,所以render时直接用变量名
const ele = <Echo name="听风是风"/>
ReactDOM.render(ele, document.getElementById("root"));
同理,react元素可以组合成代码块,组件同样可以组合成一个新组件,比如:
function Echo(props) {
return <div>我的名字是{props.name}</div>;
};
function UserList() {
return (
// 注意,只能有一个父元素,所以得用一个标签包裹
<div>
<Echo name="echo" />
<Echo name="听风" />
</div>
);
};
ReactDOM.render(<UserList />, document.getElementById("root"));
在这个例子中,组件Echo
作为基础组件,重新构建出了一个新组建UserList
,所以到这里我们可以发现,组件算自定义的react元素,它可能是由react元素组成,也可能是由组件构成。
贰 ❀ 贰 class组件
除了上面的函数组件外,我们还可以通过class创建组件,没错,就是ES6的class,看一个简单的例子:
class Echo extends React.Component {
render() {
return <div>我的名字是{this.props.name}</div>;
}
};
ReactDOM.render(<Echo name="听风" />, document.getElementById("root"));
由于ReactDOM.render
这一块代码相同,我们把目光放在class创建上,事实上这里使用了extends继承了Component类,得到了一个新组件Echo
。由于此时的Echo
并不是函数,也不能接受函数形参,但事实上我们可以通过this.props
直接访问到当前组件所传递的属性,让人心安的是,与函数组件相比,我们同样有方法获取外部传递的props。
extends中的render
方法是固定写法,它里面包含的是此组件需要渲染的dom结构,如果你了解过ES6的class类,除了render固有方法外,其实我们可以在这个类中自定义任何我们想要的属性以及方法,比如:
const o = {
a: 1,
b: 2,
};
class Echo extends React.Component {
// 这是一个自定义方法
add(a, b) {
return a + b;
}
// 这是固定方法
render() {
return <div>{this.add(this.props.nums.a, this.props.nums.b)}</div>;
}
}
ReactDOM.render(<Echo nums={o} />, document.getElementById("root"));
看着似乎有点复杂,我们来解释做了什么,首先我们在外部定义了一个包含2个数字的对象o,并将其作为nums
属性传递给了组件Echo
,在组件内除了render
方法外,我们还自定义了一个方法add
,最终渲染的文本由此方法提供,所以我们在返回的标签中调用了此方法。前面说了可以通过this.props
访问到外部传递的属性,所以这里顺利拿到了函数的两个实参并参与了计算。
那么到这里我们知道,除了一些组件固有方法属性外,我们也可以定义自己的方法用于处理渲染外的其它业务逻辑。
举个很常见的情景,在实际开发中,有时候我们处理的组件结构会相对庞大和复杂,这时候我们就能通过功能拆分,将一个大组件在内部拆分成单个小的功能块,比如下面这段代码:
class Echo extends React.Component {
handleRenderTop() {
return "我是头部";
}
// 自定义的render方法
renderTop() {
return <div>{this.handleRenderTop()}</div>;
}
handleRenderMiddle() {
// dosomething
}
renderMiddle() {
return <div>我是中间部分</div>;
}
handleRenderBottom() {
// dosomething
}
renderBottom() {
return <div>我是底部</div>;
}
// 官方提供的固定render方法
render() {
return (
<div>
{this.renderTop()}
{this.renderMiddle()}
{this.renderBottom()}
</div>
);
}
}
ReactDOM.render(<Echo />, document.getElementById("root"));
在上述代码中,假设这个组件结构和逻辑比较复杂,通过拆分我们将其分为了上中下三个部分,并创建了对应的处理方法,最终在render
中我们将其组成在一起,这样写的好处是可以让组件的结构更清晰,也利于后期对于代码的维护。当然这里也只是提供了一种思路和可能性,具体做法还需要自行探索。
其实在class组件中除了固有render
方法外,还有ES6的constructor,以及组件生命周期函数,这些都是固定写法,不过我们现在不急,后面会展开说明。
肆 ❀ props与State
肆 ❀ 壹 基本概念与区别
与vue双向数据绑定不同,react提供的是单向数据流,我们可以将react的数据流动理解成一条瀑布,水流(数据)从上往下流动,传递到了瀑布中的每个角落(组件),而这里的水流其实就是由props和State构成,数据能让看似静态的组件换发新生,所以现在我们来介绍下组件中的数据props与State,并了解它们的关系以及区别。
先说props,其实通过前面的例子我们已经得到,props就是外部传递给组件的属性,在函数组件中,可以直接通过形参props
访问,而在class组件中,我们一样能通过this.props
访问到外部传递的属性。
那么什么是State呢,说直白点,State就是组件内部定义的私有属性,这就是两者最直观的区别。
State在react中更官方的解释是状态机,状态的变化会引起视图的变化,所以我们只需要修改状态,react会自动帮我们更新视图。比如下面这个例子:
class Echo extends React.Component {
constructor(props) {
// 参照ES6,extends时,constructor内使用this必须调用super,否则报错
super(props);
this.state = { name: "echo" };
}
render() {
return (
<div>
我的名字是{this.state.name},年龄{this.props.age}
</div>
);
}
}
ReactDOM.render(<Echo age="27" />, document.getElementById("root"));
在上述例子中,外部传递的age
就是props,所以在内部也是通过this.props
访问,而内部定义的属性则是通过this.state
声明,一个在里一个在外,它们共同构成了组件Echo
的数据。
上述例子中constructor
内部调用了super
方法,这一步是必要的,如果你想在继承类的构造方法constructor
中使用this,你就一定得调用一次,这也是ES6的规定,简单复习下ES6的继承:
class Parent {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Child extends Parent {
constructor(x, y, z) {
// 本质上就是调用了超类
super(x, y);
this.z = z; // 正确
}
say() {
console.log(this.x, this.y, this.z);
}
}
const o = new Child(1, 2, 3);
console.log(o);
o.say(); //1,2,3
首先我们定义了一个父类Parent,在它的构造方法中定义了x,y两个属性,之后我们通过extends让Child类继承了Parent,并在Child的构造方法中执行了super
,这里本质上其实就是调用了父类Parent的构造函数方法,只是在执行super
时this指向了Child实例,这也让Child实例o顺利继承了Parent上定义的属性。我们可以输出实例o,可以看到它确实继承了来着Parent的属性。
super本意其实就是超类,所有被继承的类都可以叫超类,也就是我们长理解的父类,它并不是一个很复杂的概念,这里就简单复习下。
肆 ❀ 贰 不要修改props以及如何修改props
在上文中,我们介绍了props与State的基本作用与区别,一个组件可以在内部定义自己需要的属性,也可以接受外部传递的属性。事实上,比如父子组件结构,父组件定义的State也能作为props传递给子组件使用,只是对于不同组件它的含义不同,这也对应了上文瀑布的比喻,水流由props与State构成就是这个意思了。
我们说State是私有属性,虽然它可以传递给其它组件作为props使用,但站在私有的角度,我虽然大方的给你用,那你就应该只使用而不去修改它,这就是props第一准则,props应该像纯函数那样,只使用传递的属性,而不去修改它(想改也改不掉,改了就报错)。
为什么这么说,你想想,react本身就是单向数据流,父传递数据给子使用,如果在子组件内随意修改父传递的对象反过来影响了父,那这不就乱套了吗。
那么问题来了,如果父传了属性给子,子真的要改怎么办?也不是没办法,第一我们可以在父提供props同时,也提供一个修改props的方法过去给子调用,子虽然是调用点,但本质执行的是父的方法,这是可行的。第二点,将传递进来的props复制一份,自己想怎么玩就怎么玩,也不是不可以,比如:
function Child(props) {
// 拷贝一份自己玩
let num = props.num;
num++;
return <div>{num}</div>;
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { num: 1 };
}
render() {
return (
<div>
<Child num={this.state.num} />
</div>
);
}
}
ReactDOM.render(<Parent />, document.getElementById("root"));
当然如果子组件也是class组件也可以,还是这个例子,只是修改了Child部分:
class Child extends React.Component {
constructor(props) {
super(props);
// 将传递进来的props赋予子组件的state
this.state = {
num:props.num
}
}
render() {
return <div>{++this.state.num}</div>;
}
}
再或者直接赋值成this上的一条属性:
class Child extends React.Component {
constructor(props) {
super(props);
}
// 将传递进来的props赋予给this
num = this.props.num;
render() {
return <div>{++this.num}</div>;
}
}
以上便是复制一份的常规操作,我们再来看看父提供修改方法的做法:
class Child extends React.Component {
render() {
return (
<div>
<div>{this.props.num}</div>
<button type="button" onClick={() => this.props.onClick()}>
点我
</button>
</div>
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { num: 1 };
}
// 传递给子组件使用的方法
handleClick() {
// 拷贝了state中num
let num_ = this.state.num;
// 自增
num_ += 1;
// 更新state中的num
this.setState({ num: num_ });
}
render() {
return (
<div>
<Child num={this.state.num} onClick={() => this.handleClick()} />
</div>
);
}
}
ReactDOM.render(<Parent />, document.getElementById("root"));
我们来解释下这段代码,我们在Parent中定义了state,其中包含num变量,以及定义了handleClick
方法,在
<Child num={this.state.num} onClick={() => this.handleClick()} />
这行代码中,我们将state中的num
与handleClick
分别以num
与onClick
这两个变量名传递进去了。
对于事件定义react有个规则,比如我们传递给子组件的变量名是on[Click],那么具体方法定义名一般以handle[click]来命名,简单点说,on[event]与handle[event]配对使用,event就是代表你事件具体含义的名字,有一个统一的规则,这样也利于同事之间的代码协作。
在子组件内部,我们通过props能访问到传递的num与onClick这两个属性,我们将其关联到dom中,当点击按钮就会执行父组件中的handleClick
方法。有同学可能注意到handleClick
中更新num的操作了,按照我们常规理解,直接this.state.num++
不就行了吗,很遗憾,这是react需要注意的第二点,我们无法直接修改state,比如如下行为都不被允许:
// 直接修改不允许
this.state.num = 2;
// 同理,这也是直接修改了state,也不被允许
this.setState({ num: this.state.num++ });
官方推荐做法,同样也是将state中你要修改的部分拷贝出来,操作完成,再利用setState
更新。
如果你了解过vue,在深入响应式原理一文中,也有类似的要求,比如请使用Vue.set(object, propertyName, value)
去更新某个对象中的某条属性,而不是直接修改它,否则你的修改可能并不会触发视图更新,其实都是差不多的道理,这里就顺带一提了。
OK,到这里我们对于这一块知识点做个小结,props与state构成了react单向数据流的数据部分,同为属性,只是一个私有一个是从外部传递的而已。其次,props只读不可修改,若要修改请使用类似拷贝的折中方法,state除了拷贝外还得通过setState
重新赋值。前面也说了,props就是外部传递的state,所以两者都不能直接修改也不是不无道理,记住这点就好了。
伍 ❀ 总
现在是凌晨1点半(封面图也是应景了),其实写到这里,第二部分知识我想说的也都差不多了,看了眼篇幅,四千多字,再长一些知识点可能也有点多了,所以这篇就先介绍到这里。怎么说呢,关于文章的编写我心里其实还是会有遗憾的,我毕竟只是个初学者,实战项目经验还远远不足,很多东西还不能从根源去解释清楚,比如setState
可能是异步行为,所以不要用state变化作为你部分逻辑的执行判断条件,举个例子:
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
bol: true,
};
}
// 这是生命周期函数
componentDidMount(){
for (let i = 0; i < 5; i++) {
if (this.state.bol) {
console.log(i);
this.setState({ bol: false });
}
}
console.log(this.state.bol);//true
}
// 这也是生命周期函数
componentDidUpdate(){
console.log(this.state.bol);//false
}
render() {
return null;
}
}
ReactDOM.render(<Parent />, document.getElementById("root"));
我们预期的是在输出i为0之后,就修改bol状态,之后循环无法再次进入这段代码,但很遗憾,for循环会完整执行完毕并输出0-1-2-3-4,直到在生命周期componentDidUpdate
中我们才捕获到修改成功的状态。遗憾的是我目前的经验还不足以将这块知识吃透,没吃透的东西我不会写,所以这里算留个坑吧,之后一定会单独写一篇文章介绍state的问题,把这块弄情况,那么这篇文章就先说到这里了,本文结束。