React学习记录(初级)

注:本文来自胡子大哈的文章阅读笔记

1. 第一个React App

 React几乎不能单独使用,不是将JQuery下载下来塞到<head />标签中就可以使用,它在开发阶段生产阶段都需要一堆工具和库辅助,编译阶段需要借助Babel,需要Redux等第三方的状态管理工具来组织代码,如果只是要写单页面应用那也需要React-router,这就是所谓的“React.js全家桶”。当然官网也推荐了create-react-app工具来一键生成所需的工程目录,帮我们做好各种配置和依赖,可以达到直接使用的目的,安装好之后可以直接使用create-react-app hello-react来创建名为hello-react的React前端工程,这个工具会直接在后台帮我们构建好最基本的配置,推荐使用淘宝定制的cnpm命令来代替npm搞,毕竟国内,构建流程如下(前提是已安装node.js环境,安装后默认是安装了npm,即node包管理工具):

1.使用cnpm(gzip压缩支持) 命令行工具代替默认的npm

# 安装cnpm命令
npm install -g cnpm --registry=https://registry.npm.taobao.org
# 设置npm源
npm config set registry https://registry.npm.taobao.org

2.构建react环境

# 安装create-react-app工具
cnpm install -g create-react-app
# 创建my-app工程
create-react-app my-app
# 进入工程目录
cd my-app
# 启动工程
npm start

默认端口为3000,可以通过访问localhost:3000来查看刚刚运行react项目,如果要修改这个端口,可以打开package.json文件将scripts中的start键值对"start": "roadhog server"修改为"start": "set PORT=xxx&&roadhog server"(将xxx改成你想要的端口号即可)。React前端工程项目目录结构如下:
react目录结构

其中my-app\public\manifest.json文件指定了开始页面,它是执行的源头,可以修改my-app\src\App.js文件来更改首页内容,页面会自动刷新。

【注】
react项目拉下来后通常是没有依赖包的(即node_modules),在往git上推的时候一般是忽略掉这个文件夹的,所以第一步应该是先拉依赖包npm install,然后再运行。

2. React的基本信息

 可以肤浅的将React视作视图渲染工具,准确点说React是一个UI库,它是一个js库,不是什么前端框架,React将许多独立的块组装成一个页面(即前端组件化),虽然React在很多人的观念里优于VUE,毕竟生态圈摆在那里,但它也不是万能的,实际开发中往往需要结合其他库(如Redux、React-router等)来使用。之前学习的前端jsp,一个按钮往往会和某一大段的js代码绑定以执行相应的交互行为,然后想要复用这个按钮功能时,不但需要将按钮的内容和样式复制过来还要将对应的js代码复制过来,很麻烦,对于结构复杂的功能的交互很难复用,所以出现了组件化的概念。将复用率较高的组件抽离出来作为一个Component,那以后所有组件都可以继承这个组件来复用该组件中的一些内容,每个组件都有自己的显示形态(即显示的内容和样式)行为,组件的显示形态和行为可以由数据状态(state)和配置参数(props)共同决定,数据状态和配置参数的改变都会影响到这个组件的显示形态。

3. 关于JSX

 JSX是React的核心组成,通俗点讲,JSX是使用XML标记的方式直接声明页面(有点像XML的js语法的扩展),使得组件之间可以相互嵌套,React建议使用JSX替换常规的js,有如下优点:

  • JSX 执行更快,因为它在编译为 JavaScript 代码时进行了优化;
  • JSX 是类型安全的,在编译过程中就能发现错误;
  • 使用 JSX 编写模板更加简单快速;

3.1 基本原理

 正常的jsp或html都是使用DOM来表示内部的元素,一个DOM的组成往往如下:

<div class='' id=''>
    <div class=''>test</div>
</div>

包含了标签名、属性、子元素,其实js也可以表示这种结构,只有有些复杂而已,所以不为人知,上述的结构使用js可以表达为:

{
    tag: 'div',
    attrs: {className: '', id: ''},
    children: [
        {
            tag: 'div',
            attrs: {className: ''},
            childern: ['test']
        }
    ]
}

上述的方式表述不如html来的简洁明了,React改良了一下,扩展js语法(所以名字叫jsx嘛,和iPhone类似的还有一个iPhone X…),使得js可以支持直接在js中使用类似于html标签结构的语法,编译会将JSX转成上述的js结构,上述js结构使用jsx来表示的话就是:

