react中组件的类型可以分为两类,函数式组件和类组件
1. class 类组件
1.1 概念
类组件就是通过生命类并继承React.component而创建的组件
1.2 举例
class 自定义类组件名1 extends React.component {
constructor()
render()
}
// 创建一个类组件
import react from 'react';
class buttonClassComponent extends React.Component {
// 类组件中两个必须函数: construtor函数和render函数,其他的事件回调函数看着加
constructor() {
super()
this.state = {
n: 0
}
}
render() {
return(
<div>{ this.state.n }</div>
)
}
}
1.3 类组件特性
(1) 生命周期
老的生命周期
在初始化阶段废弃了:**1.componentWillMounted **
在 更新阶段废弃了:(props更新)1.componentWillReceiveProps (整个组件更新)2.componentWillUpdate
新生命周期
step1: 首次渲染
constructor -> render -> componentDidMount
step2: 再次渲染:
1. props change -\ (组件是否应该更新钩子) 返回false --结束
2. setState() ----> shouldComponentUpdate -<
3. forceUpdate()-/ 返回true --render-> 更新UI-> componentDidUpdate
step3: 销毁:
componentWillUnmount
注意点1: react组件初始化state、props是在哪个阶段 ? construtor生命周期
注意点2: react生命周期中对于组件是否更新额外提供了一个钩子:shouComponentUpate,为什么要提供这个钩子?具体的使用场景是什么?
因为有时候我们state的变化会导致组件无效更新,例如:一个函数中对state先+1后-1,本质上最终结果值是不变的,但是值在过程中是变化的。那么因为你使用了setstate就是通知UI更新。显然这是无效更新浪费了性能。所以可以在这个是否组件应该更新的钩子中判断,如果数值不便就不修要更新。
onClick = () => {
this.setState(state =>({n:state.n+1})) //进行 +1 操作
this.setState(state =>({n:state.n-1})) //再进行 -1 操作
};
shouldComponentUpdate(newprops, newstate){ //nextprops、nextstate
if(newstate.n === this.state.n){
return false;
}else{
return true;
}
}
注意点3: 什么时候初始化阶段挂载到真实dom上了?
当执行componentDidMount时候,组件就挂载在真实dom上了。
注意点4: react为什么要废弃这些生命周期?
新的异步渲染模式以及这些生命周期存在的一些副作用(参考:www.cnblogs.com/hubert-styl…
(2) 类组件中this指向规则
核心规则:
1.this定义在哪个函数中
2.该函数被谁调用
1. render中的this
class Demo extends Component{
state = {
count: 0
}
handleClick = () => {
this.setState({count: this.state.count+1})
}
render(){
return(
<div>
{this.state.count}
// 这里直接调用this.state.count,在render函数中使用this
// 按照规则:
// 1. this 使用在render函数中
// 2.render函数被React调用,所以this指向React实例Demo
<div/>
)
}
}
2. click回调函数中的this(是否使用箭头函数)
class demo extends React.component {
cosntructor() {
super()
state = { count: 0 }
}
// case1: 回调函数非箭头函数
// 此时,按照规则,this定义在handleclick中,然后handleclick被onclick事件的回调函数使
// 用,回调函数执行时没有被调用者,函数如果找不到调用者,最后就会在顶层对象中调用,也就是this会
// 指向window对象,但由于使用了ES6语法,ES6语法默认采用严格模式,严格模式下,this不会指向
// window,而是undefined,所以才会报错。
handleclick1 = function() {
this.setState({ count: this.state.count + 1 })
}
// case2: 回调函数为箭头函数
// 箭头函数内部的this就是定义时上层作用域的this,handleClick上层作用域是类的构造函数,那么handleClick的this就是构造函数的this,也就是指向Demo类的实例
handleclick2 = () => {
this.setState({ count: this.state.count + 1 })
}
render() {
<div>
<button onClick=“{ handleclick1 }”>回调函数为非箭头定义 -- 按钮</button>
<button onClick=“{ handleclick2 }”>回调函数是箭头定义 -- 按钮</button>
</div>
}
}
那么如果我不想使用箭头定义事件回调函数,直接使用function定义的事件回调函数有没有办法解决this指向问题呢?有两种方法:
第一种在onclick绑定的时候使用箭头函数定义,即 onclick =" { () => handleclick() }" ;
class Demo extends Component{
state = {
count: 0
}
handleClick = function (){
this.setState({count: this.state.count+1})
}
render(){
return(
<>
{this.state.count}
<button onClick={() => this.handleClick()}>按钮</button>
</>
)
}
}
// 点击按钮正常
第二种还是在onclick绑定的时候使用bind来绑定this,即onclick =" { this.handleclick.bind(this) }" ;
class Demo extends Component{
state = {
count: 0
}
handleClick = function (){
this.setState({count: this.state.count+1})
}
render(){
return(
<>
{this.state.count}
<button onClick={this.handleClick.bind(this)}>按钮</button>
</>
)
}
}
// 点击按钮正常
(3) 合成事件机制(SyntheticEvent)
问题1: react为什么不使用html的原生事件而要自己做一个合成事件,vue的事件为什么不自己做一个合成事件?
React 自己实现了这么一套事件机制,它在 DOM 事件体系基础上做了改进,减少了内存的消耗,并且最大程度上解决了 IE 等浏览器的不兼容问题问题2: react自己做的合成事件和原生事件有什么区别?对应的执行特点是什么?使用合成事件的时候有什么注意点
什么是合成事件与原生事件???? 在react中因为使用jsx,所以我们不可能使用到了DOM0级原生事件,而DOM1级原生事件的话需要拿到dom元素进行绑定,因此我们最常使用的就是DOM2级原生事件
- 原生事件: 通过
addEventListener绑定的事件- 合成事件: 通过 JSX 方式绑定的事件,比如
onClick={() => this.handle()}
react合成事件有什么特点?- React 上注册的事件最终会绑定在
document这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)- React 自身实现了一套事件冒泡机制,所以这也就是为什么我们
event.stopPropagation()无效的原因。- React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
- React 有一套自己的合成事件
SyntheticEvent,不是原生的,这个可以自己去看官网- React 通过对象池的形式管理合成事件对象的创建和销毁,减少了垃圾的生成和新对象内存的分配,提高了性能
问题3: react合成事件在不同版本的区别
问题4: react合成事件和原生事件之间执行优先级&顺序
这里是指react框架中,使用的事件机制不是浏览器原生的事件捕获、处于事件阶段、事件冒泡三个阶段,而是react框架中自己根据多个不同浏览器类型中的事件机制实现了自己的一套事件机制,这样就可以抹平 不同浏览器事件机制兼容问题。
相比于js原生事件的三个阶段,react框架中的合成事件只有两个阶段:事件绑定和事件触发。因此,我们将react这套事件机制称为合成事件。
1.react事件和js原生事件:
const handleClick = (e) => {e.preventDefault();}
// 原生事件
<div onclick="handleClick()"></div>
// React合成事件
<div onClick={HandleCilck}></div>
原生事件和合成事件混合使用时候的执行顺序(但是不推荐原生和react混合使用):
输出后的执行顺序:
先执行原生的捕获冒泡-addEventlister参数默认为false-冒泡阶段执行
然后冒泡到document时候执行react合成
最后执行document的事件回调
原生事件:子元素 DOM 事件监听!
原生事件:父元素 DOM 事件监听!
React 事件:子元素事件监听!
React 事件:父元素事件监听!
原生事件:document DOM 事件监听!
为什么react事件在document上注册的函数之前执行,因为react事件注册document.addEventlistener('click', dispathEvent)是先于其他document事件注册的,所以先执行。 2.为什么要有合成事件
- 对事件进行归类,可以在事件产生的任务上包含不同的优先级
- 提供合成事件对象,抹平浏览器的兼容性差异
3.合成事件机制简述 提供了一种“顶层注册,事件收集,统一触发”的事件机制
- “顶层注册”,其实是在root元素上绑定一个统一的事件处理函数
- “事件收集”, 事件触发时(实际上是root上的事件处理函数被执行),构造合成事件对象,按照冒泡或捕获的路径去组件中收集真正的事件处理函数
- “统一触发”,在收集过程之后,对收集的事件逐一执行,并共享同一个合成事件对象
版本区别
- React16事件绑定到document上
- React17事件绑定到root组件上,有利于多个react版本共存,例如微前端
- event不是原生的,是SyntheticEvent合成事件对象
react16之前的事件执行机制:(两套事件机制单独运行)
原生事件执行完捕获冒泡阶段之后,然后执行react事件机制的捕获冒泡(react父元素捕获、子元素捕获,子元素冒泡,父元素冒泡),最后document冒泡。
react17之后的事件执行机制:(两套事件机制合并运行,react优先级高)
即先走捕获阶段:react元素父子元素先捕获,原生事件父子元素捕获(捕获阶段还是各自独立走)
再走冒泡阶段:原生事件父子元素先冒泡, react元素父子元素再冒泡。
事件委托的对象是容器,即root.addEventlistener('click', dispatchEvent)
1.可以是一个页面上存在多个react版本
注意:react合成事件是react重要的基础知识
(4) setState 更新视图
setstate参考文献:
zhuanlan.zhihu.com/p/44537887
juejin.cn/post/706665…
setState(partialState,callback)
setState是可以接受两个参数的,一个parrialState,一个回调函数。因此我们可以在回调函数里面获取值
参数1: parrialState:object | function 用于产生与当前state合并的子集
参数2: callback: fucntion state更新之后被调用
setstate的核心问题:
1.setstate的执行机制 & 原理
在React内部机制能检测到的地方,setState就是异步的;在React检测不到的地方,例如setInterval,setTimeout,setState就是同步更新的。为在react有一套自定义的事件系统和生命周期流程控制,我们只需要知道只要代码进入了react的调度流程,那就是异步的。只要你没有进入react的调度流程,那就是同步的。什么东西不会进入react的调度流程?setTimeout、setInterval、在DOM上绑定原生事件等。这些操作方式会跳出react这个体系,这些都不会走React的调度流程,所以会直接更新this.state,在这种情况下调用setState就是同步的。 否则就是异步的。 zhuanlan.zhihu.com/p/445378872.对于数据的更新是 “同步” 还是 “异步”?
答:setState在react自身的合成事件或者钩子函数中被使用的时候就是“异步”执行,而在原生的事件或者setTimeout、Promise.resolve().then中都是同步的。 这里的“异步”是什么意思?“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”。
setstate主要的功能:用于数据改变之后可以更新视图
4.1 使用特性:
(1) 参数为对象:setstate({ count: 1 }, () => {})
for ( let i = 0; i < 100; i++ ) {
this.setState( { num: this.state.num + 1 } );
console.log( this.state.num ); // 会输出什么?
}
组件渲染的结果是1,并且在控制台中输出了100次0,说明每个循环中,拿到的state仍然是更新之前的。这也侧面印证了setState是异步更新,想想看如果是同步更新视图,那么这个for循环就要更新视图100次,很浪费性能。但是现在的问题是:虽然你异步更新节省了视图性能,但是这种setstate操作不符合人类的直觉。我想要正常的操作setState循环+1得不到我想要的结果。
为此,React给出了一种解决方案:setState接收的参数还可以是一个函数,在这个函数中可以拿先前的状态,并通过这个函数的返回值得到下一个状态。
(2) 参数为函数:setstate((preState, props)=> {return { count: preState.count + 1 }})
for ( let i = 0; i < 100; i++ ) {
this.setState( prevState => {
console.log( prevState.num );
return {
num: prevState.num + 1
}
} );
}
这样:(1)数据上也是我们想要的结果,视图上也是我们想要的结果;(2)同时性能上也是最优的。
4.2 实现原理
在实现之前,我们先来抽象一下setState的功能:
1.异步更新数据
1)异步更新state数据,且在短时间内将多个state合并为一个
2)为了解决异步视图更新导致的数据同步更新,可以将对象参数改为函数参数,来解决数据同步更新使用问题
2.更新视图
实现
先实现一个同步更新视图和数据的
function setState(newStateObj) {
object.assign(this.state, newStateObj) // 这里实现了state更新合并为一个:后一个state会覆盖前一个state,递推下来就是最后一个newStateObj会覆盖之前所有的setState操作,即只有最后一个有用。
renderView()
}
// object.assign(target, {a: 1}) 方法会将后一个参数的值赋给target,并且返回的仍然是target原对象
这里可能会有人有问题,比如我执行两次this.setState( { num: this.state.num + 1 } ),那么按照这个函数应该执行两次object.assign(this.state, newStateObj),那么第一次this.setState( { num: this.state.num + 1 } )即为this.setState( { num: this.state.num(=1) + 1 } ),这个时候state为{ num: 2 }, 然后第二次执行setState( { num: this.state.num + 1 } ),这时this.state.num值为上一次的state值2,那么接着上面计算值应该是3,怎么会值还是2呢?其实这里犯了一个错误,我们的newStateObj是一个引用地址,所有这里是引用地址的不管覆盖,而不是对象字面量的覆盖。如下所示,最后state最后被覆盖的是最后一个调用setState的对象参数引用地址,如果你是字面量使用Object.assign,那么就会数据递增了。但是显然我们函数调用时候参数肯定是引用地址传入。(这也就解释了为什么多次调用setState,为什么结果等价于只执行最后一次的setState结果)
注意:object.assign(target, {a: 1}) 方法会将后一个参数的值赋给target,并且返回的仍然是target原对象![]()
上面实现了1.1,但是1.2和2功能没有实现。 接下来实现1.2
const component = {
preState
}
function setState(newStateObj) {
if (Object.prototype.tostring(newStateObj) === '[object object]') { // 对象参数
object.assign(this.state, newStateObj)
}
if (Object.prototype.tostring(newStateObj) === '[object function]') {
object.assign(this.state, newStateFunction(component.prestate, props))
}
component.preState = this.state
renderView()
}
这里又有人要问,参数传递函数就不是引用地址吗?为什么传函数参数的时候就可以保证数据执行同步呢。因为这里object.assign(this.state, newStateFunction(component.prestate, props))是执行函数执行调用的结果,这个结果就是字面量形式的,所以可以保证数据变更同步,即上一个执行的结果下一个可以拿到。
但是这里还使用了一个闭包component来保存prestate,因此不合适。且这里对于多个setState调用后数据的存储上还需要进一步优化,因此官方使用的是队列保存: step1: 只要调用setState,先通过enqueueSetState,将(stateChange, this-这个组件自身)放入到队列中
setState( stateChange ) {
enqueueSetState( stateChange, this );//
renderComponent( this );//调用render渲染组件
}
//创建一个队列
const queue = [];
/**
*该方法用于保存传过来的一个个state和其对应要更新的组件,但是并不更新
*而是先放入该队列中等待操作
*/
function enqueueSetState( stateChange, component ) {
queue.push( {
stateChange,
component
} );
}
然后,当这个组件中所有的setstate都调用完之后,我们来统一处理队列,处理函数为下面的flush函数
/**
*该方法用于清除队列内容
*/
function flush() {
let item;
// 遍历
while( item = setStateQueue.shift() ) {
const { stateChange, component } = item;
// 如果没有prevState,则将当前的state作为初始的prevState
if ( !component.prevState ) {
component.prevState = Object.assign( {}, component.state ); // 这里就是给constructor初始化state的值
}
// 如果stateChange是一个方法,也就是setState的第二种形式
if ( typeof stateChange === 'function' ) {
Object.assign( component.state, stateChange( component.prevState, component.props ) ); // 执行参数参数,放回对象的字面量形式覆盖
} else {
// 如果stateChange是一个对象,则直接将对象的引用地址覆盖合并到setState中
Object.assign( component.state, stateChange );
}
component.prevState = component.state; // 覆盖之后的值赋值给当前组件的preState,留给下一次 setstate 调用
}
}
这里解决来功能1的1.1和1.2。 下面我们来看下异步更新视图的问题。在解决视图更新之前,我们来看下:之前的函数renderComponent( this ),如果一个组件中多次调用setstate,那么这个组件的地址就会被多次放入队列中,也就会被重复更新多次,显然一个组件只需要更新一次。所以需要组件去重。
setState( stateChange ) {
enqueueSetState( stateChange, this );//
renderComponent( this );//调用render渲染组件
}
所以更改如下:
const queue = []; // 一个是state数据队列
const renderQueue = []; // 一个是更新的组件队列
function enqueueSetState( stateChange, component ) {
queue.push( {
stateChange,
component
} );
// 如果renderQueue里没有当前组件,则添加到队列中
if ( !renderQueue.some( item => item === component ) ) {
renderQueue.push( component );
}
}
然后在flush方法中,我们还需要遍历renderQueue,来渲染每一个组件,所以flush更改如下:
function flush() {
let item, component;
while( item = queue.shift() ) {
// ...
}
// 渲染每一个组件
while( component = renderQueue.shift() ) {
renderComponent( component ); // 此时依然为同步更新
}
}
但是这个setstate函数依然是同步代码,即使我们已经通过state更新放入队列避免了“调用一次setstate调用就更新视图”的现象(目前实现的是:一个组件多次调用state先统一放入队列,到最后一个state执行之后再“同步”视图)。 但是我们希望异步更新,即将视图是放入异步队列中更新的。那么我们是不是可以把整个flush函数的执行放在异步任务中执行即可。我们可以利用事件队列,让flush在所有同步任务后执行
setState( stateChange ) {
enqueueSetState( stateChange, this );//
renderComponent( this );//调用render渲染组件
}
function enqueueSetState( stateChange, component ) {
// 如果queue的长度是0,也就是在上次flush执行之后第一次往队列里添加
if ( queue.length === 0 ) {
defer( flush );
}
queue.push( {
stateChange,
component
} );
if ( !renderQueue.some( item => item === component ) ) {
renderQueue.push( component );
}
}
// 也就是说先放入queue和renderQueue队列,然后同步任务执行完了,就开始执行微任务了:defer( flush ),开始执行flush清空队列(更新数据,更新视图)
2.函数式组件
2.2 函数组件特性
(1) 无生命周期
(2) 常用的Hooks及其使用场景
1. useState 更新视图
按照数据分为简单类型和引用类型,因此useState在更新这两种类型的数据上是不同的,React组件的更新机制对state只进行浅对比,也就是更新某个复杂类型数据时只要它的引用地址没变,那就不会重新渲染组件。对于数组、对象,需要使用深拷贝,即改变数组/对象的指针指向的地址,来实现组件的重新渲染。
useState使用的目的就是让你在函数式组件中响应式更新state,usestate返回的是变量的初始值和更新这个变量的方法。
函数式组件就是一个函数,但是他的返回必须是JSX格式。
注意点:
特点1.useState对于不同数据类型的更新的注意点(简单类型、数组、对象)
简单类型:
每次点击click事件函数,函数中单次调用setCount(count + 1),然后会更新dom
function Counter() {
let [count, setCount] = useState(0); // 定义state:count
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</>
);
}
2.usestate单独调用更新和批量调用更新的区别
3.useState中的setstate传入函数参数的作用
4.usestate的原理
4.1 usestate的存储机制原理
4.2 usestate的覆盖机制 && 批量更新机制
那么当我们一个组件中多次使用useState的时候,会出现什么问题呢?
function Counter() {
let [count, setCount] = useState(0);
let [num, setNum] = useState(0); // 共用一个state保存状态,修改第二个会导致第一个被覆盖。
return ( <>
<p>{num}</p>
<button onClick={() => setNum(num + 1)}>+</button>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</>
);
}
// 这里调用了定义了两个需要保证响应式更新的变量,那么就需要使用两次usestate函数,但是如果说我们每次使用usestate都额外定义一个变量,并且还需要将setCount(newval)赋值给定义的全局变量
// 如下所示:
// 自定义的useState
let state;
function useState(initialState) {
state = state || initialState;
function setState(newState) { // 这个set函数要解决我要将传入的newvalue给到具体哪一个变量,究竟是setNum还是setCount
state = newState;
render();
}
return [state, setState];
}
、
// 问题:多次使用usestate-hook函数
// 解决方案:使用数组 + 闭包
const stateArr = []
const index = 0
function useState(initalValue) {
const currentIndex = index
stateArr[currentIndex] = initalValue // 塞入初始值
function setState(newValue) { // 这里每次调用setState之后,由于一直没销毁,所以存在引用计数,故currentIndex变量会一直保存,形成闭包
stateArr[currentIndex] = newValue
render()
}
index = index + 1
return [stateArr[currentIndex], setState]
}
那么既然是使用数组形式保存,那么就存在一个序号的问题,如下:我们有一个条件判断,那么就可能存在数组序号收到条件逻辑的影响,变得混乱,造成即使你使用setcount但是改变的却是setnum值。
总结:usestate的存储机制,保证了多个变量使用usestate可以相互独立
![]()
// 创建一个函数式组件
import React from "react";
function Welcome(props) {
const [count, setCount] = React.useState(0);
return (
<div>
{count}
<h1>Hello, {props.name}</h1>
<button onClick={ () => setCount(count + 1)}>+1</button>
</div>
);
}
export default Welcome
类组件和函数式组件的使用
// 主文件,分别引入 函数式组件 和 class类组件
import './App.css';
import Button from './Components/Button.js'
import Welcome from './Components/functionComponent'
function App() {
return (
<div className="App">
<Welcome name="Sara" /> // 函数式组件
<Button></Button>
</div>
);
}
问题:useState中是否存在批量更新的合成机制
2. useEffect副作用函数
useeffect执行时机:
在react的18版本之后,useeffect会执行两次
1.这是 React18 才新增的特性。
2.仅在开发模式("development")下,且使用了严格模式("Strict Mode")下会触发。
生产环境("production")模式下和原来一样,仅执行一次。
3.之所以执行两次,是为了模拟立即卸载组件和重新挂载组件。
为了帮助开发者提前发现重复挂载造成的 Bug 的代码。
同时,也是为了以后 React的新功能做铺垫。
未来会给 React 增加一个特性,允许 React 在保留状态的同时,能够做到仅仅对UI部分的添加和删除。
让开发者能够提前习惯和适应,做到组件的卸载和重新挂载之后, 重复执行 useEffect的时候不会影响应用正常运行。
3. useContext
4. useReducer
5. useCallback
6. useMemo
7. useRef
(4) 常用的hooks
问题:
- React Hooks 为什么必须在函数组件内部执行?React 如何能够监听 React Hooks 在外部执行并抛出异常?
- React Hooks 如何把状态保存起来?保存的信息存在了哪里?
- React Hooks 为什么不能写在条件语句中?
二.类组件和函数式组件的区别
1、setState和 useState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout、Promise.resolve().then 中都是同步的。
三、react与vue常见操作区别
1.指令区别:
| 标题 | |
|---|---|