React 介绍

React 是前端最流行的框架之一,由 Facebook 产出,由于其独特的 JSX 语法与组件化开发模式,将原本前端基于 DOM 的编程方式变成了基于组件和数据编程,给前端带来的益处是颠覆性的,因为我们知道 DOM 操作是 “昂贵” 的,React 在提高应用性能的同时又大大提高了开发效率,所以受到很多前端开发者的支持,也就有了庞大的生态,如今 React 已经成为前端工程师之必备技术栈。

创建 React 项目

在创建 React 项目时,可以使用当下最流行的脚手架 create-react-appgenerator-react-webpack,前者是由 Facebook 官方出品,后者是社区提供。

create-react-app

create-react-app 适用于大部分项目,集成了对 ReactJSXES6Flow 的支持,支持热更新,默认情况下无需对 Webpack 进行配置,如果要单独配置 Webpack,需要执行命令弹出配置项,下面命令分别对应安装脚手架工具、构建项目和弹出配置项。

# 安装脚手架
$ npm install -g create-react-app
# 创建项目
$ create-react-app project-name
# 弹射 Webpack 配置文件
$ npm run eject

注意:创建 React 项目时,项目名称不能含大写字母,使用 eject 命令弹出配置项的过程不可逆。

generator-react-webpack

generator-react-webpack 适用于构建大型项目,它是需要 yeoman 的支持,几乎具备了 create-react-app 的全部功能,不同的是默认可以对 Webpack 进行配置,生成项目需要手动创建项目根目录,安装脚手架工具和构建项目的命令如下:

# 安装脚手架及依赖
$ npm install -g yo generator-react-webpack
# 创建项目根目录
$ mkdir project-name
# 进入项目目录
$ cd project-name
# 创建项目
$ yo react-webpack

目录结构

我们本次使用 create-react-app 来构建一个项目,并弹出配置项,src 目录为我们主要的开发文件,必须含有一个入口文件 index.js,所以我们在构建项目后删除 src 中的无用文件,目录结构如下(可以通过 npm run start 启动项目)。

  
    react-demo
      |- config
      | |- jest
      | | |- cssTransform.js
      | | |- fileTransform.js
      | |- env.js.js
      | |- paths.js
      | |- webpack.config.dev.js
      | |- webpack.config.prod.js
      | |- webpackDevServer.config.js
      |- public
      | |- favicon.ico
      | |- index.html
      | |- manifest.json
      |- scripts
      | |- build.js
      | |- start.js
      | |- test.js
      |- src
      | |- index.js
      |- .gitignore
      |- package.json
      |- README.md
      |- yarn.lock
  

探索 React

引入 React 变量必须大写

React 的核心模块分为两个,分别为 reactreact-dom,前者为 React 的核心逻辑,后者为 React 的渲染逻辑,在 React 中规定引入 react 模块的变量名必须大写。

/* 文件位置:~react-demo/src/index.js */
import react from 'react';
import ReactDOM from 'react-dom';

// 创建一个 JSX
const h1 = (
  <h1>hello world</h1>
)

// 渲染到页面
ReactDOM.render(h1, window.root);

如果向上面代码中将引入 react 的变量小写,报错信息如下:


React 变量错写报错
React 变量错写报错


该报错信息的意思是当前使用了 JSX,必须要有一个大写的 React,从而可以看出这是 React 所规定的,当将接收 react 的变量改成大写后,页面正常渲染。

React 必须有 createElement 方法

/* 文件位置:~react-demo/src/index.js */
// 创建一个大写的 React 对象
const React = {};

// 创建一个 JSX
const h1 = (
  <h1>hello world</h1>
)

为了进一步验证,上面代码中创建一个名为 React 的对象,报错信息如下:


React 没有 createElement 方法报错
React 没有 createElement 方法报错


这个报错非常明显的在告诉我们,React 对象中缺少了 createElement 方法,我们将代码修改如下后发现报错信息消失。

/* 文件位置:~react-demo/src/index.js */
// 创建一个大写的 React 对象
const React = {
  createElement() {}
};

// 创建一个 JSX
const h1 = (
  <h1>hello world</h1>
)

页面 “白屏” 是因为并没有使用 react-dom 进行渲染,我们定义的 h1 是一个组件,同时也是 JSX,所以会调用 createElementJSX 进行解析。

解析后的 JSX 长什么样

/* 文件位置:~react-demo/src/index.js */
import React from 'react';