// 引入React和React.js的组件父类Component
import React, { Component } from 'react'
// react-dom可以帮助渲染组件到页面上
import ReactDOM from 'react-dom'
import './index.css'

class Header extends Component {
    render() {
        return (
            <div>
                <h1 className='title'>React Hello!</h1>
            </div>
        )
    }
}

// 将组件Header渲染到id为root标签中
ReactDOM.render(
    <Header />,
    document.getElementById("root")
)

只要写React.js组件,头部必须引入React和React.js的组件父类Component,ReactDOM用于把 React 组件渲染到页面上,一般情况只要写组件,上述2个必须引入,述代码中在return()中有一串纯HTML的标签,这就是JSX语法(内部的属性不再是原来html中的class,变成了className,因为在React中,class是关键字,此外还有一个属性forhtmlFor替换),编译后变成:

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import './index.css'

class Header extends Component {
    render() {
        return (
            React.createElement(
                "div",
                null
                React.createElement(
                    "h1",
                    {className: 'title'},
                    "React Hello!"
                )
            )
        )
    }
}

ReactDOM.render(
    React.createElement(Header, null),
    document.getElementById("root")
)

可发现也是按照 标签–>属性–>子元素 的顺序来嵌套的,通过React.createElement(x, x)方法创建html标签,对于JSX可以理解为js对象,会好理解点。

 通过这个JSX语法可以结构化js对象,用来构造DOM元素,最后再将DOM元素塞到页面上即可,也就是上述代码中ReactDOM.render(x, x)函数所干的事儿:渲染组件–>构造DOM树–>插入页面中特定的元素上(上例是插入到idrootdiv元素中),可以用下面的流程表示:

JSX流程

从JSX经过了JS对象才最后渲染后页面上,是因为可能会将这个JS对象渲染到canvas或手机APP上,这是把react-dom抽出来的原因,可以想象有一个叫做react-canvas的东西可以帮我们把UI渲染到canvas上,或者有一个叫做react-app可以帮我们把它转成远程的APP(实际这东西叫ReactNative)。其次,有了这样一个对象,如果数据变化,需要更新组件的时候,就可以用比较快的算法操作这个 JavaScript 对象,而不用直接操作页面上的 DOM,这样可以尽量少的减少浏览器重排,极大地优化性能。

总结

  1. JSX 是 JavaScript 语言的一种语法扩展,长得像 HTML ,但并不是 HTML;
  2. React 可以用 JSX 来描述组件长什么样的;
  3. JSX 在编译的时候会变成相应的 JavaScript 对象描述;
  4. react-dom 负责把这个用来描述 UI 信息的 JavaScript 对象变成 DOM 元素,并且渲染到页面上;

4. 关于render()方法

下面这些情况可以通过之前创建的 my-app 工程来实践,直接在App.js中修改即可。

返回的JSX形式
 前面已经提过,React中写组件一般情况都要继承 React.js 的Component类,同时也就必须与要实现一个render()方法,这个方法必须返回一个JSX元素,但必须用一个外层JSX元素将所有内容包裹起来,如果写成多个并列JSX元素是不合法的,比如:

// 正确方式
render() {
    return (
        <div>
            <h1 className='title'>React Hello!</h1>
        </div>
    )
}

// 错误方式
render() {
    return (
        <div>...</div>
        <div>...</div>
    )
}

在JSX中插入表达式
 在JSX中也可以使用js的表达式,变量使用{}包裹,比如:

render() {
    return (
        <div>
            <h1 className='title'>Hello, {name}!</h1>
        </div>
    )
}

还可以使用{}来计算数学表达式,如{1+3},最终替换为4。除此之外,还可以用{}来包裹一个函数表达式,如{(function () { return 'is good'})()},最终替换为is good,总之,{}中可以放任何js代码(包含变量、表达式、函数。。。),render()函数可以将这些内容的最终结果渲染到页面上,而且可以连标签的属性中也可以用,如:

render() {
    return (
        <div>
            <h1 className={className}>Hello, {name}!</h1>
        </div>
    )
}

之前说过,JSX中使用classNamehtmlFor替换了原来html中的classfor,其他的和html一样(如styledata-*等)。

依据条件返回

 在上述插入表达式部分,React中利用{xx}的形式可以包含多种形式的变量,除了上述的变量、表达式、函数外,还可以直接是一段 JSX 代码,如:

