项目搭建

Create React App 脚手架。

  1. # 创建项目
  2. npx create-react-app my-app
  3. # 启动项目
  4. npm start

显示 webpack 配置

  1. # 单向操作,ejet 后不能回退
  2. npm run eject

文件结构

  1. ├── README.md 文档
  2. ├── public 静态资源
  3. ├── favicon.ico
  4. ├── index.html
  5. └── manifest.json
  6. └── src 源码
  7. ├── App.css
  8. ├── App.js 根组件
  9. ├── App.test.js
  10. ├── index.css 全局样式
  11. ├── index.js 入口文件
  12. ├── logo.svg
  13. └── serviceWorker.js pwa支持
  14. ├── package.json npm依赖
  15. ├── config
  16. ├── env.js 处理.env环境变量配置文件
  17. ├── paths.js 提供各种路径
  18. ├── webpack.config.js webpack配置文件
  19. └── webpackDevServer.config.js 开发服务器配置文件
  20. └── scripts 启动、打包和测试脚本
  21. ├── build.js 打包脚本
  22. ├── start.js 启动脚本
  23. └── test.js 测试脚本

JSX 语法

JSX是一种JavaScript语法的扩展,其格式比较像模板语言,通过JSX可以很好的描述UI,当然,在 React 中并不强制要求使用 JSX,JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖。

Babel会调用 React.createElement() 方法把 JSX 转换成虚拟 DOM 对象(React元素),然后通过ReactDOM.render()方法映射成真实DOM

  1. // JSX 写法
  2. import React, { Component } from 'react'
  3. export default class JsxTest extends Component {
  4. render() {
  5. let title = this.props.title || 'jsx语法'
  6. const greet = <p>hello, Jerry</p>
  7. const user = { firstName: "tom", lastName: "jerry" };
  8. function formatName(user) {
  9. return user.firstName + " " + user.lastName;
  10. }
  11. let name = true ? <p>jerry</p> : null
  12. return (
  13. <div>
  14. <p>{ title }</p>
  15. { greet }
  16. <p>{ formatName(user) }</p>
  17. { name }
  18. <ul>
  19. {
  20. [1, 2, 3,4 ].map((item, index) => <li key={item}>{item}</li> )
  21. }
  22. </ul>
  23. <p>这里是通过状态提升传过来的数据:{ this.props.date }</p>
  24. </div>
  25. )
  26. }
  27. }
  28. // React.createElement 写法
  29. import React, { Component } from 'react';
  30. export default class JsxTest extends Component {
  31. render() {
  32. let title = this.props.title || 'jsx语法';
  33. const greet = React.createElement("p", null, "hello, Jerry");
  34. const user = {
  35. firstName: "tom",
  36. lastName: "jerry"
  37. };
  38. function formatName(user) {
  39. return user.firstName + " " + user.lastName;
  40. }
  41. let name = true ? React.createElement("p", null, "jerry") : null;
  42. return (
  43. React.createElement("div", null,
  44. React.createElement("p", null, title), greet,
  45. React.createElement("p", null, formatName(user)),
  46. name,
  47. React.createElement("ul", null, [1, 2, 3, 4].map((item, index) => React.createElement("li", { key: item }, item))),
  48. React.createElement("p", null, "这里是通过状态提升传过来的数据:", this.props.date)
  49. )
  50. )
  51. }
  52. }

注意:由于 JSX 会编译为 **React.createElement** 调用形式,所以 **React** 库也必须包含在 JSX 代码作用域内。

例如,在如下代码中,虽然 React 并没有被直接使用,但还是需要导入:

  1. import React, { Component } from 'react';
  2. import './App.scss'
  3. import JsxTest from '../components/JsxTest';
  4. // 函数式组件
  5. function App () {
  6. return (
  7. <div className="root">
  8. <JsxTest title="React真有趣"/>
  9. </div>
  10. );
  11. }
  12. export default App;

如果你不使用 JavaScript 打包工具而是直接通过 <script> 标签加载 React,则必须将 React 挂载到全局变量中。