// 创建一个 JSX
const h1 = (
  <h1>hello world</h1>
)

// 查看 JSX 解析后的结果
console.log(h1);

打开 Chorme 浏览器控制台查看打印结果如下:


JSX 解析后的虚拟 DOM 结构
JSX 解析后的虚拟 DOM 结构


从结果可以看出 createElement 方法最终将 JSX 解析成了一个对象结构,其中 props 带表属性对象,其中的 children 代表子元素,也就是文本节点 hello worldtype 代表标签类型为 h1,这样用来表述 DOM 结构的对象被称为虚拟 DOM

模拟解析和渲染过程

在上面我们知道了 React 可以自动将 JSX 转换成虚拟 DOM,而 ReactDOMrender 方法将虚拟 DOM 渲染成了真实的 DOM,用法如下:

/* 文件位置:~react-demo/src/index.js */
import React from 'react';
import ReactDOM from 'react-dom';

// 创建 JSX
const el = (
  <h1 name="hi">
    hello
    <span>world</span>
  </h1>
)

// 渲染到页面
ReactDOM.render(el, window.root);

查看页面可以看到正常渲染了,现在就用前面对 React 的了解来简单模拟解析与渲染的过程,代码如下:

/* 文件位置:~react-demo/src/index.js */
// 创建 React 对象和 createElement 方法
const React = {
  createElement(type, props, ...children) {
    return { type, props, children };
  }
};

// 创建 JSX
const el = (
  <h1 name="hi">
    hello
    <span>world</span>
  </h1>
)

// 渲染的 render 方法
function render(vnode, container) {
  // 如果是字符串说明是文本节点,创建文本节点并插入到父元素中
  if (typeof vnode === 'string') {
    return container.appendChild(document.createTextNode(vnode));
  }

  // 如果不是字符串说明是元素节点,解构元素类型、属性和子元素的数组
  const { type, props, children } = vnode;

  // 创建元素
  const tag = document.createElement(type);

  // 循环添加属性
  for (let key in props) {
    tag.setAttribute(key, props[key]);
  }

  // 循环子元素,并递归创建子元素
  children.forEach(child => {
    render(child, tag);
  });

  // 将元素插入到容器中,root
  container.appendChild(tag);
}

// 渲染虚拟 DOM
render(el, window.root);

通过上面实现的代码同样可以完成渲染,当然仅限于简单结构,React 内部的实现更为复杂,兼容了多种组件类型和复杂的 DOM 结构。

JSX 最外层只能有一个元素

/* 文件位置:~react-demo/src/index.js */
import React from 'react';
import ReactDOM from 'react-dom';

// 创建 JSX
const el = (
  <h1 name="hi">hello</h1>
  <div>world</div>
)

ReactDOM.render(el, window.root);

在对上面代码中的 JSX 进行渲染时会有如下报错信息。


JSX 没有唯一父元素包裹报错
JSX 没有唯一父元素包裹报错


上面的报错信息告诉我们 JSX 元素必须包裹在一个闭合的标签内,所以说在写 JSX 语法的时候我们必须保证最外层只有一个元素节点。

React 的基本使用

JSX 全称为 JavaScript XML,但是和普通的 HTML 相比,有一些不同的用法,如元素属性 classforstyledangerouslyInnerHTML 以及注释写法等等。

className 属性

JSX 语法中,在标签中应使用 className 替代 HTML 中的 class 属性,因为在 JavaScriptclass 为关键字。

/* class 属性在 JSX 中的写法 */
import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<h1 className="active">hello</h1>, window.root);

htmlFor 属性

HTML 中,通过点击 label 标签让 input 输入框获取焦点是很常见的,只需要让 label 标签 for 属性的值与 input 标签的 id 值相等即可,但是在 JSX 中这这样的写法会报错,必须将 label 标签的 for 属性使用 htmlFor 替代,代码如下:

/* for 属性在 JSX 中的写法 */
import React from 'react';
import ReactDOM from 'react-dom';

const el = (
  <div>
    <h1>hello</h1>
    <label htmlFor="username">用户名</label>
    <input type="text" id="username" />
  </div>
)

ReactDOM.render(el, window.root);

style 属性

/* style 属性错误的写法 */
import React from 'react';
import ReactDOM from 'react-dom';

const el = (
  <h1 style="color: red;">hello</h1>
)

ReactDOM.render(el, window.root);

JSX 中关于 style 属性的写法发生了变化,如果用 HTML 中的写法会报错,错误信息如下:


