文章目录
一、setState
1.1 更新数据
setState是异步更新数据
// App.js
import React from 'react'
class App extends React.Component{
state = {
count:1
}
handleClick = (e)=>{
this.setState({
count: this.state.count + 1
})
// 输出旧值
console.log(this.state.count)
}
handleClick2 = (e)=>{
this.setState({
count: this.state.count + 1
})
// 输出旧值
console.log(this.state.count)
// 旧值+1
this.setState({
count: this.state.count + 1
})
console.log(this.state.count)
}
render(){
return (
// 多次调用setState,只会render 1次
console.log("render");
<div>
{this.state.count}<br/>
<button onClick={this.handleClick}>add+1</button><br/>// + 1
<button onClick={this.handleClick2}>add+1+1</button>// 仍是 +1
</div>
)
}
}
export default App
1.2 回调的setState
handleClick = (e)=>{
// 也是异步的
this.setState((state,props)=>{
return{
count: state.count + 1
}
})
this.setState((state,pros)=>{
console.log("第二次调用",state.count)
return{
count: state.count + 1
}
})
// 同时因为异步,该输出会先于前面
console.log(this.state.count)
}
handleClick = (e)=>{
this.setState(
(state,props)=>{
return{
count: state.count + 1
}
},
()=>{
console.log("状态更新后的值",this.state.count)
}
)
console.log("状态更新前的值",this.state.count)
}
1.3 组件的更新
setState作用
- 修改state
- 更新组件(UI)
组件更新会更新自己以及自己的所有子节点。
父节点和兄弟节点不会更新。
二、JSX语法的转化过程
JSX->createElement()->React元素
JSX
const element = (
<h1 className="greeting">
Hello JSX!
</h1>
)
createElement()
const element = React.createElement(
'h1',
{className:'greeting'},
'Hello JSX!'
)
React元素
const element = {
type:'h1',
props:{
className: 'greeting'
children: 'Hello JSX!'
}
}
三、组件性能的优化
3.1 减轻state
state:只存储与组件渲染相关的数据
不做渲染的数据不要放在state中,直接放在this之中即可
3.2 避免不必要的重新渲染
组件的更新策略,会导致不必要的渲染也会发生(子组件没有变化)
使用 shouldComponentUpdate(nextProps,nextState)
- 返回true就会渲染
- 返回false就不会渲染
// App.js
import React from 'react'
class App extends React.Component{
state = {
count:1
}
handleClick = (e)=>{
this.setState((state,props)=>{
return {
count: state.count + 1
}
},
()=>{
console.log(this.state.count)
}
)
}
/**
* @param nextProps 最新的props
* @param nextState 最新的状态
*/
shouldComponentUpdate(nextProps,nextState){
console.log(nextProps)
console.log(nextState)
return false
}
render(){
return (
<div>
{this.state.count}<br/>
<button onClick={this.handleClick}>add+1</button>
</div>
)
}
}
export default App
随机数示例略。
对于可能有多个值会变化的情况,我们可以将它们分到子组件去,然后通过子组件去检测是否需要更新。同时这样更新的范围也会小很多。
3.3. 纯组件
React.PureComponent与React.Component功能相似
PureComponent自动实现了shouldComponentUpdate,不需要手动比较
内部通过分别比较前后两侧props和state的值,来决是否重新渲染组件
示例略。
shallow compare 浅对比
(可以通过:浅拷贝和深拷贝来理解)
官方文档说明
大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。当数据结构很复杂时,情况会变得麻烦。
例如,你想要一个 ListOfWords 组件来渲染一组用逗号分开的单词。它有一个叫做 WordAdder 的父组件,该组件允许你点击一个按钮来添加一个单词到列表中。以下代码并不正确:
class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}
class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['marklar']
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 这部分代码很糟,而且还有 bug
const words = this.state.words;
// 不会生成新的数组words
words.push('marklar');
this.setState({words: words});
}
render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}
问题在于 PureComponent 仅仅会对新老 this.props.words 的值进行简单的对比。由于代码中 WordAdder 的 handleClick 方法改变了同一个 words 数组,使得新老 this.props.words 比较的其实还是同一个数组。即便实际上数组中的单词已经变了,但是比较结果是相同的。可以看到,即便多了新的单词需要被渲染, ListOfWords 却并没有被更新。
可以用不可变数据解决。(也就是避免产生对象、引用)可以参考官方文档的不可变数据部分。
用如下方式获取一个新的数组words
handleClick() {
this.setState(state => ({
words: [...state.words, 'marklar'],
}));
};
或
handleClick() {
this.setState(state => ({
words: state.words.concat(['marklar'])
}));
}
用如下方式获取一个新的对象colormap
function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}
如下方法不会生成新的对象colormap
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}
function updateColorMap(colormap) {
colormap.right = 'blue';
}
3.4 虚拟DOM和Diff算法
React更新视图的思想是:只要state变化就重新渲染和视图
DOM元素需要更新时,也是部分更新。
React使用虚拟DOM配合Diff算法做到的。
虚拟DOM本质上是一个JS对象,用来描述你希望在屏幕上看到的(UI)内容。也即是我们前面提到的React元素。
- 初次渲染时,React会根据初始state(Model)创建一个虚拟DOM对象(树)
- 根据虚拟DOM生成生成真正的DOM,渲染到页面中
- 当数据变化后,重新根据新的数据,创建新的虚拟DOM对象(树)
- 与上一次得到的虚拟DOM对象,使用DIFF算法对比,得到需要更新的内容
- 最终,React只将变化的内容更新大DOM中,重新渲染到页面中
render方法调用,并不意味者浏览器重新渲染,仅仅说明进行DIFF
四、路由
4.1 路由介绍
现代前端应用大多数都是SPA(单页面应用程序),也就是只有一个HTML页面程序。
为了有效的使用单个页面来管理原来多个页面功能,前端路由应运而生。
- 前端路由的功能:让用户从一个视图(页面)导航到另一个视图(页面)
- 前端路由是一套映射规则,在React中,是URL路径与组件对应关系
- 使用React路由简单来说,就是配置路径和组件(配对)
4.2 路由使用步骤
注意,视频中使用的是V5版本,笔者这里使用的V6,因此将不会完全按照视频中的来做。
npm install react-router-dom
可参考文档-第三方库-路由
也可以使用视频中的版本
npm install react-router-dom@5.2.0
导入路由的三个核心组件: Router/Route/Link 以及V6的Routes
import React from 'react';
import ReactDOM from 'react-dom/client';
import {BrowserRouter as Router,Route,Routes,Link} from 'react-router-dom'
const First = ()=>(
<p>页面一内容</p>
)
const App = () => {
return(
<Router>
<div className="App" >
<h1>React路由基础</h1>
{/* 指定入口,导航 */}
<Link to="/first">页面一</Link>
<Routes>
{/* 指定路由出口,内容 */}
<Route path='/first' element={<First />}/>
</Routes>
</div>
</Router>
)
}
const reactDom = ReactDOM.createRoot(document.getElementById('root'))
reactDom.render(<App/>)
4.3 常用组件
- Router组件:包裹整个应用,一个React应用只需要使用一次
常用的Router:HashRouter和BrowserRouter
HashRouter使用URL的哈希值实现
- Link组件:指定导航链接(a标签);to属性:浏览器地址栏中的pathname ,一个导航,与Rote中的Path一致。他是一种特别的Link,主要是Link每次会重新渲染,而它不会。
- Routes组件:形成路由渲染分支,渲染其下路由。(V5,Route自己负责自己)
- Route组件:指定路由展示组件相关信息。同时,Route在哪,内容就渲染在哪。
老版本:Router-Switch-Route
新版本:Router-Routes-Route
<Router>
<div className="App" >
<Routes>
{/* 指定路由出口,内容 */}
<Route path='/first' element={<First />}/>
</Routes>
<h1>React路由基础</h1>
{/* 指定入口,导航 */}
<Link to="/first">页面一</Link>
</div>
</Router>
4.4 编程式导航
import React from 'react';
import ReactDOM from 'react-dom/client';
import {BrowserRouter as Router,Route,Routes,Link} from 'react-router-dom'
import Home from './component/Home';
import Login from './component/Login';
const App = () => {
return(
<Router>
<div className="App" >
<h1>React路由基础</h1>
{/* 指定入口,导航 */}
<Link to="/login">登录页面</Link>
<Routes>
{/* 指定路由出口,内容 */}
<Route path='/login' element={<Login />}/>
<Route path='/home' element={<Home />}/>
</Routes>
</div>
</Router>
)
}
const reactDom = ReactDOM.createRoot(document.getElementById('root'))
reactDom.render(<App/>)
import React from 'react'
class Home extends React.Component{
render(){
return (<div>后台页面</div>)
}
}
export default Home
注意,新版本中,没有了history,因此navigate作为代替,但是它是一种hook,react中类组件不能用hook(因为hook目的就是为了代替类)。因此我们只能用函数的方式完成。
同时,其中填入负数,也就是回退多少页。它有第二个参数,用于传参。
import React from 'react'
import { useNavigate } from 'react-router-dom'
function Login(){
const navigate = useNavigate();
const handleLogin = () =>{
//...
// 编程式导航跳转
navigate('/home')
}
return(
<div>
<div>登录页面:</div>
<button onClick={()=>{handleLogin()}}>登录</button>
</div>
)
}
export default Login
4.5 默认路由
进入页面时匹配的路由
就是 path:/
此处login就是默认路由
<Routes>
{/* 指定路由出口,内容 */}
<Route path='/' element={<Login />}/>
<Route path='/home' element={<Home />}/>
</Routes>
4.6 模糊匹配模式
如下(笔者这里没有遇到该问题,可能是脚手架或者版本已经处理了该问题。
一般情况下,可以加一个exact <Route exact path=‘/detail’…>进行精确匹配。
path(Routes) | to(link) |
---|---|
/ | 会匹配所有的路由 |
/first | 会匹配 /first 、/first/a |
<Routes>
{/* 指定路由出口,内容 */}
<Route path='/' element={<Home />}/>
<Route path='/login' element={<Login />}/>
</Routes>
参考
[1]黑马程序员前端React视频教程,react零基础入门原理详解到好客租房项目实战
[2]官方文档
[3]第三方库-路由