第五课之初步认识react
课程回顾
开发环境和生产环境
开发环境和生产环境有很多通用的代码,可以将通用的代码提取出来,例如loaders,output等,所以可以形成三个文件,webpack.prod.config.js、webpack.dev.config.js、webpack.common.config.js,分离出来如何将他们分别合并,这时候就需要使用到webpack-merge,进行配置合并
课程内容
- 1.课程回顾
- 2.react介绍
- 3.react安装
- 4.生命周期、事件、数据流讲解
- 5.父子组件传值
1.什么是React
React是Facebook推出的一个JavaScript库,它的口号就是“用来创建用户界面的JavaScript库”,所以它只是和用户界面打交道,可以把它看成MVC中的V(视图)层。
为什么使用react
- 虚拟dom效率性能高,不需要操作dom(频繁操作dom很消耗性能)
- 使用组件化开发方式,高复用、维护容易,逻辑清晰
- 技术成熟,社区完善,插件齐全,适用于大型Web项目(生态系统健全),由Facebook专门的团队维护,技术支持可靠,是现在最主流的技术之一
- 全局单向数据流,数据流向清晰,大型项目不在话下
组件化
- 每一个ReactJS文件都是一个组件,含视图、逻辑操作、数据
- 复用、封装性强
单向数据流
- 子组件对于父组件传递过来的数据是【只读】的,不能直接更新父组件数据、这样组件更加简单易把握、只会在父组件修改数据,追查问题的时候可以跟快捷
- 顶层组件某个props改变会递归遍历整棵组件树,重新渲染使用这个属性的插件
虚拟dom
- React的设计中,开发者基本上无需操纵实际的DOM节点,每个React组件都是用Virtual DOM渲染的。
- 用 JavaScript 对象结构表示 DOM 树的结构,然后用这个树构建一个真正的 DOM 树
- 新的虚拟 DOM 与原来的虚拟 DOM进行比对时,它会进行同层比较,即相同的节点层进行比较,如果不同则直接将原始虚拟 DOM 中该节点层及以下的节点全部删除,重新生成新的虚拟 DOM 节点,而不会继续向下比对
jsx
- JSX 是 JavaScript 语言的一种语法扩展,长得像 HTML,但并不是 HTML。
- React.js 可以用 JSX 来描述你的组件长什么样的。
- JSX 在编译的时候会变成相应的 JavaScript 对象描述。
- react-dom 负责把这个用来描述 UI 信息的 JavaScript 对象变成 DOM 元素,并且渲染到页面上。
// 第一步:jsx代码
<button className="btn btn-blue">
<em>Confirm</em>
</button>
// 第二步解析成对象
{
type: 'button',
props: {
className: 'btn btn-blue',
children: {
type: 'em',
props: {
children: 'Confirm'
}
}
}
}
// 第三步:根据对象创建标签
// 参数1:元素名称
// 参数2:元素属性对象(null表示无)
// 参数3:当前元素的子元素string||createElement() 的返回值
React.createElement("button", {
className: "btn btn-blue"
},React.createElement("em", null, "Confirm"))
// 第四步:渲染
ReactDOM.render(dom, document.getElementById('app'))
key
-
例如:在遍历数据时,推荐在组件中使用 key 属性
-
循环需要加key:你在 JSX 模板中遍历 state 中某个数据时,为什么不加 key 值浏览器会报警告,这是因为你不再遍历的每条数据加上 key 值,更改 state 中那条数据的值,生成虚拟 DOM 后,React 就不知道原始遍历的数据和这次更新后遍历的数据一一对应的关系,就会再次重新渲染,而加上 key 值,它则能迅速比对出有差异的部分进行部分的更新。
-
为什么不建议用 index 作为 key 值:因为当你插入、 删除中间的数据时,从改变的那个数据开始,后续每个数据的 index 值就会变,从而就导致了每个数据的 key 值相应变化了,这样依旧会引起大规模渲染,这就是其中的原因
-
流程图
虚拟dom、jsx
- 从 JSX 到页面到底经过了什么样的过程:
2.react安装
// 第一步:安装react react-dom,以及jsx编译
npm i -D react react-dom @babel/preset-react
// 第二步:.babelrc添加
"presets": [
"@babel/env",
"@babel/react"
],
// 第三步:添加react.js
// 1. 导入 react
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
// 由于react实现机制,组件名称必须要大写
class MyComponet extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
// 此处注释的写法
<button className="btn btn-blue">
<em>Confirm</em>
</button>
)
}
}
// 第四步:渲染
// 参数1:虚拟dom对象 参数2:dom对象表示渲染到哪个元素内 参数3:回调函数
ReactDOM.render(<MyComponet />, document.getElementById('root'))
// 第五步:修改入口文件
entry: './src/js/react.js', //指定打包的入口文件
- 组件名称命名用大驼峰
- 如果在 JSX 中给元素添加类, 需要使用 className 代替 class
- 在 JSX 中可以直接使用 JS代码,直接在 JSX 中通过 {} 中间写 JS代码即可
生命周期
- 挂载卸载过程
- constructor():React数据的初始化,它接受两个参数:props和context,当想在函数内部使用这两个参数时,需使用super()传入这两个参数。
- componentWillMount():它代表的过程是组件已经经历了constructor()初始化数据后,但是还未渲染DOM时
- componentDidMount():组件第一次渲染完成,此时dom节点已经生成,可以在这里调用ajax请求,返回数据setState后组件会重新渲染
- componentWillUnmount ():在此处完成组件的卸载和数据的销毁。(移除计时器)
- 更新过程
- componentWillReceiveProps (nextProps):接受一个参数nextProps,通过对比nextProps和this.props是否改变,判断要做的操作,只要父组件改变都会造成子组件componentWillReceiveProps接收,也会增加组件的重绘次数,浪费性能,写法不好容易死循环
- shouldComponentUpdate(nextProps,nextState):主要用于性能优化(阻止组件渲染),因为react父组件的重新渲染会导致其所有子组件的重新渲染,这个时候其实我们是不需要所有子组件都跟着重新渲染的,因此需要在子组件的该生命周期中做判断
- componentWillUpdate (nextProps,nextState):shouldComponentUpdate返回true以后,组件进入重新渲染的流程,进入componentWillUpdate,这里同样可以拿到nextProps和nextState。
- componentDidUpdate(prevProps,prevState):react只会在第一次初始化成功会进入componentDidmount,之后每次重新渲染后都会进入这个生命周期,这里可以拿到prevProps和prevState,即更新前的props和state
- render():render函数会插入jsx生成的dom结构,react会生成一份虚拟dom树,在每一次组件更新时,在此react会通过其diff算法比较更新前后的新旧DOM树,比较以后,找到最小的有差异的DOM节点,并重新渲染
- React新增的生命周期
- getDerivedStateFromProps(nextProps, prevState):代替componentWillReceiveProps(),获取props并修改state
- getSnapshotBeforeUpdate(prevProps, prevState):一般的用法就是获取更新前的DOM
- React17版本废弃的生命周期
- componentWillMount
- componentWillReceiveProps(可以用getDerivedStateFromProps和componentDidUpdate搭配使用替代)
- componentWillUpdate
修改后的生命周期
// 之前
componentWillReceiveProps(nextProps) {
if (nextProps.isLogin !== this.props.isLogin) {
this.setState({
isLogin: nextProps.isLogin,
});
this.handleClose();
}
}
// 现在
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.isLogin !== prevState.isLogin) {
return {
isLogin: nextProps.isLogin,
};
}
return null;
}
componentDidUpdate(prevProps, prevState) {
if (prevState.isLogin !== this.state.isLogin) {
this.handleClose();
}
}
使用方式
class MyComponet extends Component {
constructor(props) {
super(props);
this.state = {};
}
// 日常使用的生命周期
// 新增的生命周期
static getDerivedStateFromProps(nextProps, prevState) {
console.log('判断前后两个 props 是否相同------getDerivedStateFromProps')
return null;
}
componentDidUpdate() {
console.log('数据已经更新---------componentDidUpdate')
}
// 组件第一次渲染完成,此时dom节点已经生成,可以在这里调用ajax请求,返回数据setState后组件会重新渲染
componentDidMount() {
console.log('组件加载后--------componentDidMount')
}
// 即将废弃
// componentWillReceiveProps(nextProps) {
// console.log('接收props--------componentWillReceiveProps')
// }
// 性能优化
// 你可以通过这个方法控制组件是否重新渲染。如果返回 false 组件就不会重新渲染。
shouldComponentUpdate(nextProps, nextState){
// 值没有发生过变化,不需要重新触发render
if(nextState.Number == this.state.Number){
return false
}
}
render() {
return (
// 此处注释的写法
<button className="btn btn-blue">
<em>Confirm</em>
</button>
)
}
}
render(不要在render里setState)
- 一个组件类必须要实现一个 render 方法。
- 这个 render 方法必须要返回一个 JSX 元素。
- 必须要用一个外层的 JSX 元素把所有内容包裹起来。
// 错误
render () {
return (
<div>第一个</div>
<div>第二个</div>
)
}
// 正确
render () {
return (
<div>
<div>第一个</div>
<div>第二个</div>
</div>
)
}
react事件
// 绑定事件,this指向会改变所以我们通过代码控制不改变this的指向
// 写法一:构造函数里绑定事件,效率高
constructor(props) {
super(props);
this.state = {};
this.showInfo = this.showInfo.bind(this);
}
showInfo() {
console.log(2312321)
}
<button className="btn btn-blue" onClick={this.showInfo}>
<em>Confirm</em>
</button>
// 写法二:使用bind this
showInfo() {
console.log(2312321)
}
<button className="btn btn-blue" onClick={this.showInfo.bind(this)}>
<em>Confirm</em>
</button>
// 写法三:方法使用箭头函数,this指向不会改变,简单易用,效率高,建议使用
showInfo=()=> {
console.log(2312321)
}
<button className="btn btn-blue" onClick={this.showInfo}>
<em>Confirm</em>
</button>
// 写法四:在标签上调用函数,每次渲染会调用,不建议使用
<button className="btn btn-blue" onClick={()=>this.showInfo()}>
<em>Confirm</em>
</button>
数据讲解
1.state:内部定义,它只是用来控制这个组件本身自己的状态,页面渲染通过setState进行完成。
2.setState:
- 当我们调用这个函数的时候,React.js 会更新组件的状态 state ,并且重新调用 render 方法,然后再把 render 方法所渲染的最新的内容显示到页面上
- React.js为了批次与效能并不会马上修改state。而是把这个对象放到一个更新队列里面,稍后才会从队列当中把新的状态提取出来合并到 state 当中,然后再触发组件更新。
// 想要即时获得改变后的state
// 方案一:使用回调函数
this.setState({ val: this.state.val + 1 }, () => {
console.log(this.state.val);
})
// 方案二:setTimeout是异步方法,react无法直到开发者异步方法中想要渲染的顺序,所以在异步方法中调用setTimeout,react会即时渲染,不会使用批量更新
setTimeout(_ => {
this.setState({
num: ++num
})
console.log(this.state.num);
}, 0)
3.props:外部传入,包括父子组件之间的通信,全局数据流的传递
- 父子组件之间的通信,子组件无法直接修改父组件的props,通过this.props接收
- 全局数据流的传递(dva)
父子组件传值
- 父组件使用子组件并添加属性进行传值
- 子组件接收属性或者方法
- 实现父组件state的使用,以及子组件props的使用
案例
// 第一步:添加一个子组件ChildCom.js,组件首字母大写
import React, { Component } from 'react'
export default class ChildCom extends Component {
constructor(props) {
super(props);
this.state = {
};
}
clickBtn = () => {
// 使用父组件的方法
this.props.addChildNum();
}
render() {
// 使用父组件的值
const { childNum } = this.props;
return (
<div>
<div>子组件值:{childNum}</div>
<button onClick={this.clickBtn}>子组件点击调用父组件方法</button>
</div>
)
}
}
// 第二步父组件添加
// 1. 导入 react
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
// 导入子组件
import ChildCom from './ChildCom';
// 2. 创建 虚拟DOM
// 参数1:元素名称 参数2:元素属性对象(null表示无) 参数3:当前元素的子元素string||createElement() 的返回值
class MyComponet extends Component {
constructor(props) {
super(props);
this.state = {
num: 1,
childNum: 1,
};
}
...
addNum = () => {
let { num } = this.state;
this.setState({
num: ++num
})
}
addChildNum = () => {
let { childNum } = this.state;
this.setState({
childNum: ++childNum
})
}
render() {
const { num, childNum } = this.state;
return (
<div>
<div>父组件值:{num}</div>
<button className="btn btn-blue" onClick={this.addNum}>
<em>Confirm</em>
</button>
<ChildCom childNum={childNum} addChildNum={this.addChildNum} />
</div>
)
}
}
ref
不借助插件的情况下,react中如何获取获取dom节点
方式一
使用原生js,需要在componentDidMount(组件渲染完成后获取)
// 原生js获取获取dom节点
document.getElementById('');
~
~
~
方式二
react提供的一个特殊的属性ref,可以绑定到render()输出的任何组件上,这个特殊的属性允许你引用 render() 返回的相应的dom节点
// 第一步:添加ref属性
<input value={num} ref="myIpt" readOnly />
// 第二步:获取节点
getDom = () => {
var dom = this.refs.myIpt;
dom.style.background='red';
dom.focus();
}
父组件如何获取子组件的数据(props,state,event)
// 第一步:父组件使用并子组件添加onRef={(child) => this.child = child}(可以取别的名字)
// 作用是设置当前父组件的this.child等于返回的对象
<ChildCom childNum={childNum} addChildNum={this.addChildNum} onRef={(child) => this.child = child} />
// 第二步:子组件ChildCom添加
componentDidMount() {
// 将child传递给this.props.onRef()方法
this.props.onRef && this.props.onRef(this);
}
// 第三步父组件使用
console.log(this.child); // ChildCom {props: {…}, context: {…}, refs: {…}, updater: {…}, clickBtn: ƒ, …}
- 百分之99的情况在不使用ref的情况下,react都能进行完成,频繁的dom操作是比较消耗性能的,所以在任何不需要ref去操作页面数据的的情况下,都不要使用ref去操作dom