JSX 中 style 属性错误写法报错
JSX 中 style 属性错误写法报错


报错信息中明确的告诉我们 style 属性必须是一个含有代表样式键值的对象,而不是一个字符串,并给出正确的结构,正确的写法如下:

/* style 属性在 JSX 中的写法 */
import React from 'react';
import ReactDOM from 'react-dom';

const el = (
  <h1 style={{color: 'red'}}>hello</h1>
)

ReactDOM.render(el, window.root);

注意:在解析 JSX 的过程中,<> 包裹 JSX 元素,元素属性中最外层的 {} 包裹 JS 代码,而内层的 {} 则代表一个 JS 对象,所以 style 是被两层 “花括号” 所包裹,并不是 mustache 语法。

取值表达式

JSX 中,所有的 JS 代码都可以写在 JSX 元素起始和闭合标签中间的 {} 内,会将执行结果渲染到该元素上。

/* 取值表达式的使用 */
import React from 'react';
import ReactDOM from 'react-dom';

const str = 'world';
const obj = { hello: 'world' };
const fn = () => <p>hello</p>;

const el = (
  <div>
    <h1>{fn()}</h1>
    <div>{str}</div>
    <div>{JSON.stringify(obj)}</div>
    <div>{true ? <span>nihao</span> : null}</div>
  </div>
)

ReactDOM.render(el, window.root);

启动项目可以看到页面上已经成功的渲染了 helloworld{ hello: 'world' }nihao,上面三元运算符结果如果为 null 则不会渲染这个节点,viod 0null 作用相同。

dangerouslySetInnerHTML 属性

JSX 中,如果想要把一个含有标签元素的字符串插入到某一个节点中,应该使用 dangerouslySetInnerHTML 替代原生 JS 中的 innerHTML

/* dangerouslySetInnerHTML 的用法 */
import React from 'react';
import ReactDOM from 'react-dom';

const str = '<h1>hello</h1>';
const el = (
  <h1 dangerouslySetInnerHTML={{__html: str}}></h1>
)

ReactDOM.render(el, window.root);

在上面的代码中,dangerouslySetInnerHTML 属性的值为对象,将要插入的 HTML 字符串作为对象中 __html 属性的值即可,设置 dangerouslySetInnerHTML 属性的 JSX 元素中不能有任何的子元素。

注意:dangerouslySetInnerHTML 属性非常危险,容易引发 XSS 攻击,轻易不要使用。

JSX 中注释的写法

JSXDOM 结构中,如果需要对代码进行注释不能使用 JS 中的 // 注释,也不能使用 HTML 中的 <!-- 注释 -->,注释必须使用 { } 包裹,写法如下:

/* 注释在 JSX 中的写法 */
import React from 'react';
import ReactDOM from 'react-dom';

const el = (
  <div>
    <h1 name="hi">hello</h1>
    {/* 这是注释,支持多行 */}
    <span>world</span>
  </div>
)

ReactDOM.render(el, window.root);

Fragment 组件

React 16.3 中提供了一个组件,类似于原生 JS 中的文档碎片,可以将多个元素包裹起来,却不会被渲染,用法如下:

/* Fragment 组件的使用 */
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';

const el = (
  <Fragment>
    <h1>hello</h1>
    <div>world</div>
  </Fragment>
)

ReactDOM.render(el, window.root);

循环动态创建 JSX 结构

React 中不存在过多的 API,最大的特点就是 JSX 语法可以将 JSHTML 混写(函数式编程),借助原生 JS 的方法实现功能,比如可以使用循环创建 JSX 结构。

/* 循环在 JSX 中的应用 */
import React from 'react';
import ReactDOM from 'react-dom';

const arr = [1, 2, 3];
const el = (
  arr.map((item, index) => {
    return (
      <li key={index}>{item}</li>
    )
  })
)

ReactDOM.render(el, window.root);

上面成功的渲染除了一个列表,但是有两点需要注意:

  • 第一点是循环一定要使用具有返回值的方法,如 mapfilter 等;
  • 第二点是每一个循环出来的 JSX 元素必须绑定一个 key 属性,可以使用数据的 id(优先),也可以使用数组的索引。

组件

在上面所有代码中的 JSX 都很不优雅,如果一个项目非常大,这样的混乱的结构是难以维护的,组件就是为了更好的维护和复用相同的 JSX 结构以及提高工作效率而存在的。

函数组件