render() {
    const isJack = true
    return (
        <div>
            <h1>
                Hello,
                {
                    isJack ? <strong>jack</strong> : <strong>stranger</strong>
                }
            </h1>
        </div>
    )
}

先是在return外定义了一个isJack的变量,然后在JSX中又有一个{isJack ? <strong>jack</strong> : <strong>stranger</strong>},这里JSX会依据变量isJack的值来选择性返回JSX的内容(简直和java中的三目运算一毛一样啊,对不对),含义也是一样的,变量为true就将<strong>jack</strong>渲染上去,否则就将<strong>stranger</strong>渲染上去,注意这里是允许使用null,就可以将内容隐藏起来(毕竟stranger这个词听着会让人不太舒服,站在客户或用户的角度,可以直接就是Hello,就不会让人不舒服了,如果是jack,那就显示Hello, Jack,不是jack就直接是Hello),改成下面的JSX:

render(){
    const isJack = false
    return (
        {
            <div>
                <h1>
                    Hello
                    {
                        isJack ? <strong>, jack</strong> ? null
                    }
                </h1>
            </div>
        }
    )
}

除了上述对直接使用,还可以将这一块单独抽出来做一个函数,接受JSX元素作为参数,比如:

  renderTest(test1, test2) {
    const flag = true
    return flag ? test1 : test2
  }

    render() {
    const isJack = false
    return (
      <div className="App">
        <header className="App-header">
          {/* <div id="root"></div> */}
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Welcome to React,
          </p>
          <p>
          {/* 调用定义的函数 */}
            renderTest,
            {
              this.renderTest(
                <strong>test1</strong>,
                <strong>test2</strong>
              )
            }
          </p>
        </header>
      </div>
    );
  }

5. 组件间的组合

 组件组合(就是组件之间相互嵌套或组合)离不开自定义组件的行为,所以先记录一下自定组件的注意点:普通的 HTML 标签都用小写字母开头,而自定义组件必须要用大写字母开头,之前定义的Header组件就是这样。下面是一个组件组合的小栗子:

// Component 已经可以直接使用
class House extends Component {
  // TODO
  render() {
    return (
      <div className="house">
        房子
        // 引用组件
        <Room />
        <Bathroom />
      </div>
    )
  }
}

class Room extends Component {
  // TODO
  render() {
    return (
        <div className="room">
          <Man />
          <Dog />
          <Dog />
        </div>
    )
  }
}

class Bathroom extends Component {
  // TODO
  render() {
    return (
      <div className="bathroom">
        浴室
      </div>  
    )
  }
}

class Man extends Component {
  // TODO
    render() {
    return (
      <div className="man">
        人
      </div>  
    )
  }
}

class Dog extends Component {
  // TODO
    render() {
    return (
      <div className="dog">
        狗
      </div>  
    )
  }
}

在进行自定义组件时,像之前的Header,死活无法引入到其他组件(我这里是App.js组件)中,后来摸索几个小时,发现,不但要在App.js中通过import Header from './Header';在头部进入Header标签,还需要Header组件中使用export default Header将该组件导出,综上,如果要在父组件中引用子组件,需要:

  1. 在子组件中声明导出该组件:export default xxx
  2. 在父组件中头部引入该组件:import Xxx from './Xxx'(虽然文件是Xxx.js,但后缀可以直接省略);

6. 事件监听

 React对于事件的监听,需要给监听的事件加上onClickonKeyDown属性即可,下面是实践的小栗子:

class Header extends Component {

    {/* 这里不能添加 funcation 关键字*/}
    doClick() {
        console.info("React Hello is on Click!")
    }

    render() {
        return (
            <div>
                {/* 这里一定需要添加 this 关键字,否则找不到 doClick */}
                <h1 onClick={this.doClick}>React Hello!</h1>
            </div>
        )
    }
}

对于事件的监听,不需要添加浏览器原生的addEventListener,React已经做了一系列on*(遵照驼峰命名法)属性封装,on*属性有一个限制,只对HTML原生的标签有效果,对于我们自定义的标签是无法使用的(就算使用也没有效果)

event

React会给每个事件监听传入一个event对象(兼容所有浏览器),在函数中的参数可以随意取名调用,如:

class Header extends Component {
    {/*这里直接传入event对象,至于名字可以随意取,取event或者e等等都行*/}
    doClick(e) {
        {/* event.target.innerHTML用于获取标签中的内容 */}
        console.info(e.target.innerHTML)
    }

