40、React
什么是React?
- React 是一个用于构建用户界面的框架(采用的是MVC模式):集中处理VIEW这一层
- 核心专注于视图,目的实现组件化开发
什么是组件化开发?
我们可以很直观的将一个复杂的页面分割成若干个独立组件,每个组件包含自己的逻辑和样式 再将这些独立组件组合完成一个复杂的页面。 这样既减少了逻辑复杂度,又实现了代码的重用
- 可组合
- 可重用
- 可维护
安装react脚手架(跑环境)
在全局下安装create-react-app
打开命令行工具输入以下代码
npm install create-react-app -g
默认会自动安装React,react-dom两部分
使用react脚手架创建项目
需要在哪个文件夹下创建react项目就在那个项目下打开命令行工具输入以下代码
create-react-app xxx
(xxx为项目名称:不能出现大写字母)
cd xxx(转到项目目录下)
yarn start或npm start启动项目即可
react和react-dom模块
在js文件的顶部导入react和react-dom
react
:核心模块(生命周期等钩子函数都是在这里定义的)
react-dom
:提供与DOM相关的功能,会在window下增加ReactDOM属性,内部比较重要的方法是render,将react元素对象或者react组件插入到页面中
import React from 'react';//核心基础 模块(生命周期)
//用来把react元素(jsx元素)渲染成真实的DOM结构并添加到页面当中
import ReactDOM,{render} from 'react-dom';
//ReactDOM中就一个方法比较常用叫render
//可以把render从react-dom中解构出来
ES6中的模块导入和导出
//=>A模块
import Temp,{lib} from "./B";//=>把导入的Temp中的部分属性方法进行解构赋值
new Temp().init(); //new Temp生成一个实例,并且执行它原型上的init方法
lib();//=>Temp.lib()
//=>B模块
export default class Temp {
init() {
console.log(`hello world`);
}
static lib(){//=>Temp.lib
}
}
react中的jsx语法与render渲染方法
JSX语法
react构建页面有自己的语法:JSX语法;
jsx是一种JS和html混合的语法,HTML 语言直接写在 JavaScript 语言之中,不加任何引号,这就是 JSX 的语法,它允许 HTML 与 JavaScript 的混写;JSX最后会通过babel进行转义
jsx中的html部分和原生html基本一样(也有不一样的比如class要用className,label的for要用htmlFor),并且属性名要使用驼峰命名法;
用jsx语法写的内容叫做jsx元素(虚拟DOM元素);
jsx元素具有不变性 :只在页面初次加载的时候渲染一次,以后只能通过更改状态和属性来重新渲染;
jsx表达式的用法
- 1、可以放JS的执行结果:在react中想将js当作变量引入到jsx中需要使用{}
- 2.在jsx中,相邻的两个jsx元素 渲染时需要外面包裹一层元素(只能有一个根元素)
function build(str) {
return <h1>{str.name}</h1><h1>{str.name}</h1>;//在jsx语法中,这种写法是错误的
}
- 3、如果多个元素需要在return后面换行,我们需要加一个'()'当作一个整体返回 - 4、可以把JSX元素当作函数的返回值 - 5、用`<`和`{}`来区分是html还是js表达式:遇到 HTML 标签(以` <` 开头),就用 HTML 规则解析;遇到代码块(以` { `开头),就用 JavaScript 规则解析 - 6、循环绑定需要给元素设置唯一的KEY属性(属性值是唯一的) **`jsx属性的用法`** 在jsx中分为普通属性和特殊属性 - 1、普通属性:和html中的一样 - 2、特殊的属性如class,要使用className;for(label中的for)要使用htmlFor - 3、style要采用对象的方式 ```javascript
import React from 'react';
import ReactDOM,{render} from 'react-dom';
let str='<h1>纯标签</h1>';
let style={background:'red'};
render(<div>
<li className="aa"></li>
<li htmlFor="aa" style={style}></li>
<li dangerouslySetInnerHTML={{__html:str}}></li>
</div>,window.root);
- 4、dangerouslyInnerHTML插入html(基本上用不到)
React.createElement
:
- React.createElement用来创建一个react对象,具有一个type属性代表当前的节点类型,还有节点的属性props;
React.createElement("div",null,"姜,",React.createElement("span",null,"帅哥")) //结果如下 {type:'div',props:{className:"red",children:['姜',{type:'span',props:{id:'handsome',children:'帅哥'}}]}}
render()
:此方法是由react-dom
模块中提供的方法
第一个参数为要渲染的jsx元素
第二个参数为要把渲染的DOM结构插入到哪个容器当中
第三个参数为渲染完成后需要执行的回调函数;
- 用来将虚拟DOM元素(jsx元素)渲染成真实的DOM;
ReactDOM.render(<div className='app'>姜<span id='handsome'>帅哥</span></div>,window.root);
react中的虚拟DOM
我们在react中创建的组件和用jsx语法写出的标签等,都是虚拟的DOM元素:
组件并不是真实的 DOM 节点,而是存在于内存之中的一种数据结构,叫做虚拟 DOM (virtual DOM)。只有当它插入文档以后,才会变成真实的 DOM 。根据 React 的设计,所有的 DOM 变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实 DOM上,这种算法叫做 DOM diff ,它可以极大提高网页的性能表现。
虚拟DOM渲染过程(虚拟DOM—>真实DOM)
1、构建虚拟DOM元素(jsx元素):
- 在js中用jsx语法写的jsx元素就是虚拟DOM元素;(render中的元素)
render(<h1 className='red'>心有猛虎,细嗅蔷薇</h1>,window.root)
2、调用render方法把虚拟DOM元素渲染成真实的DOM结构
- 1)render方法会先用babel把jsx元素转化为ES5语法
'h1', {className: 'red'}, '心有猛虎,细嗅蔷薇'
- 2)再用React.createElement方法根据上面的结果创建出一个react对象
React.createElement('h1', {className: 'red'}, '心有猛虎,细嗅蔷薇')
结果如下
{type: "h1", props: {className: "red", children: "心有猛虎,细嗅蔷薇"}}
- 3)此时render方法将react对象转化为真正的DOM结构,渲染到页面上
render(<h1 className='red'>心有猛虎,细嗅蔷薇</h1>,window.root)
自己模拟React.createElement方法与render方法:(render不完善)
function createElement(type,props,...children) {
if(children.length === 1) children = children[0];
return {type,props:{...props,children:children}}
}
let myRender = (obj, container,callback) => {
let {type, props} = obj;
let ele = document.createElement(type);//创建第一层
for (let key in props) {
if (key === 'children') {
//children可能是数组也可能是一个字符串(纯文本)
if ({}.toString.call(props[key]).slice(8,-1)==='Array') {
props[key].forEach(item => {
if (typeof item === 'object') {//如果子元素是对象,那就继续调用render
myRender(item, ele);
} else {
ele.appendChild(document.createTextNode(item));
}
})
} else {//字符串的话直接创建文本节点并插入到元素中
ele.appendChild(document.createTextNode(props[key]));
}
} else if (key === 'className') {//className要做单独处理
ele.setAttribute('class', props[key]);
} else {
ele.setAttribute(key, props[key]);//其他的直接设置(style等属性未做处理)
}
}
container.appendChild(ele);//把DOM元素插入到html中
callback && callback();//=>添加成功后触发回调函数执行
};
react中的组件
react元素是是组件组成的基本单位
组件有两种声明方式1、第一种方式是函数声明
每一个创建的组件都是一个自定义的标签,在标签上设置的属性都会转换为react对象的props属性,当react渲染DOM的时候会把props传递给下面例子中的Build函数
import React from 'react';
import ReactDOM,{render} from 'react-dom';
let school1={name:'zfpx',age:10};
let school2={name:'珠峰',age:8};
let Build=function (props) {// "函数"(组件)的参数是属性
return <p>{props&&props.name}{props&&props.age}</p>;
};
render(<div>
<Build name={school1.name} age={school1.age}/>
<Build {...school2}/>//利用解构赋值将对象中的内容解构出来传递给Build组件
<h1></h1>
</div>,window.root);
###2、第二种是以类的形式声明
import React,{Component} from 'react';
import ReactDOM from 'react-dom';
let school1 = {name:'珠峰',age:8};
let school2 = {name:'珠峰',age:0};
//1.类声明的组件有this this上有props属性
class Build extends Component{//Component这个基类上有this.setState
render(){ // 这个组件在调用时默认会调用render方法(此render相当于注册组件)
let {name,age} = this.props;
return <p>{name} {age}</p>
// return <p>{this.props.name} {this.props.age}</p>
}
}
ReactDOM.render(
此render的渲染步骤
1)先渲染外层div,将jsx元素转化为ES5的语法,然后调用React.createElement方法创建react对象,当遇到div下的子元素的时候,会进行以下操作
//1、发现Build 是一个继承Component的类,它会创建这个类的一个实例,并且执行原型上的render方法
//2、执行原型上的render返回JSX元素,然后继续执行外层的render
2)继续将上面返回的jsx元素转化为es5语法,调用React.createElement方法创建虚拟DOM元素对象,Build 这个位置被返回的元素对象替换
3)外层ReactDOM.render将上面生成的react对象渲染成真实的DOM结构,并追加到页面中;
<div>
<Build name={school1.name} age={school1.age}/>
<Build {...school2} />
</div>,window.root);
区别:类声明有状态,this,和生命周期
声明组件的规则:
- 1、首字母必须大写,目的是为了和JSX元素进行区分
- 2、组件定义后可以像JSX元素一样进行使用
- 3、可以通过render方法将组件渲染成真实DOM(做了优化 只会重新渲染改变的地方)
单闭合标签不能添加子节点;双闭合标签可以
组件中属性(props)和状态(state)的区别
属性和状态的变化都会影响视图更新,但是两者导致视图刷新的机制是不一样的;
- 1、确切的说不是因为props更新了导致的视图刷新,而是传递属性的组件(父组件)重新的渲染了,从而导致接受属性的组件(子组件)重新渲染,这样属性就也随着更新了;
- 2、状态导致的视图刷新是只要通过this.setState改变状态,视图就会刷新;
组件的数据来源有两个地方:
1、通过props(属性) 获取数据:
外界传递进来的(存在默认属性和属性校验),是只读的,不能修改;
有属性校验模块:prop-types(用来校验属性的类型和是否必须设置某属性)
用yarn add prop-types来安装。用来校验属性的
import React,{Component} from 'react';
import ReactDom,{render} from 'react-dom';
import PropTypes from 'prop-types';
// 1、默认的属性必须写在defaultProps中(不能改名字)
// 2、校验器的名字必须是propTypes(不能改名字);
class School extends Component{//类上的属性(把类当作对象添加的属性)就叫做静态属性
static defaultProps={//会先默认调用defaultProps
name:'珠峰',
age:1
};
static propTypes={//校验属性的值(number)类型和是否必须设置该属性(isRequired)
age:PropTypes.number.isRequired
};
constructor(props){
//如果想在constructor中使用props,则只能传递参数props,在constructor外面可以直接用this.props
//不能在组件中更改属性
super();
console.log(this.props)//undefined
}
render(){//渲染组件的,返回一个组件
//如果没有默认的defaultProps,也没有传递props,则this.props是一个空对象
return <h1>{this.props.name} {this.props.age}</h1>
}
}
//name={'珠峰培训'}:这样写大括号中就可以是变量
render(<School name={'珠峰培训'} age={9}/>,window.root);
####2、this.state(状态) 状态是自己的: 是可读写的(写了状态就必须给默认值); 我们可以通过this.setState({})方法来修改状态,由于修改状态会引起视图刷新(即更新DOM),而有关DOM的操作都是异步的,所以this.setState({})是异步的; ```javascript
import React,{Component} from 'react';
import ReactDom,{render} from 'react-dom';
//1、state变化可以更新视图,只能通过this.setState({})来更改状态,调用后会更新视图
class Counter extends Component{
constructor(){
super();
console.log(this.state);//默认为undefined
this.state={count:0}
}
add=(val)=>{ //val后面省略了一个参数e(事件对象),是onClick事件默认传进来的,放在val的后面,因为bind的传参机制
//setState方法会默认按最后的设置进行执行
// this.setState({count:this.state.count+val});
this.setState({count:this.state.count+val});//会执行这个
//setState有两种写法,一种是传入对象的方式,一种是传入函数的方式
//下一个状态是依赖于上一个状态时,需要写成函数的方式(回调函数)
this.setState(prevState=>({count:prevState.count+val}));//如果返回的就是一个对象,则需要用'()'包裹.如果不用小括号包裹,则需要加return。因为函数如果不加return或者return后什么也不写,函数的返回值则为undefined
}); //等同于下面这种写法
//this.setState({count:this.state.count+val},function () {
// this.setState({count:this.state.count+val});
//};
render(){
return <p>
{this.state.count}
/因为add方法是在类里面声明的,属于ES7语法,所以 add方法中的this是指向当前实例的,所以bind不需要改变this指向/
<button onClick={this.add.bind(null,2)}>+</button>
</p>
}
}
render(<Counter/>,window.root);
复合组件与数据传递
复合组件:将多个组件进行组合嵌套;
复合组件之间的数据传递:1、<font color=red>父传子</font>:父组件传递给子组件数据通过设置属性的方式
import React,{Component} from 'react';
import ReactDom,{render} from 'react-dom';
import PropTypes from 'prop-types';
import 'bootstrap/dist/css/bootstrap.css';
//1、什么是复合组件,将多个组件进行组合
//2、结构非常复杂时可以把组件分离开
//3、复合组件 有父子关系,父的数据传递给子(通过给子组件设置属性)
// Panel组件 Heading Body
class Counter extends Component{
render(){
//由于在Counter中传递了属性,所以这里可以用this.props获取到;
let {header:header,body:body}=this.props;
return (
<div className='container'>
<div className='panel-default panel'>
//这里写了之后就可以在下面的子组件中拿到对应的props值
<Header header={header}></Header>
<Body body={body}></Body>
</div>
</div>
)
}
}
//react中需要把属性一层一层向下传递:单向数据流;
class Header extends Component{
render(){
return (
<div className='panel-heading'>
//上面传递了之后就可以直接在此处使用this.props属性了;
{this.props.header};
</div>
)
}
}
class Body extends Component{
render(){
return (
<div className={'panel-body'}>
{this.props.body}
</div>
)
}
}
let data={header:'我很帅',body:'长的英俊'};
render(<Counter {...data}/>,window.root);
####2、**<font color=red>子传父</font>:**通过父亲传递给儿子一个函数,儿子调用父亲的函数将要改的值传递给父亲,父亲更新值,刷新视图; ```javascript
import React, {Component} from 'react';
import ReactDom, {render} from 'react-dom';
import 'bootstrap/dist/css/bootstrap.css';
//1、子传父 通过父亲传递给儿子一个函数,儿子调用父亲的函数将要修改的值传递给父亲,父亲更新值,刷新视图;
class Counter extends Component {
constructor() {
super();
this.state = {color: 'primary'};
}
changeColor = (color) => {
this.setState({color});
};
render() {
let {header} = this.props;
return (
<div className='container'>
<div className={'panel panel-' + this.state.color}>
<Header header={header} color={this.changeColor}></Header>
</div>
</div>
)
}
}
//react中需要把属性一层一层向下传递:单向数据流;
class Header extends Component {
clickColor = () => {
this.props.color('danger');//儿子调用父亲的方法,把要改的值传递给父亲,让父亲自己修改
};
render() {
return (
<div className='panel-heading'>
{this.props.header}
<button className={btn btn-danger
} onClick={this.clickColor}>改颜色</button>
</div>
)
}
}
let data = {header: '我很帅'};
render(<Counter {...data}/>, window.root);
ref属性
ref可以写在组件上也可以写在DOM元素上,获取的时候得到两者的结果是不同的;
- 写在组件上时(这里的组件是有状态的组件),通过refs获取的是组件的实例;
- 写在DOM元素上时,通过refs获取的是具体的DOM元素节点.(获取的是真实DOM)
ref的值可写为两种形式
1、直接写为一个值
通过ref设置的属性:可以通过this.refs.xxx获取到对应ref值为xxx的dom元素或者组件的实例
2、写成一个函数的形式
写成函数时,这个函数将会在渲染成真实DOM结构或组件被挂载后执行,函数的参数为真实的DOM结构或组件的实例
//如上面非受控组件中的例子
//1、直接将ref写为一个值
<input type="text" ref='a'/>
//2、将ref写为一个函数
<input type="text" ref={(x)=>this. c=x}/>
{/* x 代表真实的DOM(写成函数的形式会默认传入当前设置ref的DOM结构 ),上面这种写法相当于把DOM结构挂载到了this的自定义属性c上,以后可以直接用this.c拿到这个input的DOM结构*/}
resultChange=()=>{//通过ref设置的属性:可以通过this.refs.xxx获取到对应ref值为xxx的dom元素
this.c.value=parseFloat(this.refs.a.value)+parseFloat(this.refs.b.value);
};
受控组件与非受控组件(受状态控制)
组件是否与this.state有关系区分为受控与非受控组件,受控组件可以赋予默认的值
受控组件
需要与onInput/onChange/disabed/readOnly等控制value/checked的属性或事件一起使用
import React, {Component} from 'react';
import ReactDom, {render} from 'react-dom';
class Input extends Component {
//1、受状态控制的组件,必须要有onChange方法,否则不能使用;
//2、受控组件可以赋予默认值(官方推荐使用受控组件)
constructor() {
super();
this.state = {val: '', a: 1, b: 2};//初始化状态:设置默认值
}
inputChange = (val, e) => {//处理多个输入框的值映射到this.state的方法
//val:要修改的state中的属性的名称
//e:事件源(e.target.value是要修改成的值)
this.setState({[val]: parseFloat(e.target.value)||0});//更改状态
};
render() {
return (
<div>
//下面两个input都受this.state影响(受控组件)
<input type="text" value={this.state.a} onChange={this.inputChange.bind(null, 'a')}/>
<input type="text" value={this.state.b} onChange={e =>{
//方法执行的时候要用到事件对象e,所以必须要传
this.inputChange('b', e)
}}/>
{this.state.a + this.state.b}
</div>
)
}
}
render(<Input/>, window.root);
####非受控组件(基于ref来管理) ```javascript
import React,{Component} from 'react';
import ReactDom,{render} from 'react-dom';
class Input extends Component{
//输入框value值不受状态控制,不能初始化默认值
resultChange=()=>{//通过ref设置的属性:可以通过this.refs.xxx获取到对应ref值为xxx的dom元素
this.c.value=parseFloat(this.refs.a.value)+parseFloat(this.refs.b.value);
};
render(){
return (
<div onChange={this.resultChange}>
<input type="text" ref='a'/>
<input type="text" ref='b'/>
{/x代表真实的DOM(写成函数的形式会默认传入当前设置ref的DOM结构),下面这种写法相当于把元素挂载到了this的自定义属性c上/}
<input type="text" ref={(x)=>this.c=x}/>
</div>
)
}
}
render(<Input/>,document.getElementById('root'));
react元素中的事件绑定
给react元素(jsx元素)事件绑定对应的方法时,方法如果不做处理,那么执行的时候方法中的this默认是undefined
可通过如下方式解决:
1、使用bind方式;(绑定的时候处理)
2、使用ES6的箭头函数(绑定的时候处理:在外层套一层箭头函数)
3、使用ES7的箭头函数(在类中定义的时候处理:直接写为xxx=()=>{} )
注
:
Class中声明的方法this默认不绑定到实例上,需要手动绑定.ES6语法规范;
在Class中利用箭头函数声明的方法,被规定为ES7语法,方法中的this都是当前实例。
React中的生命周期
在react操作过程中,设定了很多的执行阶段,每个阶段提供了不同方法,
[图片上传失败...(image-a06d22-1541658838762)][初期渲染]
defaultProps:
- 默认属性设置与校验
constructor:
- 获取属性和设置默认初始状态
componentWillMount:
(只执行一次)
- constructor执行完成后,DOM结构开始加载之前,执行此方法,
render
(开始渲染)
componentDidMount
(只执行一次)
- DOM结构加载完成后会自动触发此函数
[状态发生改变时触发]
shouldComponentUpdate
- 状态更新时会触发此方法,返回布尔值
componentWillUpdate
- 如果shouldComponentUpdate返回true则执行此函数,否则不执行
render
(开始渲染)
componentDidUpdate
- 组件完成更新后触发此方法
[卸载组件时触发]
ReactDOM.unmountComponentAtNode
- 删除某个组件
componentWillUnmount
(组件将要卸载)
- 组件移除时会调用此方法,一般在这个方法中我们要清除定时器
[属性发生改变时触发]
componentWillReceiveProps
import React, {Component, PureComponent} from 'react';
import ReactDom, {render} from 'react-dom';
class Counter extends Component {/PureComponent是纯组件:比较的是状态的地址,如果是同一个地址,则不会更新,所以状态最好采用新的状态替换掉老的/
static defaultProps = {
name: 'zfpx'
};
constructor(props) {
super();
this.state = {number: 0};
console.log('1、constructor');
}
componentWillMount() {//同步代码放在此处最好:如获取本地的数据:在渲染之前获取数据,只渲染一次
console.log('2、父组件将要加载');
}
componentDidMount() {
this.setState({number: this.state.number + 1});
console.log('5、父组件已经挂载完成');
}
click = (e) => {
this.setState({number: this.state.number + 1});
};
//react可以在shouldComponentUpdate方法中优化 PureComponent 可以帮我们做这件事
shouldComponentUpdate(nextProps, nextState) {//分别代表下一次的属性和下一次的状态
//组件是否需要更新
console.log('父组件是否需要更新');
return nextState.number % 2;//如果此函数返回了false,就不会调用render方法了
}
//乱用this.setState({})会造成栈溢出
componentWillUpdate() {
console.log('父组件将要更新');
}
componentDidUpdate() {
console.log('父组件完成更新');
}
render() {
console.log(3、父组件render(渲染)
);
return (
<div>
<p>{this.state.number}</p>
<ChildCounter n={this.state.number}/>
<button onClick={this.click}>+</button>
</div>
)
}
}
class ChildCounter extends Component {
componentWillReceiveProps(newProps) {//第一次不会执行,之后属性更新时才会执行
/父组件渲染完成后发现有状态更新,执行componentWillReceiveProps,接着执行shouldComponentUpdate,然后子组件渲染/
/componentWillReceiveProps只写在子组件中/
console.log('子有新的属性');
}
render() {
console.log('4、child render');
return (
<div>
{this.props.n}
</div>
)
}
shouldComponentUpdate(nextProps, nextState) {
console.log('子组件是否需要更新');
return nextProps.n % 3;
}
}
render(<Counter name={'计数器'}/>, window.root);
###<font color=red>**注意**</font>: **最好不要在任何一个状态更新时触发的方法中写**`this.setState()`,如果不加条件控制的话,会再次触发状态更新而导致方法执行,这样就会造成栈溢出; ```javascript
/页面初始化时触发/
//defaultProps:
//constructor
//componentWillMount(将要挂载)页面初次加载时执行1次
//render
//componentDidMount(完成挂载)页面初次加载完成时执行1次
/状态更新时会触发/
//shouldComponentUpdate
//componentWillUpdate
//render
//componentDidUpdat
/属性更新/
//componentWillReceiveProps
/卸载/
// componentWillUnmount
获取组件的所有子节点
在函数式声明的组件中我们用props.children来获取组件的所有子节点;
在以类声明的组件中我们用this.props.children来获取组件的所有子节点;
render(<Dinner>