React 中可以通过函数创建组件,函数名称就是组件名,必须大写,必须有返回值,可以为 JSX,也可以为 null,通过单闭合和双闭合两种方式调用组件,可以通过属性传参,并通过函数组件的第一个参数接收,实现代码如下:

/* 函数组件 */
import React from 'react';
import ReactDOM from 'react-dom';

// 创建一个函数组件
function Build(props) {
  return (
    <div>
      <h1>{props.title}</h1>
      <div>{props.content}</div>
    </div>
  )
}

// 渲染组件
ReactDOM.render((
  <div>
    <Build title='1' content='1xx'></Build> {/* 双闭合 */}
    <Build title='2' content='2xx' /> {/* 单闭合 */}
  </div>
), window.root);

函数组件缺点(16.3 以前):

  • 在函数组件内部 thisundefined
  • 在函数组件内部没有状态,即只能使用通过属性传递的参数,却没有更改的能力;
  • 函数组件没有生命周期,无法使用生命周期 “钩子” 完成一些操作。

由于函数组件的缺陷,所以更适合渲染一些静态的不需要数据变化的结构,如果想要让传入的属性变化可以通过不断执行 React.render 的方式不断更新传入组件参数的值,下面是一个时钟案例,通过函数组件实现时间的变化。

/* 函数组件多次渲染 */
import React from 'react';
import ReactDOM from 'react-dom';

// 创建函数组件
function Clock(props) {
  return (
    <div>
      <h1>当前时间</h1>
      <div>{props.time}</div>
    </div>
  )
}

// 每秒渲染一次组件
setInterval(() => {
  ReactDOM.render(
    <Clock time={new Date().toLocaleString()} />,
    window.root
  );
}, 1000);

类组件

类组件解决了函数组件所有的缺陷,是通过类声明的。

