- react把真实DOM树转换为虚拟DOM树(因为操作真实DOM的性能消耗大),根据DOM Diff 算法,对比新旧虚拟DOM树,然后再更新到真实DOM树。
创建React项目:
Step1:全局安装create-react-app脚手架
npm install -g create-react-app
Step2:创建一个项目 create-react-app 项目名称
函数式组件和类式组件
- 函数式组件
js
// 函数式组件 16.8之前么有hooks,是无状态组件 如点击不能加1
function App() {
return (
<div style={{ background: 'pink' }} className="test">fighting Evelyn!</div>
)
}
export default App
- 类式组件
js
class App extends React.Component {
render() {
// JSX语法
// 要求必须有唯一根标签
return (
<div>hello Evelyn!</div>
// 在组件内部,可以在插值内部使用js表达式:
)
}
}
react中绑定事件
js
import React, { Component } from 'react'
export default class EventTest extends Component {
name = 'Evelyn'
render() {
return (
<div>
<button onClick={() => {
// 箭头函数中,this为当前类组件实例对象
console.log(this)
}}>测试this</button>
<button onClick={function () {
// 普通函数中,this为undefined
console.log(this)
}}>测试this2</button>
<button onClick={this.clickHandler1.bind(this)}>test1</button>
{/* 普通函数,需要使用bind修改this指向 */}
<button onClick={this.clickHandler2}>test2</button>
{/* 箭头函数 */}
{/* 最佳推荐方案,适合传参使用 */}
<button onClick={() => {
this.clickHandler1()
}>最佳绑定事件方案</button>
</div>
)
}
// 普通函数
clickHandler1() {
console.log(this) // undefined
}
// 箭头函数
clickHandler2 = () => {
console.log(this) // 当前类组件实例对象
}
}
【注】:其中最推荐的写法是,使用箭头函数包裹,后期传参很方便。
在react中绑定事件,不是绑定在某一个DOM元素上,而是绑定在根节点身上,采用的是事件代理,
ref
React中之前是使用ref,this.refs用来获取当前实例对象,
js
<input ref="inputRef"></input>
<button onClick={() => {
console.log(this.refs.inputRef)
}}>点击按钮获取当前输入框</button>
但是React舍弃了这种获取当前绑定DOM元素的方法, 使用React.createRef()获取,获取的时候采用this.ref名称.current
js
myRef = React.createRef()
render() {
return (
<div>
<input ref={this.myRef}></input>
<button onClick={this.handleClick}>添加</button>
</div>
)
}
handleClick = () => {
console.log(this.myRef.current.value)
this.myRef.current.value = ''
}
状态
因为react中没有使用Object.defineProperty进行数据代理, 所以使用状态数据state实现响应式,使用setState进行修改响应式数据。
js
import React, { Component } from 'react'
export default class ChangeState extends Component {
state = {
isClick: false
}
render() {
return (
<div>
<button onClick={() => {
// this.state.isClick = !this.state.isClick
// 警告:不要直接修改state状态数据!
// 修改状态数据使用 setState的函数
this.setState({
isClick: !this.state.isClick
})
}}>{this.state.isClick ? '收藏' : '取消收藏'}</button>
</div>
)
}
}
列表遍历
使用map方法进行遍历
js
export default class List extends Component {
state = {
list: ['aa', 'bb', 'cc']
}
render() {
return (
<ul>
{
this.state.list.map((item, index) => <li key={index}>{item}</li>)
}
</ul>
)
}
}
TodoList案例
设置一个todoList数组的状态值,每次点击添加,结合ref属性获取到输入框中的内容,将该内容push到todoList数组中,一定要记得是,使用setState修改state数据。
js
export default class RefTest extends Component {
myRef = React.createRef()
state = {
todoList: ['11', '22', '33']
}
render() {
return (
<div>
<input ref={this.myRef}></input>
<button onClick={this.handleClick}>添加</button>
<hr></hr>
<ul>
{
// this.state.todoList.map((item, index) => <li key={index}>{item} <button onClick={() => {
// this.handleDelClick(index)
// }}>删除</button></li>)
this.state.todoList.map((item, index) => <li key={index}>{item} <button onClick={this.handleDelClick.bind(this, index)}>删除</button></li>)
}
</ul>
</div>
)
}
handleClick = () => {
console.log(this.myRef.current.value)
this.state.todoList.push(this.myRef.current.value)
this.setState({
todoList: this.state.todoList
})
this.myRef.current.value = ''
}
handleDelClick(idx){
console.log(idx)
// 首先复制一份数组,不要修改原数组
// 数组原型上的slice,concat方法,都是浅拷贝,不会影响到原数据
let newList = this.state.todoList.slice()
newList.splice(idx, 1)
this.setState({
todoList: newList
})
}
}
条件渲染
通常使用三元运算符 、逻辑与逻辑或进行条件渲染!
setState同步&异步?
当使用setState修改状态值:
- 当setState处在同步逻辑中,异步更新状态,异步更新真实DOM;
- 当setState处在异步逻辑中,同步更新状态,同步更新真实DOM
(比如在绑定事件的回调 ,定时器中的回调(超时时间设置为0) 是异步的,因为react为了节约性能,会把多次setState合并为一次进行,而在最后一次性的更新state。)
setState函数提供了第二个参数,是一个回调,状态和DOM更新完之后就会被触发(有点类似于Vue中的nextTick),然后执行回调中的代码(比如某一些操作要在DOM更新完之后再去执行)。 (案例:BetterScroll)
属性------props
state状态值是组件内部使用,其他组件不能使用 而属性是由父组件传递过来,this.props
,组件之间传递数据,通过标签属性的形式 如果是字符串,直接引号, 如果是JS表达式,使用大括号插值语法
函数式组件:通过函数形参,得到一个对象,直接解构 类式组件:this.props
属性的验证
使用类属性,类式组件.prototype = { 数据1:验证方法 , 数据2:验证方法 } import propTypes from 'prop-types'
得到的propTypes是一个对象,里边包含了很多验证数据类型的方法。
属性和数据
状态和属性改变,都会造成render函数的重新调用, 不允许在子组件中直接修改父组件传递过来的属性!!!
受控组件和非受控组件
- 受控组件 :组件中的表单项根据
state状态
数据动态初始显示和更新显示, 当用户输入时实时同步到状态数据中,也就是实现了页面表单元素 与 state 数据的双向绑定(推荐使用) - 非受控组件:不与state数据相关联,需要手动读取表单元素的值(借助于ref属性)
js
// 非受控组件
export default class Test extends Component {
ipt = React.createRef()
render() {
return (
<div>
<input ref={this.ipt} />
<button onClick={this.getIptContent}>获取</button>
<button onClick={this.resetIptContent}>重置</button>
</div>
)
}
getIptContent = () => {
// 非受控组件 如何获取到input输入框中的内容?
// 获取到该ref对象身上的current属性,继而获取到value属性
console.log(this.ipt.current.value)
}
resetIptContent = () => {
// 非受控组件,如何赋值?
this.ipt.current.value = ''
}
}
js
// 受控组件
export default class Test extends Component {
// 创建初始状态值
state = {
uname: '',
upass: ''
}
render() {
return (
<div>
用户名:<input value={this.state.uname} onChange={this.saveUname} />
密 码:<input value={this.state.upass} onChange={this.saveUpass} />
<button onClick={() => this.login()}>登录</button>
</div>
)
}
// 涉及到this:
// 1、普通函数,使用bind修改this指向 || 使用箭头函数包裹(记得调用)
// 2、箭头函数(直接写)
login() {
console.log(this.state.uname)
console.log(this.state.upass)
}
saveUname = (e) => {
// console.log(e)
this.setState({
uname: e.target.value
}, () => {
// 写在异步函数中,可以同步获取到state状态值!
console.log('异步', this.state.uname)
})
// 写在同步函数中,可以异步获取到state状态值!
console.log('同步', this.state.uname)
}
saveUpass = (e) => {
this.setState({
upass: e.target.value
})
}
}
父子组件间通信
- 父传子 ------ 传递数据
- 子传父 ------ 传递方法(子组件通知给父组件,让父组件去修改state值)
采用的方案是:通过标签属性的形式,父传给子一个函数,父中定义函数修改state状态值,子中调用这个函数(间接实现通过父组件修改子组件)
- 组件的ref属性
iptRef = React.createRef()
在组件上绑定ref属性,ref={this.iptRef}
,然后this.iptRef.current
得到的就是这个组件实例对象
非父子组件之间的通信
- (1)状态提升 / 中间人模式
多个子组件有同一个父组件,父组件作为一个中间人,进行数据传递。
(子组件先传递数据给父组件,通过函数传参的形式,父组件将接收到的数据通过setState设置为自己的状态值,然后通过props传递给另一个子组件。)
- (2)发布订阅模式
subscribe方法和publish方法(Redux就是基于订阅发布的)
js
import React, { Component } from 'react'
export default class App extends Component {
render() {
return (
<div>
</div>
)
}
}
let bus = {
list: [],
// 订阅消息
subscribe(cb) {
this.list.push(cb)
},
// 发布消息
publish() {
this.list.forEach(cb => {
// 发布消息的时候可以传递形参
cb && cb('Evelyn')
})
}
}
bus.subscribe((text) => {
console.log(111, text)
})
bus.subscribe((text) => {
console.log(222, text)
})
bus.publish()
- (3)context状态树传参
采用生产者-消费者模式,Provider和Consumer
插槽
在父组件中调用子组件,当子组件中写了内容,这就是插槽,在子组件中通过this.props.children
读取到插槽中的内容,当插槽中有多个标签时,children属性值就是一个数组,可以根据下标读取到。
使用插槽,可以在一定程度上减少父子组件之间的通信,因为在插槽中可以直接读取到父组件中数据/状态。
组件生命周期
初始化阶段
- ComponentWillMount(组件将要挂载到真实DOM中,只执行一次,DOM上树之前,最后一次修改状态的机会)
此阶段特点:无法获取到真实DOM,可以修改state
- Render(渲染页面)
此阶段特点:不要在render中修改state数据,会造成死循环
- ComponentDidMount (只执行一次)
此阶段特点:初始化数据的作用(数据请求、订阅函数、事件监听、设置定时器,基于创建完成的DOM进行初始化)
运行中
- ComponentWillReceiveProps 父组件修改属性时触发
- ShouldComponentUpdate:返回false会阻止render调用
- ComponentWillUpdate:不能修改属性和状态,不能获取到DOM
- Render:只能访问this.props和this.state,不允许修改状态和DOM输出(setState)
- ComponentDidUpdate:能够获取到DOM,做一些更新DOM的库(BetterScroller)
销毁阶段
- ComponentWillUnmount:在删除组件之前进行收尾工作(定时器和事件监听),如定时器的timerId,在销毁阶段中清楚timerId
函数式组件
hooks------useState(管理组件状态)
js
import React, { useState } from 'react'
export default function App() {
// let res = useState('happy')
// console.log(res) // ['happy' , f ]
let [mood, setMood] = useState('happy')
// 使用useState函数的返回值是一个数组
// 1、mood表示一个状态值
// 2、setMood表示修改状态值的函数
return (
<div>
<button onClick={() => {
setMood('emo')
}}>{mood}</button>
</div>
)
}
hooks------useEffect(副作用函数)
当useEffect函数中:
第一个参数,必须传入一个回调函数!!
第二个参数,传值的几种情况(第二个参数表示依赖)
- 不传(缺失依赖)
useEffect会在第一次渲染以及每次更新渲染后都执行。(第一次渲染后执行一次useEffect,当useEffect中回调函数改变了state值,而state值改变后会触发组件的重新渲染,所以就会无限循环下去。)
js
const [count, setCount] = useState<number>(1);
useEffect(() => {
setTimeout(() => {
setCount(count + 1);
}, 1000);
console.log(`第二个参数: 不传值, 第 ${count} 次执行`);
});
// 打印log,无限循环
第二个参数: 不传值, 第 1 次执行
第二个参数: 不传值, 第 2 次执行
第二个参数: 不传值, 第 3 次执行
第二个参数: 不传值, 第 ... 次执行
- 传一个空数组------相当于是ComponentDidMount(组件挂载完成)
useEffect只会在第一次渲染之后执行一次。(第一次渲染之后执行了一次useEffect,当useEffect中回调函数改变了state值,由于第二个参数是空数组,没有依赖,相当于依赖没发生变化,所以不会执行回调函数,state无更新,不触发组件的重新渲染)
js
const [count, setCount] = useState<number>(1);
useEffect(() => {
setTimeout(() => {
setCount(count + 1);
}, 1000);
console.log(`第二个参数: 空数组, 第 ${count} 次执行`);
}, []);
// 打印log,执行一次
第二个参数: 空数组, 第 1 次执行
- 传入一个非空数组
当依赖的值发生了改变,useEffect函数会再次执行。。。
在useEffect中,return一个回调函数,相当于是模拟ComponentWillUnmount
js
useEffect(() => {
return () => {
console.log('组件将要被卸载时...')
// 模拟组件销毁的生命周期
// 解绑事件,清除定时器
}
})
useCallback 记忆函数
useState是一个记忆函数,能够记住state状态值。
因为每次调用setXXX函数,修改状态值,都会导致useState函数重新调用执行,所以,为了防止因为组件的重新渲染,导致方法被重新创建,起到缓存的作用,只有当第二个参数发生了变化,才会重新声明一次。
使用useCallback()进行包裹,相当于进行缓存,可以优化性能,第二个参数是[]
useMemo 记忆组件
useCallback(fn , inputs) === useMemo(()=>fn , inputs)
- useCallback的功能可以由useMemo所取代,也可以使用useMemo返回一个记忆函数。
- useCallback不会执行 第一个参数函数,而是将其返回;useMemo会执行 第一个函数,并且会将函数的执行结果返回并进行赋值。
- useCallback常用记忆事件函数,生成记忆后的事件函数传递给子组件;useMemo更适合于经过函数计算得到一个确定的值,比如一个记忆的组件(类似于Vue中的计算属性)
基于依赖数据改变了,就会重新计算
useRef
可以得到普通的DOM节点/组件实例对象 const usernameIpt = useRef() || React.createRef()
将ref属性绑定在DOM元素或标签属性身上,然后根据usernameIpt.current.value
如何保存一个变量? 使用useState,保存一个状态变量 使用useRef
useContext
在函数式组件中,简化消费者生产者方案,解决跨级通信的方案。
const GlobalContext = React.createContext() // 创建一个context对象
useReducer
(在单个组件中实现状态的管理)外部集中管理状态数据,和后边的redux很像。
React路由 根据不同的url地址展示不同的组件/内容
npm i react-router-dom
(默认就是6版本)
基本语法
js
import { BrowserRouter, Routes, Route } from 'react-router-dom'
export default class App extends Component {
render() {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<Film />} />
<Route path='/film' element={<Film />} />
<Route path='/cinema' element={<Cinema />} />
<Route path='/center' element={<Center />} />
<Route path='*' element={<NotFound />} />
</Routes>
</BrowserRouter>
)
}
}
重定向
-
(1)使用Navigate导航组件替代
<Route path='/' element={<Navigate to="/film" />} />
-
(2)自定义Redirect组件
js
function Redirect({to}){
const navigate = useNavgate()
useEffect(()=>{
navigate(to,{ replace : true} }
})
return null
}
<Route path='/film' element={<Redirect to="/film "/>} />
二级路由
【注】:如果想要在Route组件中渲染二级路由组件,需要将单标签写法的修改成双标签 <Route></Route>
,然后在二级路由中,使用path和element标识对应的路由。
- Step1:
js
<Routes>
<Route path='/about' element={<About />} >
<Route path='/about/news' element={<News />} />
<Route path='/about/message' element={<Message />} />
</Route>
<Route path='/home' element={<Home />} >
<Route path='/home/news' element={<News />} />
<Route path='/home/message' element={<Message />} />
</Route>
</Routes>
- Step2:在一级路由组件中使用
<Outlet/>标签
进行占位;
(<Outlet/>标签
就是用来占位的,之后插入路由组件,类似于Vue中的RouterView
)
index 显示一级路由中的默认二级路由
当存在有多个自己路由时,当无法确定匹配哪一个自己路由,此时需要用到index,即默认匹配哪一个二级路由。
js
<Routes>
<Route path='/film' element={<Film/>} >
// 即当匹配到Film组件时,二级路由默认显示NowPlaying组件
<Route index element={<Navigate to="/film/nowplaying"/>} />
<Route path="nowplaying" element={<NowPlaying/>} />
<Route path="comingsoon" element={<ComingSoon/>} />
</Route>
</Routes>
声明式导航和编程式导航
- 声明式导航:类似一个a标签,点击跳转(增加一个active的类名)
<link to="/center">个人中心<link/>
<NavLink to="/center">个人中心<NavLink/> 有高亮效果
- 编程式导航:通过编写代码实现跳转
使用useNavigate(),返回一个函数用来实现编程式导航。 可以更改页面的URL,替代组件。
js
let navigate = useNavigate()
// 通过注册点击事件中,实现页面跳转
<li key={item.id} onClick={()=>{ navigate('/center') }}>{ item.name }</li>
路由传参
- (1)query方式,通过查询字符串的方式
/test?id=${id}&page=${page}
获取当前url中的查询字符串:useSearchParams(),得到一个数组
js
// 数组中第一个元素:对象(查询参数的键值对)
// 数组中第二个元素:方法,修改查询参数
let [ searchParams , setSearchParams] = useSearchParams();
let id = searchParams.get('id'); // 通过get方法获取
- (2)params方式,使用useParams() ,得到params对象
【注】:在params传参方式中,要先使用占位符进行占位!
js
// 占位 /test/:id
const params = useParams()
// 得到的params就是一个对象
js
import {BrowserRouter, HashRouter} from 'react-router-dom'
对应两种路由模式, 如果是BrowserRouter,会和后端进行一个沟通,先去后端查找,如果没有的话,才会在前端查找
路由懒加载
问题: 所有路由组件代码是打包在一块的, 打开首页就会加载, 但我们开始只需要看到首页路由的效果, 也就是只需要执行首页路由组件代码
解决: 对路由组件进行懒加载处理
深入理解 对路由组件进行拆分/单独打包 => import函数动态引入 访问路由时才去后台加载对应的打包文件 => lazy函数 指定loading界面 =>
首页加载速度过慢,采用路由懒加载方案
js
import {lazy, Suspense} from 'react'
// 懒加载动态引入组件
const About = lazy(() => import('../pages/About'))
// 路由表
{
element: <Suspense fallback={<div>正在加载中...</div>}>
<About />
</Suspense>
}
const lazyload = (path)=>{
const Comp = lazy(()=>import(path))
return (
<Suspense>
<Comp />
</Suspense>
)
}