React.js 新手快速入门 - 开山篇

2,767 阅读16分钟

著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

系列二 - React.js 新手快速入门 - 进阶篇

在阅读之前,会有一些小伙伴觉得疑惑,作者怎么写前端文章了呢,作者不是专注后端Java吗?这是怎么了?其实不然,在4年前,那个时候还没有流行vue和react,身为后端程序员的我们,不管是java还是php程序员,都是需要写前端的,不过那个时候的前端没有现在那么多东西。我们一般叫美工画好静态页面,然后交给后端程序员,后端程序员在静态页面中加入js代码,或者把静态页面替换成jsp、velocity等静态模板语言的代码,一个动态效果的页面就完成了。随着互联网的不断发展,程序员工种的划分也越来越明细,现在的前端和作者曾经那个时候已经大不一样了,不论是思想还是语言风格,为了学习下如何自己制作页面,也为了感受下前端代码的魅力,故选择了React.js 前端框架作为学习目标。其实前端很有趣,有意思!身为后端程序员的你不打算了解一下嘛~

准备工作

在学习 react 之前,我们需要先安装对应的运行环境,工欲善其事必先利其器。首先安装好如下环境:

不知道我的读者是不是完全不懂前端,建议读者有一点点的 Html、Css、java script、es6 的基础,实在没有建议花个1~2天学习下。

熟悉官方create-react-app脚手架

react 前端项目和我们平时的java项目一样,都有其自己的项目结构,java的项目结构有IDE开发工具帮我们生产,在本文中,我们使用facebook 的 create-react-app 脚手架项目来帮我们生成 react 项目结构,操作如下:

# 全局安装官方脚手架
npm i -g create-react-app 
# 初始化创建一个基于 react 的项目
create-react-app 01_jpview_class
# 设置 npm 下载镜像源为淘宝, 和设置 maven 仓库源一个意思
npm config set registry http://registry.npm.taobao.org

这个时候就开始创建项目了,时间有点长,因为正在下载需要的插件和依赖包,完成后项目结构如下:

├── README.md        文档
├── package-lock.json
├── package.json      npm 依赖
├── public      静态资源
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
└── src      源码
   ├── App.css
   ├── App.js      根组件
   ├── App.test.js      测试
   ├── index.css      全局样式
   ├── index.js      入口
   ├── logo.svg
   └── serviceWorker.js      pwa支持

什么是JSX语法

删除src目录下的所有文件,新建一个 index.js文件,内容为

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App/>, document.querySelector('#root'))

新建 App.js文件,内容为:

import React, { Component } from "react";

export default class App extends Component{
    render(){
        return <div>
            <button>雷猴啊</button>
        </div>
    }
}
export default App;

上面的代码看起来会有感到困惑的地方,首先就是ReactDOM.render(<App />, document.querySelector('#root')); 看起来是js和html的混合体,这种语法被称之为JSX,其实际核心的逻辑完全是js来实现的。

在项目目录终端执行以下命令可以看到效果

# 下载依赖包
npm install
# 启动运行项目
npm start

学习react基础语法

以下所有代码都可以直接复制到 index.js文件中 体验效果

React 组件

React 的世界里一切皆是组件,我们使用class语法构建一个最基本的组件,组件的使用方式和HTML相同,组件的render函数返回渲染的一个JSX页面,然后使用ReactDOM渲染到页面里

import React from 'react';
import ReactDOM from 'react-dom';
// 继承React.Component表示App是一个组件
class App extends React.Component {
    render() {
      return <div> Hello React </div>
    }
  }
 // ReactDOM.render()方法把 App中的内容追加到 index.html 中 <div id="root">的标签上
ReactDOM.render(<App/>, document.querySelector('#root'))

属性传递

React组件使用和html类似的方式传递参数,在组件内部,使用this.props获取所有的传递的参数,在JSX里使用变量,使用{}包裹

import React from 'react';
import ReactDOM from 'react-dom';
// 继承React.Component表示App是一个组件
class App extends React.Component {
    render() {
      // 获取<App name="React"> 传递过来的属性name值
      return <div> Hello {this.props.name} </div>
    }
  }
 // ReactDOM.render()方法把 App中的内容追加到 index.html 中 <div id="root">的标签上
ReactDOM.render(<App name="React" />, document.querySelector('#root'))