/* 类组件 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 创建类组件
class Clock extends Component {
  // constructor(props) {
  //   super(props);
  //   this.state = {
  //     time: new Date().toLocaleString();
  //   }
  // }

  // 等价于 constructor 的写法,更简洁
  state = {
    time: new Date().toLocaleString()
  }

  // 生命周期
  componentDidMount() {
    // Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
    setInterval(() => {
      this.setState({ time: new Date().toLocaleString() });
    }, 1000);
  }

  // 渲染这个组件会调用 render 方法
  render() {
    return (
      <div>
        时间:<span>{this.state.time}</span>
      </div>
    )
  }
}

// 渲染组件
ReactDOM.render(<Clock />, window.root);

在上面的类组件中,我们同样使用了一个简单的时钟功能,可以看出类组件即有 this,又能创建和更新状态,也可以通过生命周期进行一些操作。

所有的类组件都需要继承 React.Component,这样就可以使用 React.Component 的原型方法 setState 对状态进行更新,每次更新,都会使组件重新渲染,但是只会重新渲染变化的 DOM,这是 ReactDOM 通过 diff 算法所做的优化。

类组件中添加事件

在平时开发中每个组件都会有一些对应的功能,这就需要事件的配合,在类组建中绑定事件大概有四种方式,我们还是用上面的时钟案例,给该组件添加一个按钮,在点击时卸载这个组件。

/* 方式 1:使用箭头函数直接绑定事件 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 创建类组件
class Clock extends Component {
  state = {
    time: new Date().toLocaleString()
  }

  // 生命周期
  componentDidMount() {
    // Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
    setInterval(() => {
      this.setState({ time: new Date().toLocaleString() });
    }, 1000);
  }

  // 渲染这个组件会调用 render 方法
  render() {
    return (
      <div>
        时间:<span>{this.state.time}</span>
        <button onClick={() => {
          // 卸载组件的方法
          ReactDOM.unmountComponentAtNode(window.root);
        }}>
          kill
        </button>
      </div>
    )
  }
}

// 渲染组件
ReactDOM.render(<Clock />, window.root);
/* 方式 2:使用 bind 绑定函数 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 创建类组件
class Clock extends Component {
  state = {
    time: new Date().toLocaleString()
  }

  // 生命周期
  componentDidMount() {
    // Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
    setInterval(() => {
      this.setState({ time: new Date().toLocaleString() });
    }, 1000);
  }

  // 点击事件
  handleClick() {
    ReactDOM.unmountComponentAtNode(window.root);
  }

  // 渲染这个组件会调用 render 方法
  render() {
    return (
      <div>
        时间:<span>{this.state.time}</span>
        <button onClick={this.handleClick.bind(this)}>kill</button>
      </div>
    )
  }
}

// 渲染组件
ReactDOM.render(<Clock />, window.root);

上面两种方式都有一个共同的问题,箭头函数的方式在每次执行 render 时都会创建新的箭头函数,而将函数作为原型方法,通过 bind 是为了修正方法内部的 this 指向,但是每次执行 render 时,bind 也会返回一个新的函数。

/* 方式 3:在方式 2 的基础上提前生成函数 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 创建类组件
class Clock extends Component {
  constructor(props) {
    super(props);
    this.state = {
      time: new Date().toLocaleString()
    };
    this.fn = this.handleClick.bind(this);
  }

  // 生命周期
  componentDidMount() {
    // Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
    setInterval(() => {
      this.setState({ time: new Date().toLocaleString() });
    }, 1000);
  }

  // 点击事件
  handleClick() {
    ReactDOM.unmountComponentAtNode(window.root);
  }

  // 渲染这个组件会调用 render 方法
  render() {
    return (
      <div>
        时间:<span>{this.state.time}</span>
        <button onClick={this.fn}>kill</button>
      </div>
    )
  }
}

// 渲染组件
ReactDOM.render(<Clock />, window.root);

这样就解决了上面每次执行 render 就创建新函数的问题,但是这样的写法并不优雅,又产生了新的问题,所有的事件执行函数全都添加到了组件的实例上,而且代码会随着事件的增加而越来越乱。

/* 方式 4:使用 ES7 语法将原型方法使用箭头函数 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 创建类组件
class Clock extends Component {
  state = {
    time: new Date().toLocaleString()
  }

  // 生命周期
  componentDidMount() {
    // Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
    setInterval(() => {
      this.setState({ time: new Date().toLocaleString() });
    }, 1000);
  }

  // 点击事件
  handleClick = () => {
    ReactDOM.unmountComponentAtNode(window.root);
  }

  // 渲染这个组件会调用 render 方法
  render() {
    return (
      <div>
        时间:<span>{this.state.time}</span>
        <button onClick={this.handleClick}>kill</button>
      </div>
    )
  }
}

// 渲染组件
ReactDOM.render(<Clock />, window.root);

使用 ES7 的新语法,既解决了事件处理函数方法内部 this 指向问题,又解决了每次执行 render 创建新函数的问题,但需要依赖 @babel/plugin-proposal-class-properties 插件来解析。

卸载组件后不能再更新状态

还是上面的时钟案例,我们知道卸载一个组件应该使用 ReactDOM.unmountComponentAtNode 方法,参数一个组件,执行后会卸载这个组件内部所有的组件。

当真正点击时钟组件的按钮去卸载组件,组件虽然成功卸载了,但是控制台报错了,报错信息如下:


卸载组件后更新状态报错
卸载组件后更新状态报错


这个报错信息的意思是告诉我们在组件卸载后不能再通过 setState 更新状态,所以我们要在组件卸载之前先清空调用 setState 的定时器,代码修改如下:

/* 完整的时钟组件 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 创建类组件
class Clock extends Component {
  state = {
    time: new Date().toLocaleString()
  }

  // 生命周期
  componentDidMount() {
    // Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
    this.timer = setInterval(() => {
      this.setState({ time: new Date().toLocaleString() });
    }, 1000);
  }

  // 组件将要卸载时清空定时器
  componentWillUnmount() {
    clearInterval(this.timer);
  }

  // 点击事件
  handleClick = () => {
    ReactDOM.unmountComponentAtNode(window.root);
  }

  // 渲染这个组件会调用 render 方法
  render() {
    return (
      <div>
        时间:<span>{this.state.time}</span>
        <button onClick={this.handleClick}>kill</button>
      </div>
    )
  }
}

// 渲染组件
ReactDOM.render(<Clock />, window.root);

在这个组件中用到了两个生命周期 “钩子”,componentDidMount 钩子在组件挂载后执行,类似于原生 JSwindow.onloadcomponentWillUnmount 钩子在组件将要卸载之前执行,后面会涉及更多生命周期钩子,我们会在这个 React 基础篇系列文章中一一说明。

类组件的参数传递

/* 类组件传参第一种方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

const p = { name: 'panda', age: 28 };

class Person extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div>
        <p>{this.props.name}</p>
        <p>{this.props.age}</p>
      </div>
    )
  }
}

// 分别传入想要的属性
ReactDOM.render(<Person name={p.name} age={p.age} />, window.root);
/* 类组件传参第二种方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

const p = { name: 'panda', age: 28 };

class Person extends Component {
  render() {
    const { name, age } = this.props;

    return (
      <div>
        <p>{name}</p>
        <p>{age}</p>
      </div>
    )
  }
}

// 传入整个对象
ReactDOM.render(<Person {...p} />, window.root);

上面两种传参方式第一种是将对象中希望传入的属性传递给组件,第二种方式是将整个对象通过解构的方式直接传递给组件,而组件中可以在 constructor 中的第一个参数接收 props,也可以直接使用 this.props,因为 React 在组件创建实例调用 super 之前就已经将 props 作为了实例属性。

组件参数的类型校验

React 组件传递参数时,是通过 props 取出传入的参数直接使用,传入的值类型并没有做任何的校验,这就可能造成传参时出现错误,在 React 生态中有一个第三方模块 prop-types 可以规定参数的类型,并对传入的参数进行校验,使用前需安装。

$ npm install prop-types
/* 使用 prop-types 校验传给组件的参数 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

const p = {
  name: 'panda',
  age: 28,
  gender: '男',
  hobby: ['basketball', 'swim'],
  pos: { x: 433, y: 822 },
  salary: 5000
}

class Person extends Component {
  // 定义默认属性,React 自带
  static defaultProps = {
    name: 'shen'
  }

  // 定义属性类型
  static propTypes = {
    name: PropTypes.string.isRequired, // 类型必须为字符串,必填项
    age: PropTypes.number, // 类型必须为数字
    gender: PropTypes.oneOf(['男', '女']), // 性别只能为男或女
    hobby: PropTypes.arrayOf(PropTypes.string), // 数组成员类型必须是字符串
    pos: PropTypes.shape({ // 限制模型内部类型
      x: PropTypes.number.isRequired,
      y: PropTypes.number.isRequired
    }),

    // 第一个参数为原对象,第二个参数为当前属性,第三个参数为类
    salary(obj, key, P) {
      // 自行校验
      if (obj[key] < 3000) {
        throw new Error('工资太低');
      }
    }
  }

  render() {
    const { name, age } = this.props;

    return (
      <div>
        <p>{name}</p>
        <p>{age}</p>
      </div>
    )
  }
}

ReactDOM.render(<Person {...p} />, window.root);

使用 prop-types 必须在类组件上添加一个静态属性 propTypes,在内部定义属性的类型,其中 isRequired 为必填项,如果没有传参会报错,在检测是会优先检测 React 的静态属性 defaultProps,即默认属性,如果 defaultProps 存在则视为已经有该参数。

oneOf 方法参数为一个数组,传给组件对应的参数值必须是传给 oneOf 数组中的其中一项,否则会报错,arrayOf 方法用于限制数组成员的类型,shape 方法用于限属性值为对象的内部属性类型,参数为对象。

propTypes 静态属性中以传入的属性名作为方法名,则该方法为自定义校验该属性的函数,参数的前三项为原对象,属性名和所属类,可以在函数内部自行实现校验逻辑。

setState 更新状态

在前面的时钟组件中已经简单的使用过 setState,在这里我们会对 setState 的用法通过一个计数器案例来做详细说明。

/* 计数器案例 1 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Counter extends Component {
  state = { num: 0 }

  handleClick = () => {
    this.setState({ num: this.state.num + 1 });
    this.setState({ num: this.state.num + 1 });
  }

  render() {
    return (
      <div>
        {this.state.num}
        <button onClick={this.handleClick}>+</button>
      </div>
    )
  }
}

ReactDOM.render(<Counter />, window.root);

在上面的计数器中,当我们点击按钮时会执行 handleClick,而在 handleClick 内部调用了两次 setState 更新状态,但是我们启动项目后发现只有一次是有效的,这也说明了一个问题,setState 是异步执行的,最后一次执行的会覆盖前一次,其实在 setState 方法调用时支持传入一个回调函数,代码如下:

/* 计数器案例 2 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Counter extends Component {
  state = { num: 0 }

  handleClick = () => {
    this.setState({ num: this.state.num + 1 }, () => {
      this.setState({ num: this.state.num + 1 });
    });
  }

  render() {
    console.log('render');
    return (
      <div>
        {this.state.num}
        <button onClick={this.handleClick}>+</button>
      </div>
    )
  }
}

ReactDOM.render(<Counter />, window.root);

setState 传入的回调会在更新状态成功后执行,所以将代码修改后两次 setState 都生效了,render 执行了两次,这样的写法如果调用 setState 次数多了就形成了 “回调地狱”,setState 还有另一种用法如下:

/* 计数器案例 3 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Counter extends Component {
  state = { num: 0 }
  handleClick = () => {
    this.setState(prevState => ({ num: prevState.num + 1 }));
    this.setState(prevState => ({ num: prevState.num + 1 }));
  }
  render() {
    console.log('render');
    return (
      <div>
        {this.state.num}
        <button onClick={this.handleClick}>+</button>
      </div>
    );
  }
}

ReactDOM.render(<Counter />, window.root);

setState 方法可直接传入一个函数,函数的参数为上一次更新的 state,也就是 this.state,此时执行 setState 只更新状态,不重新渲染,当最后一次更新状态后统一渲染一次(也叫 setState 合并)。

触发组件重新渲染的两种方式:

  • props 发生变化,如调用 render 并传入新的属性值;
  • 调用 setState 重新设置状态。

受控组件和非受控组件

对于组件的分类除了可以按照组件的创建方式分为函数组件和类组件,还有另外一种分类方式,就是受控组件和非受控组件,简单来说 “受控” 和 “非受控” 就是指是否受到状态的控制,这种分类方式多用于表单元素,同时也指对于表单元素数据的不同处理方式。

受控组件

下面是一个受控组件的写法,输入框的初始值是通过 valuedefaultValue 属性绑定的状态的值。

/* 受控组件 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Control extends Component {
  state = {
    msg1: 'hello',
    msg2: 'world'
  }

  render() {
    return (
      <div>
        <input type="text" value={this.state.msg1} /> {/* 报错 */}
        <input type="text" defaultValue={this.state.msg2} /> {/* 不报错 */}
      </div>
    )
  }
}

