大家一起来
React是一个用于构建用户界面的JS库,核心专注于视图,目的实现组件化开发
所谓组件化开发,其实就像堆积木一样,每个组件都包含了自己的逻辑和样式,然后再组合到一起完成一个复杂的页面
组件化的主要特点就是:可组合、可复用、可维护
那么废话不多说,让我们直接进入今天的主题,运用官方推荐的脚手架来搭建一个react项目开始学习吧
create-react-app启动react项目
第一步:全局安装create-react-app脚手架
npm i create-react-app -g
第二步:创建react项目
create-react-app 项目名
// 如:create-react-app react123
通过以上操作就会自动创建一个名为react123的react项目了,在创建的过程中,脚手架会自动为你安好react的核心包,react和react-dom,在此过程完成后即可进入第三步了
第三步:进入项目并启动服务
cd react123 && npm start
通过上面的三步曲,就会自动弹出浏览器访问一个localhost:3000(默认为3000端口)的react页面了
现在,让我们通过进入创建好的react项目中,先去看一下搭建好的结构是什么样的?
下面就为大家展示一番:
根据上图画圈所示,public目录下的index.html表示的是项目的主静态文件(包含依赖的节点),打开后发现一大堆内容,其实都可以删掉,只需要留一个root即可<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<title>React App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
说完public后,再说一下src目录,这里面的文件也都可以删掉(除了index.js主入口文件),这里就和webpack4里默认的入口文件比较类似了
先来了解下jsx
在src下的index.js中,我们就可以开启React之旅了,React主要用的是Facebook自己开发的jsx语法,这是一种js和xml的集合体,直接上代码
// 首先引入react的核心包
// 这里的命名必须叫React,如果不是的话会报错
import React from 'react';
// 这里主要用到render方法,所以直接将其解构出来使用
import { render } from 'react-dom';
// jsx语法通过babel来转义,通过预设的babel-preset-react来进行转义
// 将jsx语法解析成js来使用
let ele = (
<h1 className="big">
hi, <span>baby</span>
</h1>
);
我们可以将写好的jsx语法直接放到babel官网来解析一下
// 解析后的代码如下
React.createElement(
"h1",
{ className: "big" },
"hi, ",
React.createElement(
"span",
null,
"baby"
)
);
通过React.createElement来创建一个虚拟DOM,其中包含三个参数,分别是type、props和children
- type: 对应的标签h1
- props: 对应的属性{ className: "big" }
- children: 对应的子节点 -> 字符串'hi, ' 和一个新的React.createElement( "span", null, "baby" )
其实按照React.createElement创建的对象来看,打印出来后的结构是这样滴
根据上面打印的结构来看,其实它们都是react.element上的实例,那么就来针对打印后的对象进行一个主要的抽离,来看看是何方神圣吧{
type: 'h1',
props: {
children: [
'hi',
{
type: 'span',
props: {
children: 'baby'
}
}
],
className: 'big'
}
}
在我们转化完对象后,就可以通过ReactDOM提供的render方法去渲染这样的一个对象,最后将其渲染到页面中
上面写好的ele代码块,其实就是React.createElement的语法糖,毕竟每次写那么一大坨React.createElement的话,十有八九会写出错的(我觉得可能是100%会出错,哈哈)
所以我们看到这里可以简单的梳理一下整个渲染的执行顺序了
jsx语法 -> createElement格式 -> 转化成对象 -> 对象转换成真实dom - > render方法渲染
import React from 'react';
import { render } from 'react-dom';
// React.createElement的语法糖
let ele = (
<h1 className="big">
hi, <span>baby</span>
</h1>
);
render(ele, window.root); // 将ele渲染到root上
下图是将虚拟dom渲染到root节点下的情况
上面写的代码不多,主要就是简单写了个jsx的语法,然后通过render进行了渲染而已。在写ele的时候,我们发现和正常的html还是很相像的,但是我在这里要说一下, 其实它们不一样,接下来就看看有哪些地方不一样吧!
jsx和html写法的区别
- className 它会转化成 class
- htmlFor 它要转化成for属性 label for
- jsx元素可以嵌套
- 相邻的react元素,必须要加一层包裹起来
- jsx里面可以写js,{}里面可以写js语句
- 只支持多行注释,{/* ... */},不过很少写注释
- style标签必须写成一个对象,如:{ background: 'skyblue' }
说了这几点区别,直接开撸,没有代码更让人直观的喜爱了,hurry up go go go
import React from 'react';
import { render } from 'react-dom';
// 提示:下面注释标记的数字对应上面区别的序号,序号5和6就不写了
let singer = '周杰伦';
let style = { backgroundColor: '#0cc', color: '#fff', fontSize: '14px' };
let album = '跨时代';
let arr = ['晴天', '阴天', '下雨天'];
let ele = (
/* 4.相邻的react元素外层必须包起来(h1,label,input,p它们都是相邻的),
vue是用一个template标签包起来,
可以写一个div包起来,不过会产生一个多余的div标签
react直接采用React.Fragment来包裹最佳
*/
<React.Fragment>
{ /*1.渲染后会转成<h1 class="song">烟花易冷</h1>*/ }
<h1 className="song">
烟花易冷-
{/* 3.jsx元素可以嵌套 */}
<span>{singer}</span>
</h1>
{/* 2.htmlFor会转成<label for="inp"></label>
点击label标签会自动获取到input焦点 */}
<label htmlFor="inp">获得焦点</label>
<input type="text" id="inp" />
{/* 7.style必须是对象的形式,如style={{color: '#fff'}} */}
<p style={style}>{album}</p>
{arr.map((item, index) =>{
return <li key={index}>{item}</li>
})}
</React.Fragment>
);
render(ele, window.root);
按照上面的代码可以直观的看到一些具体的区别,那么心急吃不了臭豆腐,赶紧开始继续往下写吧。就这点东西才不是react的两把刷子呢,下面有请react的组件闪亮登场!
组件
在react中组件分为两种,一种是函数组件,一种是类组件,那么如何区分是不是组件呢?这里有一个标准,首字母要大写
what?是的,没错,小写的就是jsx元素了,哈哈,不信你来看
函数组件
import React from 'react';
import { render } from 'react-dom';
// 此为函数组件
function Song(props) {
return (
<div className="wrap">
<h1>晴天-{props.singer}</h1>
<p>为你翘课的那一天,花落的那一天</p>
<p>教室的哪一间,我怎么看不见</p>
</div>
);
}
// 组件可以通过属性传递数据
// render方法不会一直渲染,只会渲染一次
render(<Song singer="周杰伦" />, window.root)
当然有些人可能不信,如果我把Song写成song,然后再渲染,能有什么问题呢?
其实没有问题,那是不可能的,因为人家react了解你,会给你一个报错告诉你是不是想使用一个react组件进行渲染,那就把首字母大写
官方说的,童叟无欺了,哈哈
继续回到函数组件的探讨中,函数组件是个无this,无生命周期,无状态的三无产品。所以可想而知使用频率也是不如类组件的。
举个栗子:
import React from 'react';
import { render } from 'react-dom';
// 函数组件Clock
function Clock(props) {
return <p>{props.date}</p>
}
setInterval(() => {
render(<Clock date={new Date().toLocaleString()} />, window.root);
}, 1000);
可以实时的更新时间,不过由于函数组件没有状态,而且只会渲染一次,所以还要在外层加一个setInterval定时器,就有点不伦不类了
更重要的是,状态的更改都是通过属性由我们传递过去的,没有自己的状态,刷新的时候都依赖传递,这样很不好,组件的特点就是复用
所以我们就要隆重的请出组件中的巨无霸,类组件闪亮登场
类组件
还是上面的栗子,我们改成类组件的形式
// 由于类组件需要继承react的Component,所以直接解构出来使用
import React, { Component } from 'react';
import { render } from 'react-dom';
class Clock extends Component { // 继承Component
constructor() {
super(); // 继承后可以使用setState方法
// 设置组件的默认状态
this.state = { date: new Date().toLocaleString(), pos: '北京' }
}
// 需要提供一个render方法返回dom对象
render() {
return (
<React.Fragment>
<p>当前时间:{this.state.date}</p>
<p>坐标:
<span style={{color: 'skyblue'}}>{this.state.pos}</span>
</p>
</React.Fragment>
);
}
// 一不小心就展示了目前的第一个生命周期了
// componentDidMount生命周期
// 当组件渲染完成后调用此生命周期
componentDidMount() {
setInterval(() => {
// 重新设置date的状态值,
// this.setState可以更改状态刷新页面
this.setState({date: new Date().toLocaleString()});
}, 1000);
}
}
render(<Clock />, window.root);
上面通过类组件完成了同样的效果,接下来我们好像还少些什么?事件?没错,我们写js怎么能不添加事件呢,下面就来说一下如何添加事件
接着上面的代码,我们继续写,再敲一遍,不怕累,熟能生巧,是胜利
import React, { Component } from 'react';
import ReactDOM, { render } from 'react-dom';
class Clock extends Component {
constructor() {
super();
this.state = { date: new Date().toLocaleString(), pos: '北京' };
// 强制绑定this
this.handleClick = this.handleClick.bind(this);
}
/* 绑定方法有几种方式 方法中可能会用到this
1.箭头函数 (不提倡,会产生个新函数,楼下2层也是)
2.bind绑定 this.handleClick.bind(this)
3.在构造函数中绑定this (官方推荐)
4.ES7语法可以解决this指向 handleClick = () => {} (更推荐,哈哈)
*/
handleClick = () => {
console.log(this) // 直接绑给一个函数的话,此时的this是undefined
// 在方法里,我们来移除一下这个组件
// 移除组件我们就用到了ReactDOM的方法
ReactDOM.unmountComponentAtNode(window.root);
}
render() {
// 这里绑定个click事件,用的是驼峰写法onClick
// 后面要跟js语法,onClick={}
return (<React.Fragment>
<p onClick={this.handleClick}>当前时间:{this.state.date}</p>
<p>坐标:
<span style={{color: 'skyblue'}}>{this.state.pos}</span>
</p>
</React.Fragment>)
}
componentDidMount() {
this.timer = setInterval(() => {
this.setState({ date: new Date().toLocaleString() });
}, 1000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
}
render(<Clock />, window.root);
执行以上代码后,点击当前时间的p标签后,就会在root节点下移除掉该组件(dom)了,不过可能大家会忽略掉一些常见的问题,下面我就来解释一下
- 当点击移除组件后,定时器仍在执行,setState还在更新着date的状态,所以会有报错的
- 下面就着重解释一下这个问题
class Clock extends Component {
// 主要看这里(气质)
componentDidMount() {
// 和以往清除定时器的做法一样,我们直接将timer挂到this实例上
this.timer = setInterval(() => {
this.setState({date: new Date().toLocaleString()})
});
}
// 这个生命周期是组件将要被卸载的时候调用
componentWillUnmount() {
// 一般卸载组件后要移除定时器和绑定的方法
clearInterval(this.timer);
}
}
好了,上面的报错问题就这样迎刃而解了。不过,说的总比唱的好听,为什么要将timer放到this上?放到this.state里的timer状态不行吗?
放到this.state上的话说明,当前页面要依赖这个状态,状态变了视图可以刷新,setState是可以更新页面的
然而将timer直接放到当前实例上(this),即时删掉(清空)了,也不影响页面
组件讲了这么多,让我们做个小结吧
- 组件有两个数据源
- 一个是属性 外界传递的
- 一个是状态 自己拥有的
小结是完毕了,那就喝杯水继续往下看吧,我们发现react的状态很有分量啊,state状态改变后可以刷新视图改变,屌不屌!吊炸天啊简直,那么我们就单独针对这个state来说上一说吧
state状态
苍白的文字,不如真实的代码看的带劲,上代码
// 这是一个很常见很常见的栗子,计数器
import React, { Component } from 'react';
import { render } from 'react-dom';
class Counter extends Component {
constructor() {
super();
this.state = { count: 1 };
}
handleClick = () => {
// 写到这里就可以实现点击一次button就加1一次了
// this.setState({ count: this.state.count + 1 });
// this.setState也可以接受一个函数,它的第一个参数就是上一次的状态
//
this.setState(prevState => ({ count: prevState.count + 1 }));
this.setState(prevState => ({ count: prevState.count + 1 }));
this.setState(prevState => ({ count: prevState.count + 1 }));
}
render() {
return (
<div>
<span>计数器: </span>
{this.state.count}
<button onClick={this.handleClick}>点我</button>
</div>
);
}
}
render(<Counter></Counter>, window.root);
state有个特点:多个状态可以批量更新?就是说如果我上面写了三个this.setState去改变count,那按常理来说,状态就应该改变三次,点一次button就加3了
其实不然,react不会这么做,如果状态改变一次,就刷新一次页面的话,那就疯狂了,react是把状态(count)先记住,最后再一起刷新页面
于是乎就用上面重新设置的setState来保留count的状态,最后再一次性的改变。说白了就是如果更新时下一次的状态依赖于上一次的状态,就得写成函数的形式
好了,下面再来说说组件间的通信吧,先来个父子组件直接的通信
组件间的通信
组件间的通信,父子组件之间就是通过属性传递的,父 -> 子 -> 孙子
单向数据流,数据方向是单向的,子不能改父的属性 下面来看个小demo
父组件 songs.js
import React, { Component } from 'react';
import { render } from 'react-dom';
import axios from 'axios'; // axios用于请求
import List from './list'; // 子组件
import Audio from './audio'; // 子组件
import './css/songs.css';
class Songs extends Component {
constructor() {
super();
this.state = { songs: [], mp3Url: '', isPlay: false };
}
// 子传父通信 -> 父提供一个方法,子调用后将参数回传
// chooseSong为选择歌曲地址填入audio的src的方法
chooseSong = (url, isPlay) => {
// 这里子组件回传了属性更改了isPlay的状态
// 对应修改了List的同级组件Audio接收的played属性值
this.setState({mp3Url: url, isPlay});
}
render() {
return (
<div className="songs-box">
<ul>
{this.state.songs.map((item, index) => (
/*
通过传递属性的方式进行通信
这里将item解构出来,然后List子组件按照需要去拿数据
*/
<List key={index} {...item} choose={this.chooseSong}></List>
))}
</ul>
<Audio url={this.state.mp3Url} played={this.state.isPlay}></Audio>
</div>
);
}
async componentDidMount() {
// 在react中发送ajax请求 现在我们用axios
// axios封装了RESTFul 基于promise的 不支持jsonp 可用在服务端
let { data } = await axios.get('http://musicapi.leanapp.cn/search?keywords=林俊杰');
this.setState({ songs: data.result.songs });
}
}
render(<Songs></Songs>, window.root);
未播放效果
播放效果 剩余的代码我就不一一展示了,仅仅是个简单的小demo,功能很多都未完善,有兴趣的同学可以去研究一下继续写写吧,贴个地址吧通过上面的小demo,其实可以发现组件间的通信主要有三种方式
- 第一种方式是通过属性传递,父 -> 子 -> 孙子 (父传子)
- 单向数据流,数据方向是单向的,子不能改父的属性
- 第二种方式是父写好了一个方法,传递给儿子 (子传父)
- 儿子调用这个方法,在这个方法中可以去更改状态
- 第三种方式是同级组件传递
- 同级组件想要传递数据,可以找到共同的父级,没有父级就创造父级
受控组件和非受控组件
接下来了解一下受控组件与非受控组件的两个概念,他们指的是表单元素
既然是涉及到了表单元素,那我们期待的双向数据绑定应该也是该登场的了
双向数据绑定
import React, { Component } from 'react';
import { render } from 'react-dom';
class Input extends Component{
constructor(){
super();
this.state = {val: '你好'};
}
// 通过onChange事件调用该方法后,每次更改输入后val的状态值
handleChange = (e) =>{ //e是事件源
let val = e.target.value;
this.setState({val});
};
render(){
return (<div>
<input type="text" value={this.state.val} onChange={this.handleChange}/>
{this.state.val}
</div>)
}
}
render(<Input />, window.root);
双向数据绑定主要通过的是监听onChange事件,然后每次都将新输入的value值更改对应val的状态,从而再赋给input的value属性,做到了数据双向传递的实现,果然很高级,哈哈
受控组件
受状态控制的组件,必须要有onChange方法,否则不能使用
受控组件可以赋予默认值
import React, { Component } from 'react';
import { render } from 'react-dom';
class App extends Component{
constructor(){
super();
this.state = {a: '破风', b: '激战'};
}
// name表示的就是当前状态改的是哪一个
// e表示的是事件源
handleChange = (e) => { //处理多个输入框的值映射到状态的方法
let name = e.target.name;
this.setState({ [name]: e.target.value });
}
render(){
return (
<form>
<input type="text"
required={true}
value={this.state.a}
onChange={this.handleChange}
name="a"
/>
<input type="text"
required={true}
value={this.state.b}
onChange={this.handleChange}
name="b"
/>
<input type="submit" />
<p>{this.state.a}</p>
<p>{this.state.b}</p>
</form>
)
}
}
render(<App></App>, window.root);
最后再来看一下非受控组件
非受控组件
所谓非受控组件有三个特点:
- 可以操作dom,获取真实dom
- 可以和第三方库结合
- 不需要对当前输入的内容进行校验,也不需要默认值
import React, { Component } from 'react';
import { render } from 'react-dom';
// 1.函数的方式 ref
// 2.React.createRef() v16.3+
class App extends Component {
constructor() {
super();
this.aaa = React.createRef();
}
componentDidMount() {
// this.aaa.focus(); // 对应1
this.aaa.current.focus(); // 对应2
}
render() {
return (<div>
{/* 1.<input type="text" ref={input=>this.aaa = input} />*/}
{/* 2.会自动的将当前输入框 放在this.aaa.current */}
<input type="text" ref={this.aaa} />
</div>)
}
}
render(<App />, window.root);
上面的栗子呢,就是一进入页面后就会自动获取输入框的焦点了,之后大家可以试着敲敲看吧
时间不早,节目刚好
虽然react还有很多的内容要讲,而且还没有涉及到生命周期等内容
不过,大家放心,该来的总会来,一个都不能少,在之后的文章里,我会继续学习和分享的
那么今天就到这里了,谢谢大家不辞辛劳的观看,各位,安啦!!!