从零开始的react入门教程(二),从react组件说到props/state的联系与区别

react

在从零开始的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中的numhandleClick分别以numonClick这两个变量名传递进去了。

对于事件定义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的问题,把这块弄情况,那么这篇文章就先说到这里了,本文结束。

以上是 从零开始的react入门教程(二),从react组件说到props/state的联系与区别 的全部内容, 来源链接: utcz.com/z/382083.html

回到顶部