JSX

JSX是一种js的语法扩展,表面上像HTML,本质上还是通过babel转换为js执行,所有在JSX里可以使用{}来写js的语法,JSX本质上就是转换为React.createElement在React内部构建虚拟Dom,最终渲染出页面

import React from 'react';
import ReactDOM from 'react-dom';
// 继承React.Component表示App是一个组件
class App extends React.Component {
    render() {
      return (
        <div>
          // {2+2} js的计算语法,结果为4
          Hello {this.props.name}, I am {2 + 2} years old
        </div>
      )
    }
  }
 // ReactDOM.render()方法把 App中的内容追加到 index.html 中 <div id="root">的标签上
ReactDOM.render(<App name="React" />, document.querySelector('#root'))

State和事件绑定

我们到现在为止还没有更新过UI页面,React内部通过this.state变量来维护内部的状态,并且通过this.setState来修改状态,render里用到的state变量,也会自动渲染到UI,我们现在constructor()来初始化state,在JSX语法里使用this.state.num获取,然后jsx里使用onClick绑定点击事件,注意这里需要在constructor()里使用bind()方法绑定this指向,然后内部调用this.setState修改值,注意这里不能写成this.state.num+1,而是要调用this.setState,设置并返回一个全新的num值。

import React from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
    constructor(props){
      super(props)
      // 初始化构造時设置内部状态 num值为 1 
      this.state = {
        num:1
      }
      // 把handleClick()方法绑定到当前对象Counter上
      this.handleClick = this.handleClick.bind(this)
    }
    handleClick(){
      // 改变内部状态 num 的值
      this.setState({
        num:this.state.num + 1
      })
    }
    render() {
      return (
        <div>
          <p>{this.state.num}</p>
            {/*{this.handleClick} js语法调用当前对象的handleClick()方法*/}
          <button onClick={this.handleClick}>click</button>
        </div>
      )
    }
  }

ReactDOM.render(<Counter/>, document.querySelector('#root'))

生命周期

在组件内部存在一些特殊的方法,会在组件的不同阶段执行,比如组件加载完毕后会执行componentDidMount函数,组件更新的时候,会执行shouldComponentUpdate函数,如果返回true的话,就会依次执行componentWillMountrendercomponentDidMount,如果返回false的话,就不会执行。

import React from 'react';
import ReactDOM from 'react-dom';

class Counter extends React.Component {
    constructor(props){
      super(props)
      this.state = {
        num:1
      }
      this.handleClick = this.handleClick.bind(this)
    }
    // 生命方法--组件渲染完成,只执行一次
    componentDidMount(){
      console.log('componentDidMount 函数触发')
    }
    // 生命方法--避免组件重复或者无意义渲染
    shouldComponentUpdate(nextProps,nextState){
      if (nextState.num%2) {
        return true
      }
      return false
    }
    handleClick(){
      this.setState({
        num:this.state.num+1
      })
    }
    render() {
      return (
        <div>
          <p>{this.state.num}</p>
          <button onClick={this.handleClick}>click</button>
        </div>
      )
    }
  }

ReactDOM.render(<Counter/>, document.querySelector('#root'))

生命周期流程图:

表单

用户想提交数据到后台,表单元素是最常用的,一个常见的表单由forminputlabel等标签构成,我们通过onChange()方法控制value的值,最终通过state,让在的html input中输入内容和React关联起来。

import React from 'react';
import ReactDOM from 'react-dom';

class TodoList extends React.Component {
    constructor(props){
      super(props)
      this.state = {
        text:''
      }
      this.handleClick = this.handleClick.bind(this)
      this.handleChange = this.handleChange.bind(this)
    }
    handleClick(){
      // 如果内部状态 text有值,则把值清空''
      if (this.state.text) {
        this.setState({
          text:''
        })
      }
    }
    handleChange(e){
      // 获取事件元素input的值赋值给内部状态 text 中
      this.setState({
        text:e.target.value
      })
    }
    render() {
      return (
        <div>
          {/* 显示内部状态 text 的内容*/}
          {this.state.text}
          {/*input接收到输入值调用handleChange()方法*/}
          <input type="text" value={this.state.text} onChange={this.handleChange}/>
          {/*点击按钮调用handleClick()方法*/}
          <button onClick={this.handleClick}>clear</button>    
        </div>
      )
    }
  }
  