    render() {
        return (
            <div>
                <h1 onClick={this.doClick}>React Hello!</h1>
            </div>
        )
    }
}

this

this关键字指的是这个实例本身,如果在上述的doClick(e)打印this(即console.info(this)),结果为undefinedReact调用我们传过去的方法(这里是指this.doClick)时,是通过函数调用的方式,而非对象方法调用的方式,所以在函数(指doClick)中并不能获取当前对象实例信息,除非在调用函数时使用bind将实例绑定到当前实例,如下:

<h1 onClick={this.doClick.bind(this)}>React Hello!</h1>

这样以后在doClick函数中就可以打印出this对象了。

bind

bind可以用于绑定当前实例到this上,除此外,还可以通过bind来绑定一些参数,如:

class Header extends Component {

    doClick(v1, v2) {
        console.info(this, v1, v2)
    }

    render() {
        return (
            <div>
                <h1 onClick={this.doClick.bind(this, 'Hello', 'React')}>React Hello!</h1>
            </div>
        )
    }
}

这样点击后打印出来的结果为:Header {props: {…}, context: {…}, refs: {…}, updater: {…}, _reactInternalFiber: FiberNode, …} "Hello" "React",然后开始作死测试各种情况:

情况1

class Header extends Component {

    doClick(v1) {
        console.info(this, v1)
    }

    render() {
        return (
            <div>
                <h1 onClick={this.doClick.bind(this, 'Hello')}>React Hello!</h1>
            </div>
        )
    }
}

结果Header {props: {…}, context: {…}, refs: {…}, updater: {…}, _reactInternalFiber: FiberNode, …} "Hello",满足期望,拿到传入的"Hello";

情况2

class Header extends Component {

    doClick(v1) {
        console.info(v1)
    }

    render() {
        return (
            <div>
                <h1 onClick={this.doClick.bind('Hello')}>React Hello!</h1>
            </div>
        )
    }
}

结果Class {dispatchConfig: {…}, _targetInst: FiberNode, nativeEvent: MouseEvent, type: "click", target: h1, …}(这个就是event对象);

情况3

class Header extends Component {

    doClick(v1, v2) {
        console.info(v1, v2)
    }

    render() {
        return (
            <div>
                <h1 onClick={this.doClick.bind('Hello', 'Recat')}>React Hello!</h1>
                {/* <Title /> */}
            </div>
        )
    }
}

结果Recat Class {dispatchConfig: {…}, _targetInst: FiberNode, nativeEvent: MouseEvent, type: "click", target: h1, …}

情况4

class Header extends Component {

    doClick(v1, v2) {
        console.info(v1, v2)
    }

    render() {
        return (
            <div>
                <h1 onClick={this.doClick.bind('Hello', 'Recat', 'test')}>React Hello!</h1>
            </div>
        )
    }
}

结果Recat test

情况5

class Header extends Component {

    doClick(v1, v2) {
        console.info(v1, v2)
    }

    render() {
        return (
            <div>
                <h1 onClick={this.doClick.bind('Hello', 'Recat', 'test', 'test2')}>React Hello!</h1>
            </div>
        )
    }
}

结果Recat test

【总结】

  1. 如果bind(this, ...),即如果bind如果将当前实例对象绑定到this上,并传入了一些其他参数,调用方传入3个参数,接收方只定义了2个参数,如doClick (v1, v2),调用方this.doClick.bind(this, 'test1', 'test2', 'test3'),那doClick接受到的参数只取前2个传入参数(绑定的this不占参数数量),即v1=test1, v2=test2;如果调用方传入2个参数,接收方却定义了3个参数,如doClick (v1, v2, v3)this.doClick.bind(this, 'test1', 'test2'),那只多余的一个参数默认是event对象,即v1=test1, v2=test2, v3=Calss {...},如果参数再多,比如定义了4个参数,那v4就为undefined了依次类推;
  2. 如果不绑定当前实例对象,即直接传参bind('test1', 'test2' ...),这种情况完全找不到规律,(*  ̄︿ ̄)

综合实例

有只狗,一摸它就会叫,然后就跑了,实现如下:

class Header extends Component {

    bark() {
        console.log('bark')
        {/* 如果要调用 run 方法必须加 this */}
        this.run()
    }

    run() {
        console.log('run')
    }

    render() {
        return (
            <div>
                {/* 点击Dog后先咬再跑 */}
                <h1 onClick={this.bark.bind(this)}>Dog</h1>
            </div>
        )
    }
}