组件

React中有两种定义组件的方式:class组件和function组件。

class组件

class组件通常拥有状态和生命周期,继承于Component,实现 render 方法。

  1. import React, { Component } from 'react';
  2. import PropTypes from 'prop-types'
  3. export default class JsxTest extends Component {
  4. static defaultProps = {
  5. title: '测试jsx语法'
  6. }
  7. render () {
  8. let title = this.props.title
  9. const greet = React.createElement("p", null, "hello, Jerry");
  10. const user = {
  11. firstName: "tom",
  12. lastName: "jerry"
  13. };
  14. function formatName(user) {
  15. return user.firstName + " " + user.lastName;
  16. }
  17. let name = true ? React.createElement("p", null, "jerry") : null;
  18. return (
  19. React.createElement("div", null,
  20. React.createElement("p", null, title), greet,
  21. React.createElement("p", null, formatName(user)),
  22. name,
  23. React.createElement("ul", null, [1, 2, 3, 4].map((item, index) => React.createElement("li", { key: item }, item))),
  24. React.createElement("p", null, "这里是通过状态提升传过来的数据:", this.props.date)
  25. )
  26. )
  27. }
  28. }
  29. // prop 类型检查
  30. JsxTest.propTypes = {
  31. title: PropTypes.string
  32. }

function组件

函数组件通常无状态,仅关注内容展示,接收 props 返回渲染结果即可。

  1. // 函数式组件
  2. function Welcome(props) {
  3. return <h1>Hello, {props.name}</h1>;
  4. }

组件状态管理

类组件中的状态管理

  1. import React, { Component } from 'react'
  2. export default class Clock extends Component {
  3. constructor (props) {
  4. super(props)
  5. // 使用state属性维护状态,在构造函数中初始化状态
  6. this.state = {
  7. date: new Date()
  8. }
  9. }
  10. // 组件挂载时启动定时器每秒更新状态
  11. componentDidMount () {
  12. this.timer = setInterval(() => {
  13. // 使用 setState 方法更新状态
  14. this.setState({
  15. date: new Date()
  16. }, () => {
  17. this.props.change(this.state.date.toLocaleTimeString())
  18. })
  19. }, 1000)
  20. }
  21. // 组件卸载时停止定时器
  22. componentWillUnmount () {
  23. clearInterval(this.timer)
  24. }
  25. render() {
  26. return (
  27. <div>
  28. { this.state.date.toLocaleTimeString() }
  29. </div>
  30. )
  31. }
  32. }

函数式组件中的状态管理

函数式组件中的状态管理需要用到最新的 Hooks API (Hook 是 React 16.8 的新增特性)

  1. import React from 'react'
  2. import { useState, useEffect } from 'react'
  3. export default function ClockFun() {
  4. const [date, setDate] = useState(new Date());
  5. useEffect(() => {
  6. const timer = setInterval(() => {
  7. setDate(new Date())
  8. }, 1000)
  9. return () => clearInterval(timer)
  10. }, []);
  11. return (
  12. <div>{date.toLocaleTimeString()}</div>
  13. )
  14. }