ReactDOM.render(<TodoList/>, document.querySelector('#root'))

渲染列表

页面里序列化的数据,比如用户列表,都是一个数组,我们通过map函数把数组直接映射为JSX,但是我们直接渲染列表,打开console的时候会看到Each child in an array or iterator should have a unique "key" prop.报错。在渲染列表的时候,我们需要每个元素都有一个唯一的key属性,这样React在数据变化的时候,知道哪些dom应该发生变化 尤其注意key要唯一,建议每个字段唯一id,或者使用索引

import React from 'react';
import ReactDOM from 'react-dom';

class TodoList extends React.Component {
    constructor(props){
      super(props)
      // 内部装填属性初始化值
      this.state = {
        todos:['Learn React','Learn Ant-design','Learn Koa'],
        text:''
      }
      this.handleClick = this.handleClick.bind(this)
      this.handleChange = this.handleChange.bind(this)
    }
    handleClick(){
      if (this.state.text) {
        this.setState(state=>({
           // 如果内部状态 text有值,追加到解构的todos数组后
          todos:[...state.todos,state.text],
           // 如果内部状态 text有值,则把值清空''
          text:''
        }))
      }
  
    }
    handleChange(e){
      // 获取事件元素input的值赋值给内部状态 text 中
      this.setState({
        text:e.target.value
      })
    }
    render() {
      return (
        <div>
          {/*input接收到输入值调用handleChange()方法*/}
          <input type="text" value={this.state.text} onChange={this.handleChange}/>
          {/*点击按钮调用handleClick()方法*/}
          <button onClick={this.handleClick}>add</button>
          <ul>
            {/*map()循环输出JSX内容给ReactDOM*/}
            {this.state.todos.map(v=>{
              return <li key={v}>{v}</li>
            })}
          </ul>
        </div>
      )
    }
  }

ReactDOM.render(<TodoList/>, document.querySelector('#root'))

React16新增了什么

2017年9月27日,Facebook 官方发布了 React 16.0。相较于之前的 15.x 版本,v16是第一个核心模块重写了的版本,并且在异常处理,核心架构和服务端渲染方面都有更新。

  • render 函数支持返回数组和字符串
  • 异常处理,添加componentDidCatch钩子获取组件错误
  • 新的组件类型 portals 可以渲染当前容器dom之外的节点
  • 打包的文件体积减少 30%
  • 更换开源协议为MIT许可
  • Fiber架构,支持异步渲染
  • 更好的服务端渲染,支持字节流渲染
import React from 'react';
import ReactDOM from 'react-dom';

// 继承React.Component表示React16是一个组件
class React16 extends React.Component {
    // 构造器函数
    constructor(props){
      super(props)
      this.state={hasError:false}
    }
    // 生命周期函数
    componentDidCatch(error, info) {
      // 设置内部状态 hasError为true
      this.setState({ hasError: true })
    }
  
    render() {
      return (
        <div>
          {/*? : 是三目运算符*/}
          {this.state.hasError ? <div>出错了</div>:null}
          {/*使用组件ClickWithError和FeatureReturnFragments*/}
          <ClickWithError />
          <FeatureReturnFragments />
        </div>
      )
    }
  }
  // 继承React.Component表示ClickWithError是一个组件
  class ClickWithError extends React.Component{
     constructor(props){
      super(props)
      this.state = {error:false}
      // 绑定handleClick()方法到当前对象上
      this.handleClick = this.handleClick.bind(this)
    }
    handleClick(){
      // 触发调用时设置state.error值为true
      this.setState({
        error:true
      })
    }
    render() {
      if (this.state.error) {
        throw new Error('出错了!')
      }
      return <button onClick={this.handleClick}>抛出错误</button>
    }
  }
  // 继承React.Component表示FeatureReturnFragments是一个组件
  class FeatureReturnFragments extends React.Component{
    render(){
      return [
        <p key="key1">React很不错</p>,
        "文本1",
        <p key="key2">Antd-desing也很赞</p>,
        "文本2"
     ]
    }
  }
  
ReactDOM.render(<React16/>, document.querySelector('#root'))

虚拟DOM

什么是DOM?—HTML DOM 教程

