博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
② React 面向组件编程(state、props、refs)、事件处理
阅读量:3965 次
发布时间:2019-05-24

本文共 13863 字,大约阅读时间需要 46 分钟。

查看专栏其它文章:


React


本人是个新手,写下博客用于自我复习、自我总结。

如有错误之处,请各位大佬指出。
学习资料来源于:尚硅谷


组件的理解

在具体说到后续内容前,需要理解何为 “组件”

  1. 组件是用来实现特定( 局部 )功能效果的代码集合( html/css/js )

  2. 为什么需要组件:因为一个界面需要实现的功能可能会比较多,都放在一起去管理可能会比较复杂,将它们按功能 / 按区域 分成多个组件会清晰很多。不仅如此,将其划分成多个组件还有助于复用代码提高运行效率

  3. 当应用是以多组件的方式实现, 这个应用就是一个组件化的应用

在平常的学习中,组件化可能不太受重视,但在工作中,组件化将是重要的。为了更好的解释,先简单举个例子来分析一下:

在这里插入图片描述
(文字较多,不感兴趣可快速略过)

假如我们想建一个上图中的页面,如果不考虑逻辑,相信大家有能力模仿出来。初学者可能会这样,比如:用户名那个红框中的内容,初学者可能会一行一行的模仿,先定位用户名,然后设置样式,然后定位输入框,然后设置样式…从上向下。但问题也就在这里发生:

(1)如果页面的内容有一部分需要位置上的变动,那可想而知每一行的定位都失效了。解决方法大家都能想到,就是将它们再用一个标签包裹,这就靠近了组件化的概念。我们继续考虑其它情况:

(2)如果一个页面包含的内容很多且复杂,那么页面的代码量就有了一定的数量。即使大部分开发工具都有找代码功能,但仍需多次比对。想要修改相关功能,也需要去htmlstylescript三部分中耐心寻找。如果能把这三部分中,用来构建一个功能的代码提取出来,显而易见,我们能快速锁定相关功能去进行修改,这将大幅度提高代码效率。所以这提取出来的部分,就是所谓组件。这也是为什么Vue3会出现Composition API(setup函数)这种集中管理的方式。那JS提取出来我们只要导入JS文件即可,那组件该如何使用将会在下文中演示。这么做之后还有其它好处:

(3)在工作小组中,通常是用Git (或其它方式)来更新代码。如果真的要让多位成员,完善一个页面中的多个功能(通常是一人管理多个相似页面),而这些功能都在同一段代码里,极有可能发生代码覆盖问题。除此以外,一旦代码管理人员交替,如果不把重要功能抽成组件,那么想找到某功能犹如大海捞针。除了代码更清晰以外,组件化也有助于复用代码,我们考虑这个情况:

(4)如果相关功能在其他页面里也反复用到,举个最简单的例子,比如一个设计好样式的按钮,我想让该项目中的所有按钮都使用这个按钮,那与其在每个页面中都去反复写,不如抽取出来变成公共组件,之后只需要去调用一下即可。

更具体一些的就可以参考,比如:Element-UI、v-chart、VUX 等一系列有助于我们开发的 “工具”,在它们的官网中介绍自己就是:“组件库” 或 “插件”,那里面的每个小功能(比如:按钮、搜索框、图标 等等)就是 “组件”。在这些组件里就帮我们封装好了相关代码,我们直接去按方式调用就可以了。

说到这里相信大家已经对组件有所了解。至于更多使用方面的内容,将在下文介绍。


再说个题外话,还有一个概念和组件很像,那就是模块

模块

  1. 理解:向外提供特定功能的js程序,一般就是一个js文件

  2. 为什么需要模块: 因为在项目中JS代码可能会有很多,都放在一起去管理可能会比较复杂。也就是说,它和组件其实大同小异,作用都是复用代码,但模块是简化JS,提高JS的运行效率。而组件复用的代码包括 HTML、CSS 和 JS

  3. 当应用的JS都以模块来编写, 这个应用就是一个模块化的应用

也就是说,模块和组件都能方便我们在开发过程中对代码的管理和调整。


React面向组件编程


自定义组件的使用

定义组件共有两种方法。


方式1: 工厂函数组件(简单组件)

从最简单的角度出发,我们可以利用函数function,将某功能的代码放入其中,这样就能做到简单区分各部分代码的功能:

function MyComponent () {
return

工厂函数组件(简单组件)

}

现在这个函数MyComponent就是一种简单的组件,它里面存放了相关功能,并用return把内容返回出来。我们想使用这个组件,依然使用ReactDOM.render,但是这个组件我们要以标签的形式去渲染:

ReactDOM.render(
, document.getElementById('example1'))

方式2: ES6类组件(复杂组件)

学过JS的肯定不会对上面function感到陌生,但到了ES6中,ES6提供了更接近传统语言(比如C++和Java)的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

如果从未接触过Class,并对往后的用法感到困惑,可以参考文章:

那现在我们就使用class来替代function。为了更方便使用,React 提供了这种 定义组件的方式:

class MyComponent2 extends React.Component {
render () {
return

ES6类组件(复杂组件)

}}

同样地,我们需要渲染组件标签:

ReactDOM.render(
, document.getElementById('example2'))

需要额外说明的是:ES6的类,完全可以看作构造函数的另一种写法,那使用的时候,也是直接对类使用new命令(创建实例对象),跟构造函数的用法完全一致:

class Bar {
doStuff() {
console.log('stuff'); }}var b = new Bar();b.doStuff() // "stuff"

也就是说,我们想使用类内部定义的方法需要创建实例对象。我们做以下操作:

class MyComponent2 extends React.Component {
render () {
console.log(this) // MyComponent2的实例对象 return

ES6类组件(复杂组件)

}}

console.log的结果就是类的实例对象,也就是this现在指向MyComponent2的实例对象。

在这里插入图片描述
因此,我们就可以在render()里使用类中其它方法。具体有什么用,在后续就可以看到。


完整代码:

  
component_basic

从结果来看,这些内容确实被渲染出来了,并且是自定义的组件标签:

在这里插入图片描述

注意

  1. 组件名必须首字母大写

  2. 虚拟DOM元素只能有一个根元素

  3. 虚拟DOM元素必须有结束标签


组件属性①: state 及 事件处理

在说到后续内容前,需要先说一下 React 的事件处理。推荐文章:。该文章写的很清晰,细节内容就不在这里赘述了。主要内容如下:

React 元素的事件处理和 DOM 元素类似。但是有一点语法上的不同:

  1. React 事件绑定属性的命名采用驼峰式写法,而不是小写。

  2. 如果采用 JSX 的语法你需要传入一个函数作为事件处理函数,而不是一个字符串(DOM 元素的写法)。

比如:

function ActionLink() {
function handleClick(e) {
e.preventDefault(); // 如果想阻止默认行为,必须明确使用preventDefault console.log('链接被点击'); } return ( // 需要传入一个函数作为事件处理函数 点我 );}

然后将事件转到ES6 class语法前,需要先了解一下ES6类组件更多的用法。


React 把组件看成是一个状态机(State Machines)。通过与用户的交互,来实现不同状态,然后渲染 UI,让用户界面和数据保持一致。而这个状态 state 会在类的构造函数 constructor 中来初始化。

例:

class Like extends React.Component {
constructor () {
super() // state是组件对象最重要的属性, 值是对象(可以包含多个数据) // 组件被称为"状态机", 通过更新组件的state来更新对应的页面显示(重新渲染组件) this.state = {
// 初始化状态 isLikeMe: true } } render () {
// 之前提到的:this指向类的实例对象。因此可以直接使用构造函数中的state const text = this.state.isLikeMe ? '你喜欢我' : '我喜欢你' return

{
text}

}}

那在此基础上,我们将事件加进来。这个事件是点击 text 文字就能修改 stateisLikeMe 的值。修改之后,因为在 React 里,更新组件的 state,用户界面就会被重新渲染(不要操作 DOM),因此页面文字内容会因点击而变化。但是要想修改 state 中的值,需要使用 setState,不能直接:this.state.isLikeMe = !this.state.isLikeMe。因为如果直接修改 ,React 并不会重新渲染(render)页面。( 这是个很难解释且棘手的问题 。除此以外setState也可能会产生一些微妙的bug,感兴趣的可查阅文章: )

例:

class Like extends React.Component {
constructor () {
super() this.state = {
// 初始化状态 isLikeMe: true } // 绑定this为组件对象 this.change = this.change.bind(this) } change () {
// this.state.isLikeMe = !this.state.isLikeMe // 不能更新更新某个状态 this.setState({
isLikeMe: !this.state.isLikeMe }) } render () {
// this指向类的实例对象 const text = this.state.isLikeMe ? '你喜欢我' : '我喜欢你' return

{
text}

}}

在上面这段代码中,我们知道事件处理时,需要传入一个函数作为事件处理函数,因此 return 里的 change 必然是 类内部的方法,需要使用 this。但这里就会出现一个问题:类的方法默认不会绑定 this 。因此需要在构造函数 constructor 中绑定 this 。如果忘记绑定,当你调用这个函数的时候 返回的值会是 undefined。这并不是 React 的特殊行为。它是函数如何在 JavaScript 中运行的一部分。通常情况下,如果你没有在方法后面添加 () ,例如 onClick={this.handleClick},就应该为这个方法绑定 this。

如果使用 bind 觉得麻烦,这里有两种方式可以解决

(1)如果你正在使用实验性的属性初始化器语法,你可以使用属性初始化器来正确的绑定回调函数:

class Like extends React.Component {
constructor () {
super() this.state = {
isLikeMe: true } } // 注意这里: change = () => {
this.setState({
isLikeMe: !this.state.isLikeMe }) } render () {
const text = this.state.isLikeMe ? '你喜欢我' : '我喜欢你' return

{
text}

}}