setState 特性讨论

  • state 只能用 setState 更新状态而不能直接修改

    1. this.state.counter += 1; // 错误的
  • setState是批量执行的,因此对同一个状态执行多次只起一次作用,多个状态更新可以放在同一个 setState 中进行,可以给 setState 传递一个函数,确保每次调用都使用最新的 state。

    1. class Test extends React.Component {
    2. constructor(props) {
    3. super(props);
    4. this.state = {
    5. counter: 0,
    6. };
    7. }
    8. handleClick() {
    9. // 假如couter初始值为 0,执行三次以后其结果是多少?// 1
    10. this.setState({counter: this.state.counter + 1})
    11. this.setState({counter: this.state.counter + 1})
    12. this.setState({counter: this.state.counter + 1})
    13. // 假如couter初始值为 0,执行之后结果是多少?// 3
    14. this.setState((prevState, props) => ({ counter: prevState.counter + 1}))
    15. this.setState((prevState, props) => ({ counter: prevState.counter + 1}))
    16. this.setState((prevState, props) => ({ counter: prevState.counter + 1}))
    17. }
    18. render() {
    19. return (
    20. <div>
    21. <p>{this.state.counter}</p>
    22. <button onClick={this.handleClick.bind(this)}>累加</button>
    23. </div>
    24. );
    25. }
    26. }
  • setState通常是异步的,因此如果要获取到最新状态值有以下几种方式:

    setSstate在原生事件,setTimeout,setInterval,promise等异步操作中,state 会同步更新

    • 使用 setState 第二个参数

      1. // setState()函数接受两个参数,一个是一个对象,就是设置的状态,还有一个是一个回调函数,是在设置状态成功之后执行的
      2. this.setState({counter: this.state.counter + 1}, () => {
      3. console.log(this.state.counter)
      4. })
    • 使用定时器

      1. setTimeout(() => {
      2. this.setState({counter: this.state.counter + 1})
      3. console.log(this.state.counter) // 1
      4. }, 0)
    • 原生事件中修改状态 ```javascript changeValue = () => { this.setState({ counter: this.state.counter + 1 }) console.log(this.state.counter) // 1 }

document.body.addEventListener(‘click’, this.changeValue, false)

  1. - promise
  2. ```javascript
  3. const promise = new Promise(function (resolve, reject) {
  4. resolve(1)
  5. })
  6. promise.then(num => {
  7. this.setState({ counter: this.state.counter + num })
  8. console.log(this.state.counter) // 1
  9. })

事件处理

React 中使用onXXX写法来监听事件,例如:onClick、onChange。**React.Component** 创建的组件,其成员函数不会自动绑定this,需要开发者手动绑定,否则 this 不能获取当前组件实例对象。

  1. class Contacts extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. }
  5. handleClick() {
  6. console.log(this); // undefined
  7. }
  8. render() {
  9. return (
  10. <div onClick={ this.handleClick }></div>
  11. );
  12. }

React.Component三种手动绑定方法

构造函数中完成绑定,也可以在调用时使用**method.bind(this)**来完成绑定,还可以使用 arrow function来绑定。拿上例的handleClick函数来说,其绑定可以有:

  1. // 构造函数中绑定
  2. constructor(props) {
  3. super(props);
  4. this.handleClick = this.handleClick.bind(this);
  5. }
  6. // 使用bind来绑定
  7. <div onClick={this.handleClick.bind(this)}></div>
  8. // 使用 arrow function 来绑定
  9. <div onClick={() => this.handleClick()}></div>

目前还可以使用实验性的 public class fields 语法 为成员函数绑定 this。

该语法需要 babel 插件 支持。 Create React App 默认启用此语法。

  1. class LoggingButton extends React.Component {
  2. // 此语法确保 `handleClick` 内的 `this` 已被绑定。
  3. // 注意: 这是 *实验性* 语法。
  4. handleClick = () => {
  5. console.log('this is:', this);
  6. }
  7. render() {
  8. return (
  9. <button onClick={this.handleClick}>
  10. Click me
  11. </button>
  12. );
  13. }
  14. }

表单

React 中的双向绑定是组件自己绑定状态,自己修改状态实现的,这样的组件称之为受控组件

  1. import React, { Component } from 'react'
  2. export default class EventHandle extends Component {
  3. constructor(props) {
  4. super(props)
  5. this.state = {
  6. name: ''
  7. }
  8. // this.handleChange = this.handleChange.bind(this)
  9. }
  10. // 这里的 event 是合成事件,不存在兼容性问题
  11. handleChange (e) {
  12. this.setState({
  13. name: e.target.value
  14. })
  15. }
  16. render() {
  17. return (
  18. <div>
  19. <p>这是一个受控组件,React中的双向绑定是组件自己绑定状态,自己修改状态实现的,这样的组件称之为受控组件</p>
  20. <input value={ this.state.name } onChange={ (e) => { this.handleChange(e) } }></input>
  21. </div>
  22. )
  23. }
  24. }