注:和之前的一样,React调用我们传过去的方法是通过函数调用的方式,而非对象方法调用的方式,所以在函数中并不能获取当前对象实例信息,也不能获取到当前实例中其他方法,除非在调用函数时使用bind将实例绑定到当前实例,所以要想在break()中调用run()方法,必须绑定实例对象到this上传给break,否则无法使用this获取当前对象中的run方法。

6. 组件的状态sate

 组件的显示形态是由数据状态和配置参数决定的,React中使用state来存储可变化的状态,下面是一个小栗子:

class LikeButton extends Component {
    constructor() {
        super()
        {/*注意状态赋值的方式,用冒号*/}
        this.state = { isLiked: false }
    }

    handleClickOnLikeButton() {
        // 更新当前实例中 state 中的 isLiked 变量
        this.setState({
            isLiked: !this.state.isLiked
        })
    }

    render() {
        return (
            <button onClick={this.handleClickOnLikeButton.bind(this)}>
                {this.state.isLiked ? '取消' : '点赞'} ?
            </button>
        )
    }
}

根据按钮的状态渲染点赞按钮旁边的文字(“取消”或“点赞”),将isLiked放到LikeButton实例的state中,并且在构造函数中初始化,最终会根据组件中的state中的isLiked的值进行不同点赞状态的渲染,将内部状态的变化绑定到button点击监听事件中。至于不同状态的具体渲染,React在每次调用实例的setState()方法时都会重新调用ReactDOM.render()方法进行页面渲染。除了上述state只放一个状态外,还可以同时放多个状态量(用逗号分隔),这种情况下如果只有部分变量变化,那在setState()中只需要变化部分状态变量即可,不用全部放进去更新。

注:

  1. 在改变组件状态时,上述代码中使用的是this.setState(...),不能直接使用this.state=xxx,这种方式React不能监听到组件的状态变化,因此页面元素不能得到重新渲染,改变状态一定要用setState(...),这个方法接受的参数可以是一个对象(上述接受的就是一个JSX对象)或者函数;
  2. React中setState()并不是即时生效的,而是先将改变的状态缓存在一个队列中,然后在合适的时候再将状态改变,这一点可以通过在this.setState()前后增加console.info(this.state.isLiked)来查看值是一样的,中间经过setState后再打印并没有立刻就改变状态了;
  3. 基于上述第二种setState并不是即时生效的,但如果想基于刚刚设置的state中的状态变量来做一些改动几乎不可能,因为通过this.state.xxx拿到的状态是老状态,而不是刚设置的状态,如果想拿到刚设置的状态,setState()中应该传入一个函数作为参数(React会自动将上一个setState的结果传到这个函数中),此时便可以拿到上一个设置进去的状态(即使该状态此时可能并未生效);

下面是注意点2、3的小栗子:

class LikeButton extends Component {
    constructor() {
        super()
        this.state = { isLiked: false, count: 0 }
    }

    handleClickOnLikeButton() {
        console.log(this.state.count)
        this.setState({count: this.state.count + 1})
        this.setState({count: this.state.count + 1})
        console.log(this.state.count)
    }

    render() {
        return (
            <button onClick={this.handleClickOnLikeButton.bind(this)}>
                {this.state.isLiked ? '取消' : '点赞'} ?
            </button>
        )
    }
}

第一次运行输出结果为0, 0,再点击一次结果为1, 1,但我们期望第二次运行输出结果为2, 2,这说明第一次点击时2个this.setState({count: this.state.count + 1})中第2个setSate并没有生效(未生效的原因是因为this.state.count获取的还是老状态变量,而不是状态队列中刚刚设进去的1)。下面是利用在setSate()中传入函数的方式获取到上次设置的状态值prevState,如下:

// 将两次setState改成下面这样
this.setState({
    count: this.state.count + 1
})
// 传入函数,React将上次设置的状态值自动传过来,取名为prevState(这个名字随意取)
this.setState((prevState) => {
    return { count: prevState.count + 1 }
})

此时第一次点击结果为0, 0,再点击一次结果为2, 2,发现基于上一次设置的值做修改生效,这里不要和“稍后渲染”搞混,虽然是基于上次设进去值进行修改,但是打印出来还是老状态,还是稍后渲染。

setState()渲染性能