(2)如果你没有使用属性初始化器语法,你可以在回调函数中使用 箭头函数:

class Like extends React.Component {
constructor () {
super() this.state = {
isLikeMe: true } } change() {
this.setState({
isLikeMe: !this.state.isLikeMe }) } render () {
const text = this.state.isLikeMe ? '你喜欢我' : '我喜欢你' // 注意这里: return

this.change(e)}>{
text}

}}

使用(2)这个语法有个问题就是每次 Like 渲染的时候都会创建一个不同的回调函数。在大多数情况下,这没有问题。然而如果这个回调函数作为一个属性值传入低阶组件,这些组件可能会进行额外的重新渲染。通常建议 在构造函数中绑定 或 使用(1)语法 来避免这类性能问题


完整代码:

  
component_state

组件属性②: props

state 和 props 主要的区别在于 props 是不可变的,而 state 可以根据与用户交互来改变。这就是为什么有些容器组件需要定义 state 来更新和修改数据。 而子组件只能通过 props 来传递数据。(组件内部不要修改props数据!)

每个组件对象都会有 props ( properties 的简写)属性,组件标签的所有属性都保存在props中

使用演示:

function HelloMessage(props) {
return

Hello {
props.name}!

;} const element =
; ReactDOM.render(element, document.getElementById('example'));

实例中 name 属性通过 props.name 来获取。

再转到ES6类组件:

class HelloMessage extends React.Component {
render() {
return (

Hello, {
this.props.name}

); }}const element =
;ReactDOM.render(element, document.getElementById('example'));

需要说明的是,在这里能直接使用 this.props.name 并不是 React 希望我们这样做。在这里能使用,是因为 React 在组件实例化的时候,马上又给实例设置了一遍 props。完整写法应该是这样的:

class HelloMessage extends React.Component {
constructor(props) {
super(props) console.log(this.props) // 未传递props,输出 undefined ;传递props,输出 Object {(组件标签属性)} } render() {
return (

Hello, {
this.props.name}

); }}

那是否意味着我们可以只写 super() 而不用 super(props) 呢?

不是的。虽然 React 会在组件实例化的时候设置一遍 props,但在 super 调用一直到构造函数结束之前,this.props 依然是未定义的。如果这时在构造函数中调用了函数,函数中有 this.props.xxx 这种写法,直接就报错了。所以推荐使用 构造函数就设置 props


默认props

我们可以通过组件类的 defaultProps 属性为 props 设置默认值,演示代码如下:

class HelloMessage extends React.Component {
render() {
return (

Hello, {
this.props.name}

); }}HelloMessage.defaultProps = {
name: 'CSDN'};const element =
;ReactDOM.render(element, document.getElementById('example'));

当然,你也可以在设置了默认值的基础上继续传参,如果未传则显然默认值,否则显示传入的参数。

假如现在设置了多个默认值:

