听风是风

学或不学,知识都在那里,只增不减。

导航

从零开始的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的构造函数方法,只是在执行superthis指向了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的问题,把这块弄情况,那么这篇文章就先说到这里了,本文结束。

posted on 2020-10-21 01:49  听风是风  阅读(937)  评论(7编辑  收藏  举报