DOM操作成本实在是太高,所以有了在js里模拟和对比文档对象模型的方案,JSX里使用 reactcreateElement构建虚拟DOM,每次只要有修改,先对比js里面的虚拟dom树里的内容。 传统浏览器渲染流程图

虚拟DOM树结构图
参考:
react 中文官网
在线学习体验 react api

实战来总结

学完了api 的使用,是时候拿起武器开始真刀真枪的开干了,如图是实战的效果演示,具体的代码分析讲解可以直接在我的github上看到,就不在本文赘述了,我要传送代码仓库===> 项目代码地址

深入理解生命周期

React v16.0版本之前

组件初始化阶段(initialization)

如下代码中类的构造方法constructor(),Test类继承了react Component这个基类,也就继承这个react的 基类,才能有render(),生命周期等方法可以使用,这也说明为什么函数组件不能使用这些方法的原因。

super(props) 用来调用基类的构造方法constructor(), 也将父组件的props注入给子组件,供子组件读取 (组件 中props属性只读不可写,state可写) 。 而 constructor() 用来做一些组件的初始化工作,比如定义this.state的初始内 容。

import React, { Component } from 'react';
    class Test extends Component {
        constructor(props) {
        super(props);
        this.state={serviceName: jpsite}
    }
}

组件的挂载阶段(Mounting)

