React.js 新手快速入门 – 开山篇
- 2019 年 11 月 22 日
- 筆記
在阅读之前,会有一些小伙伴觉得疑惑,作者怎么写前端文章了呢,作者不是专注后端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支持
作者:蒋老湿 链接:https://juejin.im/post/5dd2b83f518825462376ae85
什么是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 KaikebaCart 复制代码
上面的代码看起来会有感到困惑的地方,首先就是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.stateState
来修改状态,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
的话,就会一次执行componentWillMount
、render
,componentDidMount
,如果返回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')) 复制代码

生命周期流程图:

表单
用户想提交数据到后台,表单元素是最常用的,一个常见的表单由form
、 input
、 label
等标签构成,我们通过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操作成本实在是太高,所以有了在js里模拟和对比文档对象模型的方案,JSX里使用 react
、 createElement
构建虚拟DOM
,每次只要有修改,先对比js里面的虚拟dom树
里的内容。 传统浏览器渲染流程图

虚拟DOM树结构图

参考:
实战来总结
学完了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); } } 复制代码
组件的挂载阶段(Mounting)
此阶段分为componentWillMount
,render
,componentDidMount
三个时期。
- componentWillMount:在组件挂载到
DOM
前调用,且只会被调用一次,在这里面调用this.setState
不会引起组件的重新渲染,也可以把写在这里面的内容改写到constructor()
中,所以在项目中很少这么使用。 - render:根据组件的
props
和state
(无论两者是重传递或重赋值,无论值是否有变化,都可以引起组件重新render
) ,内部return
一个React
元素(描述组件,即UI),该元素不负责组件的实际渲染工作,之后由React
自身根据此元素去渲染出页面DOM。render
是纯函数(Pure function:函数的返回结果只依赖于它的参数;函数执行过程里面没有副作用)
,不能在里面执行this.setState
等操作,会有改变组件状态的副作用。 - componentDidMount:组件挂载到
DOM
后调用,且只会被调用一次
组件的更新阶段(update)
在组件的更新阶段中,存在很多生命方法,从上图可以很直观的看到,有 componentWillReceiveProps
, shouldComponentUpdate
,componentWillUpdate
,render
,componentDidUpdate
。
- componentWillReceiveProps(nextProps):此方法只调用于
props
引起的组件更新过程中,参数nextProps
是父组件传给当前组件的新props
。但父组件render
方法的调用不能保证重传给当前组件的props
是有变化的,所以在此方法中根据nextProps
和this.props
来查明重传 的props
是否改变,以及如果改变了要执行啥,比如根据新的props
调用this.setState
出发当前组件的重新render
- shouldComponentUpdate(nextProps,nextState):此方法通过比较
nextProps
,nextState
及当前组件的this.props
,this.state
,返回true时当前组件将继续执行更新过程,返回false则当前组件更新停止,以此可用来减少组件的不必要渲染,优化组件性能。 这边也可以看出,就算componentWillReceiveProps()
中执行了this.setState
,更新了state
,但在render
前 (如shouldComponentUpdate
,componentWillUpdate
),this.state
依然指向更新前的state
,不然nextState
及当前组件的this.state
的对比就一直是true了。 - componentWillUpdate(nextProps, nextState):此方法在调用
render
方法前执行,在这边可执行一些组件更新发生前的工作,一般较少用。 - render:
render
方法在上文讲过,这边只是重新调用。 - componentDidUpdate(prevProps, prevState):此方法在组件更新后被调用,可以操作组件更新的
DOM
,prevProps
和prevState
这两个参数指的是组件更新前的props
和state
在此阶段需要先明确下react组件更新机制。setState引起的state更新,或父组件重新render引起的props更新,更新后的state和props相比较之前的结果,无论是否有变化,都将引起子组件的重新render。详细了解可看=>这篇文章 造成组件更新有两类(三种)情况:
父组件重新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> } } 复制代码
- 在
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
了,不然就重复做一样的渲染了。
- 组件本身调用
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
函数之前的 所有函数(componentWillMount
,componentWillReceiveProps
,componentWillUpdate
)都被 getDerivedStateFromProp
s替代。
也就是用一个静态函数getDerivedStateFromProps
来取代被deprecate的几个生命周期函数,就是强制开发者在 render
之前只做无副作用的操作,而且能做的操作局限在根据props
和state
决定新的state
React v16.0刚推出的时候,是增加了一个componentDidCatch
生命周期函数,这只是一个增量式修改,完全不影 响原有生命周期函数;但是,到了React v16.3,大改动来了,引入了两个新的生命周期函数。
新的生命周期函数getDerivedStateFromProps和getSnapshotBeforeUpdate
- getDerivedStateFromProps:
getDerivedStateFromProps
本来(React v16.3中)是只在创建和更新(由父组件引发部分),也就是不是不由 父组件引发,那么getDerivedStateFromProps
也不会被调用,如自身setState
引发或者forceUpdate
引发。这样的话理解起来有点乱,在React v16.4中改正了这一点,让getDerivedStateFromProps
无论是Mounting
还是Updating
,也无论是因为什么引起的Updating
,全部都会被调用,具体可看React v16.4 的生命周期图。
static getDerivedStateFromProps(props, state)
在组件创建时和更新时的render
方法之前调用,它应该返回 一个对象来更新状态,或者返回null来不更新任何内容。
- getSnapshotBeforeUpdate:
getSnapshotBeforeUpdate()
被调用于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> ); } } 复制代码