我觉得:
代码代码,无非就是翻译。把想做的事用编程语言体现出来,第一步、第二步、第三步...
要想高效的解决问题,执行时机是特别需要关注的点。什么阶段做什么事情,什么阶段能做什么事情,我觉得是很重要的。
问题出来了,进行分析,熟读API执行时机、组件生命周期,快速发现问题、高效解决问题。
前辈说:别为了完成任务而完成任务,要想路越走越宽,一定要理解需求,为什么要这么设计,理解需求背景,把自己也当成设计师,适当时刻,提出见解。
正题
React18,函数组件执行时机
函数式组件的执行顺序是从上到下执行的,像普通的Javascript函数一样,当React需要渲染一个函数式组件时,它会调用这个函数,并传递props作为参数。组件函数内部的代码随后会按照书写的顺序执行。
在函数体内部,你可以定义状态钩子(如useState)、副作用钩子(如ueEffect)等,然后返回JSX,返回的JSX描述了组件的UI结构。以下是函数式组件的简单示例和执行顺序的说明:
function MyComponent(props) {
// 1、执行useState,定义state变量
const [count, setCount] = useState(0);
useEffect(() => {
// 2、这个函数会在组件被渲染到屏幕之后执行
})
// 3、执行任何其他逻辑代码
const increment = () => {
setCount(count + 1);
}
// 4、返回JSX,即函数式组件的渲染输出
return (
)
}
执行顺序是:
- useState 和其他Hooks被调用,初始化状态或其他Hooks相关的操作
- 执行任何自定义的逻辑代码,如事件处理函数的定义等
- 返回JSX,这一步是实际的“渲染”步骤,但它只是返回了一个需要渲染的元素的描述,
并不是真正的渲染行为。
总结: 组件函数返回的JSX会被React转换成虚拟DOM( Virtual DOM) , React库随后会根据这个虚拟DOM来更新实际的DOM。如果使用了副作用钩子useEffect,那么它的内容会在渲染发生后(即浏览器绘制屏幕之后)执行。重要的是要理解,render在React的上下文中指的是整个组件函数的执行过程,而不仅仅是返回JSX的那部分。因此,当我们说“函数式组件被渲染时”,实际上是指整个组件函数从上到下的执行。
"组件函数返回的JSX会被React转换成虚拟DOM (Mirtual DOM),React库随后会根据这个虚拟DOM来更新实际的DOM。如果使用了副作用钩子useEffect,那么它的内容会在渲染发生后 (即浏览器绘制屏幕之后)执行”这句话怎么理解?
这句话描述了React在渲染函数式组件时的几个关键步骤和它们的执行时机。我们可以将其分解成以下几个部分来更清晰地理解:
- 组件函数返回的JSX会被React转换成虚拟DOM (Virtual DOM): 当React执行函数式组件时,组件返回的JSX被转换成React元素,它们是轻量级的对象表示,这个过程是在内存中进行的,称为虚拟DOM。虚拟DOM是实际DOM的抽象表示,允许React在进行任何实际的DOM操作之前预先计算出可能的更新。
- React库随后会根据这个虚拟DOM来更新实际的DOM: 一旦组件的虚拟DOM被创建(或更新),React接下来会进行DOM差异比较(diffing algorithm),找出与上一次渲染相比发生变化的部分。然后,React只会更新那些实际改变了的DOM元素,而不是整个DOM树。这个过程被称为协调(Reconciliation)
- 如果使用了副作用钩子useEffect,那么它的内容会在渲染发生后执行: useEffect是一个副作用钩子,允许你在函数式组件中执行副作用操作,例如数据获取、订阅或手动修改DOM。useEffect中的代码不会在组件的主渲染流程中同步执行,而是在组件输出已经被提交到DOM之后再延迟执行。这意味着,它们通常是在所有 DOM变更完成之后运行的,因此不会阻塞浏览器的绘制过程。
根据useEffect的依赖数组,它可以在组件每次渲染后运行,或者只在依赖项变化时运行。此外,useEffect可以返回一个清理函数,在组件卸载前或依赖项发生变化导致副作用重新运行前调用,用于执行必要的清理工作。
总结这三个部分,你可以这样理解:
- 首先,返回的JSX被转换成虚拟DOM;
- 然后,React根据虚拟DOM的差异更新实际DOM;
- 最后,DOM更新后,useEffect中定义的副作用操作会被执行,这个时刻通常是在所有的DOM更新都已经被浏览器处理完毕之后。这个过程确保了DOM操作的效率,并且允许副作用在DOM稳定之后运行,避免了可能的渲染问题和不一致状态。
React.memo
用过该Hook的人都知道,它是用来缓存组件的,当props没有发生改变时,不会进行不必要的渲染,导致多余的性能浪费。
需要注意的是,它还有第二个参数,它是个函数,可以自定义比较方法,由编写者来决定深浅比较,是否将该组件缓存下不渲染!
官方点说: React.memo是一个高阶组件,用于对函数组件进行性能优化,通过记忆组件渲染结果,避免在相同props的情况下重新渲染组件。默认情况下,React.memo只会对组件的props进行浅比较,如果props没有变化,那么就不会重新渲染组件。
然而,有些场景下你可能需要更细致的控制来决定是否需要重新渲染组件。为了实现这一点,React.memo允许你传递第二个参数:一个自定义比较函数。这个函数接收两个参数,分别是组件的前一个props和新的props。
自定义比较函数:
function areEqual(prevProps, nextProps) {
// 返回true则不更新组件(即props相等,不重新渲染)
// 返回false则更新组件(即props不相等,需要重新渲染)
}
这个函数应该返回一个布尔值,指示前后两次的props是否相等。如果返回true,则React.memo将跳过渲染组件;如果为false,则组件将重新渲染。
在自定义比较函数中,你可以实现深比较、比较特定属性,或者根据复杂逻辑来决定是否需要更新。这给了我们更多的灵活性来优化组件渲染性能。
示例:
以下是一个使用React.memo和自定义比较函数的简单示例:
const MyComponent = React.memo(function MyComponent(props){
// 你的组件逻辑
}, areEqual);
function areEqual(prevProps, nextProps) {
// 如果对比逻辑确定props没有变化,则不需要再渲染组件
if (prevProps.value === nextProps.value) {
return true; // 不重新渲染
}
return false; // 重新渲染
}
在这个例子中,如果value属性没有变化,则组件不会重新渲染,否则它将更新。
注意事项
在使用React.memo和自定义比较函数时,要注意几个要点:
性能权衡:虽然自定义比较可以避免不必要的渲染,但比较函数本身也会消耗一定的资源。在复杂的比较逻辑中,可能会影响性能,因此需要谨慎使用。
只有函数组件:React.memo仅适用于函数组件。如果你需要优化类组件的渲染性能,可以考虑使用PureComponent或shouldComponentUpdate生命周期方法。
记忆的副作用:如果你依赖组件渲染中的副作用(如果在渲染函数中发起网络请求),使用React.memo可能会导致这些副作用不被触发。这是因为如果组件渲染被跳过,那么这些副作用也不会执行。
总而言之,React.memo和自定义比较函数是一个强大的组合,能够帮助你在确保性能的同时,精确控制组件何时更新,在实际应用中,你应该基于组件的具体性能需求和行为来决定是否使用它们。
useEffect和useLayoutEffect的区别?
执行时机是我想要强调的点,看看?
useEffect的执行时机:
useEffect 用于在组件渲染到屏幕之后执行副作用操作。这里的“之后”指的是所有的DOM变更已经完成,组件的输出已经被渲染到屏幕上。在这个时候,React会异步执行useEffect中的副作用函数,这意味着:
useEffect中的代码不会在组件的渲染过程中执行,它不会阻塞DOM的更新。
React将在当前的渲染任务完成后,在下一个事件循环(event loop)中运行useEffect的副作用函数。
因为useEffect是异步执行的,所以你的副作用代码不应该包括含任何同步变更DOM的操作,这些操作可能需要立即反应在屏幕上。
useLayoutEffect的执行时机:
useLayoutEffect 的用法与useEffect几乎相同,但它的执行时机不同。useLayoutEffect中的副作用函数是在所有DOM变更之后,但在浏览器进行绘制之前执行的。这意味着:
useLayoutEffect中的代码会在DOM更新完成后立即同步执行,它会阻塞浏览器的绘制,直到你的副作用代码执行完毕。
因为useLayoutEffect是在浏览器绘制前执行,所以它适用于需要同步读取或更新DOM的情况,例如,测量DOM元素的大小或位置,或者执行需要立即反应在屏幕上的DOM更新。
useLayoutEffect的同步特性可以确保用户看到的屏幕永远是经过你副作用代码更新后的状态,防止出现视觉上的不一致或闪烁。
总的来说
useLayoutEffect和useEffect的主要区别在于它们的执行时机和对浏览器绘制的影响。useEffect是为大多数副作用设计的,它不会阻塞用户界面的更新。而useLayoutEffect用于那些必须同步执行的副作用,以保证操作完成后才绘制页面,从而避免界面出现错误或不一致的状态。在实践中,大多数情况下应该优先使用useEffect,只有在特殊情况下,当你需要进行DOM测量或同步更新时,才使用useLayoutEffect。
‘useEffect用于在组件渲染到屏幕之后执行副作用操作’,有一个问题:组件都已经渲染到屏幕上了,如果我们在useEffect中调用了接口,更新了数据,那么组件是不是又再次渲染、绘制了一遍呢?
是的,如果在useEffect中执行了一个接口调用,并且接口返回的数据导致组件状态更新(例如,通过useState Hook的更新函数设置新的状态),那么组件会再次渲染。
这里是一个简化的流程:
- 组件初始化渲染,React将DOM渲染到屏幕上。
- useEffect被调用,因为它是在渲染后执行的,所以它不会阻塞屏幕的更新。
- 在useEffect内部,执行异步接口调用。
- 当异步接口调用完成并获得数据后,你可能通过更新状态来反应这个数据。
- 状态更新会触发组件的重新渲染。
- React再次调用组件函数,根据新的状态生成新的DOM,并进行必要的DOM更新。
- 如果useEffect指定了依赖(dependencies),并且依赖项在最新渲染中发生了变化,则useEffect会再次执行。
在这个过程中,你的组件可能会有两次渲染:第一次是初始渲染,第二次是因状态更新(由于接口返回数据)导致的重新渲染。React会尽量优化这个过程,只更新实际改变了的DOM元素。
‘组件初始渲染,React将DOM渲染到屏幕上’这句话怎么理解?对于函数组件而言,初始渲染指的是函数至上而下执行到没有执行useEffect Hook?
针对函数组件的“初始渲染”实际上指的是React处理组件函数体时从开始到结束的过程。在这个过程中,React执行组件函数,计算出它返回的JSX应该如何被转换为DOM更新。这一过程包括以下步骤:
- 执行函数组件:当组件首次被渲染时,React会调用该函数组件,传入props作为参数。组件函数从上到下执行,因此任何在函数体中定义的Hooks(例如useState或useContext)都会在这个时候被初始化和调用。
- 返回JSX:组件函数返回JSX,这是组件的UI描述。React将这些JSX转换成React元素,这是对UI的轻量级描述。
- 构建虚拟DOM:React使用返回的React元素来构建虚拟DOM树。虚拟DOM是实际DOM的一个轻量级抽象。
- DOM比对:React将当前的虚拟DOM与上次渲染的虚拟DOM进行比对(如果是首次渲染,则与空虚拟DOM比对),计算出需要进行的最小化更新。
- 渲染到DOM:React将计算出的更新应用到真实DOM上,这通常涉及到修改、添加或删除DOM节点。
- 屏幕更新:一旦DOM更新完成,屏幕上就会展示出最新的界面给用户。此时用户可以看到第一次渲染的结果。
在这个初始渲染的过程中,useEffect Hook不会被执行。因为useEffect是设计为在DOM更新完成后执行的,确切地说,是在浏览器绘制了更新后的界面到屏幕上之后,在useEffect地副作用函数中,你可以执行与DOM无关的操作,例如数据获取、订阅、计时器设置等,并且可以根据需要设置状态,从而可能引发进一步的渲染。
因此,”组件初始渲染,React将DOM渲染到屏幕上“应该理解为:组件的函数体被执行,返回的JSX转换为虚拟DOM,React计算出必要的DOM更新,然后应用到真实的DOM上,最终界面更新显示在屏幕上。在这个过程完全完成之后,useEffect Hook才会执行它们的副作用函数。
useRef
useRef 是用来保存一个在组件的生命周期中持续存在的值,对这个值的修改不会引发组件的重新渲染。这个值可以是DOM引用,也可以是任何可变的数据。
useRef与useState最主要的区别
const currentRef = useRef(null);
currentRef.current = 1; // 组件不会重新渲染
const [value, setValue] = useState<string>('');
setValue('1'); // 修改状态,组件重新渲染
有些时候,更改一个值,你不想让视图发生变化,可以借助useRef的力量。
useContext
useContext是开发中再常见不过的Hook了,你需要去了解它。
useContext 是 React 提供的一个 Hook,它能够让你在函数组件中访问到 React 的 Context。通过使用 Context,你可以在组件树中跨层级地传递数据,避免了层层传递 props 的麻烦。这在管理全局状态、主题、语言等方面非常有用。
创建 Context
在使用 useContext 之前,你需要先创建一个 Context 对象。这可以通过 React.createContext 完成:
const MyContext = React.createContext(defaultValue);
defaultValue 是 Context 的默认值,用于在组件树中没有匹配到 Provider 时作为回退值使用。
提供 Context
要使用 Context 在组件之间共享数据,首先需要使用 Context.Provider 组件包裹组件树中的一部分,将数据传递给它的 value 属性:
<MyContext.Provider value={/* 某个值 */}>
{/* 渲染子组件 */}
</MyContext.Provider>
在 Provider 下的所有组件都可以访问到 value 中传递的数据,无论它们在组件树中位于何处。
消费 Context
在函数组件中,你可以使用 useContext Hook 来订阅 Context 的变化并读取 Context 的值:
const value = useContext(MyContext);
这样,你就可以在组件中直接访问到 value,而不需要通过 props 传递。
注意事项
- 当
Context的value发生变化时,所有使用了useContext并订阅了这个Context的组件都会重新渲染。因此,如果value是一个对象,应当避免在每次渲染时创建一个新的对象,否则会触发不必要的重新渲染。
// 避免这样使用,因为这会在每次App组件渲染时创建一个新的value对象
<UserContext.Provider value={{ name: 'Zs', age: 25 }}>
- 在使用 Context 之前,考虑是否真的需要它。对于一些简单的跨层级数据传递,适当的组件组合或使用状态提升可能更为合适。Context 更适用于那些需要全局访问的数据,如用户认证信息、主题或语言设置等。
使用 useContext 可以大大简化跨层级的数据流,并使得代码更加清晰。但是,合理使用 Context 依赖于对组件的设计和应用的结构
‘如果 value 是一个对象,应当避免在每次渲染时创建一个新的对象’,如何避免?
为了避免在每次渲染时创建一个新的对象,你可以使用 React 的 useState 或 useReducer Hooks 来管理状态,并确保只在数据实际改变时更新对象。这样,你可以保证 Context value 是稳定的,从而避免不必要的渲染。
下面是避免在每次渲染时创建新对象的一些方法:
使用 useState
可以将状态保存在一个状态变量中,并通过状态更新函数来更新状态,这样只有在状态实际改变时,对象才会更新。
import React, { useState } from 'react';
import UserContext from './UserContext';
function App() {
// 使用useState管理user对象
const [user, setUser] = useState({ name: 'Zs', age: 2 });
// ...其他逻辑代码
return (
// 传递已存在的user对象
<UserContext.Provider value={user}>
{/* 子组件 */}
</UserContext.Provider>
);
}
确保只在setUser被调用时更新user对象,而不是在每次App组件渲染时。
useState解构出的设置更新函数,当两次接收到的对象长的都一样,但是引用地址并不相同,会发生二次渲染吗? 在 React 中,使用
useStateHook 解构出的设置状态函数(通常命名为setState或类似的名称)时,如果你连续两次设置完全相同的状态值,React 会尝试优化以避免不必要的渲染。
对于原始类型(如数字、字符串、布尔值),React 会比较新旧值,如果它们相等,则不会触发组件的重新渲染。
对于对象或数组等复杂类型,即使两个对象的内容完全一样,但如果它们的引用地址不同,React 默认会认为状态发生了改变。然而,在实践中,React 会对连续的 setState 调用进行批处理,并且只在浏览器的下一个渲染周期中使用最新的状态值来更新组件一次,因此实际上不会引发两次渲染。
例如,考虑以下代码段:
const [state, setState] = useState({ count: 0 });
// 假设这段代码在事件处理函数中被调用
setState({ count: 1 }); // 第一次设置状态
setState({ count: 1 }); // 紧接着第二次设置相同的状态值
在上述的示例中,即使这两行代码分别创建了两个有着相同内容但引用地址不同的对象,React 通常情况下也不会引起两次渲染。这是因为 React 会将这两次 setState 调用合并在一起,并只使用最后一次调用的状态值来重新渲染组件。
如果你想确保 React 不会进行不必要的渲染,尽量保持状态值的稳定性(例如,避免在每次渲染时创建新的对象或数组),或者使用 useMemo、useCallback 等Hook来缓存复杂数据结构。此外,你也可以使用 React.memo 或 shouldComponentUpdate 生命周期钩子来避免不必要的子组件渲染。
使用 useMemo
如果你需要基于计算得出一个对象作为 value,可以使用 useMemo Hook 来缓存这个对象。这将确保只在依赖项改变时重新计算和创建对象。
import React, { useMemo } from 'react';
import UserContext from './UserContext';
function App() {
// 一些状态
const [name, setName] = useState('Zs');
const [age, setAge] = useState(25);
// 使用useMemo缓存对象
const user = useMemo(() => {
return { name, age };
}, [name, age]); // 依赖项列表,当name或age改变时,对象才会重新创建
// ...其他逻辑代码
return (
<UserContext.Provider value={user}>
{/* 子组件 */}
</UserContext.Provider>
);
}
在这个例子中,user 对象将只有在 name 或 age 改变时才会重新创建,避免了不必要的重新渲染。
使用 useReducer
对于更复杂的状态逻辑,可以使用 useReducer。这是一种更加适合管理包含多个子值的状态对象的方法,它也确保了对象的一致性。
import React, { useReducer } from 'react';
import UserContext from './UserContext';
function userReducer(state, action) {
switch (action.type) {
case 'SET_NAME':
return { ...state, name: action.name };
case 'SET_AGE':
return { ...state, age: action.age };
default:
return state;
}
}
function App() {
const [user, dispatch] = useReducer(userReducer, { name: 'Zs', age: 25 });
// ...其他逻辑代码
return (
<UserContext.Provider value={user}>
{/* 子组件 */}
</UserContext.Provider>
);
}
通过使用 useReducer,你可以确保 user 对象只在它的某个属性改变时才会更新,从而避免了不必要的渲染。
采用上述的方法,你就可以避免在每次渲染时不必要地创建一个新的对象,并确保仅在实际需要时重新创建对象,从而提升了性能。
持续记录,持续更新
待续...