在上述按钮监听事件中,进行了多次(栗子中是2次)setState更新状态,虽然如此,但实际页面组件只做了一次渲染,而不是两次,React自动将js事件循环的消息队列中同一个消息中的setState进行合并后再重新渲染,这意味着在搞东西时,多次调用setState并不会导致性能下降。这和上面的“React在每次调用实例的setState()方法时都会重新调用ReactDOM.render()方法进行页面渲染”并不矛盾,只是将多次渲染合并在一起而已。

7. 组件的props

 这个就是组件的属性,由于组件是可以不断可以进行复用的,那就希望组件的某些属性不要太死,否则不能进行有效的复用,props指的组件的可配置性,可以根据不同的场景进行配置,每个组件都可以有一个props参数,包含了这个组件中所有定义的组件配置,具体使用如下:

// 1. 定义一个点赞组件,可以自定义说明文本
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class LikeButtonPlus extends Component {
    constructor() {
        super()
        this.state = { isLiked: false }
    }
    handleClickOnLikeButton() {
        this.setState({
            isLiked: !this.state.isLiked
        })
    }
    render() {
        // 定义取消已赞状态的说明文本变量
        const likedText = this.props.likedText || "取消"
        // 定义未点赞状态的说明文本变量
        const unlikedText = this.props.unlikedText || "点赞"
        return (
            <button onClick={this.handleClickOnLikeButton.bind(this)}>
                {this.state.isLiked ? likedText : unlikedText} ?
            </button>
        )
    }
}

export default LikeButtonPlus

// 2. 在其他组件中使用,传入已定义的属性 props: likedText和unlike
<LikeButtonPlus likedText="like" unlikedText="unlike" />

使用的时候,直接将参数放在自定义组件标签中即可,所有的属性都会作为props对象的键值,这样就可以对按钮状态说明文本进行自定义,最终效果为:点赞属性效果,组件中参数可以是任意类型的数据(字符串、数字、对象、函数…),下面是传入一个数组对象调用:

// 按钮组件中render中渲染语句
const text = this.props.text || {likedText: '取消', unlikedText: '点赞'}
return (
    <button onClick={this.handleClickOnLikeButton.bind(this)}>
        {this.state.isLiked ? text.likedText : text.unlikedText} ?
    </button>
)

// 在其他组件中使用,直接传入一个数组对象text
<LikeButtonPlus text={{ likedText: "like", unlikedText: "unlike" }} />

更厉害的是,还可以通过props传递函数,如:

// 组件中通过 props 获取传递的函数并执行
handleClickOnLikeButton() {
    this.setState({
        isLiked: !this.state.isLiked
    })
    // 如果父组件中 onClick2 函数不为空就执行传过来的函数
    if (this.props.onClick2) {
        this.props.onClick2()
    }
}

// 父组件中传入函数
<LikeButtonPlus onClick2={() => console.info('Click on like button!')} />

7.1 defaultProps

 在上述使用props时,是先定义了两个变量(或者包含这两个变量的对象)likedTextunlikedText,就是在具体渲染DOM时,如果父类组件中在组件标签中给了对应的props属性,就使用父组件中给的属性,否则使用子类组件中设置的,即const likedText = this.props.likedText || "取消"是做了一个逻辑或操作,如果属性很多,我们需要作很多逻辑或的操作,使用效果不佳,由此 React 提供了defaultProps来对组件进行可配置化组件属性进行默认配置(直接用this.props...),示例代码如下:

class LikeButtonPlus extends Component {

    // 定义默认可配置属性,React 会自动应用,不用手动调用
    static defaultProps = {
        text: {
            likedText: '取消',
            unlikedText: '点赞'
        }
    }

    ...

    render() {
        return (
            <button onClick={this.handleClickOnLikeButton.bind(this)}>
                {/* 自动调用,React 会自动判断父组件标签中是否传入对应的属性,有就用,没有就用默认的 */}
                {this.state.isLiked ? this.props.text.likedText : this.props.text.unlikedText} ?
            </button>
        )
    }
}

7.2 props的不可变性

 通俗点说,就是父组件通过标签给子组件传入相应的props属性,这个props一旦传入子组件后就不能改变了,如果子组件尝试对自身props作修改将出错,比如:

// 父组件调用部分
<LikeButtonPlus text={{ likedText: "like", unlikedText: "unlike" }} />

// 子组件修改部分
handleClickOnLikeButton() {
    // 尝试修改
    this.props.text = {
        likedText: '取消2',
        unlikedText: '点赞2'
    }
    this.setState({
        isLiked: !this.state.isLiked
    })
}