class Person extends React.Component {
render() {
return (
  • 姓名: {
    this.props.name}
  • 性别: {
    this.props.sex}
  • 年龄: {
    this.props.age}
) }}// 指定属性的默认值Person.defaultProps = {
sex: '男', age: 18}

现在的情况是:必须传入name,sex和age不传则显示默认值。

在组件标签中如果一个一个传,数据比较多的情况下比较难看,我们可以这么做:

(如果不了解扩展运算符可参考文章:)

const person = {
name: 'Tom', sex: '女', age: 18}//扩展属性: 将对象的所有属性通过props传递ReactDOM.render(
, document.getElementById('example1'))

如果传递参数名称和默认值名称不同,那显然就只能一个一个传了。

const person2 = {
myName: 'JACK', age: 17}ReactDOM.render(
, document.getElementById('example2'))

prop-types

现在就可以用到之前提到却没用到的prop-types。

在这里插入图片描述
它可以帮我们在运行时检查React props里的属性类型。

Props 验证使用 propTypes,它可以保证我们的应用组件被正确使用,React.PropTypes 提供很多验证器 (validator) 来验证传入数据是否有效。当向 props 传入无效数据时,JavaScript 控制台会抛出警告。

演示代码:

class Person extends React.Component {
render() {
return (
  • 姓名: {
    this.props.name}
  • 性别: {
    this.props.sex}
  • 年龄: {
    this.props.age}
) }}// 对标签属性进行限制Person.propTypes = {
name: PropTypes.string.isRequired, sex: PropTypes.string, age: PropTypes.number}const person = {
name: 'Tom', sex: '女', age: 18}ReactDOM.render(
, document.getElementById('example1'))

更多验证器说明如下:

MyComponent.propTypes = {
// 可以声明 prop 为指定的 JS 基本数据类型,默认情况,这些数据是可选的 optionalArray: React.PropTypes.array, optionalBool: React.PropTypes.bool, optionalFunc: React.PropTypes.func, optionalNumber: React.PropTypes.number, optionalObject: React.PropTypes.object, optionalString: React.PropTypes.string, // 可以被渲染的对象 numbers, strings, elements 或 array optionalNode: React.PropTypes.node, // React 元素 optionalElement: React.PropTypes.element, // 用 JS 的 instanceof 操作符声明 prop 为类的实例。 optionalMessage: React.PropTypes.instanceOf(Message), // 用 enum 来限制 prop 只接受指定的值。 optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), // 可以是多个对象类型中的一个 optionalUnion: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, React.PropTypes.instanceOf(Message) ]), // 指定类型组成的数组 optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), // 指定类型的属性构成的对象 optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), // 特定 shape 参数的对象 optionalObjectWithShape: React.PropTypes.shape({
color: React.PropTypes.string, fontSize: React.PropTypes.number }), // 任意类型加上 `isRequired` 来使 prop 不可空。 requiredFunc: React.PropTypes.func.isRequired, // 不可空的任意类型 requiredAny: React.PropTypes.any.isRequired, // 自定义验证器。如果验证失败需要返回一个 Error 对象。不要直接使用 `console.warn` 或抛异常,因为这样 `oneOfType` 会失效。 customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error('Validation failed!'); } } }}

组合使用 props + state (※)

以下实例演示了如何在应用中组合使用 state 和 props 。我们可以在父组件中设置 state, 并通过在子组件上使用 props 将其传递到子组件上。在 render 函数中, 我们设置 name 和 site 来获取父组件传递过来的数据。

class WebSite extends React.Component {
constructor() {
super(); this.state = {
name: "baidu", site: "https://www.baidu.com" } } render() {
return (
); }}class Name extends React.Component {
render() {
return (

{
this.props.name}

); }} class Link extends React.Component {
render() {
return ( {
this.props.site}
); }} ReactDOM.render(
, document.getElementById('example'));

如果乍一看觉得无法理解,你可以这样看:父组件 WebSite 其实就是这样的:

class WebSite extends React.Component {
constructor() {
super(); this.state = {
name: "baidu", site: "https://www.baidu.com" } } render() {
return (

{
this.state.name}

{
this.state.site}
); }}

我们把 h1 标签和 a 标签中的内容提取出来,放到了子组件中。但是在子组件当中显然没有 state,所以子组件中的 name / site 需要靠父组件的传递。在之前的内容里已经介绍了 props 的用法,我们只要将其改为组件标签,然后根据 命名 传递参数过去即可。

