在阅读之前,会有一些小伙伴觉得疑惑,作者怎么写前端文章了呢,作者不是专注后端Java吗?这是怎么了?其实不然,那个时候还没有流行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
D:\BaiduNetdiskDownload>create-react-app test_demo Creating a new React app in D:\BaiduNetdiskDownload\test_demo. Installing packages. This might take a couple of minutes. Installing react, react-dom, and react-scripts.. ]\fetchMetadata: sill resolueWithNewModule schema-utilsC2050checlinGnst.
这个时候就开始创建项目了,时间有点长,因为正在下载需要的插件和依赖包,完成后项目结构如下:
├── 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'))
Hello React
属性传递
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'))
Hello React
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'))
Hello Raect,I am 4 years old
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
的话,就会依次执行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); this.state={serviceName: jpsite} } }
组件的挂载阶段(Mounting)
此阶段分为componentWillMount
,render
,componentDidMount
三个时期。
- componentWillMount:在组件挂载到
DOM
前调用,且只会被调用一次,在这里面调用this.setState
不会引起组件的重新渲染,也可以把写在这里面的内容改写到constructor()
中,所以在项目中很少这么使用。 - render:根据组件的
props
和state
(无论两者是重传递或重赋值,无论值是否有变化,都可以引起组件重新render
) ,内部return
一个React
元素(描述组件,即UI),该元素不负责组件的实际渲染工作,之后由React
自身根据此元素去渲染出页面DOM。render
是纯函数(Pure function:函数的返回结果只依赖于它的参数;函数执行过程里面没有副作用)
,不能在render()
里面执行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
)都可以被getDerivedStateFromProps
替代。
也就是用一个静态函数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> ); } }