ReactDOM.render(<Control />, window.root);

上面的代码中是两种绑定初始值的方式,使用 defaultValue 属性可以正常的将状态中的属性作为初始值绑定到页面的输入框内,但是随着输入的变化并没更新状态的作用,而使用 value 做了同样的绑定后,虽然页面正常显示初始值,但是控制台报错了,报错信息如下:


受控组件赋初始值报错
受控组件赋初始值报错


输入框的值可以通过输入改变,但受控组件要求状态的值要随着输入框内的值改变而更新,而报错信息告诉我们想要达到这样的目的必须要给表单元素绑定一个 onChange 事件,这个功能其实就是输入框与数据的双向绑定,修改后的实现如下:

/* 受控组件 —— 修改后 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Control extends Component {
  state = {
    msg: 'hello'
  }

  changeHandler = e => {
    this.setState({ msg: e.target.value });
  }

  render() {
    return (
      <div>
        <input
          type="text"
          value={this.state.msg}
          onChange={this.changeHandler}
        />
        {this.state.msg}
      </div>
    )
  }
}

ReactDOM.render(<Control />, window.root);

上面的代码中在 onChange 事件中调用了 setState 并更新了状态,但是如果有多个输入框,要保证 onChange 事件的复用,实现不同的输入框输入时 onChange 事件时更新不同的状态,实现如下:

/* 受控组件 —— 多个输入框复用 onChange */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Control extends Component {
  state = {
    msg1: 'hello',
    msg2: 'world'
  }

  changeHandler = e => {
    const val = e.target.name;
    this.setState({ [val]: e.target.value });
  }

  render() {
    return (
      <div>
        <input
          type="text"
          name="msg1"
          value={this.state.msg1}
          onChange={this.changeHandler}
        />
        {this.state.msg1}
        <br />
        <input
          type="text"
          name="msg2"
          value={this.state.msg2}
          onChange={this.changeHandler}
        />
        {this.state.msg2}
      </div>
    )
  }
}