因为这段代码量比较小,你可能会觉得这么做很多余。但如果代码量比较大,里面有很多功能,我们这时根据功能把代码拆分成多个组件,就会清晰的多。


再看一个情景:假如子组件需要父组件所有的 props 参数,这个时候一个一个参数的写会很麻烦,比如:<Name name={this.props.name} url={this.props.url} .../>

那么我们该如何把父属性直接赋值给子组件的 props 参数呢?如下写法即可:<Name props={this.props}/>

这样写就非常简洁了,也就子组件和父组件都有了同样数据结构的 props 参数。

在这里需要补充说明的是:父组件和子组件的参数名推荐是相同的。因为如果它们的参数名是不同的,但是表示的值是同一个,那么当调试页面时,写这段代码的人可能还记得这个问题,但是其他人可能就会遇到问题,导致效率低下,不便维护。


面试题:请区别一下组件的props和state属性

  1. state: 组件自身内部可变化的数据

  2. props: 从组件外部向组件内部传递数据,组件内部只读不修改


组件属性③: refs

React 支持一种非常特殊的属性 Ref ,你可以用来绑定到 render() 输出的任何组件上。这个特殊的属性允许你引用 render() 返回的相应的支撑实例( backing instance )。这样就可以确保在任何时间总是拿到正确的实例。

使用方法:

(1)绑定一个 ref 属性到 render 的返回值上:

(2)在其它代码中,通过 this.refs 获取支撑实例:

var input = this.refs.myInput;var inputValue = input.value;

应用到类组件上:

class MyComponent extends React.Component {
handleClick() {
// 使用原生的 DOM API 获取焦点 this.refs.myInput.focus(); } render() {
// 当组件插入到 DOM 后,ref 属性添加一个组件的引用于到 this.refs return (
); }} ReactDOM.render(
, document.getElementById('example'));

但上面这个 api 已经过时了,下面的是最新的用法:(请保证 React 版本为较新版本)

class MyComponent extends React.Component {
constructor(props) {
super(props); this.myRef = React.createRef(); } focus = () => {
this.myRef.current.focus(); } render() {
return (
); }}

通过在class中使用React.createRef()方法创建一些变量,可以将这些变量绑定到标签的ref中。那么该变量的current则指向绑定的标签dom。

需要注意:React 并不推荐过度使用 ref如果能通过state做到的事情,就不应该使用 refs


综合例:

需求: 自定义组件, 功能说明如下:

  1. 点击按钮, 提示第一个输入框中的值

  2. 当第2个输入框失去焦点时, 提示这个输入框中的值

//定义组件class MyComponent extends React.Component {
constructor(props) {
super(props) // 调用父类(Component)的构造函数 // 将自定义的函数强制绑定为组件对象 this.handleClick = this.handleClick.bind(this) // 将返回函数中的this强制绑定为指定的对象, 并没有改变原来的函数中的this } // 自定义的方法中的this默认为null handleClick () {
// 得到绑定在当前组件对象上的input的值 alert(this.msgInput.value) } handleBlur (event) {
alert(event.target.value) } render () {
//组件内的标签都可以定义ref属性来标识自己 //在组件中可以通过this.msgInput来得到对应的真实DOM元素 //作用: 通过ref获取组件内容特定标签对象, 进行读取其相关数据 return (
this.msgInput = input}/>{
' '}
{
' '}
) }}// 渲染组件标签ReactDOM.render(
, document.getElementById('example'))

转载地址:http://mmyki.baihongyu.com/

你可能感兴趣的文章
数据类型之标量
查看>>
调试 Perl 脚本
查看>>
增强的for循环语句
查看>>
方法的可变参数
查看>>
静态导入
查看>>
java 泛型
查看>>
控制结构
查看>>
标准输入输出
查看>>
运算符
查看>>
数据类型之列表与数组
查看>>
比较字符串
查看>>
Java EE 精萃
查看>>
Open Source 精萃
查看>>
Java EE 简介
查看>>
Weblogic 简介
查看>>
观察者模式 (Observer)
查看>>
Java 集合框架
查看>>
Weblogic 精萃
查看>>
Servlet 精萃
查看>>
XStream 精萃
查看>>