官方文档中提到
1.声明式编程:
声明式编程是目前整个大前端开发的模式:Vue、React、Flutter、SwiftUI;
它允许我们只需要维护自己的状态,当状态改变时,React可以根据最新的状态去渲染我们的UI界面;
2.组件化开发:
组件化开发页面目前前端的流行趋势,我们会将复杂的界面拆分成一个个小的组件;
如何合理的进行组件的划分和设计也是后面我会讲到的一个重点;
3.多平台适配:
2013年,React发布之初主要是开发Web页面;
2015年,Facebook推出了ReactNative,用于开发移动端跨平台;(虽然目前Flutter非常火爆,但是还是有很多公司在使用
ReactNative);
2017年,Facebook推出ReactVR,用于开发虚拟现实Web应用程序;(VR也会是一个火爆的应用场景);
组件化
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<div id="app">div>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin>script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin>script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js">script>
<script type="text/babel">
class App extends React.Component {
constructor() {
super()
this.state = {
message: 'Hello React'
}
//这样绑定this也行
// this.onClick=this.onClick.bind(this)
}
onClick() {
this.setState({
message: 'Hello Vue'
})
}
render() {
// 用bind绑定当前class中的this,不然在app.render这个组件时使用的this是undefined,
//因为class中默认是严格模式,调用的时候相当于拿出这个onClick调用,得到的是undefined
箭头函数中的this指向外部环境,此处为 render() 方法,
render()方法中的this为组件实例,可以获取到setState()
return (<div><h1>{this.state.message}</h1>
<button onClick={this.onClick.bind(this)}>点击修改</button></div>)
}
}
const app = ReactDOM.createRoot(document.querySelector('#app'))
//这里渲染的时候会去new APP出一个实例对象,然后调用onClick方法,所以不绑定this的话会出错
app.render(<App />)
script>
body>
html>
在类组件中绑定的函数使用this为什么会出现问题?
因为在app.render这个组件时使用的this是undefined,class中默认是严格模式,调用的时候相当于拿出这个onClick调用,得到的是undefined
class Foo {
constructor(name){
this.name = name;
}
display(){
console.log(this.name);
}
}
var foo = new Foo('Saurabh');
foo.display = foo.display.bind(foo);
foo.display(); // Saurabh
var display = foo.display;
display(); // Saurabh
1.直接写箭头函数
class App extends React.Component {
// 第一种方式
// constructor() {
// super()
// this.state = {
// count: 0
// }
// }
// 第二种方式
state = {
count: 0
}
// 只需要箭头函数就可以
addJian=() => {
this.setState ({
count: this.state.count + 1
})
}
render() {
return (
<div>
<h1>计数器:{this.state.count}</h1>
<button onClick={this.addJian}>+1</button>
</div>
)
}
}
ReactDOM.render(<App/>,document.getElementById('root'))
2.利用箭头函数自身不绑定this的特点
class App extends React.Component {
state = {
count: 0,
};
// 事件处理程序
onIncrement() {
console.log('事件处理程序中的this:', this)
this.setState({
count:this.state.count + 1
})
}
render() {
return (
<div>
<h1> 计数器: {this.state.count}</h1>
{/* 箭头函数中的this指向外部环境,此处为 render() 方法 */}
{/* render() 方法中的this为组件实例,可以获取到setState() */}
<button onClick = { () => {this.onIncrement()}}>+1</button>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
3.Function.prototype.bind()
class App extends React.Component{
constructor() {
//super() 语法要求
super()
this.state = {
count:0
}
//bind改变this指向
this.onIncrement = this.onIncrement.bind(this)
}
onIncrement() {
this.setState ({
count:this.state.count + 1
})
}
render() {
return (
<div>
<h1>计数器:{this.state.count}</h1>
<button onClick={this.onIncrement}>+1</button>
</div>
)
}
}
ReactDOM.render(<App/>,document.getElementById('root'))
JSX是什么?
JSX是一种JavaScript的语法扩展(eXtension),也在很多地方称之为JavaScript XML,因为看起就是一段XML语法;
它用于描述我们的UI界面,并且其完成可以和JavaScript融合在一起使用;
它不同于Vue中的模块语法,你不需要专门学习模块语法中的一些指令(比如v-for、v-if、v-else、v-bind);
React 团队之所以认为模板不是最佳实现,原因在于,React 团队认为模板分离了技术栈,分散了组件内的关注点。其次,模板还会引入更多的概念,类似模板语法、模板指令等。
React认为渲染逻辑本质上与其他UI逻辑存在内在耦合
比如UI需要绑定事件(button、a原生等等);
比如UI中需要展示数据状态;
比如在某些状态发生改变时,又需要改变UI;
JSX的顶层只能有一个根元素,所以我们很多时候会在外层包裹一个div元素(或者使用后面我们学习的Fragment);
为了方便阅读,我们通常在jsx的外层包裹一个小括号(),这样可以方便阅读,并且jsx可以进行换行书写;
JSX中的标签可以是单标签,也可以是双标签;
✓ 注意:如果是单标签,必须以/>结尾;
JSX嵌入变量作为子元素
情况一:当变量是Number、String、Array类型时,可以直接显示
情况二:当变量是null、undefined、Boolean类型时,内容为空;
✓ 如果希望可以显示null、undefined、Boolean,那么需要转成字符串;
✓ 转换的方式有很多,比如toString方法、和空字符串拼接,String(变量)等方式;
情况三:Object对象类型不能作为子元素(not valid as a React child)
◼ JSX嵌入表达式
运算表达式
三元运算符
执行一个函数
在事件执行后,我们可能需要获取当前类的对象中相关的属性,这个时候需要用到this
如果我们这里直接打印this,也会发现它是一个undefined
◼ 为什么是undefined呢?
原因是btnClick函数并不是我们主动调用的,而且当button发生改变时,React内部调用了btnClick函数;
而它内部调用时,并不知道要如何绑定正确的this;
例:
class App extends React.Component {
constructor() {
super()
}
btnclick() {
console.log(this)
}
render() {
return (
<div>
<button onClick={this.btnclick}>456</button>
</div>
)
}
}
相当于如下代码
class中默认是严格模式,调用的时候相当于拿出这个onClick调用,得到的是undefined
class Person{
add(){
console.log(this)
}
}
const p=new Person();
const pp=p.add
pp()
解决方式
方案一:bind给btnClick显示绑定this
方案二:使用 ES6 class fields 语法
方案三:事件监听时传入箭头函数(个人推荐)
class App extends React.Component {
constructor() {
super()
this.btnclick3=this.btnclick3.bind(this)
}
btnclick() {
console.log(this)
}
//Class Fields,就是在类种定义箭头函数
btnclick2 = () => {
console.log(this)
}
btnclick3() {
console.log(this)
}
render() {
return (
<div>
{/* 重点掌握 */}
<button onClick={() => this.btnclick()}>第一种方式</button>
<button onClick={this.btnclick2}>第二种方式</button>
<button onClick={this.btnclick3}>第三种方式</button>
</div>
)
}
}
◼ 实际上,jsx 仅仅只是 React.createElement(component, props, …children) 函数的语法糖。
所有的jsx最终都会被转换成React.createElement的函数调用。
createElement需要传递三个参数:
◼ 参数一:type
当前ReactElement的类型;
如果是标签元素,那么就使用字符串表示 “div”;
如果是组件元素,那么就直接使用组件的名称;
◼ 参数二:config
所有jsx中的属性都在config中以对象的属性和值的形式存储;
比如传入className作为元素的class;
◼ 参数三:children
存放在标签中的内容,以children数组的方式进行存储;
当然,如果是多个元素呢?React内部有对它们进行处理,处理的源码在下方
我们知道默认jsx是通过babel帮我们进行语法转换的,所以我们之前写的jsx代码都需要依赖babel。
这个manifest.json文件就是做PWA的
什么是PWA?
就是可以将网页变成作为app图标,点击后即可打开这个网页
组件的划分方式
函数式组件和类组件
react中事件绑定采用小驼峰(btnClick)
因为脚手架是基于node和webpack开发的,当我们想要自己对react脚手架进行webpack配置时,我们并没有在目录结构中看到任何webpack相关的内容?
原因是React脚手架将webpack相关的配置隐藏起来了(其实从Vue CLI3开始,也是进行了隐藏);
如果我们希望看到webpack的配置信息,应该怎么来做呢?
我们可以执行一个package.json文件中的一个脚本:“eject”: “react-scripts eject”
这个操作是不可逆的,所以在执行过程中会给与我们提示;
执行npm run eject
不过一般对webpack不是非常熟悉的人不建议这样做
第二种方法是使用craco这个库进行合并webpack的配置
初次挂载:以类组件为例,在渲染时首先会执行组件的constructor,然后执行类组件的render函数,然后执行componentDidMount函数
更新:在进行new Props 或setState或force时,会调用render函数,然后调用componentDidUpdate函数
销毁:会执行componentWillUnmount
父传子:
父
子
类型校验和默认值,需单独引入propTypes这个包
在react中想要实现插槽有两种方式
1.使用children属性,我们知道一个组件其实就是react.createElement的语法糖,而这个语法糖函数接收三个参数,元素,属性,子节点
那么当我们在组件插入元素时,可以在this.props中解构出children属性,如果插入的是一个元素就是一个对象,如果插入的是多个元素就是一个数组
父组件
子组件
方式二:使用props传jsx
父组件
子组件
爷
孙
在函数组件中
孙
当Home组件在Provide外面。但是又想获取数据,可以在默认值中获取
defalult value
开发中我们并不能直接通过修改state的值来让界面发生更新:
因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;
React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;
我们必须通过setState来告知React数据已经发生了变化;
react18以前:
在组件生命周期或React合成事件中,setState是异步;
在setTimeout或者原生dom事件中,setState是同步;
react18以后:
在React18之后,默认所有的操作都被放到了批处理中(异步处理)。
setState接收一个新的状态
该接收到的新状态不会被立即执行,而是存入到pendingStates(等待队列)中
判断isBatchingUpdates(是否是批量更新模式)
1>. isBatchingUpdates: true 将接收到的新状态保存到dirtyComponents(脏组件)中
2>. isBatchingUpdates: false 遍历所有的dirtyComponents, 并且调用其 updateComponent方法更新pendingStates中的state 或者props。执行完之后,回将isBatchingUpdates: true。
什么情况下isBatchingUpdates为true或者false?
在 react 的同步代码的多次 setState 会被 batch 为一次更新
在一个异步的事件循环里面多次 setState,react 不会 batch
react18以前:
this.setState({ number: this.state.number + 1 });
console.log(this.state);//0
this.setState({ number: this.state.number + 1 });
console.log(this.state);//0
setTimeout(() => {
this.setState({ number: this.state.number + 1 });
console.log(this.state);//2
this.setState({ number: this.state.number + 1 });
console.log(this.state);//3
}, 0);
同步代码两个setState放到一个批处理执行,由于执行的时候两个setState中指向的number都是0,所以两个set结束是1,打印的时候是异步的所以打印两个0,到settimeout执行时,代码变成同步的,所以执行一次打印一次number就加一
react18以后:
this.setState({ number: this.state.number + 1 });
console.log(this.state);//0
this.setState({ number: this.state.number + 1 });
console.log(this.state);//0
setTimeout(() => {
this.setState({ number: this.state.number + 1 });
console.log(this.state);//1
this.setState({ number: this.state.number + 1 });
console.log(this.state);//1
}, 0);
由于react18以后都是异步的,但是这四个setState不会同时进入一次批处理,因为事件循环机制,所以前两个set是一个批处理,在setTimeout中的两个set又是一次批处理,前两次同理打印是0,最后number为1,后两个也是异步的,所以打印两个1,最终结果是2
将setState中第一个参数写为函数,不要写对象
例如
第一个参数prevState是是上一次的状态,所以这样执行完结果是2,但是两次打印都是2,因为是批处理获取结果
this.setState((prevState, props) => {
return { number: prevState.number + 1 };
}, () => {
console.log(this.state);//2
});
this.setState((prevState, props) => {
return { number: prevState.number + 1 };
}, () => {
console.log(this.state);//2
});
把第二个setState加上宏任务和微任务包裹即可,原理是利用事件循环进行多次批处理
this.setState((prevState, props) => {
return { number: prevState.number + 1 };
}, () => {
console.log(this.state);//1
});
setTimeout(() => {
this.setState((prevState, props) => {
return { number: prevState.number + 1 };
}, () => {
console.log(this.state);//2
})
}, 0)
1.setState设计为异步,可以显著的提升性能;
如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;例如在一个函数里多次调用setState,最终只会更新一次,因为有批处理
2.如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;
state和props不能保持一致性,会在开发中产生很多的问题;
◼ 方式一:setState的回调
setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;
格式如下:setState(partialState, callback)
◼ 方式二:我们也可以在生命周期函数
1.我们使用之前的一个嵌套案例:
在App中,我们增加了一个计数器的代码;
当点击+1时,会重新调用App的render函数;
而当App的render函数被调用时,所有的子组件的render函
数都会被重新调用;
◼ 那么,我们可以思考一下,在以后的开发中,我们只要是修改了
App中的数据,所有的组件都需要重新render,进行diff算法,
性能必然是很低的:
事实上,很多的组件没有必须要重新render;
它们调用render应该有一个前提,就是依赖的数据(state、
props)发生改变时,再调用自己的render方法;
◼ 如何来控制render方法是否被调用呢?
通过shouldComponentUpdate方法即可;
◼ React给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有
返回值:
◼ 该方法有两个参数:
参数一:nextProps 修改之后,最新的props属性
参数二:nextState 修改之后,最新的state属性
◼ 该方法返回值是一个boolean类型:
返回值为true,那么就需要调用render方法;
返回值为false,那么久不需要调用render方法;
默认返回的是true,也就是只要state发生改变,就会调用render方法;
例:
父组件
对比state
shouldComponentUpdate(nextProps, newState) {
// App进行性能优化的点
if (this.state.message !== newState.message || this.state.counter !== newState.counter) {
return true
}
return false
}
子组件
对比state和props
shouldComponentUpdate(newProps, nextState) {
// 自己对比state是否发生改变: this.state和nextState
if (this.props.message !== newProps.message) {
return true
}
return false
}
如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。
我们来设想一下shouldComponentUpdate中的各种判断的目的是什么?
props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false;
◼ 事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?
将class继承自PureComponent。
PureComponent原理是内置了shouldComponentUpdate函数,在函内部通过浅层比较来判断是否render源码中函数叫shallowEqual
我们在修改state中的数据时,不能直接修改,比如
this.state = {
books: [
{ name: "你不知道JS", price: 99, count: 1 },
{ name: "JS高级程序设计", price: 88, count: 1 },
{ name: "React高级设计", price: 78, count: 2 },
{ name: "Vue高级设计", price: 95, count: 3 },
],
}
下面这种做法不会重新执行render函数,因为是浅比较
const newBook = { name: “Angular高级设计”, price: 88, count: 1 }
// 1.直接修改原有的state, 重新设置一遍
// 在PureComponent是不能引入重新渲染(re-render)
this.state.books.push(newBook)
this.setState({ books: this.state.books })
正确的做法
// 2.赋值一份books, 在新的books中修改, 设置新的books
const books = [...this.state.books]
books.push(newBook)
this.setState({ books: books })
◼ 在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作:
管理焦点,文本选择或媒体播放;
触发强制动画;
集成第三方 DOM 库;
我们可以通过refs获取DOM;
◼ 如何创建refs来获取对应的DOM呢?
◼ 方式二:传入一个对象
对象是通过 React.createRef() 方式创建出来的;
使用时获取到创建的对象其中有一个current属性就是对应的元素;
函数式组件是没有实例的,所以无法通过ref获取他们的实例:函数组件想要获取dom应该用useref或者forwardref
受控组件定义:
在react中,受控组件指的是通过回调函数来更新当前值的组件
例一个表单, 数据是由 React 管理的就是: 受控组件
受控组件的更新流程:
1,可以通过在初始state中设置表单的默认值
2,每当表单的值发生变化时,调用onChange事件处理器,
3,事件处理器通过事件对象e拿到改变后的状态,改变state;
1.基本使用
this.setState({
message: "你好啊, 李银河"
})
2.setState可以传入一个回调函数
好处一: 可以在回调函数中编写新的state的逻辑
好处二: 当前的回调函数会将之前的state和props传递进来
this.setState((state, props) => {
// 1.编写一些对新的state处理逻辑
// 2.可以获取之前的state和props值
console.log(this.state.message, this.props)
return {
message: "你好啊, 李银河"
}
})
3.setState在React的事件处理中是一个异步调用
如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码
那么可以在setState中传入第二个参数: callback
this.setState({ message: "你好啊, 李银河" }, () => {
console.log("++++++:", this.state.message)
})
那么说明是高阶组件呢?
高阶组件的英文是 Higher-Order Components,简称为 HOC;
官方的定义:高阶组件是参数为组件,返回值为新组件的函数;
◼ 我们可以进行如下的解析:
首先, 高阶组件本身不是一个组件,而是一个函数;
其次,这个函数的参数是一个组件,返回值也是一个组件;
高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式;
以前学的memo和forwardref就是高阶组件的使用
某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM
元素上的)。
◼ Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
第二个参数(container)是一个 DOM 元素;
在之前的开发中,我们总是在一个组件中返回内容时包裹一个div元素:
我们又希望可以不渲染这样一个div应该如何操作呢?
使用Fragment,这样就不会渲染上面的div了,只会渲染id为root的div
◼ StrictMode 是一个用来突出显示应用程序中潜在问题的工具:
与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
它为其后代元素触发额外的检查和警告;
严格模式检查仅在开发模式下运行;它们不会影响生产构建;
◼ 可以为应用程序的任何部分启用严格模式:
严格模式检查的是什么?
1.识别不安全的生命周期:
2.使用过时的ref API
3.检查意外的副作用
style 接受一个采用小驼峰命名属性的 JavaScript 对象,而不是 CSS 字符串;
并且可以引用state中的状态来设置相关的样式;
内联样式的优点:
1.内联样式, 样式之间不会有冲突
2.可以动态获取当前state中的状态
◼ 内联样式的缺点:
1.写法上都需要使用驼峰标识
2.某些样式没有提示
3.大量的样式, 代码混乱
4.某些样式无法编写(比如伪类/伪元素)
普通的css我们通常会编写到一个单独的文件,之后再进行引入。
但是普通的css都属于全局的css,样式之间会相互影响;
◼ 这种编写方式最大的问题是样式之间会相互层叠掉;
css modules并不是React特有的解决方案,而是所有使用了类似于webpack配置的环境下都可以使用的。
如果在其他项目中使用它,那么我们需要自己来进行配置,比如配置webpack.config.js中的modules: true等。
◼ React的脚手架已经内置了css modules的配置:
.css/.less/.scss 等样式文件都需要修改成 .module.css/.module.less/.module.scss 等;
之后就可以引用并且进行使用了;
◼ css modules确实解决了局部作用域的问题,也是很多人喜欢在React中使用的一种方案。
◼ 但是这种方案也有自己的缺陷:
引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的;
所有的className都必须使用{style.className} 的形式来编写;
不方便动态来修改某些样式,依然需要使用内联样式的方式;
index.module.scss文件
.root {
display: 'block';
position: 'absolute';
// 此处,使用 global 包裹其他子节点的类名。此时,这些类名就不会被处理,在 JSX 中使用时,就可以用字符串形式的类名
// 如果不加 :global ,所有类名就必须添加 styles.title 才可以
:global {
.title {
.text {
}
span {
}
}
.login-form { ... }
}
}
组件:
import styles from './index.module.scss'
const 组件 = () => {
return (
{/* (1) 根节点使用 CSSModules 形式的类名( 根元素的类名: `root` )*/}
{styles.root}>
{/* (2) 所有子节点,都使用普通的 CSS 类名*/}
"title">
"text">登录
登录
)
}
因为在react的脚手架中缺少对less的loader,所以我们要使用这样一个库叫craco
在与src文件夹同级下创建一个craco.config.js
craco.config.js
const CracoLessPlugin = require('craco-less');
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { '@primary-color': '#1DA57A' },
javascriptEnabled: true,
},
},
},
},
],
};
先npm install @craco/craco,如果报错了,因为我们的react脚手架版本太高了
那么执行npm install @craco/craco@alpha安装alpha版本
package.json 的修改如下
“scripts”: {
然后npm install craco-less@alpha
然后less的配置就可以生效了
事实上CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态;
CSS-in-JS通过JavaScript来为CSS赋予一些能力,包括类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用、动态修
改状态等等;
虽然CSS预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点;
所以,目前可以说CSS-in-JS是React编写CSS最为受欢迎的一种解决方案;
安装:npm install styled-components
使用:
◼ styled-components的本质是通过函数的调用,最终创建出一个组件:
这个组件会被自动添加上一个不重复的class;
styled-components会给该class添加相关的样式;
另外,它支持类似于CSS预处理器一样的样式嵌套:
支持直接子代选择器或后代选择器,并且直接编写样式;
可以通过&符号获取当前元素;
直接伪类选择器、伪元素等;
styled-components的本质是通过函数的调用,最终创建出一个组件:
这个组件会被自动添加上一个不重复的class;
◼ React在JSX给了我们开发者足够多的灵活性,你可以像编写JavaScript代码一样,通过一些逻辑来决定是否添加某些class:
这个时候我们可以借助于一个第三方的库:classnames
很明显,这是一个用于动态添加classnames的一个库。
理解JavaScript纯函数
函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;
在react开发中纯函数是被多次提及的;
比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是一个纯函数;
确定的输入,一定会产生确定的输出;
函数在执行过程中,不能产生副作用;
副作用概念的理解:
在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;
如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;
reducer是一个纯函数;
reducer做的事情就是将传入的state和action结合起来生成一个新的state;
reducer.js
第一个参数是state,第二个参数action是派发的dispatch
index.js
导入reducer这个纯函数,通过createStore创建出store
使用store的数据
Redux要求我们通过action来更新数据:
所有数据的变化,必须通过派发(dispatch)action来更新;
action是一个普通的JavaScript对象,用来描述这次更新的type和content;
我们能不能在派发dispatch后能自动获取数据的变动,而不用log呢
通过subscribe,传入一个回调函数,当数据发生改变后来回调,相当于watch
◼ 单一数据源
整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中:
Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;
单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改;
◼ State是只读的
唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State:
这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;
这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;
◼ 使用纯函数来执行修改
通过reducer将 旧state和 actions联系在一起,并且返回一个新的State:
随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分;
但是所有的reducer都应该是纯函数,不能产生任何的副作用;
在react中使用redux
上面这种明显很麻烦,要在挂载前用subscribe监听数据改变,并且给数据设置初始值
目前我们使用一个react-redux的库可以简化这些操作
先npm install react-redux
在index.js中引入
将store通过provide注入
Provider存在的意义相当于可以替换掉redux中的subscribe。
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from "react-redux"
import App from './App'
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
2. connect:连接UI组件和容器组件以及Redux
在组件中使用
这里会编写一个MapStateToProp函数,作用为将需要用到的store中的数据映射到组件里,而不是直接映入store的全部数据,提高性能,然后使用的时候是在props中拿。
import React, { PureComponent } from 'react'
import "./App.css"
import { connect } from "react-redux"
export class App extends PureComponent {
render() {
return (
<div>
<h2 className='title'>{ this.props.num}</h2>
</div>
)
}
}
const MapStateToProps = (state) => {
return {
num:state.num
}
}
export default connect(MapStateToProps)(App)
派发dispatch,解耦
使用的时候也是通过props使用,因为addNumber也被映射到props了
const mapDispatchToProps = (dispatch) => ({
addNumber(num) {
dispatch(addNumber(num))
}
})
export default connect(mapStateToProps, mapDispatchToProps)(About)
connect是一个高阶函数,因为他接收的参数为函数,返回的是一个高阶组件,传入一个组件为参数
事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给redux来管理;
但是在redux中如何可以进行异步的操作呢?
答案就是使用中间件(Middleware);
为什么要使用中间件呢?
因为默认action接收的是一个对象,那怎么把异步请求的数据放到return的对象中呢?
我们考虑return一个函数,但是action不支持,所以我们使用中间件。
redux-thunk是如何做到让我们可以发送异步的请求呢?
我们知道,默认情况下的dispatch(action),action需要是一个JavaScript的对象;
redux-thunk可以让dispatch(action函数),action可以是一个函数;
该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;
✓ dispatch函数用于我们之后再次派发action;
✓ getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;
使用redux-thunk
actionCreators.js
export const changeBannersAction = (banners) => ({
type: actionTypes.CHANGE_BANNERS,
banners
})
export const fetchHomeMultidataAction = () => {
return function(dispatch, getState) {
// 异步操作: 网络请求
axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
const banners = res.data.data.banner.list
dispatch(changeBannersAction(banners))
})
}
}
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from "react-redux"
import store from "./store"
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
在store的index.js
import { createStore, applyMiddleware, compose } from "redux"
import thunk from "redux-thunk"
import reducer from "./reducer"
// redux-devtools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)))
export default store
在项目中我们可能在登录时在redux中存数据,在home…这些页面存数据,但是这样不方便管理,所以我们需要进行模块化
那么我们需要将这些模块进行合并统一管理
使用combineReducers
import counterReducer from "./counter"
import homeReducer from "./home"
import userReducer from "./user"
// 将两个reducer合并在一起
const reducer = combineReducers({
counter: counterReducer,
home: homeReducer,
user: userReducer
})
combineReducers实现原理(了解)
function reducer(state = {}, action) {
// 返回一个对象, store的state
return {
counter: counterReducer(state.counter, action),
home: homeReducer(state.home, action),
user: userReducer(state.user, action)
}
}
事实上,它也是将我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函
数了);
在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;
Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。
在前面我们学习Redux的时候应该已经发现,redux的编写逻辑过于的繁琐和麻烦。
并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);
Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题;
在很多地方为了称呼方便,也将之称为“RTK”;
安装Redux Toolkit:
◼ Redux Toolkit的核心API主要是如下几个:
configureStore:包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供
的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension。
createSlice:接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的actions。
createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分
派动作类型的 thunk
createSlice主要包含如下几个参数:
◼ name:用户标记slice的名词
在之后的redux-devtool中会显示对应的名词;
◼ initialState:初始化值
第一次初始化时的值;
◼ reducers:相当于之前的reducer函数
对象类型,并且可以添加很多的函数;
函数类似于redux原来reducer中的一个case语句;
函数的参数:
✓ 参数一:state
✓ 参数二:调用这个action时,传递的action参数;
◼ createSlice返回值是一个对象,包含所有的actions;
Redux Toolkit默认已经给我们继承了Thunk相关的功能:createAsyncThunk
当createAsyncThunk创建出来的action被dispatch时,会存在三种状态:
pending:action被发出,但是还没有最终的结果;
fulfilled:获取到最终的结果(有返回值的结果);
rejected:执行过程中有错误或者抛出了异常;
我们可以在createSlice的entraReducer中监听这些结果:
这里写成async和await会阻塞后面的代码,所以建议通过then回调的方式,并且不用监听状态,直接通过action的函数改变数据
为什么使用toolkit可以直接修改state中的值,而不是返回一个合并对象,这样做难道不怕浅拷贝数据没变吗?
事实上Redux Toolkit底层使用了immerjs的一个库来保证数据的不可变性。
// connect的参数:
// 参数一: 函数
// 参数二: 函数
// 返回值: 函数 => 高阶组件
import { PureComponent } from "react";
import store from "../store"
export function connect(mapStateToProps, mapDispatchToProps, store) {
// 高阶组件: 函数
return function(WrapperComponent) {
class NewComponent extends PureComponent {
constructor(props) {
super(props)
this.state = mapStateToProps(store.getState())
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
// this.forceUpdate()
this.setState(mapStateToProps(store.getState()))
})
}
componentWillUnmount() {
this.unsubscribe()
}
render() {
const stateObj = mapStateToProps(store.getState())
const dispatchObj = mapDispatchToProps(store.dispatch)
return <WrapperComponent {...this.props} {...stateObj} {...dispatchObj}/>
}
}
return NewComponent
}
}
需求:我们现在想要在派发dispath前打印1派发之后打印state
其实就是做一个中间件的拦截
function thunk(store) {
const next = store.dispatch
function dispatchThunk(action) {
if (typeof action === "function") {
action(store.dispatch, store.getState)
} else {
next(action)
}
}
store.dispatch = dispatchThunk
}export default thunk
方式一:组件中自己的state管理;
方式二:Context数据的共享状态;
方式三:Redux管理应用状态;
目前项目中我采用的state管理方案:
UI相关的组件内部可以维护的状态,在组件内部自己来维护;
大部分需要共享的状态,都交给redux来管理和维护;
从服务器请求的数据(包括请求的操作),交给redux来维护;
其实SPA最主要的特点就是在前后端分离的基础上加了一层前端路由.、
◼ 前端路由的核心是什么呢?改变URL,但是页面不进行整体的刷新。
npm install react-router-dom
路由的两种模式:
◼ BrowserRouter或HashRouter
Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件;
BrowserRouter使用history模式;
HashRouter使用hash模式;
路由映射配置:
◼ Routes:包裹所有的Route,在其中匹配一个路由
Router5.x使用的是Switch组件
◼ Route:Route用于路径的匹配;
path属性:用于设置匹配到的路径;
element属性:设置匹配到路径后,渲染的组件;
✓ Router5.x使用的是component属性
app.jsx的路由配置
路由配置和跳转:
◼ Link和NavLink:
通常路径的跳转是使用Link组件,最终会被渲染成a元素;
◼ 需求:路径选中时,对应的a元素变为红色
◼ 这个时候,我们要使用NavLink组件来替代Link组件:
className:传入函数,函数接受一个对象,包含isActive属性
◼ Navigate用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:
配置notFound页面
假设home页面中有两个路由页面,是recommend和ranking
home组件
Outlet组件用于在父路由元素中作为子路由的占位元素。相当于vue中的router-view
由于 Lazy 往往是从远程加载,在加载完成之前 react 并不知道该如何渲染该组件。此时如果不显示任何内容,则会造成不好的用户体验。因此 Suspense 还有一个强制的参数为 fallback,表示 Lazy 组件加载的过程中应该显示什么内容。往往 fallback 会使用一个加载动画。
目前我们实现的跳转主要是通过Link或者NavLink进行跳转的,实际上我们也可以通过JavaScript代码进行跳转。
我们知道Navigate组件是可以进行路由的跳转的,但是依然是组件的方式。
如果我们希望通过JavaScript代码逻辑进行跳转(比如点击了一个button),那么就需要获取到navigate对象。 ◼ 在Router6.x版本之后,代码类的API都迁移到了hooks的写法:
如果我们希望进行代码跳转,需要通过useNavigate的Hook获取到navigate对象进行操作;
那么如果是一个函数式组件,我们可以直接调用,但是如果是一个类组件呢?
我们可以自己封装一个高阶组件来实现
import { useState } from "react"
import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom"
// 高阶组件: 函数
function withRouter(WrapperComponent) {
return function(props) {
// 1.导航
const navigate = useNavigate()
// 2.动态路由的参数: /detail/:id
const params = useParams()
// 3.查询字符串的参数: /user?name=why&age=18
const location = useLocation()
const [searchParams] = useSearchParams()
const query = Object.fromEntries(searchParams)
const router = { navigate, params, location, query }
return <WrapperComponent {...props} router={router}/>
}
}
export default withRouter
跳转
const { navigate } = this.props.router
navigate("/home/songmenu")
传递参数有二种方式:
动态路由的方式;
search传递参数;
动态路由的概念指的是路由中的路径并不会固定:
比如/detail的path对应一个组件Detail; 如果我们将path在Route匹配时写成/detail/:id,那么 /detail/abc、/detail/123都可以匹配到该Route,并且进行显示;
这个匹配规则,我们就称之为动态路由; 通常情况下,使用动态路由可以为路由传递参数。
params形式
const { navigate } = this.props.router
navigate("/detail/" + id)
◼ 目前我们所有的路由定义都是直接使用Route组件,并且添加属性来完成的。
◼ 但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:
在早期的时候,Router并且没有提供相关的API,我们需要借助于react-router-config完成;
在Router6.x中,为我们提供了useRoutes API可以完成相关的配置;
import { Link, Navigate, Route, Routes, useNavigate, useRoutes } from 'react-router-dom'
<div>
{useRoutes(routes)}
</div>
routes
React.lazy是懒加载,做分包操作
import Home from '../pages/Home'
import HomeRecommend from "../pages/HomeRecommend"
import HomeRanking from "../pages/HomeRanking"
import HomeSongMenu from '../pages/HomeSongMenu'
import NotFound from '../pages/NotFound'
import Detail from '../pages/Detail'
import { Navigate } from 'react-router-dom'
import React from 'react'
const About = React.lazy(() => import("../pages/About"))
const Login = React.lazy(() => import("../pages/Login"))
const routes = [
{
path: "/",
element: <Navigate to="/home"/>
},
{
path: "/home",
element: <Home/>,
children: [
{
path: "/home",
element: <Navigate to="/home/recommend"/>
},
{
path: "/home/recommend",
element: <HomeRecommend/>
},
{
path: "/home/ranking",
element: <HomeRanking/>
},
{
path: "/home/songmenu",
element: <HomeSongMenu/>
}
]
},
{
path: "/about",
element: <About/>
},
{
path: "/login",
element: <Login/>
},
{
path: "/detail/:id",
element: <Detail/>
},
{
path: "*",
element: <NotFound/>
}
]
export default routes
如果我们对某些组件进行了异步加载(懒加载),那么需要使用Suspense进行包裹:,fallback是还没加载出来的时候显示的
◼ 我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:
◼ class组件可以定义自己的state,用来保存组件自己内部的状态; 函数式组件不可以,因为函数每次调用都会产生新的临时变量;
◼ class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑; 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次;
函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
◼ class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;
函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
◼ 复杂组件变得难以理解:
我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;
比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在
componentWillUnmount中移除);
而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
◼ 难以理解的class:
很多人发现学习ES6的class是学习React的一个障碍。
比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;
虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;
◼ 组件复用状态很难:
在前面为了一些状态的复用我们需要通过高阶组件;
像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;
或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;
这些代码让我们不管是编写和设计上来说,都变得非常困难;
Hook指的类似于useState、useEffect这样的函数
➢ Hooks是对这类函数的统称;
简单总结一下hooks:
它可以让我们在不编写class的情况下使用state以及其他的React特性;
但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;
我们在普通函数中无法使用hook,除非这个函数以use开头
useState来自react,需要从react中导入,它是一个hook;
✓ 参数:初始化值,如果不设置为undefined;
✓ 返回值:数组,包含两个元素;
➢ 元素一:当前状态的值(第一调用为初始化值);
➢ 元素二:设置状态值的函数;
我们可以通过数组的解构,来完成赋值会非常方便。
✓ 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
useEffect的解析:
通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;
例:点击按钮counter加一,页面的title总是显示counter的数字
import React, { memo, useEffect, useState } from 'react'
const Menu = memo(() => {
const [conter, setconter] = useState(1)
useEffect(() => {
document.title=conter
})
return (
<div>
<div>{conter}</div>
<button onClick={e => setconter(conter + 1)}>点击加一</button>
</div>
)
})
export default Menu
需要清除Effect
◼ 在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除:
比如我们之前的事件总线或Redux中手动调用subscribe;
都需要在componentWillUnmount有对应的取消订阅;
Effect Hook通过什么方式来模拟componentWillUnmount呢?
◼ useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B:
◼ 为什么要在 effect 中返回一个函数?
这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;
如此可以将添加和移除订阅的逻辑放在一起;
它们都属于 effect 的一部分;
◼ React 何时清除 effect?
React 会在组件更新和卸载的时候执行清除操作;
正如之前学到的,effect 在每次渲染的时候都会执行;
useEffect的参数的回调函数相当于componentDidMount,return的函数相当于 componentWillUnmount生命周期
useEffect(()=>{},[]) // 模拟 componentDidMount
useEffect(()=>{},[xxx]) // 模拟 componentDidUpdate
现在有多个effet,如果每次都渲染页面执行所有的effet,那么对页面的性能降低,某些代码我们只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网
络请求、订阅和取消订阅);
我们如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?
useEffect实际上有两个参数:
参数一:执行的回调函数;
参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)
import React, { memo, useEffect, useState } from 'react'
const Menu = memo(() => {
const [conter, setconter] = useState(1)
useEffect(() => {
console.log('conter改变了');
},[conter])
useEffect(() => {
console.log('发送网络请求')
},[])
return (
<div>
<div>{conter}</div>
<button onClick={e => setconter(conter + 1)}>点击加一</button>
</div>
)
})
export default Menu
但是,如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []:相当于只会执行一次
当组件上层最近的
◼ useCallback实际的目的是为了进行性能的优化。
◼ 如何进行性能的优化呢?
useCallback会返回一个函数的 memoized(记忆的) 值;
在依赖不变的情况下,多次定义的时候,返回的值是相同的;
◼ 通常使用useCallback的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存;
作用例
import React, { memo,useCallback,useState } from 'react'
const Menu = memo(function (props) {
console.log(props);
return <div><button onClick={props.add}>111</button></div>
})
const App = memo(() => {
const [msg, usemsg] = useState('222')
const [conter, useconter] = useState(1)
//这样写不会刷新子组件,因为useCallback函数会让每次的函数对象都是同一个,所以传入子组件的props没变,所以子组件不会刷新
const add = useCallback(() => {
useconter(conter+1)
}, [conter])
//这样写点击按钮会刷新子组件,因为每次都传入了一个新的函数对象add
// const add = () => {
// useconter(conter+1)
// }
return (
<div>
<Menu add={add}></Menu>
<h1>conter:{conter}</h1>
<h1>msg:{msg}</h1>
<button onClick={e=>usemsg(Math.random())}>点击</button>
</div>
)
})
export default App
但是我们发现,如果将effet中的count去掉,那么就会发现这个useCallback包裹的函数中引用的count永远都是第一次 const [conter, useconter] = useState(1)的count了,因为是个闭包,没有创建新的函数对象,每次都引用counter=1的counter,导致点击后变成2,后面点击一直是2
import React, { memo,useCallback,useState } from 'react'
const Menu = memo(function (props) {
console.log(props);
return <div>
<button onClick={props.add}>点击加一</button>
<button onClick={props.add2}>点击产生新的函数对象</button>
</div>
})
const App = memo(() => {
const [msg, usemsg] = useState('222')
const [conter, useconter] = useState(1)
//这样写不会刷新子组件,因为useCallback函数会让每次的函数对象都是同一个,所以传入子组件的props没变,所以子组件不会刷新
const add = useCallback(() => {
useconter(conter+1)
}, [])
//这样写点击按钮会刷新子组件,因为每次都传入了一个新的函数对象add
const add2 = () => {
useconter(conter+1)
}
return (
<div>
<Menu add={add} add2={add2}></Menu>
<h1>conter:{conter}</h1>
<h1>msg:{msg}</h1>
<button onClick={e=>usemsg(Math.random())}>点击</button>
</div>
)
})
export default App
那么我们可以使用useref优化,原因是useRef, 在组件多次渲染时, 返回的是同一个引用的对象,他不会随着组件重新渲染而去重新生成一个新的ref引用对象
import React, { memo,useCallback,useState,useRef } from 'react'
const Menu = memo(function (props) {
console.log(props);
return <div>
<button onClick={props.add}>点击加一</button>
</div>
})
const App = memo(() => {
const [msg, usemsg] = useState('222')
const [conter, useconter] = useState(1)
//这样也能不刷新子组件
const Menuref = useRef()
Menuref.current=conter
const add = useCallback(() => {
useconter(Menuref.current+1)
}, [])
return (
<div>
<Menu add={add}></Menu>
<h1>conter:{conter}</h1>
<h1>msg:{msg}</h1>
<button onClick={e=>usemsg(Math.random())}>点击</button>
</div>
)
})
export default App
以上就是对闭包陷阱的两种解决方案
◼useMemo实际的目的也是为了进行性能的优化
useMemo返回的也是一个memoized(记忆的)值;
在依赖不变的情况下,多次定义的时候,返回的值是相同的;
import React, { memo, useCallback } from 'react'
import { useMemo, useState } from 'react'
function calcNumTotal(num) {
console.log("calcNumTotal的计算过程被调用~")
let total = 0
for (let i = 1; i <= num; i++) {
total += i
}
return total
}
const App = memo(() => {
const [count, setCount] = useState(0)
// 1.不依赖任何的值, 进行计算
const result = useMemo(() => {
return calcNumTotal(50)
}, [])
// 2.依赖count
const result = useMemo(() => {
return calcNumTotal(count*2)
}, [count])
return (
<div>
<h2>计算结果: {result}</h2>
<h2>计数器: {count}</h2>
<button onClick={e => setCount(count+1)}>+1</button>
</div>
)
})
export default App
下面这两个写法的意义是一样的,其实useCallback是对传入的函数进行优化的,而useMemo是对返回的值进行优化的
function fn() {}
const increment = useCallback(fn, [])
const increment2 = useMemo(() => fn, [])
附加:
下面的代码在点击按钮后helloworld组件不会重新渲染,因为传入的result没变
import React, { memo } from 'react'
import { useMemo, useState } from 'react'
const HelloWorld = memo(function(props) {
console.log("HelloWorld被渲染~")
return <h2>Hello World</h2>
})
function calcNumTotal(num) {
console.log("calcNumTotal的计算过程被调用~")
let total = 0
for (let i = 1; i <= num; i++) {
total += i
}
return total
}
const App = memo(() => {
const [count, setCount] = useState(0)
const result = calcNumTotal(50)
return (
<div>
<h2>计算结果: {result}</h2>
<h2>计数器: {count}</h2>
<button onClick={e => setCount(count+1)}>+1</button>
<HelloWorld result={result} />
</div>
)
})
export default App
下面的代码helloworld组件会重新渲染,因为info会被重新定义,相当于一个新的引用,导致渲染
import React, { memo } from 'react'
import { useMemo, useState } from 'react'
const HelloWorld = memo(function(props) {
console.log("HelloWorld被渲染~")
return <h2>Hello World</h2>
})
function calcNumTotal(num) {
console.log("calcNumTotal的计算过程被调用~")
let total = 0
for (let i = 1; i <= num; i++) {
total += i
}
return total
}
const App = memo(() => {
const [count, setCount] = useState(0)
const result = calcNumTotal(50)
// 4.使用useMemo对子组件渲染进行优化
const info = { name: "why", age: 18 }
return (
<div>
<h2>计算结果: {result}</h2>
<h2>计数器: {count}</h2>
<button onClick={e => setCount(count+1)}>+1</button>
<HelloWorld result={result} info={info} />
</div>
)
})
export default App
那我们想优化,怎么样使helloworld组件不被多次渲染呢?
使用usememo
import React, { memo } from 'react'
import { useMemo, useState } from 'react'
const HelloWorld = memo(function(props) {
console.log("HelloWorld被渲染~")
return <h2>Hello World</h2>
})
function calcNumTotal(num) {
console.log("calcNumTotal的计算过程被调用~")
let total = 0
for (let i = 1; i <= num; i++) {
total += i
}
return total
}
const App = memo(() => {
const [count, setCount] = useState(0)
const result = calcNumTotal(50)
// 4.使用useMemo对子组件渲染进行优化
const info = useMemo(() => ({name: "why", age: 18}), [])
return (
<div>
<h2>计算结果: {result}</h2>
<h2>计数器: {count}</h2>
<button onClick={e => setCount(count+1)}>+1</button>
<HelloWorld result={result} info={info} />
</div>
)
})
export default App
useRef返回一个ref对象,不管组件渲染多少次,useref返回的都是同一个对象,比如字面量对象在定义后组件重新渲染后会重新定义一个新的内存对象,而useref不会
useref只能获取HTML元素和类组件,不能获取函数组件,因为函数组件没有实例
import React, { memo,useRef } from 'react'
const App = memo(() => {
const Menuref = useRef()
const add = () => {
console.log(Menuref.current)
}
return (
<div>
<h1 ref={Menuref}>11</h1>
<button onClick={add}>点击获取dom</button>
</div>
)
})
export default App
函数组件没有实例,所以无法获取
import React, { memo,createRef,forwardRef } from 'react'
const Child=forwardRef((props,ref)=>{
return (
<div>
<input defaultValue="11111" ref={ref}></input>
</div>
);
})
const App = memo(() => {
const Menuref = createRef()
const add = () => {
console.log(Menuref.current)
}
return (
<div>
<Child ref={Menuref}></Child>
<button onClick={add}>点击获取dom</button>
</div>
)
})
export default App
◼ 我们先来回顾一下ref和forwardRef结合使用:
通过forwardRef可以将ref转发到子组件;
子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;
◼ 通过useImperativeHandle可以值暴露固定的操作:
通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起;
所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象;
useImperativeHandle对传入的ref做了一层包装,这种结合还可以用作子组件向外暴露自己的方法给父组件进行调用
import React, { memo,forwardRef,useImperativeHandle,useRef } from 'react'
const Child = forwardRef((props, ref) => {
const inputRef = useRef()
const foo = () => {
console.log(111);
}
useImperativeHandle(ref, () => {
return {
// 子组件对父组件传入的ref进行处理
focus1(){
inputRef.current.focus()
},
bar() {
foo()
}
}
})
return (
<div>
<input defaultValue="11111" ref={inputRef}></input>
</div>
);
})
const App = memo(() => {
const Menuref = useRef()
const add = () => {
Menuref.current.focus1()
}
return (
<div>
<Child ref={Menuref}></Child>
<button onClick={add}>点击使用子组件的方法</button>
</div>
)
})
export default App
◼ useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:
useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
◼ 如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。
需求:所有的组件在创建和销毁时都进行打印
自定义hook要以use开头
import React, { memo, useEffect, useState } from 'react'
function useLogLife(cName) {
useEffect(() => {
console.log(cName + "组件被创建")
return () => {
console.log(cName + "组件被销毁")
}
}, [cName])
}
const Home = memo(() => {
useLogLife("home")
return <h1>Home Page</h1>
})
const About = memo(() => {
useLogLife("about")
return <h1>About Page</h1>
})
const App = memo(() => {
const [isShow, setIsShow] = useState(true)
useLogLife("app")
return (
<div>
<h1>App Root Component</h1>
<button onClick={e => setIsShow(!isShow)}>切换</button>
{ isShow && <Home/> }
{ isShow && <About/> }
</div>
)
})
export default App
例2:获取滚动位置
因为只监听一次,不能再渲染后多次监听,所以用useeffet
import { useState, useEffect } from "react"
function useScrollPosition() {
const [ scrollX, setScrollX ] = useState(0)
const [ scrollY, setScrollY ] = useState(0)
useEffect(() => {
function handleScroll() {
setScrollX(window.scrollX)
setScrollY(window.scrollY)
}
window.addEventListener("scroll", handleScroll)
return () => {
window.removeEventListener("scroll", handleScroll)
}
}, [])
return [scrollX, scrollY]
}
export default useScrollPosition
例3
获取local storage并且能修改
拓展:useState 里可以传入函数,return的值作为state
import { useEffect } from "react"
import { useState } from "react"
function useLocalStorage(key) {
// 1.从localStorage中获取数据, 并且数据数据创建组件的state
const [data, setData] = useState(() => {
const item = localStorage.getItem(key)
if (!item) return ""
return JSON.parse(item)
})
// 2.监听data改变, 一旦发生改变就存储data最新值
useEffect(() => {
localStorage.setItem(key, JSON.stringify(data))
}, [data])
// 3.将data/setData的操作返回给组件, 让组件可以使用和修改值
return [data, setData]
}
export default useLocalStorage
◼ 在之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux中的connect:
但是这种方式必须使用高阶函数结合返回的高阶组件;
并且必须编写:mapStateToProps和 mapDispatchToProps映射的函数;
◼ useSelector的作用是将state映射到组件中:
参数一:将state映射到需要的数据中;
参数二:可以进行比较来决定是否组件重新渲染;(后续讲解)
◼ useSelector默认会比较我们返回的两个对象是否相等;
如何比较呢? const refEquality = (a, b) => a === b; 也就是我们必须返回两个完全相等的对象才可以不引起重新渲染;
◼ useDispatch非常简单,就是直接获取dispatch函数,之后在组件中直接使用即可;
◼ 我们还可以通过useStore来获取当前的store对象;
import React, { memo } from 'react'
import { useSelector, useDispatch } from "react-redux"
import { addNumberAction, subNumberAction } from './store/modules/counter'
const App = memo((props) => {
// 1.使用useSelector将redux中store的数据映射到组件内
const { count } = useSelector((state) => ({
count: state.counter.count
}))
// 2.使用dispatch直接派发action
const dispatch = useDispatch()
function addNumberHandle(num, isAdd = true) {
if (isAdd) {
dispatch(addNumberAction(num))
} else {
dispatch(subNumberAction(num))
}
}
return (
<div>
<h2>当前计数: {count}</h2>
<button onClick={e => addNumberHandle(1)}>+1</button>
<button onClick={e => addNumberHandle(6)}>+6</button>
<button onClick={e => addNumberHandle(6, false)}>-6</button>
</div>
)
})
export default App
问题:用了memo包裹组件后,点击按钮只改变count而不改变message,子组件为什么也会刷新?
import React, { memo } from 'react'
import { useSelector, useDispatch } from "react-redux"
import { addNumberAction, subNumberAction } from './store/modules/counter'
const Home = memo((props) => {
const { message } = useSelector((state) => {
return { message: state.counter.message }
})
return <h1>{message}</h1>
})
const App = memo((props) => {
const { count } = useSelector((state) => ({
count: state.counter.count
}))
const dispatch = useDispatch()
function addNumberHandle(num, isAdd = true) {
if (isAdd) {
dispatch(addNumberAction(num))
} else {
dispatch(subNumberAction(num))
}
}
return (
<div>
<h2>当前计数: {count}</h2>
<button onClick={e => addNumberHandle(1)}>+1</button>
</div>
)
})
export default App
因为useSelector的state改变了,每次改变state,useSelector默认会比较我们返回的两个对象是否相等;
如何比较呢? const refEquality = (a, b) => a === b;
也就是我们必须返回两个完全相等的对象才可以不引起重新渲染;
所以虽然在home组件中message没有改变,但是useSelector默认监听的是整个state,引用的对象发生了改变,会导致home重新刷新一次
那怎么解决呢?
使用shallowEqual
在useSelector第二个参数传入shallowEqual,shallowEqual是一个react-redux包里面的函数,目的是做一个浅层比较
const { message } = useSelector((state) => ({
message: state.counter.message
}), shallowEqual)
首先我们来说一下SPA页面的概念
SPA页面就是只有一个html页面,SPA页面只在初始化的时候加载必须的html,css,js,一旦加载完成,不会因为用户的1的操作而重新加载整个页面,而是通过路由跳转的形式来转换ui
好处:
1.对用户体验好,减少了不必要的渲染
2.SPA对服务器压力小
3.前后端职责分离,明确分工
缺点:
1.首次加载耗时,因为前端渲染的话得要把js下载下来执行
2.不利于SEO优化(搜索引擎优化)
针对seo,例如百度的搜索引擎,百度会爬取项目的html文件,但是对于spa页面来说,这个index.html
中并没有什么内容(打包出来东西基本都在bundle.js的代码,得要浏览器执行才行),body也只是有一个id为root的div,所以他只能爬到一些标题之类的文字,然后去存到数据库,在搜索的时候进行排名展示,那从而不能给网站带来流量。
SSR(Server Side Rendering,服务端渲染),指的是页
面在服务器端已经生成了完成的HTML页面结构,不需要浏
览器解析;
对应的是CSR(Client Side Rendering,客户端渲染),
我们开发的SPA页面通常依赖的就是客户端渲染;
什么是同构?
一套代码既可以在服务端运行又可以在客户端运行,这就是同构应用。
ssr步骤:
在服务器上,获取整个应用的数据。
然后,在服务器上,将整个应用渲染成 HTML 并在响应中发送。
然后,在客户端,加载整个应用程序的 JavaScript 代码。
然后,在客户端,将 JavaScript 逻辑与服务器生成的整个应用程序的 HTML 连接起来(这就是 “水化”)。
在进行 SSR 时,我们的页面会呈现为 HTML。
但仅 HTML 不足以使页面具有交互性。例如,浏览器端 JavaScript 为零的页面不能是交互式的(没有 JavaScript 事件处理程序来响应用
户操作,例如单击按钮)。
为了使我们的页面具有交互性,除了在 Node.js 中将页面呈现为 HTML 之外,我们的 UI 框架(Vue/React/…)还在浏览器中加载和呈现
页面。(它创建页面的内部表示,然后将内部表示映射到我们在 Node.js 中呈现的 HTML 的 DOM 元素。)
◼ 这个过程称为hydration。
react 会生成一棵Fiber 单链表,树中每个节点对应了一个组件。
hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。
React Fiber是一个类似双向链表的数据结构;ReactDom会根据jsx,为每个dom节点生成一个fiber节点,child指向第一个子节点、sibling指向下一个兄弟节点、return指向父节点的数据结构就是fiber数据结构。fiber就是一个类似双向链表的数据结构。
共同点:vue和diff算法,都是不进行跨层级比较,只做同级比较
区别:
1 vue对比节点时,当节点元素类型相同,类名不同时,认为是不同的元素,删除重新创建,而react认为是同类型的节点,进行修改操作
2 vue列表对比的时候,采用从两端到中间的方式,旧集合和新集合两端各存在两个指针,两两进行比较,每次对比结束后,指针向队列中间移动;react则是从左往右一次对比,利用元素的index和lastindex进行比较
react主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。Vue 使用双向指针,边对比,边更新DOM
结论:因为我们不能每次都保证条件或循环语句都会执行,在多个setState设置后,我们如果在if语句中使用会造成hooks的指针发生错乱,导致顺序不同
分析:
函数本身不能保存状态,我们需要额外维护一个有序的表,在执行 setState 之类的 hook 时,将它们保存到这个表里。
每一个useState都会在当前组件中创建一个hook对象 ,并且这个对象中的next属性始终执行下一个useState的hook对象
这些对象以一种类似链表的形式存在 Fiber.memoizedState 中
而函数组件就是通过fiber这个数据结构来实现每次render后name address不会被useState重新初始化
例子:
如果我们在条件语句中定义了 useState,这样的话会导致可能第一次只有两个 useState,对应的 count 和 num 的索引为 0 和 1。但是下一次满足条件,有了四个 useState 了,对应的 count 和 num 的索引就变成 0 和 3 了。这样的话 num 的索引值发生了变化,它在不同情况下从数组中取得的值就是不一样了,不是它自身的值,这样就会导致错误。
链接:原文出处
React 所有事件都挂载在 document 对象上
当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
所以会先执行原生事件,然后处理 React 事件
最后真正执行 document 上挂载的事件