组件通信

props 属性传递可以用于父子组件相互通信

  1. // index.js
  2. ReactDOM.render(<App title="React 真的很有趣" />, document.querySelector('#root'));
  3. // App.js
  4. <h2>{this.props.title}</h2>

如果父组件传递的是函数,则可以把子组件的数据传入父组件,这种做法称之为 状态提升,兄弟组件之间的通信也可以通过状态提升来实现。
完整示例代码:https://stackblitz.com/edit/web-platform-zgnh33?file=liftState.html

  1. <Clock change={this.onChange} />
  2. this.timerID = setInterval(() => {
  3. this.setState({ date: new Date()}, ()=>{
  4. // 每次状态更新就通知父组件
  5. this.props.change(this.state.date);
  6. });
  7. }, 1000);

生命周期

React v16.0 之前的生命周期在 React v16 推出的 Fiber 之后就不合适了,因为如果要开启 async rendering,在render函数之前的所有函数,都有可能被执行多次。所以 v16.0 之前和之后的生命周期有所修改,但是大部分团队不见得会跟进升到16版本,所以16前的生命周期还是很有必要掌握的,何况16也是基于之前的修改。

React 16.0 之前的生命周期

React 核心概念 - 图1

初始化 (initialization) 阶段

初始化阶段会执行 constructor() 做一些组件的初始化工作

  1. import React, { Component } from 'react';
  2. class Test extends Component {
  3. constructor(props) {
  4. super(props)
  5. this.state= {
  6. counter: 1
  7. }
  8. }
  9. }

挂载 (Mounting) 阶段

此阶段分为componentWillMount,render,componentDidMount三个时期

  • componentWillMount:在组件挂载到DOM前调用,只会被调用一次,在这调用 this.setState 不会引起组件重新渲染,也可以把写在这边的内容提前到 constructor() 中,所以项目中很少用。
  • render:根据组件的 props 和 state 返回一个React元素(描述组件,即UI),不负责组件实际渲染工作,之后由React自身根据此元素去渲染出页面DOM。render是纯函数(pure function:函数的返回结果只依赖于它的参数;函数执行过程里面没有副作用),不能在里面执行this.setState,会有改变组件状态的副作用。
  • componentDidMount:组件挂载到DOM后调用,只会被调用一次。

更新 (update) 阶段

在讲述此阶段前需要先明确下react组件更新机制。setState 引起的 state 更新或父组件重新 render 引起的props更新,更新后的 state 和 props 相对之前无论是否有变化,都将引起子组件的重新render。

  • componentWillReceiveProps(nextProps, nextState)
  • shouldComponentUpdate(nextProps, nextState)
  • componentWillUpdate(nextProps, nextState)
  • render
  • componentDidUpdate(prevProps, prevState)

造成组件更新有两类(三种)情况:

  • 1.父组件重新 render

父组件重新render引起子组件重新 render 的情况有两种

a. 直接使用,每当父组件重新 render 导致的重传 props,子组件将直接跟着重新渲染,无论 props 是否有变化。可通过 shouldComponentUpdate 方法优化。

  1. class Child extends Component {
  2. // 应该使用这个方法,否则无论props是否有变化都将会导致组件跟着重新渲染
  3. shouldComponentUpdate(nextProps, nextState){
  4. if(nextProps.someThings === this.props.someThings){
  5. return false
  6. }
  7. }
  8. render() {
  9. return <div>{this.props.someThings}</div>
  10. }
  11. }

b. 在 componentWillReceiveProps 方法中,将 props 转换成自己的 state

  1. class Child extends Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. someThings: props.someThings
  6. }
  7. }
  8. // 父组件重传 props 时就会调用这个方法
  9. componentWillReceiveProps(nextProps, nextState) {
  10. this.setState({ someThings: nextProps.someThings })
  11. }
  12. render() {
  13. return <div>{this.state.someThings}</div>
  14. }
  15. }