ReactDOM.render(<Control />, window.root);

上面通过给 input 标签添加和状态的变量名相同的 name 属性,在触发 onChange 事件时用 name 属性作为更新状态数据的键值。

受控组件的好处是,可以实时对输入框输入的值进行校验,并可以随着输入框的内容更新而更新状态,进而更新视图。

非受控组件

非受控组件与受控组件相比就是直接操作 DOM 来操作表单元素,直接操作 DOM 可以在 componentDidMount 生命周期内(DOM 完全挂载),写法如下:

/* 非受控组件 —— 直接操作 DOM(不建议) */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class UnControl extends Component {
  componentDidMount() {
    const username = document.getElementById('username');
    username.value = 123;
    console.log(username.value);
  }

  render() {
    return (
      <div>
        <input type="text" id="username" />
      </div>
    )
  }
}

ReactDOM.render(<UnControl />, window.root);

当然在 React 中并不会这么写,React 专门给我们提供了操作 DOM 属性 ref,用法如下:

/* 非受控组件 —— ref 常用写法 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class UnControl extends Component {
  handleClick = () => {
    // 打印输入框的值
    console.log(this.userDom.value);
  }

  render() {
    return (
      <div>
        <input
          type="text"
          id="username"
          ref={dom => this.userDom = dom}
        />
        <button onClick={this.handleClick}>Click</button>
      </div>
    )
  }
}

ReactDOM.render(<UnControl />, window.root);

使用 ref 属性的方式通常会在其中传入一个函数,这个函数的参数就是当前表单元素对应的 DOM,通常情况下会使用类组件的一个属性来存储这个 DOM,方便在其他的事件或生命周期 “钩子” 中使用。

React 16.3 中推出了操作非受控组件的新的 API React.createRef 方法,返回值是一个对象,将这个对象绑定在表单元素的 ref 上,则可以通过这个对象的 current 属性获取这个表单元素的 DOM 元素。

/* 非受控组件 —— React 16.3 新 API */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class UnControl extends Component {
  userDom = React.createRef();

  handleClick = () => {
    // 打印输入框的值
    console.log(this.userDom.current.value);
  }

  render() {
    return (
      <div>
        <input type="text" id="username" ref={this.userDom} />
        <button onClick={this.handleClick}>Click</button>
      </div>
    )
  }
}

