文章目录
- React介绍
- React开发环境搭建
- 项目目录说明以及相关调整
- JSX基础
- JSX介绍
- JSX中使用js表达式
- JSX列表渲染
- JSX条件渲染
- JSX样式处理
- JSX注意事项
- 组件基础
- 组件的概念
- 函数组件
- 类组件
- 事件绑定
- 如何绑定事件
- 获取事件对象
- 传递额外参数
- 组件状态
- 状态不可变
- 表单处理
- 受控表单组件
- 非受控表单组件
- 阶段性实践项目
参考课程地址:
React入门到实战
React介绍
React是什么?
React是一个用于构建用户界面的JavaScript库,它主要用于构建UI,很多人认为React是MVC中的V(视图)。React起源于Facebook的内部项目,用来架设Instagram的网站,并于2013年5月开源。React拥有较高的性能,代码逻辑非常简单,越来越多的人已开始关注和使用它。
React有什么特点
- 声明式UI(JSX):写UI就和写普通的HTML一样,抛弃命令式的繁琐实现
- 组件化:组件是react中最重要的内容,组件可以通过搭积木的方式拼成一个完整的页面,通过组件的抽象可以增加复用能力和提高可维护性
- 跨平台:react既可以开发web应用也可以使用同样的语法开发原生应用(react-native),比如安卓和ios应用,甚至可以使用react开发VR应用,想象力空间十足,react更像是一个
元框架
为各种领域赋能 - 高效:React通过对DOM的模拟,最大限度地减少与DOM的交互,提高渲染效率。
- 单向数据流:React实现了单向数据流,即数据只能从父组件传递到子组件,而不能反向传递,这样可以避免数据的混乱和冲突,也方便数据的管理和追踪。
React开发环境搭建
现在的前端开发环境已经趋于工程化,我们一般使用脚手架来创建一个项目的初始环境
那么什么是脚手架呢?
在这里我们使用的脚手架就是create react app:
$ npx create-react-app react-basic
说明:
- npx create-react-app 是固定命令,create-react-app是React脚手架的名称
- react-basic表示项目名称,可以自定义,保持语义化
- npx 命令会帮助我们临时安装create-react-app包,然后初始化项目完成之后会自自动删掉,所以不需要全局安装create-react-app
启动项目:
$ yarn start
or
$ npm start
什么是npx,他和npm有什么区别?
项目目录说明以及相关调整
- 目录说明
- src 目录是我们写代码进行项目开发的目录
- package.json 中俩个核心库:react 、react-dom
- 目录调整
- 删除src目录下自带的所有文件,只保留app.js根组件和index.js
- 创建index.js文件作为项目的入口文件,在这个文件中书写react代码即可
入口文件index.js说明:
//react框架的核心包
import React from 'react'
//专门做渲染方面的包
import ReactDOM from 'react-dom'
//应用的全局样式文件
import './index.css'
// 引入根组件App
import App from './App'// 通过调用ReactDOM的render方法渲染App根组件到id为root的dom节点上
ReactDOM.render(<React.StrictMode><App /></React.StrictMode>,document.getElementById('root')
)//如果是react18版本
//注意把严格模式去掉,否则会影响useEffect的执行时间
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />
);
这个index.css里面一般是做一些全局样式的初始化:
JSX基础
JSX介绍
概念:JSX是 JavaScript XML(HTML)的缩写,表示在 JS 代码中书写 HTML 结构
作用:在React中创建HTML结构(页面UI结构)
优势:
- 采用类似于HTML的语法,降低学习成本,会HTML就会JSX
- 充分利用JS自身的可编程能力创建HTML结构
注意:JSX 并不是标准的 JS 语法,是 JS 的语法扩展,浏览器默认是不识别的,脚手架中内置的 @babel/plugin-transform-react-jsx 包,用来解析该语法
接比如下面的图片,我们写的是左边,其实编译的时候变成了右边:
JSX中使用js表达式
语法
const name = '柴柴'<h1>你好,我叫{name}</h1> // <h1>你好,我叫柴柴</h1>
可以使用的表达式
- 字符串、数值、布尔值、null、undefined、object( [] / {} )
- 1 + 2、‘abc’.split(‘’)、[‘a’, ‘b’].join(‘-’)
- fn()
特别注意
if 语句/ switch-case 语句/ 变量声明语句,这些叫做语句,不是表达式,不能出现在 {} 中!!
技巧:能够通过console.log打印出来的就是表达式
JSX列表渲染
页面的构建离不开重复的列表结构,比如歌曲列表,商品列表等,我们知道vue中用的是v-for,react这边如何实现呢?
实现:使用数组的map 方法
// 来个列表
const songs = [{ id: 1, name: '痴心绝对' },{ id: 2, name: '像我这样的人' },{ id: 3, name: '南山南' }
]function App() {return (<div className="App"><ul>{songs.map(item => <li>{item.name}</li>)}</ul></div>)
}export default App
注意点:需要为遍历项添加 key 属性,提高diff性能
- key 在 HTML 结构中是看不到的,是 React 内部用来进行性能优化时使用
- key 在当前列表中要唯一的字符串或者数值(String/Number)
- 如果列表中有像 id 这种的唯一值,就用 id 来作为 key 值
- 如果列表中没有像 id 这种的唯一值,就可以使用 index(下标)来作为 key 值
JSX条件渲染
作用:根据是否满足条件生成HTML结构,比如Loading效果
实现:可以使用 三元运算符
或 逻辑与(&&)运算符
// 来个布尔值
const flag = true
function App() {return (<div className="App">{/* 条件渲染字符串 */}{flag ? 'react真有趣' : 'vue真有趣'}{/* 条件渲染标签/组件 */}{flag ? <span>this is span</span> : null}</div>)
}
export default App
如果是复杂的条件渲染,我们可以把整个逻辑封装成一个函数,然后直接在{}
中执行函数即可。
例如:
JSX样式处理
行内样式
function App() {return (<div className="App"><div style={{ color: 'red' }}>this is a div</div></div>)
}export default App
行内样式优化版
const styleObj = {color:red
}function App() {return (<div className="App"><div style={ styleObj }>this is a div</div></div>)
}export default App
类名方式
.title {font-size: 30px;color: blue;
}
function App() {return (<div className="App"><div className='title'>this is a div</div></div>)
}export default App
动态类名
import './app.css'
const showTitle = true
function App() {return (<div className="App"><div className={ showTitle ? 'title' : ''}>this is a div</div></div>)
}
export default App
JSX注意事项
- JSX必须有一个根节点,如果没有根节点,可以使用
<></>
(幽灵节点)替代 - 所有标签必须形成闭合,成对闭合或者自闭合都可以
- JSX中的语法更加贴近JS语法,属性名采用驼峰命名法
- class -> className
- for -> htmlFor
- JSX支持多行(换行),如果需要换行,需使用
()
包裹,防止bug出现
组件基础
组件的概念
React的组件概念是指将UI界面拆分成可复用的代码片段,每个片段都是一个独立的模块,可以有自己的状态和逻辑,也可以接收外部传入的数据和事件。React的组件可以用函数或类的形式来定义,也可以用JSX语法来描述组件的结构和样式。React的组件可以嵌套和组合,形成复杂的应用界面。React的组件的优点是可以提高开发效率和代码质量,也可以保证UI和数据的一致性。
函数组件
使用 JS 的函数(或箭头函数)创建的组件,就叫做函数组件
接下来我们来看一个函数组件的定义和渲染的例子:
// 定义函数组件
function HelloFn () {return <div>这是我的第一个函数组件!</div>
}// 定义类组件
function App () {return (<div className="App">{/* 渲染函数组件 */}<HelloFn /><HelloFn></HelloFn></div>)
}
export default App
约定说明
- 组件的名称必须首字母大写,react内部会根据这个来判断是组件还是普通的HTML标签
- 函数组件必须有返回值,表示该组件的 UI 结构;如果不需要渲染任何内容,则返回 null
- 组件就像 HTML 标签一样可以被渲染到页面中。组件表示的是一段结构内容,对于函数组件来说,渲染的内容是函数的返回值就是对应的内容
- 使用函数名称作为组件标签名称,可以成对出现也可以自闭合
类组件
使用 ES6 的 class 创建的组件,叫做类(class)组件
// 引入React
import React from 'react'// 定义类组件
class HelloC extends React.Component {render () {return <div>这是我的第一个类组件!</div>}
}function App () {return (<div className="App">{/* 渲染类组件 */}<HelloC /><HelloC></HelloC></div>)
}
export default App
约定说明
- 类名称也必须以大写字母开头
- 类组件应该继承 React.Component 父类,从而使用父类中提供的方法或属性
- 类组件必须提供 render 方法,render 方法必须有返回值,表示该组件的 UI 结构
事件绑定
如何绑定事件
- 语法
on + 事件名称 = { 事件处理程序 }
,比如:<div onClick={ onClick }></div>
- 注意点
react事件采用驼峰命名法,比如:onMouseEnter、onFocus
在函数组件中:
// 函数组件
function HelloFn () {// 定义事件回调函数const clickHandler = () => {console.log('事件被触发了')}return (// 绑定事件<button onClick={clickHandler}>click me!</button>)
}
类组件中的事件绑定,整体的方式和函数组件差别不大,唯一需要注意的是:因为处于class类语境下,所以定义事件回调函数以及写法上有不同
- 定义的时候: class Fields语法
- 使用的时候: 需要借助this关键词获取
import React from "react"class CComponent extends React.Component {// class FieldsclickHandler = (e, num) => {// 这里的this指向的是正确的当前的组件实例对象 // 可以非常方便的通过this关键词拿到组件实例身上的其他属性或者方法console.log(this)}clickHandler1 () {// 这里的this 不指向当前的组件实例对象而指向undefined 存在this丢失问题console.log(this)}render () {return (<div><button onClick={(e) => this.clickHandler(e, '123')}>click me</button><button onClick={this.clickHandler1}>click me</button></div>)}
}function App () {return (<div><CComponent /></div>)
}export default App
为什么这里会出现this指向不明的问题?
当然现在的主流写法都是函数组件,无需过多考虑this的指向问题。
获取事件对象
事件对象是一个用来记录事件发生时的相关信息的对象,它可以用来解决一些事件处理的问题,比如:
- 获取事件的类型、目标、阶段、时间等基本信息,用于判断事件的性质和来源。
- 获取事件的坐标、按键、按钮等细节信息,用于实现一些交互效果,比如拖拽、缩放、滚动等。
- 阻止事件的默认行为或冒泡传播,用于避免一些不期望的结果,比如链接跳转、表单提交、事件重复触发等。
- 传递一些自定义的数据或参数,用于实现一些特定的功能,比如传递状态、标识、回调函数等。
事件对象的属性和方法可能因为不同的浏览器或事件类型而有所差异,所以在使用事件对象时,需要注意兼容性和适用性的问题。
获取事件对象e只需要在 事件的回调函数中 补充一个形参e即可拿到
// 函数组件
function HelloFn () {// 定义事件回调函数const clickHandler = (e) => {console.log('事件被触发了', e)}return (// 绑定事件<button onClick={clickHandler}>click me!</button>)
}
传递额外参数
解决思路: 改造事件绑定为箭头函数,在箭头函数中完成参数的传递
import React from "react"// 如何获取额外的参数?
// onClick={ onDel } -> onClick={ () => onDel(id) }
// 注意: 一定不要在模板中写出函数调用的代码 onClick = { onDel(id) } bad!!!!!!const TestComponent = () => {const list = [{id: 1001,name: 'react'},{id: 1002,name: 'vue'}]const onDel = (e, id) => {console.log(e, id)}return (<ul>{list.map(item =>(<li key={item.id}>{item.name}<button onClick={(e) => onDel(e, item.id)}>x</button></li>))}</ul>)
}function App () {return (<div><TestComponent /></div>)
}export default App
很多人这个地方都是自己硬背,什么如果只传事件参数,那就直接写回调参函数名,如果还要传递额外参数,那就要用箭头函数包一下。其实完全没必要,只要理解了就都说通了,这个地方我们怎么去理解?
我们要知道:我们不需要主动去调用事件处理函数,我们只是把事件处理函数交出去,换句话说我们只需要写一个声明语法,而不是调用语法,当事件被触发的时候会有监听器或者计时器去调用。
组件状态
一个前提:在React hook出来之前,函数式组件是没有自己的状态的,所以我们统一通过类组件来讲解
初始化状态
- 通过class的实例属性state来初始化(
必须是state属性
) - state的值是一个对象结构,表示一个组件可以有多个数据状态
class Counter extends React.Component {// 初始化状态state = {count: 0}render() {return <button>计数器</button>}
}
读取状态
class Counter extends React.Component {// 初始化状态state = {count: 0}render() {// 读取状态return <button>计数器{this.state.count}</button>}
}
修改状态
- 语法
this.setState({ 要修改的部分数据 })
- setState方法作用
- 修改state中的数据状态
- 更新UI
- 思想
- 数据驱动视图,也就是只要修改数据状态,那么页面就会自动刷新,无需手动操作dom
- 注意事项
- 不要直接修改state中的值,必须通过setState方法进行修改
这里其实就是MVVM思想的体现,vue也是这个原理,其核心特点就是双向绑定,也就是说:当view变化的时候,数据也会改变。当数据发生变化的时候,view也会跟着改变。这里setState方法体现的就是后者。
class Counter extends React.Component {// 定义数据state = {count: 0}// 定义修改数据的方法setCount = () => {this.setState({count: this.state.count + 1})}// 使用数据 并绑定事件render () {return <button onClick={this.setCount}>{this.state.count}</button>}
}
状态不可变
概念:不要直接修改状态的值,而是基于当前状态创建新的状态值
- 错误的直接修改
state = {count : 0,list: [1,2,3],person: {name:'jack',age:18}
}
// 直接修改简单类型Number
this.state.count++
++this.state.count
this.state.count += 1
this.state.count = 1// 直接修改数组
this.state.list.push(123)
this.state.list.spice(1,1)// 直接修改对象
this.state.person.name = 'rose'
- 基于当前状态创建新值
this.setState({count: this.state.count + 1list: [...this.state.list, 4],person: {...this.state.person,// 覆盖原来的属性 就可以达到修改对象中属性的目的name: 'rose'}
})
表单处理
使用React处理表单元素,一般有俩种方式:
- 受控组件 (推荐使用)
- 非受控组件 (了解)
受控表单组件
什么是受控组件? input框自己的状态被React组件状态控制
React组件的状态的地方是在state中,input表单元素也有自己的状态是在value中,React将state与表单元素的值(value)绑定到一起,由state的值来控制表单元素的值,从而保证单一数据源特性
实现步骤:
以获取文本框的值为例,受控组件的使用步骤如下:
- 在组件的state中声明一个组件的状态数据
- 将状态数据设置为input标签元素的value属性的值
- 为input添加change事件,在事件处理程序中,通过事件对象e获取到当前文本框的值(即用户当前输入的值)
- 调用setState方法,将文本框的值作为state状态的最新值
说白了就是将表单的值交给react的状态来控制。
代码实现:
import React from 'react'class InputComponent extends React.Component {// 声明组件状态state = {message: 'this is message',}// 声明事件回调函数changeHandler = (e) => {this.setState({ message: e.target.value })}render () {return (<div>{/* 绑定value 绑定事件*/}<input value={this.state.message} onChange={this.changeHandler} /></div>)}
}function App () {return (<div className="App"><InputComponent /></div>)
}
export default App
非受控表单组件
什么是非受控组件?
非受控组件就是通过手动操作dom的方式获取文本框的值,文本框的状态不受react组件的state中的状态控制,直接通过原生dom获取输入框的值
实现步骤
- 导入createRef 函数
- 调用createRef函数,创建一个ref对象,存储到名为msgRef的实例属性中
- 为input添加ref属性,值为msgRef
- 在按钮的事件处理程序中,通过msgRef.current即可拿到input对应的dom元素,而其中msgRef.current.value拿到的就是文本框的值
代码实现:
import React, { createRef } from 'react'class InputComponent extends React.Component {// 使用createRef产生一个存放dom的对象容器msgRef = createRef()changeHandler = () => {console.log(this.msgRef.current.value)}render() {return (<div>{/* ref绑定 获取真实dom */}<input ref={this.msgRef} /><button onClick={this.changeHandler}>click</button></div>)}
}function App () {return (<div className="App"><InputComponent /></div>)
}
export default App
阶段性实践项目
练习说明
- 拉取项目模板到本地,安装依赖,run起来项目
https://gitee.com/react-course-series/react-component-demo - 完成tab点击切换激活状态交互
- 完成发表评论功能
- 完成删除评论功能
注意:生成独立无二的id 可以使用 uuid 包 yarn add uuid
import { v4 as uuid } from 'uuid'
uuid() // 得到一个独一无二的id
app.js代码如下:
import './index.css'
import avatar from './images/avatar.png'
import React from 'react'
import { v4 as uuid } from 'uuid'// 时间格式化
function formatDate (time) {return `${time.getFullYear()}-${time.getMonth()}-${time.getDate()}`
}class App extends React.Component {state = {// hot: 热度排序 time: 时间排序tabs: [{id: 1,name: '热度',type: 'hot'},{id: 2,name: '时间',type: 'time'}],active: 'hot',list: [{id: 1,author: '刘德华',comment: '给我一杯忘情水',time: new Date('2021-10-10 09:09:00'),// 1: 点赞 0:无态度 -1:踩attitude: 1},{id: 2,author: '周杰伦',comment: '哎哟,不错哦',time: new Date('2021-10-11 09:09:00'),// 1: 点赞 0:无态度 -1:踩attitude: 0},{id: 3,author: '五月天',comment: '不打扰,是我的温柔',time: new Date('2021-10-11 10:09:00'),// 1: 点赞 0:无态度 -1:踩attitude: -1}],userComment:""}//tab栏切换回调事件tabSwitch = (type) => {if (type === 'hot') {this.setState({active: 'hot',list:this.state.list.sort((item1,item2) => -(item1.attitude - item2.attitude))})}else {this.setState({active: 'time',list:this.state.list.sort((item1,item2) => -(item1.time - item2.time))})}}//添加评论回调函数addComment = () => {//写法1://push方法的返回值是数组长度,不是数组本身// const newList = [...this.state.list]// newList.push(// {// id: uuid(),// author: '临时用户',// comment: this.state.userComment,// time: new Date(),// // 1: 点赞 0:无态度 -1:踩// attitude: 0// }// )// this.setState(// {// list:newList,// userComment:""// }// )//写法2:解构赋值this.setState({list:[...this.state.list,{id: uuid(),author: '临时用户',comment: this.state.userComment,time: new Date(),// 1: 点赞 0:无态度 -1:踩attitude: 0}],userComment:""})}//评论框变化回调函数commentChange = (e) => {console.log(e.target.value)this.setState({userComment: e.target.value})}//删除评论的回调事件deleteComment = (id) => {this.setState({list:this.state.list.filter(item => item.id !== id)})}render () {return (<div className="App"><div className="comment-container">{/* 评论数 */}<div className="comment-head"><span>5 评论</span></div>{/* 排序 */}<div className="tabs-order"><ul className="sort-container">{this.state.tabs.map(tab => (<likey={tab.id}className={tab.type === this.state.active ? 'on' : ''}onClick={() => this.tabSwitch(tab.type)}>按{tab.name}排序</li>))}</ul></div>{/* 添加评论 */}<div className="comment-send"><div className="user-face"><img className="user-head" src={avatar} alt="" /></div><div className="textarea-container"><textareacols="80"rows="5"placeholder="发条友善的评论"className="ipt-txt"value={this.state.userComment}onChange={this.commentChange}/><button className="comment-submit" onClick={this.addComment}>发表评论</button></div><div className="comment-emoji"><i className="face"></i><span className="text">表情</span></div></div>{/* 评论列表 */}<div className="comment-list">{this.state.list.map(item => (<div className="list-item" key={item.id}><div className="user-face"><img className="user-head" src={avatar} alt="" /></div><div className="comment"><div className="user">{item.author}</div><p className="text">{item.comment}</p><div className="info"><span className="time">{formatDate(item.time)}</span><span className={item.attitude === 1 ? 'like liked' : 'like'}><i className="icon" /></span><span className={item.attitude === -1 ? 'hate hated' : 'hate'}><i className="icon" /></span><span className="reply btn-hover" onClick={() => this.deleteComment(item.id)}>删除</span></div></div></div>))}</div></div></div>)}
}export default App