根据官网的描述

在 componentWillReceiveProps 中调用 this.setState() 将不会引起第二次渲染。

因为 componentWillReceiveProps 中判断props是否变化了,若变化了,this.setState将引起state变化,从而引起render,此时就没必要再做第二次因重传props引起的render了,不然重复做一样的渲染了。

  • 2.组件本身调用 setState,无论 state 有没有变化。可通过 shouldComponentUpdate 方法优化

    1. class Child extends Component {
    2. constructor(props) {
    3. super(props);
    4. this.state = {
    5. someThings:1
    6. }
    7. }
    8. // 应该使用这个方法,否则无论state是否有变化都将会导致组件重新渲染
    9. shouldComponentUpdate(nextProps, nextStates){
    10. if(nextStates.someThings === this.state.someThings){
    11. return false
    12. }
    13. }
    14. handleClick = () => { // 虽然调用了setState ,但state并无变化
    15. const preSomeThings = this.state.someThings
    16. this.setState({
    17. someThings: preSomeThings
    18. })
    19. }
    20. render() {
    21. return <div onClick = {this.handleClick}>{this.state.someThings}</div>
    22. }
    23. }

    卸载阶段

    此阶段只有一个生命周期方法:componentWillUnmount ,此方法在组件被卸载前调用,可以在这里执行一些清理工作,比如清除组件中使用的定时器清除 componentDidMount 中手动创建的DOM元素等,以避免引起内存泄漏。

    React 16.3 的生命周期

React 核心概念 - 图2

React 16.4 的生命周期

React 核心概念 - 图3

React v16.3,引入了两个新的生命周期函数,getDerivedStateFromPropsgetSnapshotBeforeUpdate

getDerivedStateFromProps 本来(React v16.3中)是只在创建和更新(由父组件引发部分)时出发,也就是不是由父组件引发的 Updating, getDerivedStateFromProps 不会被调用,如自身 setState 或者 forceUpdate 引发的 Updating。

在React v16.4中改正了这一点,让 getDerivedStateFromProps 无论是 Mounting 还是 Updating,也无论是因为什么引起的 Updating,都会被调用。

getDerivedStateFromProps

static getDerivedStateFromProps(nextProps, prevState) 在组件创建时和更新时的 render 方法之前调用,它应该返回一个对象来更新状态,或者返回 null 不更新任何内容。

  1. static getDerivedStateFromProps (nextProps, prevState) {
  2. console.log('getDerivedStateFromProps', nextProps, prevState)
  3. if (nextProps.msg !== prevState.name) {
  4. return { name: nextProps.msg }
  5. }
  6. return null
  7. }

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate() 被调用于 render 之后,可以读取但无法使用 DOM 的时候。它使您的组件可以在可能更改之前从 DOM 捕获一些信息(例如滚动位置)。此生命周期返回的任何值都将作为参数传递给componentDidUpdate。

官网给的例子:

  1. class ScrollingList extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.listRef = React.createRef();
  5. }
  6. getSnapshotBeforeUpdate(prevProps, prevState) {
  7. // 我们是否要添加新的 items 到列表?
  8. // 捕捉滚动位置,以便我们可以稍后调整滚动.
  9. if (prevProps.list.length < this.props.list.length) {
  10. const list = this.listRef.current;
  11. return list.scrollHeight - list.scrollTop;
  12. }
  13. return null;
  14. }
  15. componentDidUpdate(prevProps, prevState, snapshot) {
  16. // 如果我们有snapshot值, 我们已经添加了 新的items.
  17. // 调整滚动以至于这些新的items 不会将旧items推出视图。
  18. // (这边的snapshot是 getSnapshotBeforeUpdate 方法的返回值)
  19. if (snapshot !== null) {
  20. const list = this.listRef.current;
  21. list.scrollTop = list.scrollHeight - snapshot;
  22. }
  23. }
  24. render() {
  25. return (
  26. <div ref={this.listRef}>{/* ...contents... */}</div>
  27. );
  28. }
  29. }