ReactDOM.render(<UnControl></UnControl>, window.root);

我们其实把 React.createRef 的返回值存储为了类组件的一个属性,并将这个属性传入 ref,这样可以在其他的事件或生命周期 “钩子” 中操作 DOM,如果存在多个这样的表单元素,许多次调用 React.createRef,并分别将存储返回值的类组件属性传入各个表单的 ref 中。

非受控组件的好处是,操作 DOM 方便,可以与更多基于 DOM 操作的第三方库结合。

复合组件

复合组件指的就是存在父子关系的组件嵌套,在 React 中有三种形式的父子组件嵌套:

  • 父组件中返回 JSX 中直接包含子组件;
  • children 的方式引入子组件;
  • render props 的方式引入子组件。

第一种是直接将子组件在父组件中引入,并放在父组件 render 方法返回的 JSX 中。

/* 复合组件 —— 第一种方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 父组件
class Parent extends Component {
  render() {
    return (
      <div>
        这是父组件
        <Child />
      </div>
    )
  }
}

// 子组件
class Child extends Component {
  render() {
    return (
      <div>这是子组件</div>
    )
  }
}

ReactDOM.render(<Parent />, window.root);

我们前面提到过组件可以通过单闭合或者双闭合的方式调用,第二种方式就是利用双闭合的调用方式,在父组件中引入子组件,把父组件中某些 JSX 放在双闭合的子组件标签中,作为参数传递给子组件,在子组件中通过 propschildren 属性进行接收,并放入对应的位置。

/* 复合组件 —— 第二种方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 父组件
class Parent extends Component {
  render() {
    return (
      <div>
        这是父组件
        <Child>
          <div>父组件传递给子组件的 JSX</div>
        </Child>
      </div>
    )
  }
}

// 子组件
class Child extends Component {
  render() {
    return (
      <div>
        这是子组件
        {this.props.children}
      </div>
    )
  }
}

ReactDOM.render(<Parent />, window.root);

第三种方式是将子组件作为一个函数的返回值,而函数作为父组件的 props 参数传入父组件,父组件返回的 JSX 中调用函数返回子组件,又叫 render props

/* 复合组件 —— 第三种方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// 父组件
class Parent extends Component {
  render() {
    return (
      <div>
        这是父组件
        {this.props.buildChild()}
      </div>
    )
  }
}

// 子组件
class Child extends Component {
  render() {
    return (
      <div>
        这是子组件
        {this.props.children}
      </div>
    )
  }
}

// render props 函数
const buildChildFn = () => {
  return <Child />
}

ReactDOM.render(<Parent buildChild={buildChildFn} />, window.root);

总结

这是系列关于 React 基础的文章,本篇是关于 React 的一些基础知识,也包含了一些 React 16 版本的一些新增内容,比较适合不了解 React 框架的同学们从零开始入门,在后面会陆续更新关于复合组件参数传递、生命周期等内容。