render() {
    return (
        <button onClick={this.handleClickOnLikeButton.bind(this)}>
            {this.state.isLiked ? this.props.text.likedText : this.props.text.unlikedText} ?
        </button>
    )
}

在触发onClick监听事件后,出现如下的错误信息:
props不可变

根据上述的报错信息可以看到第18行代码报错,提示不能对只读属性text(即props属性)赋值。

待解决
但是在作死的这条路上从没有停过,我试着在子类组件中这样去修改:

// 子组件修改部分
handleClickOnLikeButton() {
    // 尝试修改
    // this.props.text = {
    //     likedText: '取消2',
    //     unlikedText: '点赞2'
    // }
    this.props.text.likedText = '取消2'
    this.props.text.unlikedText = '点赞2'
    this.setState({
        isLiked: !this.state.isLiked
    })
}

render() {
    return (
        <button onClick={this.handleClickOnLikeButton.bind(this)}>
            {this.state.isLiked ? this.props.text.likedText : this.props.text.unlikedText} ?
        </button>
    )
}

然后,是的,修改成功了o((⊙﹏⊙))o.,还挺尴尬的,效果如下:

props修改成功

 抛开上述的插曲,React中子类组件对于父类组件传过来的props是不能改变的,React希望子组件在输入确定的props属性后,能够输出确定的UI显示形态,如果在输入props属性到输出UI形态中间,props再被修改,这种行为会导致具体页面渲染形态不可预知,所以 React 是禁止这种中间行为来修改子组件的属性。但已经输入的props属性还是有途径修改的:通过组件调用者(父组件)主动重新渲染DOM可以达到目标,示例如下:

// 1. 父组件
class App extends Component {

  constructor() {
    super()
    this.state = {
      text: {
        likedText: 'unliked',
        unlikedText: 'liked'
      }
    }
  }

  handleClickOnChange() {
    this.setState({
      text: {
        likedText: '取消2',
        unlikedText: '点赞2'
      }
    })
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
        <div>
          <h3>测试修改props</h3>
          <LikeButtonPlus text={this.state.text} />
          <button onClick={this.handleClickOnChange.bind(this)}>修改props</button>
        </div>
      </div>
    );
  }
}

子类组件中不进行任何props的修改行为,在原先组件标签中赋值的props属性为unliked, unliked,但中间可以通过按钮“修改props”修改已经传给子组件的props属性,效果如下:

修改组件的props

父类组件比原来多了构造器(主要是为了引入state状态量),然后将需要传入子组件中的props塞到这里面,这样么干的目的是为了在修改props中的text属性能够让APP组件够主动去调render()方法主动重新渲染页面,父组件重新渲染时子组件LikeButtonPlus也会收到通知需要重新渲染,从而到达改变props的目的。

8. stateprops的区别

stateprops有点相似,正常情况下,如果一个组件中没有设置state叫“无状态”组件,反之,如果组件中设置了state叫“状态”组件,因为状态管理相对比较复杂,所以实际过程中尽量避免“状态”组件的定义,这个理念也是React所提倡的(鼓励“无状态”组件)。下面记录一下stateprops的具体区别:

  • state:在组件内部的构造体中进行初始化,仅用于组件管理自身的可变状态,外部组件不能访问、修改,自身可通过this.setState进行状态更新(setState会导致当前组件的重新渲染);
  • props:在子类组件进行定义,调用方作为父组件可以在子组件标签中传入props属性以控制子组件相关属性,子组件无法控制、修改它自己的props属性(只能作类似于初始化的操作defaultProps),只能通过调用方组件传入props来控制,否则自身的props不变。

9. 函数组件

 上述所有的组件都是一个组件就是一个类的组织方式,函数组件则是一个函数一个组件(有点颠覆),比如:

render() {
    // 定义一个函数组件
    const FunctionComponent = (props) => {
      const fc = (event) => alert('This is a Function Component!')
      return (
        <div>
          <h1>函数组件</h1>
          <button onClick={fc}>点我测试</button>
        </div>
      )
    }

    return (
      <div className="App">
        {/*调用函数组件*/}
        <FunctionComponent />
      </div>
    );
}

函数组件的引用和之前那种继承Component类是一样,还是通过<Xxx />的形式引用,函数组件支持props、可以提供render方法,但不支持设置state

10. 列表数据渲染

数组的渲染

 下面是一个数组的渲染简单示例:

