1.React组件分类
函数组件
1.不具备"状态、ref、周期函数"等内容,第一次渲染完毕后,无法基于组件内部的操作来控制其更新,因此称之为静态组件!。但是具备属性及插槽,父组件可以控制其重新渲染
2.渲染流程简单,渲染速度较快
3.基于FP(函数式编程)思想设计,提供更细粒度的逻辑组织和复用
类组件
1.具备“状态、ref、周期函数、属性、插槽"等内容,可以灵活的控制组件更新,基于钩子函数也可灵活掌控不同阶段处理不同的事情。
2.渲染流程繁琐,渲染速度相对较慢
3.基于OOP(面向对象编程)思想设计,更方便实现继承等
React Hooks组件,就是基于React 中新提供的 Hook函数,可以让函数组件动态化!
基础Hook
useState使用状态管理
useEffect使用周期函数
useContext使用上下文信息
额外的 Hook
useReducer useState的替代方案,借鉴redux处理思想,管理更复杂的状态和逻辑
useCallback构建缓存优化方案
useMemo构建缓存优化方案
useRef 使用ref获取DOM
useImperativeHandle配合forwardRef (ref转发)一起使用
useLayoutEffect 与useEffect相同,但会在所有的DOM变更之后同步调用effect‘
…
2.回顾之前学习的两个hooks
createRef:
createRef
API提供了一种简单的方法来创建一个容器用来存储对组件实例的引用。createRef
在组件的constructor
中被调用,并返回一个可分配给ref属性的React引用。当组件实例化时,React会创建一个引用,并将其与组件的ref属性进行绑定,可以通过这个引用来访问组件的属性和方法。可以将ref值传递给子组件,并在子组件中使用props
设置。
forwardRef:
ref直接赋值给一个函数组件的时候是报错的,需要配合forwardRef实现一个ref的转发,就是将ref的值转发到组件的内部,这样就可以在父组件中拿到函数子组件中的某个元素
例子:
使用forwardRef
实现ref
转发的例子:
import React, { forwardRef } from 'react';
function MyComponent(props, ref) {
return (
<input type="text" ref={ref} />
);
}
export default forwardRef(MyComponent);
在以上代码中,我们使用forwardRef
方法来将ref
向下传递到组件内部。forwardRef
接受一个函数组件作为参数,并返回一个新的组件,该组件可以接受ref
作为第二个参数。
然后在父组件中,我们可以使用ref
来访问MyComponent
组件内部的input
元素。以下是一个使用ref
访问MyComponent
组件内部输入框值的例子:
import React, { useRef } from 'react';
import MyComponent from './MyComponent';
function ParentComponent() {
const myRef = useRef(null);
function handleClick() {
const node = myRef.current;
console.log(node.value);
}
return (
<div>
<MyComponent ref={myRef} />
<button onClick={handleClick}>Click Me</button>
</div>
);
}
export default ParentComponent;
在以上代码中,我们通过将myRef
传递给MyComponent
,将ref
向下传递到组件内部,并将其分配给input
元素。在handleClick
方法中,我们使用myRef.current
来访问input
元素的value
属性。
为什么报错??报错的底层原因?
如果直接将ref赋值给函数而不是函数的实例,则会导致TypeError: Cannot add property X, object is not extensible错误。底层原因是在JavaScript中,函数是一个非扩展的对象。这意味着不能像处理普通对象一样将属性添加到函数中。因此,直接将ref分配给函数是不允许的。当将ref分配给函数时,React会尝试将ref对象分配给该函数的[[Prototype]],从而导致该错误。如果想要使用类似的模式,可以考虑将ref分配给函数的属性。这个模式可以使函数组件更加可控,从而避免尝试向函数分配非法属性。
那么为什么非扩展的对象不能像处理普通对象一样将属性添加到函数中??
在JavaScript中,对象是一组键值对的集合,而函数是一个特殊的对象,它可以包含属性和方法。对象可以根据需要动态添加新属性,但是函数却是一个非扩展的对象,它不能添加新属性。
函数对象是非扩展的,是因为在运行时引擎对它们进行了优化,使其能够快速执行函数调用。如果一个函数是可扩展的,那么引擎就必须为每个函数实例预留额外的空间,这会影响性能。
另外,在ECMAScript中,函数被视为一种特殊类型的对象,但函数不会像对象那样包含内部属性和方法。因此,函数会由于其执行的上下文而隐式地获取一些属性和方法。
因此,虽然在JavaScript中函数看起来像对象,但它们实际上是一种与对象不同的特殊类型,这就是为什么非扩展的函数对象不能像处理普通对象一样添加属性的原因。
3.useState函数
useState返回的是一个长度为 2 的数组,其中第一个元素是状态对象,第二个元素是更新状态的函数。这个更新状态的函数与 Class 组件中的 setState 函数类似,但是它不会自动进行合并更新,而是直接替换。
例如,以下代码使用了 useState
来维护一个数字类型的状态:
import React from 'react';
const MyComponent = () => {
const [count, setCount] = React.useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click me</button>
</div>
);
};
export default MyComponent;
在这个例子中,useState
的初始值为0,这个值赋给了count
变量。setCount
函数用于更新这个状态值,它接受一个新的状态值作为参数,调用这个函数后,count
的值会被替换为这个新值。
因此,与 Class 组件中的state不同,useState返回的是一个数组,而不是一个对象。但是,使用数组的下标来获取状态和更新函数并不是非常方便或可读性很高的,因此,可以使用数组解构来获取这两个变量。
注意点
没问题,以下是修改后的回答:
函数组件「或者Hooks组件」不是类组件,所以没有实例的概念。调用函数组件会执行函数体,产生一个私有的上下文,函数内部的局部变量和状态只在这个私有上下文中存在。因此,在函数组件中不涉及this
的处理。这里的this都是undefined
4.探索hooks组件底层机制
函数组件的每一次渲染或更新的过程中,都会执行一次函数体,产生一个全新的私有上下文,这是函数组件和类组件最大的不同之处。这也就意味着,函数组件中的内部代码也需要重新执行。
当函数组件被渲染时
会创建一个新的私有上下文和这个上下文中的变量。这些变量的值可能来源于组件的props、组件自身的state、Hooks以及其他局部变量。渲染完成后,这些变量的值就会被保存在私有上下文中,供后续的更新过程使用。
当组件进行更新时
会执行函数体,并根据新的props、state、Hooks等值,生成新的私有上下文,更新这些变量的值。在更新完成后,这些变量的值也会被保存在私有上下文中,供下一次更新使用。
示例:
由于函数组件没有实例的概念,所以在函数组件中不可能像类组件那样保存实例级别的变量。这也是为什么「Hooks」把状态与组件自身解耦,让每次调用都是一个新的过程,且依赖项变化时才会重新渲染。
5.继续深究
usestate是否有缓存??
useState 确实有缓存机制,每次重新渲染组件时,并不会重新创建 useState 声明的状态变量,而是直接读取上一次保存的状态。这是因为,React 会在内存中将组件的状态缓存起来,以便在下一次重新渲染时进行比较与处理。当一个组件重复渲染时,由于组件内部代码会重新执行,所以每次调用useState时,都会返回相同的状态值和更新函数,而不是重新创建这些状态值和更新函数。diff算法后面会写
当然,这个缓存机制并不是绝对的,它受到多种因素的影响,例如组件的依赖关系、组件的状态变化频率、调用 useState 的位置等等。在某些情况下,useState 的变量会被重新创建,导致缓存失效。
缓存在什么地方?
React 会将所有组件的状态和相关信息存储在内存中,而非持久化存储在硬盘上,因此当关闭浏览器窗口或刷新页面时,缓存的信息会被清空。
useState细节处理和同步异步
官方推荐写法:
每个状态分开!!!
2.在React18中,我们基于useState创建出来的“修改状态的方法”,它们的执行也是异步的原理:等同于类组件中的this.setState基于异步操作&更新队列,实现状态的批处理,在任何地方修改状态,都是采用异步编程的。
如果想要变成同步的代码该怎么办????---->flushSync
什么时候会变成同步的??
使用 useState
或 useReducer
钩子函数更新状态时,如果在合成事件或生命周期函数中触发,那么它的更新也是异步的。也就是说,在这种情况下,无论执行多少次更新操作,实际上只有最后一次更新操作被添加到了更新队列中,而之前的更新操作都被忽略了。
但是,如果使用 useState
或 useReducer
钩子函数更新状态的操作被放在其他异步操作中(例如定时器、手动事件绑定等),那么它的更新操作实际上是同步执行的,而不是异步的。这是因为,React 无法感知到这些操作,也就无法将它们添加到更新队列中进行批量处理。
为什么react无法感知???这些操作????
React 并不知道在一些异步操作中进行的状态更新操作,是因为 React 的更新机制这是基于同步的调用栈。
当我们在合成事件或生命周期函数中触发状态更新操作时,React 会立即响应这些操作并将它们添加到更新队列中,从而在组件进行下一次更新时批量处理这些操作,以达到性能优化的目的。
但是,对于一些异步操作(比如定时器回调函数、Promise 等)而言,状态更新操作并不是在 React 的调用栈中执行的,而是在异步上下文中执行的。由于 React 只能追踪自己的调用栈中发生的状态更新操作,因此它无法知道一些异步操作中进行的状态更新操作,也就无法将它们添加到更新队列中来进行批量处理。
6.useState函数更新和优化机制
渲染一次,x最后是11
为什么???
1次渲染就是挂在跟新队列中最后统一跟新:
那么使用flushSync能解决这个问题吗???
useState自带了性能优化的机制:
每一次修改状态值的时候,会拿最新要修改的值和之前的状态值做比较「基于Object.is作比较」如果发现两次的值是一样的,则不会修改状态,也不会让视图更新「可以理解为︰类似于PureComponent,在shouldcomponentUpdate中做了浅比较和优化」
11是因为每一次循环都用的第一次跟新的那个闭包中的那个10!!!
其实这里也应该是一次,(这里的两次没有计算初始化那一次!!),那为什么这里会是两次,因为第一次可能浏览器还没有识别过来,在第二次flushsync的时候我们的x还没更新为11,打了个时间差所以进行了第二次渲染!!!
7.需求:让函数只更新一次,但是最后的结果是20
使用函数式
我们往更新队列里面放的时候放的是一个函数,最后更新处理的时候会把函数依次执行从而实现结果累计
8.初始化state的时候可以写成函数也可写成具体的值
看看这种写法