此阶段分为componentWillMountrendercomponentDidMount三个时期。

  • componentWillMount:在组件挂载到DOM前调用,且只会被调用一次,在这里面调用this.setState不会引起组件的重新渲染,也可以把写在这里面的内容改写到constructor()中,所以在项目中很少这么使用。
  • render:根据组件的propsstate(无论两者是重传递或重赋值,无论值是否有变化,都可以引起组件重新render ,内部return 一个React元素(描述组件,即UI),该元素不负责组件的实际渲染工作,之后由React自身根据此元素去渲染出页面DOM。render是纯函数 (Pure function:函数的返回结果只依赖于它的参数;函数执行过程里面没有副作用)不能在render()里面执行this.setState等操作,会有改变组件状态的副作用。
  • componentDidMount:组件挂载到DOM后调用,且只会被调用一次

组件的更新阶段(update)

在组件的更新阶段中,存在很多生命方法,从上图可以很直观的看到,有 componentWillReceivePropsshouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate

  • componentWillReceiveProps(nextProps):此方法只调用于props引起的组件更新过程中,参数nextProps是父组件传给当前组件的新props。但父组件render 方法的调用不能保证重传给当前组件的props是有变化的,所以在此方法中根据nextPropsthis.props来查明重传 的props是否改变,以及如果改变了要执行啥,比如根据新的props调用this.setState触发当前组件的重新render
  • shouldComponentUpdate(nextProps,nextState):此方法通过比较nextPropsnextState及当前组件的this.propsthis.state,返回true时当前组件将继续执行更新过程,返回false则当前组件更新停止,以此可用来减少组件的不必要渲染,优化组件性能。 这边也可以看出,就算componentWillReceiveProps()中执行了this.setState,更新了state,但在render前 (如shouldComponentUpdatecomponentWillUpdate),this.state依然指向更新前的state,不然nextState 及当前组件的this.state的对比就一直是true了。
  • componentWillUpdate(nextProps, nextState):此方法在调用render方法前执行,在这边可执行一些组件更新发生前的工作,一般较少用。
  • renderrender方法在上文讲过,这边只是重新调用。
  • componentDidUpdate(prevProps, prevState):此方法在组件更新后被调用,可以操作组件更新的DOMprevPropsprevState这两个参数指的是组件更新前的propsstate

在此阶段需要先明确下react组件更新机制。setState引起的state更新,或父组件重新render引起的props更新,更新后的stateprops相比较之前的结果,无论是否有变化,都将引起子组件的重新render。详细了解可看=>这篇文章 造成组件更新有两类(三种)情况:

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

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

    class Child extends Component {
       // 应该使用这个方法,否则无论props是否有变化都将会导致组件跟着重新渲染
       shouldComponentUpdate(nextProps){ 
           if(nextProps.someThings === this.props.someThings){
               return false
           }
       }
       render() {
           return <div>{this.props.someThings}</div>
       }
    }
    
  2. componentWillReceiveProps方法中,将props转换成自己的state

    class Child extends Component {
       constructor(props) {
           super(props);
           this.state = {
               someThings: props.someThings
           };
       }
       componentWillReceiveProps(nextProps) { // 父组件重传props时就会调用这个方法
           this.setState({someThings: nextProps.someThings});
       }
       render() {
           return <div>{this.state.someThings}</div>
       }
    }
    

    根据官网的描述: 在componentWillReceiveProps方法中,将props转换成自己的state 是因为componentWillReceiveProps中判断props是否变化了,若变化了,this.setState将引起state变化,从而引起render,此时就没必要再做第二次因重传props来引起render了,不然就重复做一样的渲染了。

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

shouldComponentUpdate() {
    // 组件是否需要更新,返回布尔值,优化点
    console.log("5.组件是否应该更新?");
    return true;
  }

卸载阶段

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

React v16.0版本之后(2019.11.20)

原来(React v16.0前)的生命周期在React v16推出的Fiber之后就不合适了,因为如果要开启async rendering, 在render函数之前的所有函数,都有可能被执行多次。

原来(React v16.0前)的生命周期有哪些是在render前执行的呢?

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

如果开发者开了async rendering,而且又在以上这些render前执行的生命周期方法做AJAX请求的话,那AJAX将被无谓地多次调用。。。明显不是我们期望的结果。而且在componentWillMount里发起AJAX,不管多快得到结果也赶不上首次render,而且componentWillMount在服务器端渲染也会被调用到(当然,也许这是预期的结果),这样的IO操作放在componentDidMount里更合适。

禁止不能用比劝导开发者不要这样用的效果更好,所以除了shouldComponentUpdate,其他在render函数之前的所有函数(componentWillMountcomponentWillReceivePropscomponentWillUpdate)都可以被getDerivedStateFromProps替代。

也就是用一个静态函数getDerivedStateFromProps来取代被deprecate的几个生命周期函数,就是强制开发者在render之前只做无副作用的操作,而且能做的操作局限在,根据propsstate决定新的state

React v16.0刚推出的时候,是增加了一个componentDidCatch生命周期函数,这只是一个增量式修改,完全不影响原有生命周期函数;但是,到了React v16.3,大改动来了,引入了两个新的生命周期函数。

新的生命周期函数getDerivedStateFromProps和getSnapshotBeforeUpdate

  • getDerivedStateFromPropsgetDerivedStateFromProps 本来(React v16.3中)是指在创建和更新(由父组件引发部分),也就是不由 父组件引发,那么getDerivedStateFromProps也不会被调用,如自身setState引发或者forceUpdate引发。这样的话理解起来有点乱,在React v16.4中改正了这一点,让getDerivedStateFromProps无论是Mounting还是Updating,也无论是因为什么引起的Updating,全部生命函数都会被调用,具体可看React v16.4 的生命周期图。

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

  • getSnapshotBeforeUpdategetSnapshotBeforeUpdate() 被调用于render之后,适用场景是可以读取但无法使用DOM的时候。它使您的组件可以在更改之前从DOM捕获一些信息(例如滚动位置)。此生命周期返回的任何值都将作为参数传递给 componentDidUpdate()
class ScrollingList extends React.Component {
   constructor(props) {
       super(props);
       this.listRef = React.createRef();
   }
   getSnapshotBeforeUpdate(prevProps, prevState) {
       //我们是否要添加新的 items 到列表?
       // 捕捉滚动位置,以便我们可以稍后调整滚动.
       if (prevProps.list.length < this.props.list.length) {
           const list = this.listRef.current;
           return list.scrollHeight - list.scrollTop;
       }
       return null;
   }
   componentDidUpdate(prevProps, prevState, snapshot) {
       //如果我们有snapshot值, 我们已经添加了 新的items.
       // 调整滚动以至于这些新的items 不会将旧items推出视图。
       // (这边的snapshot是 getSnapshotBeforeUpdate方法的返回值)
       if (snapshot !== null) {
           const list = this.listRef.current;
           list.scrollTop = list.scrollHeight - snapshot;
        }
   }
   render() {
       return (
           <div ref={this.listRef}>{/* ...contents... */}</div>
       );
   }
}

扫码关注公众号,回复20191120获取本文所有源码

☞☞点击这里购买云服务器☜体验代码效果☜