<div>
    <h3>列表渲染</h3>
    {[
    <span>test1</span>,
    <span>test2</span>,
    <span>test3</span>
    ]}
</div>

之前说过{xx}可以放任意对象,当然也可以放数组对象(即[...]中的内容),React 会自动将数组中的每个元素在页面上罗列渲染,效果如下:

列表数据

使用for循环渲染数组数据

 通过上述数组的渲染方式,具体数据的渲染示例代码如下:

// 定义数组
const users = [
  { username: 'Jerry', age: 21, gender: 'male' },
  { username: 'Tomy', age: 22, gender: 'male' },
  { username: 'Lily', age: 19, gender: 'female' },
  { username: 'Lucy', age: 20, gender: 'female' }
]

class App extends Component {
  render() {
    // 定义每个用户渲染后的JSX的数组,每个元素中存放的是已带标签的jsx元素
    const usersElements = []
    // 循环数组中的每个用户,将每个属性 push 到数组中
    for (let user of users) {
      usersElements.push(
        <div>
            {/* 获取每个用户中三个属性 */}
          <div>name: {user.username}</div>
          <div>gender: {user.gender}</div>
          <div>age: {user.age}</div>
          <hr/>
        </div>
      )
    }

    return (
      <div className="App">
        <div>
          <h3>使用map渲染数据</h3>
          <div>
              {/*将每个用户的属性数组塞进去*/}
              {usersElements}
            </div>
        </div>
      </div>
    );
  }
}

最终的效果如下:

map渲染列表

上述过程是手动取出数组中的各个对象的属性,然后再用封装成一个包含对应个数的jsx数组,然后将这个数组交给另一个jsx去渲染,最后一步就是{xx}xx是数组,React自动将数组中的每个元素在页面上罗列渲染。总而言之,上述的数组数据的渲染被分裂成了2个步骤:

  1. 从数组中取元素封装成数组;
  2. React 渲染第1步中的数组;

但实际,2个步骤是可以合并的,ES6中的map函数(和java中有点像)可以很轻松的遍历到数组中每个元素,代码如下:

<div>
    <h3>使用map渲染自动渲染</h3>
    {
    users.map((user) => {
        return (
        <div>
            <div>name: {user.username}</div>
            <div>age: {user.age}</div>
            <div>gender: {user.gender}</div>
            <hr />
        </div>
        )
    })
    }
</div>

抽出数组中的每个元素独立成为组件

 上述使用map函数来自动遍历效果还不错,但为了结构上的清晰,想把每个数组中的“用户”这个元素的结构进一步抽出来作为一个单独的组件,意思就是把上述使用for循环渲染数组数据中提到的:

<div>
    {/* 获取每个用户中三个属性 */}
    <div>name: {user.username}</div>
    <div>gender: {user.gender}</div>
    <div>age: {user.age}</div>
    <hr/>
</div>

就是这个东西抽离出来,因为这里面覆盖了一个“用户”中所有的属性,把这些属性单独提出来就是一个结构鲜明的“User”对象(有点像JavaBean),单独定一个结构已确定的User组件:

class User extends Component {
    render() {
        const {user} = this.props

        return (
            <div>
                <div>name: {user.username}</div>
                <div>gender: {user.gender}</div>
                <div>age: {user.age}</div>
                <hr/>
            </div>
        )
    }
}

export default User

然后在父组件中调用时,只要给定user这个props即可将数据渲染到User组件上:

const users = [
  { username: 'Jerry', age: 21, gender: 'male' },
  { username: 'Tomy', age: 22, gender: 'male' },
  { username: 'Lily', age: 19, gender: 'female' },
  { username: 'Lucy', age: 20, gender: 'female' }
]

class App extends Component {

  render() {
    return (
      <div className="App">
        <div>
          <h3>map渲染每个抽离的组件</h3>
          {
            users.map((user) => {
              return (
                <div>
                  <User user={user} />
                </div>
              )
            })
          }
        </div>
      </div >
    );
  }
}

map的作用仅仅是为了取出数组中每个“用户”,然后将每个用户通过<User />标签传入user属性,即可将该用户相应的三个属性渲染到User组件上。这样搞完,数组数据确实渲染上去了,但是控制台通常会有如下的错误信息:

Warning: Each child in an array or iterator should have a unique "key" prop.

即提示数组遍历的每个<User />元素应该有一个唯一键值(称之为key的一个prop

【附】

项目地址React_primary

  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值