第七节:React原理揭秘
- 为什么setState() 更新数据是异步的?
- 知道JSX语法的转化过程吗?
- 说出React组件的更新机制?
- 如何对React组件进行性能优化?
- React的虚拟DOM和Diff算法
1,为什么setState() 更新数据是异步的?
- setState()更新数据是异步的
- 后面的setState不依赖于前面的setState
- 可以多次调用setState, 只会触发一次重新渲染render
state = {
count: 1
}
handleClick = () => {
// 注意: setState异步更新数据的
this.setState({
count: this.state.count + 1 // 此时:count: 2
})
console.log('count', this.state.count) // 此时:count 1, 因为setState是异步的
this.setState({
count: this.state.count + 1 // 此时: count 2
})
}
如果想实现后面一个setState 依赖前面一个setState,推荐语法:
-
推荐使用: setState((state, props) => {})
参数:state: 表示最新的state
参数: props: 表示最新的props
handleClick = () => {
this.setState((state, props) => {
console.log('我是最新的:', state.count)
return {
count: state.count + 1
}
})
this.setState((state, props) => {
console.log('我是第二次的:', state.count)
return {
count: state.count + 1
}
})
console.log(this.state.count)
}
如果在一个方法里面,两次调用setState(), 推荐使用上面最新的方法,不然会有异步问题,第二次setState的值还是以前的值。
- setState的第二个参数,是一个回调函数,即:在状态更新后,也就是页面完全渲染后,会立即执行的
setState(updater, [callback])
使用:
this.setState({
(state, props) => {},
() => {console.log('这个回调函数会在状态更新后立即执行')}
})
this.setState(
(state, props) => {
console.log('我是最新的:', state.count)
return {
count: state.count + 1
}
},
() => {
console.log('状态更新之后', this.state.count)
document.title = '更新成功之后' + this.state.count
}
)
总结一下
- setState设计为异步,可以显著的提升性能。如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;
- 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步。state和props不能保持一致性,会在开发中产生很多的问题;
2, JSX 语法转化的过程
- JSX仅仅是createElement()方法的语法糖
- JSX语法是被@babel/preset-react插件编译为createElement()方法
- React元素:是一个对象,用来描述你希望在屏幕上看到的内容
graph LR
A{JSX语法}
A-->B(createElement)
B-->C{React元素}
-JSX语法
const element1 = <h1 className="greeting">Hello JSX</h1>
- createElement()方法
const element2 = React.createElement(
'h1',
{
className: 'greeting'
},
"Hello JSX"
)
-React元素
const element3 = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello JSX'
}
}
3 组件更新机制
- setState()的两个作用: 1,修改state. 2, 更新组件UI
- 过程: 父组件重新渲染时, 也会重新渲染子组件,但只会渲染当前组件以及它下面的所有子组件
4, 组件性能优化
-
减轻state: 只存储跟组件渲染相关的数据
注意:不做渲染的数据不要方法state中,比如定时器ID,对应这种需要在多个方法中用到的数据,应该放在this中
-
避免不必要的重新渲染
注意:组件更新机制里面,父组件更新会引起子组件也被更新,如果子组件没有任何变化时,也会重新渲染,如果避免不必要的重新渲染,使用钩子函数
shouldComponentUpdate(nextProps, nextState)
原理:通过返回值决定组件是否重新渲染,返回true表示重新渲染,false表示不重新渲染
触发时机:更新阶段的钩子函数,组件重新渲染之前执行。
shouldComponentUpdate —> render
shouldComponentUpdate(nextProps, nextState) { // 根据条件,决定是否重新渲染组件 // 最新的状态 console.log('最新的state', nextState) // 更新前的状态 console.log('this.state', this.state) return false }随机数案例(1)
import React from 'react'
class RanDom extends React.Component {
state = {
number: 0
}
handleClick = () => {
this.setState(() => {
return {
number: Math.floor(Math.random() * 3)
}
})
}
// 因为两次生成的随机数可能相同,如果相同,此时,不需要重新渲染
shouldComponentUpdate(nextProps, nextState) {
console.log('最新状态:', nextState, ', 当前状态:', this.state.number)
// if (nextState.number === this.state.number) {
// return false
// }
// return true
// 优化写法
return nextState.number !== this.state.number
}
render() {
console.log('render')
return (
<div>
<h1>随机数:{ this.state.number }</h1>
<button onClick={this.handleClick}>重新生成</button>
</div>
)
}
}
export default RanDom;
随机案例子组件2
class NumberBox extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
console.log('最新Props:', nextProps, ', 上一次Props:', this.props)
if (nextProps.number === this.props.number) {
return false
}
return true
}
render() {
console.log('子组件render')
return (
<h1>随机数:{ this.props.number }</h1>
)
}
}
- React组件复用 1, 思考:如果两个组件中的部分功能相似或相同,如果复用相似的功能
2, 复用相似的功能,注意复用组件的什么 ?
1, state. 2, 操作state的方法(组件状态逻辑)
3,如何复用?
方式1: render props模式。 方式2: 高阶组件(HOC)
注意:这两种方式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式
-
纯组件
纯组件的内部对比是:shallow compare(浅层对比)
对于引用类型来说,只比较对象的引用地址是否相同,所以:state或着props中属性值为引用类型时,应该创建新数据,不要直接修改原数据
// 对象正确做法 const newObj = { ...state.obj, number: 2} setState({obj: newObj}) // 数组正确做法 // 不要使用数组的push / unshift 等直接修改当前数组的方法 // 应该用concat 或 slice 等这些返回新数组的方法,或着下面的方法 this.setState({ list: [...this.state.list, {新数据}] })
5, 虚拟DOM和Diff算法
- react 更新视图的思想是: 只要state变化就重新渲染视图
- 特点: 思路清晰
- 问题: 组件中只有一个DOM元素需要更新时,也得把整个组件的内容重新渲染到页面中
- 理想状态: 部分更新,只要更新变化的地方
- 问题: React 是如何做到部分更新的? 虚拟DOM配合Diff算法
虚拟DOM: 本质上就是一个JS对象,用了描述你希望在屏幕上看到的内容(UI),
执行的过程中;
1, 初次渲染时, React 会根据初始state(Model),创建一个虚拟DOM对象,
2,React会根据虚拟DOM生成真正的DOM, 渲染到页面中
3,当数据变化后(setState()), 重新根据新的数据,创建新的虚拟DOM对象树
4,与上一次得到的虚拟DOM对象,使用Diff算法对比,找不同点,得到需要更新的内容
5, 最终,React只将变化的内容更新到DOM中, 重新渲染到页面
注意:render方法调用并不意味着浏览器中的重新渲染
// render 方法调用仅仅说明要进行diff算法
6,总结
1, 从工作角度,应用第一,原理第二
2,原理有助于更好地理解React的自身运行机制
3,setState() 异步更新数据
4,父组件更新导致子组件更新,纯组件提升性能
5,思路清晰简单为前提,虚拟DOM和Diff保效率
6,虚拟DOM —> state + JSX 共同构成
7,虚拟DOM的真正价值从来都不是性能,而是让他脱离了浏览器环境限制
第八节:React路由
1, React 基础知识
现代的前端应用大多都是SPA,单页应用程序, 也就是只有一个HTML页面的应用程序,因为它的用户体验更好,对服务器的压力更小,所以更受欢迎。为了有效的使用单页面来管理原来多页面的功能,前端路由应运而生。
- 前端路由的功能: 让用户从一个视图导航到另一个视图
- 前端路由是一套映射规则,在React中,是URL路径与组件的对应关系
- 使用React路由简单来说,就是配置路径和组件
2, 路由使用步骤
1, 安装
npm install react-router-dom
yarn add react-router-dom
2, 导入路由的三个核心组件: Router/Route/Link
import {BrowserRouter as Router, Route, Link } from 'react-router-dom'
3, 使用Router 组件包裹整个应用(重要)
<Router>
<div className="App">
xxx
</div>
</Router>
4, 使用Link组件作为导航菜单(路由入口)
<Link to="/first"> 页面一 </Link>
5, 使用Route组件配置路由规则和要展示的组件(路由出口)
const First = () => <p> 页面一的页面内容 </p>
<Router>
<div>
<Link to="/first">页面一</Link>
<Route path="/first" component={First}></Route>
</div>
</Router>
3, 常用组件说明
- Router 组件: 包裹整个应用,一个React应用只需要使用一次
- 两种常用Router: HashRouter 和 BrowserRouter
- HashRouter: 使用URL的哈希值实现的(localhost:3000/#/first)
- 推荐:BrowserRouter, 使用H5的history API 实现的(localhost:3000/first)
- Link组件: 用于指定导航链接(a标签)
- Route组件, 指定路由展示组件相关信息,Route写在那里,组件就展示在那个位置
4, 路由的执行过程
1, 点击Link组件(a标签),修改了浏览器地址栏中的URL
2,React路由监听到地址栏URL的变化
3,React路由内部遍历所有Route组件, 使用路由规则path与pathname进行匹配
4, 当路由规则path能够匹配地址栏中的pathname时, 就在当前位置上展示该Route组件内容
5, 编程式导航
- 场景: 点击登陆按钮,登陆成功之后,通过代码跳转到后台首页
- 编程式导航: 通过JS代码来实现页面跳转
- history是React路由提供的,用户获取浏览器历史记录的相关信息
- push(path): 跳转到某个页面,参数path表示要跳转的路径
- go(n): 前进或后退到某个页面,参数n表示前进或后退页面数量
// 类组件
class Login extends Component {
handleLogin = () => {
this.props.history.push('/home')
}
}
// 函数式组件
const Home = (props) => {
const handleBack = () => {
props.history.go(-1)
}
return (
<div>
<h2>我是后端首页</h2>
<button onClick={handleBack}></button>
</div>
)
}
6, 默认路由
- 默认路由: 表示进入页面时就会匹配的路由
- 默认路由path = "/"
<Route path="/" component={Home} />
7, 匹配模式
(1)模糊匹配模式
- React 默认情况下,路由配置规则是: 模糊匹配模式
- 模糊匹配规则: 只有pathname以path 开头就会匹配成功
Pathname: 代表Link组件的to属性,也就是:location.pathname
Path: 代表Route组件的path属性
path="/" 配置所有pathname
path="/first" 配置: /first 或 /first/a 或 /first/b/c
(2)精准匹配模式
- 给Route组件添加exact属性,就让其变成精确匹配模式
- 精确匹配: 只有当path和pathname完全匹配时,才会展示该路由
// 此时, 该组件只能匹配:pathname="/" 这一种情况
<Route exact path="/" component />
8 嵌套路由
使用步骤:
1, 设置嵌套路由的path, 格式需要以父路由path开头
<Router>
<div>
<Route path="/home" component={Home} />
</div>
</Router>
const Home = () => {
<div>
<Route path="/home/news" component={News} />
</div>
}
9, 总结
1, React 路由可以有效的管理多个视图实现SPA
2, Router组件包裹整个应用, 只需要使用一次
3,Link组件是入口, Route组件是出口
4,通过props.history实现编程式导航
5,默认是模糊匹配,添加exact变成精确匹配
6, React 路由的一切都是组件,可以像思考组件一样思考路由