类组件和函数式组件生命周期
当使用类组件和函数组件编写 React 应用时,它们的生命周期方法略有不同。以下是类组件和函数组件分别对应的生命周期方法:
类组件的生命周期方法:
import React, { Component } from "react";
class LifecycleComponent extends Component {
constructor(props) {
super(props);
console.log("Constructor called");
}
componentDidMount() {
console.log("Component did mount");
}
componentDidUpdate(prevProps, prevState) {
console.log("Component did update");
}
componentWillUnmount() {
console.log("Component will unmount");
}
render() {
console.log("Render called");
return <div>Lifecycle Component</div>;
}
}
export default LifecycleComponent;
函数组件的生命周期方法(使用 React Hooks):
import React, { useEffect } from "react";
const FunctionComponent = () => {
useEffect(() => {
console.log("Component did mount");
return () => {
console.log("Component will unmount");
};
}, []);
useEffect(() => {
console.log("Component did update");
});
console.log("Render called");
return <div>Function Component</div>;
};
export default FunctionComponent;
需要注意的是,函数组件和类组件的生命周期方法有所不同,但通过使用 React Hooks,函数组件可以实现与类组件相似的生命周期功能。使用函数组件和 React Hooks 可以更简洁和灵活地编写 React 应用,并且在 React 17 及以后的版本中,推荐使用函数组件和 React Hooks 来开发新的组件。
useMemo,React.memo,useCallBack,三者的区别
useMemo
, React.memo
, 和 useCallback
都是用于性能优化的 React Hooks,但它们的作用和使用方式有所不同。
useMemo:
useMemo
用于在函数组件内部进行计算,并缓存计算结果。它接受一个计算函数和依赖数组作为参数,并返回计算结果。在依赖项发生变化时,useMemo
会重新计算结果,并在后续渲染中提供缓存的值,避免不必要的重复计算。适用于需要进行昂贵的计算或处理的场景。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
React.memo:
React.memo
是一个高阶组件(Higher-order Component),用于对函数组件进行浅层比较的性能优化。它接受一个组件作为参数,并返回一个被优化后的组件。React.memo
会对组件的输入进行浅层比较,如果组件的输入没有发生变化,则会返回缓存的结果,避免不必要的重新渲染。适用于避免组件不必要的渲染的场景。
const MyComponent = React.memo(function MyComponent(props) {
/* 组件的渲染逻辑 */
});
useCallback:
useCallback
用于在函数组件中缓存回调函数,以避免不必要的函数重新创建。它接受一个回调函数和依赖数组作为参数,并返回一个缓存后的回调函数。在依赖项发生变化时,useCallback
会返回一个新的回调函数。适用于将回调函数传递给子组件时,避免不必要的函数重新创建。
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
综上所述,useMemo
用于缓存计算结果,React.memo
用于优化函数组件的渲染,而 useCallback
用于缓存回调函数。它们在不同场景下的使用可以帮助提高性能和避免不必要的重新计算或渲染。
React.memo() 和 useMemo() 的主要区别
主要区别:
作用对象不同:
React.memo()
主要用于优化函数组件的渲染,它会对组件的输入进行浅层比较,以确定是否重新渲染组件。而useMemo()
主要用于缓存计算结果,它接受一个计算函数和依赖数组,并在依赖项变化时重新计算结果。返回值类型不同:
React.memo()
返回一个经过优化的组件,它会在组件的输入未发生变化时,返回缓存的结果,避免不必要的重新渲染。而useMemo()
返回缓存的计算结果,用于避免不必要的重复计算。使用方式不同:
React.memo()
是一个高阶组件,通过对组件的包装来实现优化。它可以直接应用于函数组件,例如React.memo(MyComponent)
。而useMemo()
是一个 React Hook,它在函数组件内部使用,接受一个计算函数和依赖数组作为参数,例如const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
。优化对象不同:
React.memo()
主要用于避免组件不必要的重新渲染,它通过对组件的输入进行浅层比较来确定是否重新渲染。而useMemo()
主要用于避免不必要的重复计算,它通过缓存计算结果,在依赖项发生变化时重新计算结果。
综上所述,React.memo()
和 useMemo()
在使用方式、作用对象和返回值类型上有所不同。它们分别应用于优化函数组件的渲染和缓存计算结果的场景,可以根据具体的需求选择适合的方法进行性能优化。
useLayoutEffect 和 useEffect 有什么区别?
useLayoutEffect
和 useEffect
都是 React 提供的 Hook,用于在函数组件中执行副作用操作。它们的主要区别在于执行时机。
useLayoutEffect:
useLayoutEffect
在浏览器布局与绘制之后同步执行,但在屏幕更新之前执行。它的执行时机与浏览器的渲染阶段紧密相关,适合处理需要立即更新 DOM 的操作。由于 useLayoutEffect
是同步执行的,如果在其中执行耗时较长的操作,可能会阻塞页面的渲染,导致性能问题。因此,一般情况下,应该避免在 useLayoutEffect
中执行耗时操作。
useEffect:
useEffect
在浏览器布局与绘制之后异步执行,即在组件渲染完成后延迟执行。它不会阻塞页面的渲染,适合处理不需要立即更新 DOM 的操作,如数据获取、订阅事件、启动定时器等。由于 useEffect
是异步执行的,它不会阻塞组件的渲染过程,因此更适合处理较长耗时的操作。
总结区别:
useLayoutEffect
在浏览器布局与绘制之后同步执行,适合处理需要立即更新 DOM 的操作。useEffect
在浏览器布局与绘制之后异步执行,适合处理不需要立即更新 DOM 的操作。
在大多数情况下,推荐使用 useEffect
。只有当需要在 DOM 更新后立即执行操作,并且操作可能导致页面重新布局时,才需要使用 useLayoutEffect
。
问题:既然 memo 对性能优化有好处,为什么不把每个组件都包一下
将每个组件都使用 React.memo()
进行包裹并不是一个通用的性能优化策略,因为它可能会对开发过程和代码维护性产生一些负面影响。以下是一些考虑因素:
性能收益有限:
React.memo()
的作用是对组件的输入进行浅层比较,以确定是否重新渲染组件。这种比较本身也是有一定开销的,因此并不是所有的组件都能从中获得明显的性能收益。只有在组件的渲染成本较高、或者其输入属性发生频繁变化时,使用React.memo()
才能带来实际的性能提升。组件间依赖关系:在某些情况下,一个组件的重新渲染可能会导致其父组件以及其他相关组件的重新渲染。如果在这些相关组件中过度使用
React.memo()
,可能会导致过多的比较和不必要的重新渲染,反而降低性能。开发和维护复杂性:使用
React.memo()
包裹每个组件会导致代码的冗余,增加开发和维护的复杂性。此外,对于一些具有较复杂渲染逻辑的组件,手动管理React.memo()
可能会引入错误或不必要的优化。优化的合适时机:性能优化应该基于具体场景和实际需求。在进行性能优化之前,应该先进行性能分析,确定哪些组件的性能是瓶颈,并针对性地进行优化。将
React.memo()
应用于那些真正需要优化的组件,可以更加有效地提升性能。
综上所述,尽管 React.memo()
可以对一些组件的性能进行优化,但并不是适用于所有组件的通用策略。在使用 React.memo()
之前,需要进行性能分析,并根据具体场景和需求,有选择地应用于那些真正需要优化的组件。
除了上述 react 常用的 hooks,你还会用哪些 hooks?
除了常用的 React Hooks,还有一些其他的 React Hooks 可以在开发中使用,具体取决于项目的需求和场景。以下是一些常见的其他 React Hooks:
useState:用于在函数组件中添加状态管理。它返回一个状态值和更新状态的函数。
useReducer:用于在函数组件中使用复杂的状态逻辑。它类似于 Redux 中的 reducer,接受一个状态和操作状态的函数。
useContext:用于在函数组件中使用 React 的上下文(Context)。它接受一个上下文对象,并返回当前上下文的值。
useRef:用于在函数组件中创建一个可变的引用。它返回一个可变的 ref 对象,可以在组件的生命周期中保持引用不变。
useImperativeHandle:用于在使用
ref
进行父子组件通信时,控制子组件暴露给父组件的实例值和方法。useLayoutEffect:类似于
useEffect
,但在浏览器布局与绘制之后同步执行,但在屏幕更新之前执行。useDebugValue:用于在自定义 Hooks 中提供自定义的调试值,以便在 React 开发者工具中进行检查。
useContextSelector:一个第三方库提供的 Hook,用于在函数组件中根据上下文选择特定的值。
这只是一小部分其他可用的 React Hooks,实际上还有更多的第三方库提供了各种有用的自定义 Hooks。根据具体的需求,你可以根据项目的要求选择适合的 Hooks 进行使用。
React18 有哪些更新?
一、 Render API
为了更好的管理root节点
,React 18
引入了一个新的 root API
,新的 root API
还支持 new concurrent renderer
(并发模式的渲染),它允许你进入concurrent mode
(并发模式)。
// React 17
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
const root = document.getElementById("root")!;
ReactDOM.render(<App />, root);
// React 18
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = document.getElementById("root")!;
ReactDOM.createRoot(root).render(<App />);
同时,在卸载组件时,我们也需要将 unmountComponentAtNode
升级为 root.unmount
:
// React 17
ReactDOM.unmountComponentAtNode(root);
// React 18
root.unmount();
tips:我们如果在 React 18
中使用旧的 render api
,在项目启动后,你将会在控制台中看到一个警告:
这表示你可以将项目直接升级到 React 18
版本,而不会直接造成 break change
。如果你需要保持着 React 17
版本的特性的话,那么你可以无视这个报错,因为它在整个 18
版本中都是兼容的。
除此之外,React 18
还从 render
方法中删除了回调函数
,因为当使用Suspense
时,它通常不会有预期的结果。
在新版本中,如果需要在 render
方法中使用回调函数,我们可以在组件中通过 useEffect
实现:
// React 17
const root = document.getElementById("root")!;
ReactDOM.render(<App />, root, () => {
console.log("渲染完成");
});
// React 18
const AppWithCallback: React.FC = () => {
useEffect(() => {
console.log("渲染完成");
}, []);
return <App />;
};
const root = document.getElementById("root")!;
ReactDOM.createRoot(root).render(<AppWithCallback />);
最后,如果你的项目使用了ssr
服务端渲染,需要把hydration
升级为hydrateRoot
:
// React 17
import ReactDOM from "react-dom";
const root = document.getElementById("root");
ReactDOM.hydrate(<App />, root);
// React 18
import ReactDOM from "react-dom/client";
const root = document.getElementById("root")!;
ReactDOM.hydrateRoot(root, <App />);
另外,还需要更新 TypeScript
类型定义,如果你的项目使用了 TypeScript
,最值得注意的变化是,现在在定义props
类型时,如果需要获取子组件children
,那么你需要显式的定义它
,例如这样:
// React 17
interface MyButtonProps {
color: string;
}
const MyButton: React.FC<MyButtonProps> = ({ children }) => {
// 在 React 17 的 FC 中,默认携带了 children 属性
return <div>{children}</div>;
};
export default MyButton;
// React 18
interface MyButtonProps {
color: string;
children?: React.ReactNode;
}
const MyButton: React.FC<MyButtonProps> = ({ children }) => {
// 在 React 18 的 FC 中,不存在 children 属性,需要手动申明
return <div>{children}</div>;
};
export default MyButton;
二、 setState 自动批处理
React 18
通过在默认情况下执行批处理来实现了开箱即用的性能改进。
批处理是指为了获得更好的性能,在数据层,将多个状态更新
批量处理,合并成一次更新
(在视图层,将多个渲染
合并成一次渲染
)。
1. 在 React 18 之前:
在React 18 之前
,我们只在 React 事件处理函数
中进行批处理更新。默认情况下,在promise
、setTimeout
、原生事件处理函数
中、或任何其它事件内
的更新都不会进行批处理:
情况一:React 事件处理函数
import React, { useState } from "react";
// React 18 之前
const App: React.FC = () => {
console.log("App组件渲染了!");
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<button
onClick={() => {
setCount1((count) => count + 1);
setCount2((count) => count + 1);
// 在React事件中被批处理
}}
>
{`count1 is ${count1}, count2 is ${count2}`}
</button>
);
};
export default App;
点击 button,打印 console.log:
可以看到,渲染次数和更新次数是一样的,即使我们更新了两个状态,每次更新组件也只渲染一次。
但是,如果我们把状态的更新放在promise
或者setTimeout
里面:
情况二:setTimeout
import React, { useState } from "react";
// React 18 之前
const App: React.FC = () => {
console.log("App组件渲染了!");
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={() => {
setTimeout(() => {
setCount1((count) => count + 1);
setCount2((count) => count + 1);
});
// 在 setTimeout 中不会进行批处理
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
点击 button,重新打印 console.log:
可以看到,每次点击更新两个状态,组件都会渲染两次,不会进行批量更新。
情况三:原生 js 事件
import React, { useEffect, useState } from "react";
// React 18 之前
const App: React.FC = () => {
console.log("App组件渲染了!");
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
useEffect(() => {
document.body.addEventListener("click", () => {
setCount1((count) => count + 1);
setCount2((count) => count + 1);
});
// 在原生js事件中不会进行批处理
}, []);
return (
<>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</>
);
};
export default App;
点击 button,重新打印 console.log:
可以看到,在原生 js 事件中,结果跟情况二是一样的,每次点击更新两个状态,组件都会渲染两次,不会进行批量更新。
2. 在 React 18 中:
在 React 18
上面的三个例子只会有一次 render
,因为所有的更新都将自动批处理。这样无疑是很好的提高了应用的整体性能。
不过以下例子会在 React 18
中执行两次 render:
import React, { useState } from "react";
// React 18
const App: React.FC = () => {
console.log("App组件渲染了!");
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={async () => {
await setCount1((count) => count + 1);
setCount2((count) => count + 1);
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
总结:
- 在 18 之前,只有在 react 事件处理函数中,才会自动执行批处理,其它情况会多次更新
- 在 18 之后,任何情况都会自动执行批处理,多次更新始终合并为一次
三、flushSync
批处理是一个破坏性改动
,如果你想退出批量更新,你可以使用 flushSync
:
import React, { useState } from "react";
import { flushSync } from "react-dom";
const App: React.FC = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={() => {
flushSync(() => {
setCount1((count) => count + 1);
});
// 第一次更新
flushSync(() => {
setCount2((count) => count + 1);
});
// 第二次更新
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
注意:flushSync
函数内部的多个 setState
仍然为批量更新,这样可以精准控制哪些不需要的批量更新。
有关批处理
和flushSync
的更多信息,你可以参阅 React 官方的Automatic batching deep dive(批处理深度分析)。
四、关于卸载组件时的更新状态警告
我们在开发时,偶尔会遇到以下错误:
这个错误表示:无法对未挂载(已卸载)的组件执行状态更新。这是一个无效操作,并且表明我们的代码中存在内存泄漏。
实际上,这个错误并不多见,在以往的版本中,这个警告被广泛误解,并且有些误导。
这个错误的初衷,原本旨在针对一些特殊场景,譬如 你在useEffect里面设置了定时器,或者订阅了某个事件,从而在组件内部产生了副作用,而且忘记return一个函数清除副作用,则会发生内存泄漏……
之类的场景
但是在实际开发中,更多的场景是,我们在 useEffect 里面发送了一个异步请求,在异步函数还没有被 resolve 或者被 reject 的时候,我们就卸载了组件
。 在这种场景中,警告同样会触发。但是,在这种情况下,组件内部并没有内存泄漏,因为这个异步函数已经被垃圾回收了,此时,警告具有误导性。
关于这点,React 官方也有解释:
综上所述原因,在 React 18
中,官方删除了这个报错。
有关这个报错的更多信息,你可以参阅 React 官方的说明,点击这里查看。
五、关于 React 组件的返回值
- 在
React 17
中,如果你需要返回一个空组件
,React 只允许返回null
。如果你显式的返回了undefined
,控制台则会在运行时抛出一个错误。 - 在
React 18
中,不再检查因返回undefined
而导致崩溃。既能返回null
,也能返回undefined
(但是React 18
的dts
文件还是会检查,只允许返回null
,你可以忽略这个类型错误)。
关于组件返回值的官方解释: github.com/reactwg/rea…
六、Strict Mode
不再抑制控制台日志:
当你使用严格模式
时,React 会对每个组件进行两次渲染
,以便你观察一些意想不到的结果。在 React 17
中,取消了其中一次渲染
的控制台日志,以便让日志更容易阅读。
为了解决社区对这个问题的困惑,在 React 18
中,官方取消了这个限制。如果你安装了React DevTools
,第二次渲染的日志信息将显示为灰色,以柔和的方式显式在控制台。
关于 Strict Mode 的官方解释: github.com/reactwg/rea…
七、 Suspense 不再需要 fallback 来捕获
在 React 18
的 Suspense
组件中,官方对 空的fallback
属性的处理方式做了改变:不再跳过 缺失值
或 值为null
的 fallback
的 Suspense
边界。相反,会捕获边界并且向外层查找,如果查找不到,将会把 fallback
呈现为 null
。
更新前:
以前,如果你的 Suspense
组件没有提供 fallback
属性,React 就会悄悄跳过它,继续向上搜索下一个边界:
// React 17
const App = () => {
return (
<Suspense fallback={<Loading />}> // <--- 这个边界被使用,显示 Loading 组件
<Suspense> // <--- 这个边界被跳过,没有 fallback 属性
<Page />
</Suspense>
</Suspense>
);
};
export default App;
React 工作组发现这可能会导致混乱、难以调试的情况发生。例如,你正在 debug 一个问题,并且在没有 fallback
属性的 Suspense
组件中抛出一个边界来测试一个问题,它可能会带来一些意想不到的结果,并且 不会警告
说它 没有fallback
属性。
更新后:
现在,React 将使用当前组件的 Suspense
作为边界,即使当前组件的 Suspense
的值为 null
或 undefined
:
// React 18
const App = () => {
return (
<Suspense fallback={<Loading />}> // <--- 不使用
<Suspense> // <--- 这个边界被使用,将 fallback 渲染为 null
<Page />
</Suspense>
</Suspense>
);
};
export default App;
这个更新意味着我们不再跨越边界组件
。相反,我们将在边界处捕获并呈现 fallback
,就像你提供了一个返回值为 null
的组件一样。这意味着被挂起的 Suspense
组件将按照预期结果去执行,如果忘记提供 fallback
属性,也不会有什么问题。
关于 Suspense 的官方解释: github.com/reactwg/rea…
新的 API
一、useId
const id = useId();
支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration
的不兼容,这解决了在 React 17
及 17
以下版本中已经存在的问题。因为我们的服务器渲染时提供的 HTML
是无序的
,useId
的原理就是每个 id
代表该组件在组件树中的层级结构。
有关 useId 的更多信息,请参阅 useId post in the working group。
二、useSyncExternalStore
useSyncExternalStore
是一个新的 api,经历了一次修改,由 useMutableSource
改变而来,主要用来解决外部数据撕裂问题。
useSyncExternalStore 能够通过强制同步更新数据让 React 组件在 CM 下安全地有效地读取外接数据源。 在 Concurrent Mode 下,React 一次渲染会分片执行(以 fiber 为单位),中间可能穿插优先级更高的更新。假如在高优先级的更新中改变了公共数据(比如 redux 中的数据),那之前低优先的渲染必须要重新开始执行,否则就会出现前后状态不一致的情况。
useSyncExternalStore
一般是三方状态管理库使用,我们在日常业务中不需要关注。因为 React
自身的 useState
已经原生的解决的并发特性下的 tear(撕裂)
问题。useSyncExternalStore
主要对于框架开发者,比如 redux
,它在控制状态时可能并非直接使用的 React
的 state
,而是自己在外部维护了一个 store
对象,用发布订阅模式
实现了数据更新,脱离了 React
的管理,也就无法依靠 React
自动解决撕裂问题。因此 React
对外提供了这样一个 API。
目前 React-Redux 8.0
已经基于 useSyncExternalStore
实现。
有关 useSyncExternalStore 的更多信息,请参阅 useSyncExternalStore overview post 和 useSyncExternalStore API details。
三、useInsertionEffect
const useCSS = (rule) => {
useInsertionEffect(() => {
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
};
const App: React.FC = () => {
const className = useCSS(rule);
return <div className={className} />;
};
export default App;
这个 Hooks 只建议 css-in-js
库来使用。 这个 Hooks 执行时机在 DOM
生成之后,useLayoutEffect
之前,它的工作原理大致和 useLayoutEffect
相同,只是此时无法访问 DOM
节点的引用,一般用于提前注入 <style>
脚本。
有关 useInsertionEffect 的更多信息,请参阅 Library Upgrade Guide for 。
Concurrent Mode(并发模式)
Concurrent Mode(以下简称 CM
)翻译叫并发模式,这个概念我们或许已经听过很多次了,实际上,在去年这个概念已经很成熟了,在 React 17
中就可以通过一些试验性
的 api 开启 CM
。
CM 本身并不是一个功能,而是一个底层设计
并发模式可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整,该模式通过使渲染可中断来修复阻塞渲染
限制。在 Concurrent
模式中,React
可以同时更新多个状态。
说的太复杂可能有点拗口,总结一句话就是:
React 17
和 React 18
的区别就是:从同步不可中断更新
变成了异步可中断更新
。
重点来了,阅读下面的部分请勿跳过:
我们在文章开始提到过:在 React 18
中,提供了新的 root api
,我们只需要把 render
升级成 createRoot(root).render(<App />)
就可以开启并发模式了。
那么这个时候,可能有同学会提问:开启并发模式
就是开启了并发更新
么?
NO! 在 React 17
中一些实验性功能里面,开启并发模式
就是开启了并发更新
,但是在 React 18
正式版发布后,由于官方策略调整,React 不再依赖并发模式
开启并发更新
了。
换句话说:开启了并发模式
,并不一定开启了并发更新
!
一句话总结:在 18
中,不再有多种模式,而是以是否使用并发特性
作为是否开启并发更新
的依据。
从最老的版本到当前的v18
,市面上有多少个版本的React
?
可以从架构角度来概括下,当前一共有两种架构:
- 采用不可中断的
递归
方式更新的Stack Reconciler
(老架构) - 采用可中断的
遍历
方式更新的Fiber Reconciler
(新架构)
新架构可以选择是否开启并发更新
,所以当前市面上所有 React
版本有四种情况:
- 老架构(v15 及之前版本)
- 新架构,未开启并发更新,与情况 1 行为一致(v16、v17 默认属于这种情况)
- 新架构,未开启并发更新,但是启用了并发模式和一些新功能(比如
Automatic Batching
,v18 默认属于这种情况) - 新架构,开启并发模式,开启并发更新
并发特性
指开启并发模式
后才能使用的特性,比如:
useDeferredValue
useTransition
关系图:
了解清楚他们的关系之后,我们可以继续探索并发更新
了:
并发特性:
一、startTransition
在 v18 中运行如下代码:
import React, { useState, useEffect, useTransition } from "react";
const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
const [isPending, startTransition] = useTransition();
useEffect(() => {
// 使用了并发特性,开启并发更新
startTransition(() => {
setList(new Array(10000).fill(null));
});
}, []);
return (
<>
{list.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};
export default App;
由于 setList
在 startTransition
的回调函数中执行(使用了并发特性
),所以 setList
会触发并发更新
。
startTransition
,主要为了能在大量的任务下也能保持 UI 响应。这个新的 API 可以通过将特定更新标记为“过渡”
来显著改善用户交互,简单来说,就是被 startTransition
回调包裹的 setState
触发的渲染被标记为不紧急渲染,这些渲染可能被其他紧急渲染
所抢占。
- 有关 startTransition 的更多信息,请参阅 Patterns for startTransition。
二、useDeferredValue
返回一个延迟响应的值,可以让一个state
延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValue
和 startTransition
一样,都是标记了一次非紧急更新。
从介绍上来看 useDeferredValue
与 useTransition
是否感觉很相似呢?
- 相同:
useDeferredValue
本质上和内部实现与useTransition
一样,都是标记成了延迟更新
任务。 - 不同:
useTransition
是把更新任务变成了延迟更新任务,而useDeferredValue
是产生一个新的值,这个值作为延时状态。(一个用来包装方法,一个用来包装值)
所以,上面 startTransition
的例子,我们也可以用 useDeferredValue
来实现:
import React, { useState, useEffect, useDeferredValue } from "react";
const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
useEffect(() => {
setList(new Array(10000).fill(null));
}, []);
// 使用了并发特性,开启并发更新
const deferredList = useDeferredValue(list);
return (
<>
{deferredList.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};
export default App;
然后启动项目,查看一下打印的执行堆栈图:
此时我们的任务被拆分到每一帧不同的 task
中,JS脚本
执行时间大体在5ms
左右,这样浏览器就有剩余时间执行样式布局和样式绘制,减少掉帧的可能性。
- 有关 useDeferredValue 的更多信息,请参阅 New in 18: useDeferredValue。
三、普通情况
我们可以关闭并发特性,在普通环境中运行项目:
import React, { useState, useEffect } from "react";
const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
useEffect(() => {
setList(new Array(10000).fill(null));
}, []);
return (
<>
{list.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};
export default App;
启动项目,查看一下打印的执行堆栈图:
可以从打印的执行堆栈图看到,此时由于组件数量繁多(10000 个),JS 执行时间为500ms
,也就是意味着,在没有并发特性的情况下:一次性渲染 10000 个标签的时候,页面会阻塞大约0.5秒
,造成卡顿,但是如果开启了并发更新,就不会存在这样的问题。
这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为时间切片(time slice)
结论
- 并发更新的意义就是
交替执行
不同的任务,当预留的时间不够用时,React
将线程控制权交还给浏览器,等待下一帧时间到来,然后继续被中断的工作 并发模式
是实现并发更新
的基本前提时间切片
是实现并发更新
的具体手段- 上面所有的东西都是基于
fiber
架构实现的,fiber
为状态更新提供了可中断的能力
提到 fiber 架构,那就顺便科普一下 fiber 到底是个什么东西:
关于 fiber,有三层具体含义:
- 作为
架构
来说,在旧的架构中,Reconciler(协调器)
采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为Stack Reconciler
,stack 就是调用栈;在新的架构中,Reconciler(协调器)
是基于 fiber 实现的,节点数据保存在 fiber 中,所以被称为fiber Reconciler
。 - 作为静态
数据结构
来说,每个 fiber 对应一个组件,保存了这个组件的类型对应的 dom 节点信息,这个时候,fiber 节点就是我们所说的虚拟DOM
。 - 作为动态
工作单元
来说,fiber 节点保存了该节点需要更新的状态,以及需要执行的副作用。
结语
以上是本次 React
所升级的大致内容,如有错误,敬请指正。
Reac 生命周期是怎样的?
React 生命周期指的是组件在不同阶段经历的一系列方法调用,用于控制组件的初始化、渲染、更新和卸载等过程。在 React 16.3 版本之前,生命周期方法主要分为三个阶段:挂载阶段、更新阶段和卸载阶段。从 React 16.3 版本开始,一些生命周期方法被标记为过时,并引入了新的生命周期方法。
下面是 React 组件的常见生命周期方法:
挂载阶段:
constructor()
:组件的构造函数,在组件创建时调用,用于初始化状态和绑定方法。static getDerivedStateFromProps(props, state)
:静态方法,用于根据新的 props 更新状态。在组件实例化、接收新的 props 或调用setState
时触发。render()
:渲染方法,返回组件要渲染的内容。componentDidMount()
:组件挂载后调用,通常用于进行异步数据获取、订阅事件等副作用操作。
更新阶段:
static getDerivedStateFromProps(props, state)
:同挂载阶段,用于根据新的 props 更新状态。shouldComponentUpdate(nextProps, nextState)
:决定组件是否需要重新渲染,默认返回true
。通过在此方法中进行性能优化,避免不必要的重新渲染。render()
:同挂载阶段,渲染方法。getSnapshotBeforeUpdate(prevProps, prevState)
:在组件更新前获取 DOM 信息,通常配合componentDidUpdate
使用。componentDidUpdate(prevProps, prevState, snapshot)
:组件更新后调用,通常用于处理更新后的副作用操作。
卸载阶段:
componentWillUnmount()
:组件卸载前调用,用于清理定时器、取消订阅等清理操作。
此外,从 React 17 版本开始,以下生命周期方法被废弃:
componentWillMount()
componentWillReceiveProps(nextProps)
componentWillUpdate(nextProps, nextState)
在新版本中,推荐使用 componentDidMount
、componentDidUpdate
和 getDerivedStateFromProps
等新的生命周期方法来替代旧的生命周期方法。
需要注意的是,React 16.8 版本引入了 Hooks,通过使用函数组件和一系列的 Hooks(如 useState
、useEffect
等)来替代类组件的生命周期方法。使用 Hooks 可以更灵活地管理组件的状态和副作用操作。
虚拟 DOM 实现原理?
虚拟 DOM(Virtual DOM)是 React 中一种用于提高性能的技术。它是一个轻量级的 JavaScript 对象树,与实际的 DOM 树结构相对应,用于描述页面的结构和状态。
虚拟 DOM 的实现原理如下:
初始渲染:
- 在组件首次渲染时,React 会创建一个初始的虚拟 DOM 树,结构与实际的 DOM 树相同。
- 虚拟 DOM 树是一个纯 JavaScript 对象,与实际的 DOM 树相比,操作它的成本更低。
更新过程:
- 当组件的状态发生变化时,React 会生成一个新的虚拟 DOM 树,与之前的虚拟 DOM 树进行对比。
- 对比过程会找出两个虚拟 DOM 树之间的差异,称为变更(diff)。
- React 使用 diff 算法来高效地计算出最小的变更集合,以减少对实际 DOM 的操作次数。
应用变更:
- 在应用变更阶段,React 会根据计算得到的最小变更集合,将这些变更应用到实际的 DOM 树上。
- React 使用 DOM 操作来更新只有变化的部分,而不是重新渲染整个页面。
- 这样可以减少实际 DOM 操作的次数,提高性能。
虚拟 DOM 的优势在于它将实际 DOM 操作的成本降到最低。通过使用虚拟 DOM,React 可以批量地进行实际 DOM 操作,减少对浏览器布局和绘制的影响,从而提高应用的性能。
需要注意的是,虚拟 DOM 仅是 React 的一部分,它并不是所有框架或库都采用的技术。虚拟 DOM 的实现原理可以帮助我们理解 React 的性能优化机制,但并不代表所有前端应用框架的工作原理。
diff 的原理?
虚拟 DOM 的 diff 算法是 React 在更新过程中用于比较两个虚拟 DOM 树之间差异的核心算法。它的目标是找出最小的变更操作,以减少对实际 DOM 的操作次数,提高性能。
React 的 diff 算法基于以下原则:
同层比较:React 仅比较同层级的组件,不会跨层级比较。这意味着 React 不会递归遍历整个虚拟 DOM 树,而是在同一层级上进行比较。
唯一标识:每个虚拟 DOM 节点都应该有唯一的标识(通常是一个 key 属性),用于在更新过程中识别节点的变化。
组件重排避免:React 会尽可能地重用已存在的组件实例,而不是销毁并重新创建。这样可以避免不必要的组件重排(重新渲染)。
基于以上原则,React 的 diff 算法执行以下步骤:
树的遍历:从根节点开始,逐层比较两棵虚拟 DOM 树的节点。
节点的比较:React 会比较两个节点的类型(元素类型或组件类型)和 key 属性,以确定节点是否相同。
不同节点类型:如果两个节点的类型不同,则 React 会将旧节点判定为无效,需要完全重建。这会导致旧节点及其子节点的卸载和新节点的挂载。
相同节点类型:
- 如果两个节点的类型相同,React 会比较它们的属性和子节点。
- 对比节点属性:React 会检查属性的变化情况,并更新实际 DOM 上的对应属性。
- 对比子节点:React 会递归地比较两个节点的子节点。这里会使用递归和循环的方式进行同层级的节点比较。
节点更新:在比较过程中,React 会记录下需要进行更新的节点,并将这些更新操作应用到实际 DOM 上,以确保与虚拟 DOM 树的一致性。
通过 diff 算法,React 可以高效地计算出最小的变更集合,从而最小化对实际 DOM 的操作。这种优化方式可以大幅提高应用的性能和响应速度。
值得注意的是,虽然 React 的 diff 算法在大多数情况下表现良好,但在某些特定场景下,仍可能存在一些性能瓶颈。因此,对于特定的应用程序,有时需要手动优化或采用其他技术来进一步提高性能。
Vue vs React 的区别
Vue.js 和 React 是两个流行的前端 JavaScript 框架,它们都提供了用于构建交互式用户界面的工具和技术。以下是 Vue.js 和 React 的一些区别:
学习曲线:
- Vue.js 的学习曲线相对较低,因为它采用了模板语法和基于 HTML 的模板,使得对于初学者来说更易于理解和上手。
- React 的学习曲线较陡峭,因为它使用了 JSX 语法和 JavaScript 编写组件,需要对 JavaScript 和函数式编程有一定的了解。
开发方式:
- Vue.js 提供了一种更传统的模板驱动开发方式,将模板、逻辑和样式组织在一起,使得组件的开发和维护更加直观和简单。
- React 推崇组件化开发,使用 JSX 语法编写组件,将模板和逻辑耦合在一起,使得组件更加灵活和可复用。
数据绑定:
- Vue.js 使用双向数据绑定,可以轻松地将数据的变化反映到视图上,同时也可以通过视图的变化更新数据。
- React 使用单向数据流,通过 props 和 state 管理组件的数据,父组件可以将数据传递给子组件,子组件不能直接修改父组件的数据。
生态系统:
- React 拥有庞大的生态系统和活跃的社区支持,提供了丰富的第三方库和工具,可以用于构建复杂的应用程序。
- Vue.js 生态系统也逐渐壮大,并且有一些流行的插件和库,但相对于 React 来说规模较小。
开发团队支持:
- React 由 Facebook 和社区维护,拥有强大的技术支持和更新频率。
- Vue.js 由一个开源的社区团队维护,也有积极的更新和社区支持。
灵活性:
- React 提供了更大的灵活性和可定制性,可以与其他库和框架结合使用,如 Redux、MobX 等。
- Vue.js 在设计上更加一体化,提供了更多开箱即用的功能和插件,减少了对其他库的依赖。
总的来说,Vue.js 更适合初学者和小型项目,因为它具有低学习曲线和简单的语法。React 更适合构建大型、复杂的应用程序,因为它提供了更高的灵活性和可扩展性。选择使用哪个框架取决于项目的需求、团队的技能和个人偏好。
React 的 diff 算法和 Vue 的 diff 算法区别
React 和 Vue 在 diff 算法的实现上有一些区别。以下是 React 和 Vue 的 diff 算法的一些区别:
粒度不同:
- React 的 diff 算法是基于组件的虚拟 DOM 树的比较,它将整个组件树进行深度优先遍历,并比较每个组件的差异。
- Vue 的 diff 算法是基于模板的编译结果的比较,它将模板编译为渲染函数,然后比较渲染函数的差异。Vue 的 diff 算法更细粒度,只比较模板中实际使用的部分。
触发时机:
- React 的 diff 算法是在组件状态发生变化时触发的,即组件重新渲染时才会执行 diff 算法。
- Vue 的 diff 算法是在数据变化时触发的,Vue 通过侦听数据的变化,自动进行 diff 算法的执行。
逻辑实现:
- React 的 diff 算法使用了一种称为 Fiber 的算法来实现增量更新,它通过将更新过程分解为多个优先级较低的小任务,使得 React 可以在多个帧中完成更新,以提高性能和用户响应能力。
- Vue 的 diff 算法使用了一种双端队列的算法,在比较过程中采用双向循环指针来减少比较的次数,以提高性能。
数据依赖追踪:
- Vue 的 diff 算法使用了数据依赖追踪的技术,当数据发生变化时,Vue 可以追踪到受影响的组件,并只更新受影响的组件。
- React 的 diff 算法不具备数据依赖追踪的能力,因此在更新过程中,React 需要比较整个组件树,以确定哪些组件需要更新。
总的来说,React 的 diff 算法更侧重于组件的更新和复杂度的控制,而 Vue 的 diff 算法更侧重于模板的编译和数据的变化追踪。两者在实现上有所不同,但目标都是为了最小化对实际 DOM 的操作,并提高应用的性能和响应能力。
setState 到底是异步还是同步?
在 React 中,setState
方法既可以是异步的,也可以是同步的,具体取决于使用场景和调用方式。
异步更新:
- 当在 React 事件处理函数、生命周期方法(除了
componentWillUnmount
)、异步回调函数(如setTimeout
或fetch
的回调)中调用setState
,React 会将多个setState
调用合并为一个更新批次,以提高性能并避免不必要的重渲染。在这种情况下,setState
是异步执行的,即不会立即触发重新渲染。 - 异步更新时,React 会对多个
setState
调用进行批量处理,并对最终结果进行 diff 比较,最小化对实际 DOM 的操作。
- 当在 React 事件处理函数、生命周期方法(除了
同步更新:
- 在某些情况下,
setState
可能会同步执行,即立即触发重新渲染。 - 当在 React 事件处理函数、生命周期方法中使用
setState
时,如果在setState
后立即访问组件的状态,可以获取到最新的更新后的状态,这暗示着setState
可能是同步的。 - 通常,当
setState
的调用发生在 React 事件处理函数内部,且没有嵌套的异步操作(如setTimeout
)时,setState
将同步执行。
- 在某些情况下,
需要注意的是,无论是异步更新还是同步更新,React 会对更新进行优化,以尽量减少实际 DOM 操作的次数,提高性能。
如果需要在 setState
更新后执行某些操作,可以使用 setState
的回调函数或 componentDidUpdate
生命周期方法来处理。这样可以确保在组件重新渲染后执行相应的逻辑。
总结起来,setState
既可以是异步的也可以是同步的,具体取决于调用方式和使用场景。在大多数情况下,setState
是异步的,但在某些情况下(如在事件处理函数内部且没有嵌套的异步操作),它可能是同步的。
React 中的 setState 为什么需要异步操作?
在 React 中,setState
的异步操作是为了提高性能和优化渲染过程。
当调用 setState
时,React 会将状态更新添加到一个队列中,然后在适当的时机批量处理这些更新。这种批量处理的方式有以下几个原因:
性能优化:批量处理状态更新可以减少不必要的重渲染次数。如果每次调用
setState
都立即触发重新渲染,可能会导致频繁的更新和重绘,影响性能。通过异步操作,React 可以合并多个状态更新,一次性进行更新和渲染,减少了不必要的计算和 DOM 操作。更新顺序和批量更新:在 React 中,多个
setState
调用可能会被合并为一个更新批次,从而提高性能。如果setState
是同步的,那么在一个生命周期方法内多次调用setState
会立即触发组件的重新渲染,这可能会导致多次重绘和不必要的计算。通过异步操作,React 可以在适当的时机将多个setState
合并为单个更新批次,从而优化渲染过程。避免数据竞争和更新冲突:在异步操作中,React 使用事务机制来处理状态更新。在一个更新批次中,React 会对状态更新进行合并和排序,以避免数据竞争和更新冲突。这确保了组件在更新过程中的一致性和可预测性。
需要注意的是,尽管 setState
是异步的,但在某些情况下,React 也提供了一些同步的方式来处理状态更新,如在事件处理函数中使用 event.persist()
或使用函数式的 setState
形式。这些方式可以确保在某些特定场景下立即获取更新后的状态。
总结起来,React 中的 setState
异步操作是为了提高性能、优化渲染过程、合并和排序状态更新,并避免数据竞争和更新冲突。这样可以在适当的时机批量处理多个状态更新,减少不必要的计算和重绘,提高应用的性能和响应性能。
什么时候setState
会进行同步操作?(好像不对)
在大多数情况下,setState
在 React 中是异步执行的。但是,有一些情况下 setState
可能会以同步方式执行:
- 在 React 的事件处理函数中:当在 React 组件的事件处理函数(如点击事件、表单输入事件等)中调用
setState
,React 会同步执行状态更新,以确保在事件处理函数中立即获取到更新后的状态。
handleClick() {
this.setState({ count: this.state.count + 1 });
}
- 在
componentDidUpdate
生命周期方法中调用setState
:当在组件的componentDidUpdate
生命周期方法中调用setState
,setState
会同步执行。但需要注意的是,需要在调用setState
之前进行条件判断,以避免无限循环的更新。
componentDidUpdate(prevProps, prevState) {
if (this.props.data !== prevProps.data) {
this.setState({ data: this.props.data });
}
}
需要注意的是,虽然在上述情况下 setState
是同步执行的,但在其他生命周期方法(如 componentDidMount
、componentDidCatch
等)和异步操作(如 setTimeout
、fetch
请求等)中调用 setState
仍然是异步的。
当 setState
是同步执行时,需要谨慎处理,以避免出现不必要的计算和重渲染,以及潜在的性能问题。
React 官方对于setState
特定情况下进行同步操作的优化方案是什么?(好像不对)
React 官方提供了一种优化方案,用于在特定情况下以同步方式执行 setState
。这个方案基于函数式的 setState
形式,可以传递一个回调函数作为参数。
当在 setState
中使用回调函数时,React 会将该回调函数作为同步任务执行,以确保在回调函数中立即获取到更新后的状态。这种方式可以避免异步更新带来的延迟。
下面是使用回调函数的示例:
this.setState(
(prevState) => {
return { count: prevState.count + 1 };
},
() => {
console.log("State updated:", this.state.count);
}
);
在上述示例中,setState
接受一个回调函数作为第一个参数。回调函数会接收前一个状态 prevState
作为参数,并返回一个包含更新的状态对象。在回调函数的第二个参数中,可以执行在状态更新完成后需要立即执行的代码。
通过使用回调函数形式的 setState
,可以确保在特定情况下以同步方式获取更新后的状态,并立即执行相关代码。这在某些场景下非常有用,例如在更新状态后需要基于新状态进行计算或触发其他操作。
需要注意的是,使用回调函数形式的 setState
并不意味着所有的 setState
都会以同步方式执行,它仅适用于特定的情况和特定的用途。在大多数情况下,setState
仍然是异步执行的,以保证性能和渲染的优化。
React 中 setState
后想要拿到更新的state
值应该怎么处理?(好像不对)
在 React 中,setState
是一个异步操作,不能立即获取到更新后的状态值。如果需要在 setState
完成后获取更新后的状态值,可以使用回调函数或者在生命周期方法中处理。
使用回调函数:
setState
方法接受一个回调函数作为第二个参数,该回调函数会在状态更新完成并重新渲染后被调用。可以在回调函数中获取更新后的状态值。this.setState({ count: 1 }, () => {
console.log(this.state.count); // 获取更新后的状态值
});在生命周期方法中处理: 可以在生命周期方法中处理更新后的状态值。常用的方法包括
componentDidUpdate
、componentDidMount
等。componentDidUpdate(prevProps, prevState) {
console.log(this.state.count); // 获取更新后的状态值
}注意,在生命周期方法中获取更新后的状态值时,需要进行适当的条件判断,以避免不必要的操作和无限循环的更新。
另外,如果需要立即获取更新后的状态值,而不是等到下一次渲染或回调函数被触发,可以使用 this.setState
的函数式形式,并在回调函数中处理。
this.setState(
(prevState) => {
// 基于前一个状态 prevState 进行计算
return { count: prevState.count + 1 };
},
() => {
console.log(this.state.count); // 获取更新后的状态值
}
);
通过使用回调函数或在生命周期方法中处理,可以获取到更新后的状态值,并进行相应的操作或计算。
React 组件通信如何实现?
在 React 中,有几种常见的方式可以实现组件之间的通信:
Props(属性):
- 父组件可以通过将数据或回调函数作为属性传递给子组件来实现通信。
- 子组件通过
props
对象接收来自父组件的数据,并可以调用回调函数与父组件通信。 - 这是 React 中最常用和基本的组件通信方式。
State(状态)提升:
- 如果多个组件共享相同的状态,可以将该状态提升到它们共同的父组件中,并通过
props
传递给子组件。 - 父组件通过修改状态并将新的状态通过
props
传递给子组件,实现组件之间的通信。
- 如果多个组件共享相同的状态,可以将该状态提升到它们共同的父组件中,并通过
Context(上下文):
- Context 允许在组件树中共享数据,而不必通过逐层传递
props
。 - 可以在上层组件中创建一个 Context,并通过提供者(Provider)将数据传递给后代组件,在后代组件中通过消费者(Consumer)来访问这些数据。
- Context 允许在组件树中共享数据,而不必通过逐层传递
事件传播:
- 父组件可以通过在子组件上定义事件处理函数,并将其作为
props
传递给子组件。 - 子组件可以触发事件并调用传递的事件处理函数,从而将信息传递回父组件。
- 父组件可以通过在子组件上定义事件处理函数,并将其作为
全局状态管理库:
- 对于大型应用程序或需要在多个组件之间共享状态的情况,可以使用全局状态管理库(如 Redux、MobX 或 React Context API)来管理应用程序的状态。
- 这些库提供了一种集中式的状态管理机制,允许多个组件访问和更新共享的状态。
每种通信方式都有其适用的场景,选择适当的方式取决于应用程序的需求和复杂性。在大多数情况下,使用 props
进行数据传递和事件传播已经能够满足组件之间的通信需求。如果应用程序变得更加复杂,可以考虑使用其他方式来管理状态和实现更高级的组件通信。
React 如何进行组件/逻辑复用?
在 React 中,有几种方式可以实现组件和逻辑的复用:
组件复用:
- 创建可复用的组件是 React 中常见的做法。通过将通用的 UI 和功能封装到一个独立的组件中,可以在应用程序的不同部分多次使用它。
- 可以通过定义有意义和独立的组件来提高复用性,使其具有可配置的属性和可选的子组件,以适应不同的用例。
- 将组件的样式和样式相关逻辑与组件本身分离,可以进一步提高组件的复用性。
高阶组件(Higher-Order Components,HOC):
- 高阶组件是一个函数,接受一个组件作为参数并返回一个新的增强组件。
- HOC 可以在不修改原始组件代码的情况下,通过包装原始组件来添加、修改或封装功能。
- HOC 提供了一种将共享逻辑应用于多个组件的方式,从而实现逻辑的复用。
Render Props:
- Render Props 是一种通过将一个函数作为 prop 传递给组件,从而向组件提供可复用逻辑的技术。
- 这个函数返回一个 React 元素,可以在组件中渲染并使用其中的数据和功能。
- 通过使用 Render Props,可以将可复用的逻辑封装为一个函数,并通过不同的 Render Props 将其注入到不同的组件中,实现逻辑的复用。
React Hooks:
- React Hooks 是 React 16.8 引入的一项特性,使函数组件能够拥有状态和其他 React 功能。
- 通过使用自定义的 Hooks,可以将逻辑封装为可复用的函数,并在多个组件中使用。
- 自定义 Hooks 可以将状态管理、副作用处理等逻辑进行抽象和封装,实现逻辑的复用。
这些技术和模式可以单独或组合使用,以实现组件和逻辑的复用。选择适当的方式取决于具体的需求和应用程序的复杂性。通过提取通用的功能和逻辑,并将其封装为可复用的组件或函数,可以极大地提高 React 应用程序的可维护性和开发效率。
mixin、hoc、render props、react-hooks 的优劣如何?
下面是对 mixin、HOC、Render Props 和 React Hooks 的优劣进行简要比较:
Mixin(混入)
优点:
- 可以在多个组件之间共享功能和逻辑。
- 可以直接修改组件的原型,对组件进行扩展。
缺点:
- 可能导致命名冲突和组件耦合。
- 可能引入不可预测的副作用和难以维护的代码。
- ES6 中的类不支持原生的 mixin,需要使用其他工具或库来实现。
HOC(高阶组件)
优点:
- 提供了一种简单的方式来封装和复用组件逻辑。
- 可以通过包装组件来添加、修改或封装功能,而无需修改原始组件。
- 兼容 React 16 之前的版本。
缺点:
- 可能引入嵌套过深的组件结构,增加了代码复杂性。
- 可能导致命名冲突和组件耦合。
- 对于多个 HOC 的组合,可能会难以理解和调试。
Render Props(渲染属性)
优点:
- 提供了一种将可复用逻辑注入到组件中的方式。
- 更灵活,可以自定义传递的数据和功能。
- 不需要额外的依赖,是 React 自带的模式。
缺点:
- 可能导致组件层级过深,增加了代码复杂性。
- 对于多个 Render Props 的组合,可能会导致嵌套回调地狱。
React Hooks
优点:
- 提供了一种函数式的方式来管理组件的状态和副作用。
- 可以将逻辑封装为自定义的 Hooks,实现逻辑的复用。
- 提供了更简洁和可读的代码结构。
缺点:
- 需要理解 Hooks 的使用规则和限制。
- Hooks 在 React 16.8 之后引入,可能需要升级项目的 React 版本。
总体而言,HOC、Render Props 和 React Hooks 都是用于组件逻辑复用的常用技术和模式。选择适当的方式取决于具体的需求、开发团队的偏好以及项目的特点。每种方式都有其独特的优点和缺点,可以根据具体情况进行选择和权衡。
你是如何理解 fiber 的?
Fiber 是 React 中用于实现虚拟 DOM 和调度的一种架构。它是 React 16 引入的重大改进,旨在改善 React 的性能和交互体验。
在传统的 React 渲染过程中,当进行组件更新时,React 会递归地遍历整个组件树,进行同步的阻塞式更新。这种方式在大型应用或复杂组件树的情况下,可能导致渲染过程长时间阻塞主线程,造成页面卡顿、不流畅的用户体验。
Fiber 的目标是将渲染过程分解为多个小任务单元,使 React 能够灵活地中断、终止和恢复渲染过程,以更好地控制渲染的优先级和时间分配。它引入了一个可中断和恢复的渲染流程,使得 React 可以在渲染过程中优先处理用户交互、动画和高优先级任务。
Fiber 的核心思想包括以下几点:
虚拟 DOM 的改进:
- Fiber 使用一种新的虚拟 DOM 数据结构,称为 Fiber 节点(Fiber Node)。
- Fiber 节点保存了组件的相关信息,如类型、属性、状态等,并构成一个链表结构,形成一个渲染任务的工作单元。
调度和优先级:
- Fiber 使用优先级调度算法,将渲染任务划分为不同的优先级,如高、中、低等。
- React 可以根据任务的优先级动态地调整任务的执行顺序和时间分配,以提供更好的用户体验。
增量渲染:
- Fiber 允许 React 在多个帧之间分割渲染任务,将渲染过程分解为多个小任务单元。
- React 可以根据时间限制和优先级,中断当前任务并转移到下一个任务,以避免长时间的阻塞。
任务优先级的协调:
- Fiber 使用协调算法来确定哪些组件需要更新和重新渲染。
- React 可以根据组件的更新优先级和依赖关系,动态地决定是否跳过或中断某些任务。
通过引入 Fiber 架构,React 能够更好地控制渲染过程,提供更好的性能和用户体验。它允许 React 在多个任务之间进行交错处理,并根据任务的优先级和时间限制智能地分配资源。这种可中断和恢复的渲染方式使得 React 在复杂应用和高负载场景下能够更好地响应用户操作和保持页面流畅。
异步渲染有哪两个阶段?
异步渲染在 React 中可以分为两个阶段:
Reconciliation(协调阶段):
- 在协调阶段,React 会遍历组件树,比较前后两次渲染的差异,找到需要更新的部分。
- 这个阶段称为协调是因为 React 会协调组件的状态和属性,确定哪些组件需要更新,哪些组件需要被添加或移除,以及如何更新它们。
- 在协调阶段,React 会构建 Fiber 节点树,形成一个任务队列,用于描述需要进行的工作单元。
Commit(提交阶段):
- 在提交阶段,React 将根据协调阶段生成的 Fiber 节点树执行实际的 DOM 操作,将变更应用到真实的 DOM 上。
- 这个阶段称为提交是因为 React 会将更改提交给浏览器进行渲染和绘制。
- 在提交阶段,React 会遍历 Fiber 节点树,并执行相应的 DOM 操作,如创建、更新或删除元素,更新属性和样式等。
这两个阶段是异步渲染的关键组成部分。在协调阶段,React 通过构建 Fiber 节点树来描述任务和工作单元,确定更新的优先级和顺序。在提交阶段,React 会执行实际的 DOM 操作,将变更应用到页面上。通过将渲染过程分解为协调和提交两个阶段,React 能够更好地控制渲染的优先级、时间分配和用户体验。
你对 Time Slice 的理解?
Time Slice(时间切片)是 React 中用于实现异步渲染的一种机制。它允许将渲染工作分解为多个较小的时间片段,以提高页面的响应性和流畅性,避免长时间的阻塞。
在传统的同步渲染中,当 React 开始进行渲染工作时,它会一直执行直到完成,阻塞主线程,导致页面无法响应用户的交互或动画效果。这可能会导致页面卡顿、用户体验下降。
Time Slice 的目标是将渲染工作分解为多个时间片段,每个时间片段只占用一小段时间,然后将控制权交还给浏览器,使其能够响应其他任务和用户交互。
具体来说,Time Slice 实现了以下几个关键点:
任务分割:React 将渲染工作划分为多个较小的任务单元,称为时间片。每个时间片的执行时间应该尽量保持在 5 毫秒以内,以避免长时间的阻塞。
优先级调度:React 使用优先级调度算法来确定哪些任务应该优先执行。它可以根据任务的优先级和剩余时间,动态地调整任务的执行顺序。
中断和恢复:在每个时间片的执行过程中,React 可以在必要时中断任务的执行,并将控制权交还给浏览器。这样,浏览器可以执行其他任务,如处理用户交互、动画或网络请求。
增量更新:React 在每个时间片执行期间,只更新当前时间片内需要更新的部分。这样可以避免一次性更新整个组件树,提高渲染的效率。
通过引入 Time Slice 机制,React 可以更好地控制渲染过程,提高页面的响应性和流畅性。它允许 React 在多个时间片之间进行交错处理,将渲染工作分解为小任务单元,并根据任务的优先级和时间限制智能地分配资源。这种异步渲染的方式使得 React 在复杂应用和高负载场景下能够更好地响应用户操作,保持页面的流畅性。
redux 的工作流程?
Redux 是一个用于管理应用状态的 JavaScript 库,它遵循一种单向数据流的工作流程。以下是 Redux 的基本工作流程:
定义状态(State):
- 在 Redux 中,应用的状态被存储在一个单一的 JavaScript 对象中,称为状态树(State Tree)或应用状态(Application State)。
- 开发者需要定义应用的初始状态,并根据应用需求确定状态的结构和属性。
触发动作(Action):
- 动作是描述状态变化的普通 JavaScript 对象,它们包含一个
type
字段来表示动作的类型,并可以携带其他自定义的数据。 - 开发者通过调用动作创建函数(Action Creator)来创建动作,并将动作发送给 Redux。
- 动作是描述状态变化的普通 JavaScript 对象,它们包含一个
处理动作(Reducer):
- Reducer 是纯函数,它接收当前的状态和被触发的动作作为参数,并根据动作的类型来更新状态。
- Reducer 应该返回一个新的状态对象,而不是直接修改原始状态,以保持状态的不可变性。
状态更新(Store):
- Store 是 Redux 的核心对象,它保存应用的状态树,并提供一些方法来管理状态。
- 当动作被触发时,Redux 会调用 Reducer 来处理动作,并根据 Reducer 返回的新状态更新应用的状态树。
状态订阅(Subscription):
- 开发者可以通过订阅(Subscription)机制,将回调函数注册到 Store 中,以便在状态发生变化时得到通知。
- 当状态更新时,Redux 会调用订阅的回调函数,开发者可以在回调函数中执行相应的操作,如更新用户界面。
通过这个工作流程,Redux 实现了一种可预测、可维护的状态管理机制。开发者可以通过派发动作来触发状态的变化,而不直接修改状态,从而实现了状态的可追踪和可调试性。Redux 还提供了中间件(Middleware)机制,用于处理异步操作和扩展 Redux 的功能。这样,开发者可以更好地管理复杂的应用状态,并提供可预测的数据流和一致的应用行为。
react-redux 是如何工作的?
React-Redux 是将 React 和 Redux 结合起来使用的官方库,它提供了一种连接 React 组件与 Redux 状态管理的方式。React-Redux 主要通过两个关键组件来实现工作:Provider 和 Connect。
下面是 React-Redux 的工作流程:
创建 Redux Store:
- 首先,使用 Redux 创建一个全局的 Redux Store,该 Store 存储应用的状态树,并提供状态的更新和访问方法。
Provider 组件:
- 在应用的根组件中,使用 React-Redux 提供的 Provider 组件包裹整个应用。
- Provider 组件通过 React 的 Context 特性将 Redux Store 传递给所有的子组件,使得子组件能够访问到 Redux Store。
Connect 函数:
- 在需要访问 Redux 状态的组件中,使用 Connect 函数连接组件与 Redux。
- Connect 函数接收两个参数:
mapStateToProps
和mapDispatchToProps
,用于指定组件需要访问的状态和动作方法。
mapStateToProps:
- mapStateToProps 是一个函数,它定义了组件需要从 Redux Store 中获取的状态。
- 该函数接收 Redux Store 中的状态作为参数,并返回一个对象,其中包含组件所需的状态属性。
mapDispatchToProps:
- mapDispatchToProps 是一个函数或对象,它定义了组件需要派发的动作方法。
- 如果传递的是一个函数,该函数接收
dispatch
方法作为参数,并返回一个对象,其中包含组件需要派发的动作方法。 - 如果传递的是一个对象,对象中的每个属性都应该是一个动作创建函数,React-Redux 会自动将其包装成一个派发动作的函数。
连接组件与 Redux:
- Connect 函数会将 mapStateToProps 返回的状态属性和 mapDispatchToProps 返回的动作方法注入到组件的 props 中。
- 当 Redux Store 的状态发生变化或组件需要派发动作时,React-Redux 会自动更新组件,并确保组件能够正确响应状态的变化。
通过 Provider 和 Connect,React-Redux 实现了将 Redux 状态管理与 React 组件连接起来的功能。它简化了在 React 组件中访问和更新 Redux 状态的过程,提供了一种声明式的方式来处理状态和动作,使得开发者能够更轻松地使用 Redux 来管理应用的状态。
redux 中如何进行异步操作?
在 Redux 中进行异步操作时,可以使用中间件(Middleware)来处理。Redux 中最常用的异步操作中间件是 Redux Thunk 和 Redux Saga。
Redux Thunk:
- Redux Thunk 是 Redux 官方推荐的中间件,它允许在 Redux 动作中编写异步逻辑。
- 安装 Redux Thunk:使用 npm 或 yarn 安装 redux-thunk 包。
- 创建异步动作:编写一个返回函数的动作创建函数,这个函数接收
dispatch
和getState
作为参数。 - 在返回的函数中,可以执行异步操作,如发起网络请求或定时器等,并在异步操作完成后再派发其他动作来更新状态。
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
const fetchData = () => {
return (dispatch, getState) => {
dispatch({ type: "FETCH_DATA_REQUEST" });
// 异步操作
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => {
dispatch({ type: "FETCH_DATA_SUCCESS", payload: data });
})
.catch((error) => {
dispatch({ type: "FETCH_DATA_FAILURE", payload: error });
});
};
};
const store = createStore(reducer, applyMiddleware(thunk));
store.dispatch(fetchData());Redux Saga:
- Redux Saga 是一个强大的 Redux 中间件,它使用了 ES6 的生成器(Generators)来处理异步操作。
- 安装 Redux Saga:使用 npm 或 yarn 安装 redux-saga 包。
- 创建 Saga:编写一个 Saga 函数,使用
takeEvery
或其他监听器来捕获特定的动作,并在其中执行异步操作。 - 在 Saga 函数中,可以使用各种效果(Effects)函数来处理异步操作,如
call
调用函数、put
派发动作等。
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import { takeEvery, call, put } from "redux-saga/effects";
function* fetchData() {
try {
yield put({ type: "FETCH_DATA_REQUEST" });
// 异步操作
const response = yield call(fetch, "https://api.example.com/data");
const data = yield call([response, "json"]);
yield put({ type: "FETCH_DATA_SUCCESS", payload: data });
} catch (error) {
yield put({ type: "FETCH_DATA_FAILURE", payload: error });
}
}
function* rootSaga() {
yield takeEvery("FETCH_DATA", fetchData);
}
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
store.dispatch({ type: "FETCH_DATA" });
无论是使用 Redux Thunk 还是 Redux Saga,都可以在 Redux 中处理异步操作。它们提供了一种结构化和可扩展的方式来管理复杂的异步逻辑,使得 Redux 在处理异步操作时更加灵活和易用。具体选择哪种中间件取决于项目需求和个人偏好。
redux 异步中间件 redux-thunk 的优劣?
Redux Thunk 是 Redux 官方推荐的异步操作中间件之一,它有一些优点和一些限制。
优点:
简单易用:相对于其他复杂的异步中间件,Redux Thunk 的概念和使用方式相对简单。它只需要编写返回函数的动作创建函数,并在函数中执行异步操作。
轻量级:Redux Thunk 是一个轻量级的中间件,它的代码量相对较小,易于理解和维护。
与现有的 Redux 生态系统兼容:Redux Thunk 与 Redux 生态系统中的其他工具和库兼容性良好,可以与 React-Redux、DevTools 等无缝集成。
广泛使用:Redux Thunk 是 Redux 中最常用的异步中间件之一,因此在社区中有很多相关的资源和示例可以参考。
限制:
只支持简单的异步操作:Redux Thunk 主要用于处理简单的异步操作,如发起网络请求、定时器等。对于复杂的异步流程,如并行请求、条件触发等,Redux Thunk 的处理方式相对有限。
逻辑容易分散:在 Redux Thunk 中,异步操作的逻辑被分散在动作创建函数中,可能会导致代码的可读性和可维护性下降,特别是在处理多个异步操作时。
不支持取消操作:Redux Thunk 本身不提供对取消操作的支持。如果需要处理取消操作或中断正在进行的异步任务,可能需要额外的处理。
总体而言,Redux Thunk 是一个简单而常用的异步中间件,适用于处理简单的异步操作。如果项目中的异步逻辑相对简单,并且希望使用较少的代码和概念来处理异步操作,那么 Redux Thunk 是一个不错的选择。然而,对于更复杂的异步流程,可能需要考虑使用其他的异步中间件,如 Redux Saga 或 Redux Observable,它们提供了更强大和灵活的异步处理能力。选择适合项目需求的中间件是根据具体情况来决定的。
合成事件是什么?
合成事件(Synthetic Events)是 React 中的一种事件系统,它是对原生 DOM 事件的封装和抽象。React 使用合成事件来处理用户操作和交互,提供了一种跨浏览器、跨平台的事件处理机制。
合成事件的主要特点和优势包括:
跨浏览器兼容性:合成事件封装了底层的原生 DOM 事件,使得开发者无需关心不同浏览器之间的兼容性问题。
性能优化:合成事件是 React 对事件处理的优化机制之一。React 使用事件委托(Event Delegation)的方式来处理事件,将事件监听器绑定在最上层的 DOM 节点上,从而减少了事件监听器的数量,提高了性能。
事件池:合成事件实现了事件池机制,通过重用事件对象来提高性能。当事件被触发时,React 会从事件池中获取一个事件对象并传递给事件处理函数,事件处理完毕后会将事件对象重置并放回池中,以供下次使用。
事件代理:合成事件利用事件冒泡机制,通过在最上层的 DOM 节点上绑定事件监听器,实现了事件代理(Event Delegation)。这意味着只需要在最上层的节点上添加一个事件监听器,就可以处理该节点及其子节点上的事件,避免了给每个子节点都添加事件监听器的开销。
扩展功能:合成事件除了封装了原生 DOM 事件的属性和方法外,还提供了额外的功能,比如阻止事件冒泡和默认行为、对移动端触摸事件的支持等。
使用合成事件,可以像处理原生 DOM 事件一样在 React 组件中处理用户操作和交互。React 提供了一系列的事件处理方法,如 onClick、onChange、onSubmit 等,可以通过这些方法来注册事件处理函数并处理合成事件。
React Hooks 原理?
React Hooks 是 React 16.8 版本引入的一项特性,它提供了一种在函数组件中使用状态和其他 React 特性的方式,避免了类组件的繁琐和冗余。
React Hooks 的实现原理主要涉及两个方面:函数组件的状态管理和生命周期的模拟。
1. 函数组件的状态管理:
在函数组件中使用状态,最常用的 Hook 是 useState
。useState
实际上是一个函数,它返回一个数组,其中包含当前状态的值和更新状态的函数。React 使用了一种称为“链表”的数据结构来跟踪状态的变化。
当函数组件第一次渲染时,React 会创建一个链表节点来存储状态值,并将其与组件实例相关联。每当状态发生变化时,React 会通过链表找到与该状态关联的节点,并更新状态的值。这些状态的更新是通过 useState
返回的更新函数来触发的。
2. 生命周期的模拟:
在类组件中,可以使用生命周期方法来处理组件的挂载、更新和卸载等阶段的操作。而在函数组件中,没有类似的生命周期方法。为了模拟生命周期的行为,React 引入了 useEffect
Hook。
useEffect
允许在函数组件中执行副作用操作(如订阅事件、数据获取等)。它接受一个函数作为参数,该函数会在组件渲染完成后执行,并且可以返回一个清理函数,用于处理组件卸载时的清理操作。
React 使用了一种称为“Fiber”的新的协调算法,它通过将组件树的工作分解为一系列小的任务单元,使得 useEffect
中的副作用操作可以在组件渲染过程中正确地被调度和执行。
除了 useState
和 useEffect
,React 还提供了其他的 Hooks,如 useContext
、useReducer
、useCallback
、useMemo
等,它们通过类似的机制来提供不同的功能。
总的来说,React Hooks 的原理基于链表结构来管理函数组件的状态,并通过类似生命周期的 useEffect
来处理副作用操作。这种机制使得函数组件具有了类似于类组件的状态管理和生命周期控制的能力,同时简化了组件的编写和维护。
为什么 ReactHooks 中不能有条件判断
在 React Hooks 中,原则上是不建议在条件判断语句中使用 Hooks 的。这是因为 React Hooks 的使用规则要求 Hooks 的调用必须保持在组件的顶层,不能放在条件判断、循环或嵌套函数中。
这个规则的原因主要有两点:
1. Hooks 的调用顺序必须保持一致: React Hooks 依赖于调用顺序来确定每个 Hook 的状态,如果在条件判断中使用 Hooks,那么不同条件下的 Hook 调用顺序可能会发生变化,导致状态的错乱或不一致。这会导致组件的行为变得不可预测,并且可能引发错误。
2. Hooks 的规则依赖于静态分析: React 使用静态分析来检查 Hooks 的调用顺序是否正确。如果 Hooks 的调用存在在条件判断中的情况,静态分析工具无法在编译时确定 Hooks 的调用顺序,从而无法进行正确的检查。这可能会导致难以发现的 bug 和问题。
虽然在某些特定情况下,React 允许在条件判断中使用 Hooks,但这些情况非常有限,并且需要遵循严格的规则和限制。例如,可以在条件判断的每个分支中使用相同的 Hooks,并确保每个分支都返回相同的组件结构。这样可以确保 Hooks 的调用顺序保持一致,但仍然需要小心处理,以避免可能导致问题的情况。
总结起来,遵循 React Hooks 的规范和最佳实践,将 Hooks 的调用保持在组件的顶层,可以确保代码的可读性、可维护性和稳定性。如果需要在条件判断中处理特定的逻辑,可以通过其他方式来实现,如使用状态变量或条件渲染的方式来控制组件的行为。
Class 组件 VS Hook
Class 组件和 Hook 是在 React 中用于编写组件的两种不同的方式。
Class 组件:
Class 组件是 React 最早引入的一种组件编写方式,它是通过继承 React 的 Component 类来创建的。在 Class 组件中,组件的状态(state)和生命周期方法(lifecycle methods)都是以类成员函数的形式定义的。
优点:
- 生命周期方法:Class 组件提供了一系列的生命周期方法,如
componentDidMount
、componentDidUpdate
、componentWillUnmount
等,用于处理组件的挂载、更新和卸载等阶段的操作。 - 实例化和面向对象:Class 组件创建的实例可以存储状态和方法,并且支持面向对象的编程风格。
- 广泛使用:Class 组件是 React 最早的组件编写方式,在早期的 React 项目和教程中广泛使用,相关资源和示例较多。
缺点:
- 繁琐的语法:Class 组件的语法相对复杂,需要了解和掌握类的概念、继承、构造函数等知识。
- 代码冗余:在 Class 组件中,为了处理状态和生命周期,需要编写较多的模板代码,包括构造函数、事件处理函数的绑定等。
- 难以维护:随着组件的复杂度增加,Class 组件中的生命周期方法和状态管理逻辑可能变得混乱和难以维护。
Hooks:
Hooks 是 React 16.8 版本引入的一种新的组件编写方式,它可以让函数组件具有类组件的状态管理和生命周期控制的能力,同时简化了组件的编写和维护。Hooks 是以函数的形式定义的,通过调用一系列的 Hook 函数来使用。
优点:
- 简洁的语法:Hooks 使用函数式编程的思想,语法相对简洁,不需要了解和使用类的概念,减少了模板代码的编写。
- 状态和副作用的处理:Hooks 提供了一系列的 Hook 函数,如
useState
、useEffect
、useContext
等,用于处理状态、副作用和上下文等场景。 - 更容易测试:由于 Hooks 是纯函数,没有类的实例和复杂的生命周期,所以更容易编写和执行单元测试。
缺点:
- 较新的特性:Hooks 是在 React 16.8 版本引入的,相对于 Class 组件来说是较新的特性,可能在一些旧项目或库中不被支持。
- 破坏性改变:Hooks 的引入改变了 React 组件编写的方式,可能需要对现有的 Class 组件进行重构和迁移。
总体而言,Hooks 是 React 推荐的组件编写方式,它提供了更简洁、可复用和可测试的方式来处理组件的状态和副作用。对于新项目或需要重构的项目,推荐使用 Hooks 来编写组件。然而,对于一些已经使用 Class 组件编写的项目,可以根据实际情况来决定是否进行迁移。
自定义过哪些 Hook
在 React 中,可以自定义各种自定义 Hook 来封装可重用的逻辑,以便在函数组件中共享和复用。以下是一些常见的自定义 Hook 的示例:
1. useStateWithLocalStorage
import { useState, useEffect } from "react";
function useStateWithLocalStorage(key, initialValue) {
const [state, setState] = useState(() => {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state));
}, [key, state]);
return [state, setState];
}
这个自定义 Hook 可以将状态值保存在 localStorage 中,实现了在刷新页面后仍然可以保留状态的功能。
2. useFetch
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const result = await response.json();
setData(result);
setLoading(false);
} catch (error) {
setError(error);
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, isLoading, error };
}
这个自定义 Hook 可以用于发起网络请求并获取数据,返回数据、加载状态和错误信息,简化了网络请求的处理逻辑。
3. useFormInput
import { useState } from "react";
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
const handleChange = (event) => {
setValue(event.target.value);
};
return {
value,
onChange: handleChange,
};
}
这个自定义 Hook 可以用于处理表单输入,返回输入值和变更处理函数,简化了表单处理的逻辑。
以上只是一些示例,你可以根据具体需求自定义各种不同的 Hook,用于封装和共享特定的逻辑。自定义 Hook 可以使你的代码更加模块化、可复用和易于测试,提高了组件的可维护性和开发效率。
说说对 React refs 的理解?
在 React 中,Refs 是一种用于访问组件实例或 DOM 元素的机制。Refs 允许我们直接访问 DOM 节点或在函数组件中访问子组件的实例。它提供了一种逃离 React 数据流的方式,可以在某些情况下获取或修改底层 DOM 元素或组件实例。
使用 Refs 可以实现以下功能:
1. 访问 DOM 元素: 使用 Refs 可以获取或修改底层 DOM 元素的属性和方法。这对于需要直接操作 DOM 的场景非常有用,比如聚焦元素、测量元素的尺寸、添加动画效果等。
2. 访问组件实例: 在某些情况下,可能需要在父组件中直接访问子组件的实例。通过 Refs,我们可以获取子组件的引用,并调用子组件的方法、访问其状态或属性。
在 React 中,Refs 有两种常见的使用方式:
1. 创建 Refs: 可以通过 React.createRef()
函数来创建一个 Ref 对象。在类组件中,可以将 Ref 对象附加到类组件的实例属性上。在函数组件中,可以使用 useRef()
Hook 来创建 Ref 对象。
// 类组件中使用 Refs
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
console.log(this.myRef.current); // 访问 DOM 元素
console.log(this.myRef.current.someMethod()); // 访问组件实例的方法
}
render() {
return <div ref={this.myRef}>Hello, Refs!</div>;
}
}
// 函数组件中使用 Refs
function MyComponent() {
const myRef = useRef();
useEffect(() => {
console.log(myRef.current); // 访问 DOM 元素
console.log(myRef.current.someMethod()); // 访问组件实例的方法
}, []);
return <div ref={myRef}>Hello, Refs!</div>;
}
2. 使用 Refs: 创建 Refs 后,可以将其传递给 React 元素的 ref
属性。Refs 的值会在组件渲染时自动更新,并且可以通过 ref.current
属性来访问实际的 DOM 元素或组件实例。
function MyComponent() {
const inputRef = useRef();
const handleClick = () => {
inputRef.current.focus(); // 聚焦输入框
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={handleClick}>Focus Input</button>
</div>
);
}
需要注意的是,Refs 是一种逃离 React 数据流的机制,应该在必要时使用。在大多数情况下,推荐使用 React 的数据流来管理状态和操作组件。Refs 应该作为一种辅助手段,用于处理特殊的需求,如直接操作 DOM 或访问子组件的实例。
React context 的理解?
React Context(上下文)是一种在 React 组件树中共享数据的方法,用于解决跨组件传递数据的问题。它可以让你在组件之间共享数据,而不需要通过 props 层层传递。
React Context 包含两个主要的概念:
1. Context Provider(上下文提供者): 上下文提供者是一个 React 组件,它通过创建上下文并将数据传递给后代组件。上下文提供者通过将数据和相应的更新函数封装在上下文对象中,使得后代组件可以访问和使用这些数据。
2. Context Consumer(上下文消费者): 上下文消费者是一个 React 组件,它通过订阅上下文来获取上下文提供者中的数据。上下文消费者可以通过一个函数或类组件的方式来访问上下文数据,从而使用这些数据来渲染组件或执行其他操作。
使用 React Context 的基本流程如下:
创建一个上下文对象,通过调用
React.createContext()
来完成。在上下文提供者中,通过将数据传递给上下文对象的
Provider
组件来共享数据。上下文提供者可以包裹需要访问共享数据的组件树的一部分。在上下文消费者中,通过调用上下文对象的
Consumer
组件或使用useContext
Hook 来订阅上下文,并获取共享的数据。
下面是一个简单的示例,演示了如何使用 React Context:
// 创建上下文对象
const MyContext = React.createContext();
// 上下文提供者组件
function MyContextProvider({ children }) {
const [data, setData] = useState("Hello, Context");
const updateData = () => {
setData("Updated Context Data");
};
return (
<MyContext.Provider value={{ data, updateData }}>
{children}
</MyContext.Provider>
);
}
// 上下文消费者组件
function MyComponent() {
const { data, updateData } = useContext(MyContext);
return (
<div>
<p>{data}</p>
<button onClick={updateData}>Update Context</button>
</div>
);
}
// 使用上下文提供者包裹组件树的一部分
function App() {
return (
<MyContextProvider>
<MyComponent />
</MyContextProvider>
);
}
在上面的示例中,MyContextProvider
是上下文提供者组件,它通过 MyContext.Provider
来提供共享数据 data
和更新函数 updateData
。MyComponent
是上下文消费者组件,它通过 useContext
Hook 来订阅上下文,并获取共享的数据和更新函数。
通过使用 React Context,我们可以避免通过 props 将数据层层传递给需要的组件,而是直接在组件中订阅上下文并获取数据。这样可以简化组件之间的数据传递,提高了组件的灵活性和可重用性。
详细介绍一下 fibber 的实现原理
React Fiber 是 React v16 引入的一种对调度和协调渲染的新的核心算法。它的目标是提高 React 的性能,并改善用户界面的响应性和交互性。React Fiber 的实现原理可以分为以下几个关键步骤:
协调阶段(Reconciliation Phase): React Fiber 使用一种增量渲染的方式进行协调。在协调阶段,React Fiber 会遍历整个组件树,并根据组件的更新优先级确定哪些组件需要更新,以及如何更新。这个过程是可中断的,可以根据优先级和时间片来分割任务,从而实现更平滑的用户界面响应。
任务切片(Task Slicing): React Fiber 将协调阶段的任务切分为多个小的任务单元,每个任务单元称为一个 Fiber。这些小任务单元可以被中断、恢复和优先级调度,以便在多个渲染周期中分散工作,避免长时间的阻塞,提高用户界面的响应性。
Fiber 数据结构: 每个 Fiber 对象都包含了组件的相关信息,如组件的类型、状态、属性等。Fiber 对象之间通过指针形成一个链表结构,形成了 React Fiber 树。这种链表结构使得 React 可以在协调过程中快速地进行任务切换和优先级调度。
优先级调度(Priority Scheduling): React Fiber 使用调度器来确定任务的优先级。每个任务都有一个优先级标记,React Fiber 根据任务的优先级来决定任务的执行顺序。这使得 React 可以在高优先级任务需要被执行时,暂停低优先级任务的执行,从而保证用户界面的响应性。
可中断和恢复(Interruptible and Resumable): React Fiber 具有可中断和恢复的特性。它可以在任意时刻中断任务的执行,并在下一个时间片或其他高优先级任务执行完毕后恢复执行。这种特性使得 React Fiber 可以更好地响应用户的交互,避免阻塞整个渲染过程。
通过上述原理和机制,React Fiber 实现了更高效的渲染和协调过程,提高了 React 应用程序的性能和响应性。它使得 React 能够更好地处理大型组件树、复杂的交互和动画效果,为开发者提供更好的用户体验。
请简述虚拟 dom 与 diff 算法
虚拟 DOM(Virtual DOM)和差异算法(Diff Algorithm)是 React 中用于高效更新和渲染用户界面的关键技术。
虚拟 DOM: 虚拟 DOM 是 React 中的一种表示界面结构的轻量级 JavaScript 对象树。它通过使用纯 JavaScript 对象来描述用户界面的结构和状态,并且与实际的浏览器 DOM 元素相对应。虚拟 DOM 具有与真实 DOM 类似的层次结构和属性,并且可以被 React 组件进行修改、操作和渲染。通过使用虚拟 DOM,React 可以在内存中构建和操作整个界面树,而无需直接操作浏览器的实际 DOM,从而提供了更高的性能和效率。
差异算法: 差异算法是 React 使用的一种比较虚拟 DOM 的技术,它用于确定前后两个虚拟 DOM 树之间的差异,并仅更新需要更改的部分。差异算法的目标是最小化对实际 DOM 的操作次数,以减少浏览器的重绘和回流,从而提高性能。React 使用一种称为 "Reconciliation" 的差异算法来比较前后两个虚拟 DOM 树的差异,并生成最小化的更新操作。
差异算法的主要步骤如下:
- 树的遍历: 差异算法首先会逐层遍历前后两个虚拟 DOM 树的节点,比较它们的类型和属性。
- 节点比较: 差异算法会比较同级节点之间的差异,确定哪些节点需要更新、删除或插入。
- 递归处理子节点: 如果两个节点类型相同,差异算法会递归地比较它们的子节点。
- 生成差异操作: 在比较过程中,差异算法会生成一系列的更新操作,描述如何将前一个虚拟 DOM 树转换为后一个虚拟 DOM 树。这些操作包括更新属性、插入新节点、移动节点和删除节点等。
- 应用差异操作: 最后,React 将根据生成的差异操作列表,批量地将这些操作应用于实际 DOM,以实现界面的更新和渲染。
通过使用虚拟 DOM 和差异算法,React 实现了高效的界面更新。当应用程序状态发生变化时,React 会构建并比较虚拟 DOM 树,找出需要更新的部分,并应用最小化的变更操作,从而减少了对实际 DOM 的直接操作,提高了性能和用户体验。
调用 setState 之后发生了什么?
在 React 中,当调用组件的 setState
方法后,以下是大致的操作流程:
状态更新:
setState
方法被调用后,React 会将传入的状态更新合并到组件的当前状态中。这是一个异步操作,React 可能会将多个setState
调用合并为一个更新批次,以提高性能。触发重新渲染: 一旦状态更新完成,React 会标记组件为“脏”(dirty),表示组件的状态已经发生变化。接下来,React 将调用组件的
render
方法来重新计算组件的虚拟 DOM。虚拟 DOM 比较: React 会将前一次渲染的虚拟 DOM 树与当前计算得到的虚拟 DOM 树进行比较,找出两者之间的差异。
差异更新: 根据差异比较的结果,React 会生成一系列的差异操作,描述如何将前一次渲染的虚拟 DOM 树转换为当前的虚拟 DOM 树。这些操作包括更新属性、插入新节点、移动节点和删除节点等。
应用差异操作: 最后,React 将根据生成的差异操作列表,批量地将这些操作应用于实际 DOM,以实现界面的更新和渲染。这个过程通常称为“调和”(Reconciliation)。
值得注意的是,React 在执行 setState
后,并不会立即触发重新渲染。React 会在适当的时机进行批量更新,以提高性能。具体的触发时机和更新策略可能受到 React 的内部调度机制、组件优先级等因素的影响。
此外,React 还提供了一些生命周期方法(如 componentDidUpdate
)和钩子函数(如 useEffect
),可以让开发者在状态更新后执行额外的操作,比如处理副作用、发送网络请求等。
为什么虚拟 dom 会提高性能?
虚拟 DOM 在 React 中被引入的主要目的之一是为了提高性能。下面是虚拟 DOM 如何帮助提高性能的几个方面:
减少直接操作实际 DOM 的次数: 直接操作实际 DOM 是一项昂贵的操作,因为它涉及到浏览器的重绘和回流,对性能有很大的影响。虚拟 DOM 充当了一个中间层,它在内存中构建和操作整个界面树,而不是直接操作实际 DOM。通过将对实际 DOM 的操作减少到最低限度,虚拟 DOM 可以显著提高性能。
批量更新和优化渲染过程: 虚拟 DOM 具有批量更新的能力。当状态发生变化时,React 会将多个
setState
调用合并为一个更新批次,并进行一次性的渲染。这样可以避免不必要的中间渲染,并减少实际 DOM 的操作次数。此外,虚拟 DOM 的比较和差异更新算法可以帮助 React 找出需要更新的部分,从而避免对整个界面进行重新渲染,进一步提高性能。高效的 Diff 算法: 虚拟 DOM 使用差异算法(Diff Algorithm)来比较前后两个虚拟 DOM 树之间的差异,并生成最小化的更新操作。这意味着 React 只会更新真正发生变化的部分,而不会重新渲染整个界面。通过减少不必要的更新操作,虚拟 DOM 可以大大提高渲染的效率。
跨平台支持: 虚拟 DOM 的设计使得它不仅限于浏览器环境,还可以在其他环境(如服务器端)进行渲染。这种跨平台的支持使得开发者可以在不同的场景中共享组件逻辑和代码,并提供一致的开发体验。
需要注意的是,虚拟 DOM 本身并不是解决所有性能问题的银弹。在某些特定的场景下,直接操作实际 DOM 或手动优化渲染过程可能会更有效。然而,对于大多数应用程序和常见的界面交互,虚拟 DOM 通过减少对实际 DOM 的操作和优化渲染过程,提供了一种高效、可靠的方式来构建响应性能良好的用户界面。
Shadow DOM 和 Virtual DOM 有什么区别?
Shadow DOM(影子 DOM)和 Virtual DOM(虚拟 DOM)是两个不同的概念,用于解决不同的问题。
Shadow DOM: Shadow DOM 是一种浏览器技术,用于将组件的样式和结构封装起来,使其与页面的其他部分隔离开来。它提供了一种将 DOM 树分隔为独立的作用域的方式,使得组件可以拥有私有的 CSS 样式和 DOM 结构,并防止其与其他元素发生冲突。Shadow DOM 可以通过使用 <template>
标签、<slot>
标签和 attachShadow
方法来创建和使用。
Virtual DOM: Virtual DOM 是 React 中的一种技术,用于提高渲染性能和优化界面更新的效率。它是一个轻量级的 JavaScript 对象树,类似于实际的 DOM 树,用于描述用户界面的结构和状态。Virtual DOM 充当了一个中间层,React 可以在内存中构建和操作整个界面树,而不是直接操作实际的 DOM。通过使用虚拟 DOM,React 可以进行批量更新、差异比较和最小化的更新操作,以减少对实际 DOM 的操作次数,提高性能。
区别总结如下:
目的不同: Shadow DOM 旨在将组件的样式和结构进行封装和隔离,以避免与页面其他部分发生冲突;而 Virtual DOM 旨在提供一种高效的方式来比较和更新用户界面,以提高性能。
应用范围不同: Shadow DOM 是浏览器提供的技术,用于创建和管理组件的私有作用域;而 Virtual DOM 是 React 框架内部的一种技术,用于管理组件的渲染和更新。
实际性质不同: Shadow DOM 是浏览器原生的功能,用于在组件内部创建独立的 DOM 作用域;而 Virtual DOM 是 React 框架自行实现的概念,是一个纯 JavaScript 对象树,用于描述界面结构和状态。
需要注意的是,虽然 Shadow DOM 和 Virtual DOM 是不同的概念,但它们并不互斥,可以在同一个应用程序中同时使用。例如,React 可以与 Web 组件(使用 Shadow DOM)一起使用,以实现更好的封装和隔离效果。
写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?
在 React 和 Vue 项目中,在列表组件中使用key
属性是为了帮助框架更高效地渲染和更新列表项。
作用:
唯一标识列表项:
key
属性用于给列表中的每个项分配一个唯一的标识。这个标识在整个列表中必须是唯一的,帮助框架准确地识别每个列表项。优化列表渲染性能: 通过给列表项提供唯一的
key
属性,React 和 Vue 可以在重绘列表时更高效地进行 DOM 操作。使用key
属性可以帮助框架识别新增、删除或重新排序的列表项,从而避免重新渲染整个列表,而只更新必要的部分。保持组件状态: 在 React 中,使用
key
属性可以帮助框架保持组件的状态。当列表中的项重新排序或发生变化时,React 会尽可能地保留组件的状态,而不是重新创建一个新的组件实例。
注意事项:
唯一性要求:
key
属性的值必须在列表中是唯一的,不能重复。通常可以使用列表项的唯一标识符(如 ID)作为key
的值。稳定的标识:
key
属性的值在列表项之间应该是稳定的,不会频繁变化。如果列表项的标识在更新过程中频繁变化,可能导致不必要的重新渲染和性能问题。不要使用索引作为
key
: 在列表中使用索引作为key
是一种常见的错误做法。索引不保证在列表项添加、删除或重新排序时保持稳定,可能导致不正确的渲染和更新。
下面是一个在 React 中使用key
属性的示例:
function MyComponent() {
const items = [
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
{ id: 3, name: "Item 3" },
];
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
在上述示例中,每个列表项都被分配了一个唯一的key
,这样 React 可以更高效地渲染和更新列表。
React 中 setState 什么时候是同步的,什么时候是异步的?
在 React 中,setState
的执行可能是同步的,也可能是异步的,具体取决于它被调用的上下文和环境。
同步情况:
- 在事件处理函数或生命周期方法(除去
componentDidMount
和componentDidUpdate
)中调用setState
时,setState
通常是同步的。这意味着状态立即更新,并且后续代码可以访问到更新后的状态。
- 在事件处理函数或生命周期方法(除去
异步情况:
- 在 React 合成事件处理函数中(例如按钮点击事件)调用
setState
时,setState
通常是异步的。React 会对连续的setState
调用进行批处理,以提高性能。这意味着多个setState
调用会被合并为一个更新操作,只触发一次组件更新。 - 在
componentDidMount
和componentDidUpdate
生命周期方法中调用setState
时,setState
通常是异步的。这是为了避免在组件更新过程中导致无限循环的问题。在这些生命周期方法中,如果需要同步访问更新后的状态,可以使用componentDidUpdate
的第二个参数或componentDidUpdate
之前的this.state
来获取更新后的状态。
- 在 React 合成事件处理函数中(例如按钮点击事件)调用
需要注意的是,即使在异步情况下,React 会保证在组件更新后(即 DOM 更新后)才会执行setState
回调函数,以确保在更新后的状态下执行逻辑。
如果需要依赖先前的状态或更新后的状态来进行计算,请使用函数形式的setState
,而不是对象形式。函数形式的setState
接受先前的状态作为参数,并返回新的状态,这样可以确保状态更新是基于最新的状态进行的。例如:
this.setState((prevState) => ({
count: prevState.count + 1,
}));
总结起来,setState
在 React 中的同步或异步行为取决于调用时的上下文和环境,这是为了提高性能和避免潜在的问题。在大多数情况下,可以将setState
视为异步操作,但需要注意处理异步更新的情况。
React setState 笔试题,下面的代码输出什么?
class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0,
};
}
componentDidMount() {
this.setState({ val: this.state.val + 1 });
console.log(this.state.val); // 第 1 次 log
this.setState({ val: this.state.val + 1 });
console.log(this.state.val); // 第 2 次 log
setTimeout(() => {
this.setState({ val: this.state.val + 1 });
console.log(this.state.val); // 第 3 次 log
this.setState({ val: this.state.val + 1 });
console.log(this.state.val); // 第 4 次 log
}, 0);
}
render() {
return null;
}
}
以下是代码的输出结果:
第 1 次 log: 0 第 2 次 log: 0 第 3 次 log: 2 第 4 次 log: 3
解析:
- 在
componentDidMount
生命周期方法中,首先调用setState
将val
的值从 0 更新为 1。然而,由于setState
是异步的,所以在第一个console.log
语句执行时,this.state.val
仍然是 0。 - 接下来,再次调用
setState
将val
的值从 0 更新为 1。同样地,由于setState
是异步的,第二个console.log
语句输出的仍然是 0。 - 然后,通过
setTimeout
在下一个事件循环中执行回调函数。此时,React 已经更新了状态,this.state.val
的值为 2。因此,第三个console.log
语句输出 2。 - 最后,再次调用
setState
将val
的值从 2 更新为 3。由于是在同一个事件循环中的回调函数中调用的setState
,所以第四个console.log
语句输出 3。
需要注意的是,在同一事件循环中的多个setState
调用会被合并为一个更新操作,只触发一次组件更新。因此,连续的两次setState
调用并不会导致两次组件更新。
其他回答:
1、第一次和第二次都是在 react 自身生命周期内,触发时 isBatchingUpdates 为 true,所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0。
2、两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。
3、setTimeout 中的代码,触发时 isBatchingUpdates 为 false,所以能够直接进行更新,所以连着输出 2,3。
输出: 0 0 2 3
聊聊 Redux 和 Vuex 的设计思想
Redux 和 Vuex 是两个流行的状态管理库,分别用于 JavaScript 和 Vue.js 应用程序。它们都具有相似的设计思想,以下是它们的设计思想的主要方面:
单一数据源:
- Redux 和 Vuex 都采用了单一数据源的设计思想,即整个应用程序的状态被存储在一个单一的数据结构中。
- 这样做的好处是可以更方便地跟踪和调试应用程序的状态变化,以及实现时间旅行和状态回滚等功能。
状态不可变性:
- Redux 和 Vuex 都鼓励状态的不可变性,即不直接修改状态,而是通过创建新的状态对象来实现状态的更新。
- 这样做的好处是可以更好地追踪状态的变化,避免出现意外的副作用,并且方便实现性能优化,如使用浅比较来避免不必要的重新渲染。
纯函数更新状态:
- Redux 和 Vuex 都要求使用纯函数来更新状态,即给定相同的输入,始终返回相同的输出,且没有副作用。
- 这种设计思想使得状态更新可预测和可测试,简化了状态管理的复杂性。
基于订阅机制:
- Redux 和 Vuex 都使用订阅机制来实现状态的响应式更新。当状态发生变化时,订阅者会被通知并执行相应的操作。
- 这种设计思想使得组件能够订阅感兴趣的状态,并在状态变化时自动更新,实现了组件间的解耦。
异步操作的处理:
- Redux 和 Vuex 都提供了中间件来处理异步操作,例如 Redux 的 Redux Thunk 和 Redux Saga,以及 Vuex 的 Actions 和插件机制。
- 这样做的好处是可以将异步逻辑从组件中抽离出来,统一处理异步操作的状态变化,使得代码更清晰和可维护。
总结: Redux 和 Vuex 都遵循了类似的设计思想,包括单一数据源、状态不可变性、纯函数更新状态、基于订阅机制和异步操作的处理。它们的设计目标都是简化状态管理的复杂性,提供可预测、可维护和可测试的状态管理解决方案。具体选择 Redux 还是 Vuex,取决于你使用的技术栈(JavaScript 还是 Vue.js)以及个人偏好。
Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法。
虚拟 DOM(Virtual DOM)相对于直接操作原生 DOM 的优势主要体现在性能方面,但具体的性能优劣会受到多种因素的影响。下面是对虚拟 DOM 和原生 DOM 的比较以及一些想法:
原生 DOM 操作的缺点:
- 直接操作原生 DOM 可能会导致频繁的 DOM 更新和重绘,这对于性能是一种负担。每次修改 DOM 都会触发浏览器的重新渲染过程,这可能会导致页面的闪烁和卡顿。
- 原生 DOM 操作通常需要手动处理复杂的 DOM 更新逻辑,例如手动追踪变化、手动处理 DOM 差异等。这可能会导致代码复杂性增加,可维护性降低。
虚拟 DOM 的优点:
- 虚拟 DOM 将 DOM 操作抽象为 JavaScript 数据结构,通过比较前后两个虚拟 DOM 的差异来最小化对实际 DOM 的修改。只需要对实际 DOM 进行最小的更新,可以减少 DOM 操作和重绘的次数,从而提高性能。
- 虚拟 DOM 可以批量处理 DOM 更新,通过批量更新实现优化,减少了不必要的重绘和回流。将多个 DOM 操作合并为一次更新,可以减少浏览器的重排和重绘次数。
- 虚拟 DOM 可以提供更简洁的编程模型,让开发人员专注于数据和视图的关系,而不需要过多关心底层 DOM 操作的细节。这提高了开发效率和可维护性。
然而,需要注意的是,虚拟 DOM 并不是适用于所有场景的万能解决方案。在某些情况下,直接操作原生 DOM 可能更高效,特别是对于简单的页面或特定的性能要求。此外,虚拟 DOM 的引入也会增加额外的内存开销和计算开销。
综上所述,虚拟 DOM 在大多数情况下可以提供更好的性能和开发体验,但在特定的场景下,直接操作原生 DOM 可能更加高效。在选择使用虚拟 DOM 还是原生 DOM 时,需要综合考虑应用的性能需求、复杂度、开发人员的熟悉程度等因素。
为什么 Vuex 的 mutation 和 Redux 的 reducer 中不能做异步操作?
Mutation 必须是同步函数 一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
在组件中提交 Mutation 你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。
import { mapMutations } from "vuex";
export default {
// ...
methods: {
...mapMutations([
"increment", // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
"incrementBy", // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: "increment", // 将 `this.add()` 映射为 `this.$store.commit('increment')`
}),
},
};
redux 为什么要把 reducer 设计成纯函数
在 Redux 中,将 Reducer 设计为纯函数具有以下几个重要的原因:
可预测性:纯函数的输出仅由输入决定,没有副作用。这使得 Reducer 的行为是可预测的,相同的输入将始终产生相同的输出。这对于应用的状态管理非常重要,因为它确保了状态更新的可靠性和一致性。
可测试性:纯函数的特性使得对 Reducer 进行单元测试变得非常容易。你可以为 Reducer 提供不同的输入并验证其输出是否符合预期,而无需担心外部状态的影响或其他副作用。
可组合性:由于纯函数不依赖于外部状态,你可以轻松地将多个 Reducer 组合在一起形成一个更大的 Reducer。这种组合性使得 Redux 的状态管理更加模块化和可扩展,你可以通过组合简单的 Reducer 来构建复杂的状态更新逻辑。
可回溯性:由于纯函数不会修改原始状态,而是创建新的状态对象,因此 Redux 可以轻松地实现时间旅行和回滚功能。这允许你在应用的不同状态间进行导航和调试,以便更好地理解应用的状态变化。
性能优化:由于纯函数的可预测性,Redux 可以通过对比状态对象的引用来优化状态更新过程。如果新旧状态相同,则可以避免不必要的重新计算和通知。
总而言之,将 Reducer 设计为纯函数是 Redux 架构的核心原则之一。纯函数的特性提供了可预测性、可测试性、可组合性、可回溯性和性能优化等优势,使得 Redux 在状态管理方面具有强大的功能和灵活性。
react-router 里的 <Link>
标签和 <a>
标签有什么区别
<Link>
标签和 <a>
标签在 React Router 中用于导航链接,但它们有一些重要的区别。
浏览器行为:
<Link>
标签:使用<Link>
标签导航时,React Router 会拦截点击事件并使用 JavaScript 动态更改 URL,而不会刷新整个页面。这样可以实现单页应用的无刷新导航。<a>
标签:使用普通的<a>
标签导航时,浏览器会发起新的页面请求,导致整个页面重新加载。
应用状态保持:
<Link>
标签:由于<Link>
标签的导航是无刷新的,React Router 可以轻松地在不同的路由之间保持应用的状态。当前页面的组件会卸载,新页面的组件会加载,但共享的状态会保持不变。<a>
标签:使用<a>
标签导航时,每次导航都会重新加载整个页面,应用的状态将会重置。
激活样式:
<Link>
标签:在 React Router 中,可以通过设置activeClassName
或activeStyle
属性来指定导航链接在当前活动路由时应用的样式。<a>
标签:使用<a>
标签时,需要手动编写 JavaScript 逻辑来判断当前活动路由,并为相应的导航链接添加样式。
总体而言,<Link>
标签是 React Router 提供的专门用于导航的组件,它提供了无刷新导航、应用状态保持和激活样式等功能。而 <a>
标签是 HTML 提供的原生导航元素,它会触发浏览器的页面加载行为。在使用 React Router 构建单页应用时,通常推荐使用 <Link>
标签来实现导航链接。
React 和 Vue 的 diff 时间复杂度从 O(n^3) 优化到 O(n) ,那么 O(n^3) 和 O(n) 是如何计算出来的?
O(n^3) 和 O(n) 是用来表示算法的时间复杂度的符号表示法,用于衡量算法在输入规模增大时的运行时间增长速度。
O(n^3) 表示算法的时间复杂度是随着输入规模 n 的增大而呈立方级增长。具体来说,如果算法的时间复杂度为 O(n^3),那么当输入规模增加到 n 时,算法的运行时间会增长为 n 的三次方。这意味着算法的运行时间与输入规模的立方成正比。
O(n) 表示算法的时间复杂度是随着输入规模 n 的增大而呈线性增长。具体来说,如果算法的时间复杂度为 O(n),那么当输入规模增加到 n 时,算法的运行时间会增长为 n。这意味着算法的运行时间与输入规模成正比。
React 和 Vue 的 diff 算法的时间复杂度从 O(n^3) 优化到 O(n) 是通过使用不同的算法思路和优化技巧实现的。在早期版本中,React 和 Vue 的 diff 算法采用了一种简单粗暴的比较策略,将整个虚拟 DOM 树进行逐层对比,导致时间复杂度较高。
后来,React 和 Vue 引入了优化的 diff 算法,例如 React 使用了基于 Fiber 架构的 Reconciliation 算法,Vue 使用了基于双指针的优化算法。这些算法利用了虚拟 DOM 的特性,通过差异化比较和局部更新的方式来减少比较的次数,从而将时间复杂度优化到 O(n)。
需要注意的是,时间复杂度的具体计算和分析是通过对算法的具体实现进行评估和推导得出的,而不是简单地通过观察算法的代码行数来确定。因此,时间复杂度的具体计算需要对算法的实现细节和执行过程进行仔细分析。
你对虚拟 dom 和 diff 算法的理解,实现 render 函数
虚拟 DOM
虚拟 DOM 是一个轻量级的 JavaScript 对象,它描述了 DOM 树的结构。虚拟 DOM 的创建和更新比真实的 DOM 更快,因为它只是一个 JavaScript 对象,不需要进行 DOM 操作。
Diff 算法
Diff 算法用于比较两个虚拟 DOM 树之间的差异,并生成一个补丁(patch)对象。补丁对象包含了更新真实 DOM 所需的最小操作。Diff 算法可以高效地找出两个虚拟 DOM 树之间的差异,并生成最小的补丁对象。
实现 render 函数
function render(virtualDOM, container) {
// 创建一个真实的 DOM 元素
const realDOM = createElement(virtualDOM);
// 将真实的 DOM 元素添加到容器中
container.appendChild(realDOM);
// 递归渲染子元素
if (virtualDOM.children) {
virtualDOM.children.forEach((child) => {
render(child, realDOM);
});
}
}
function createElement(virtualDOM) {
// 创建一个真实的 DOM 元素
const element = document.createElement(virtualDOM.type);
// 设置属性
for (const key in virtualDOM.props) {
element.setAttribute(key, virtualDOM.props[key]);
}
return element;
}
总结
虚拟 DOM 和 Diff 算法是 React 高性能渲染的关键。虚拟 DOM 的创建和更新比真实的 DOM 更快,Diff 算法可以高效地找出两个虚拟 DOM 树之间的差异,并生成最小的补丁对象。这使得 React 能够以更快的速度更新 DOM,从而提供更好的用户体验。
生命周期都有哪几种,分别是在什么阶段做哪些事情?为什么要废弃一些生命周期?
React 生命周期是指组件在创建、更新和销毁过程中所经历的各个阶段。每个阶段都有特定的钩子函数,可以让你执行一些特定操作。
React 生命周期钩子函数
React 有以下几种生命周期钩子函数:
- 挂载阶段:
constructor
:在组件实例化时被调用,用于初始化状态和绑定事件。getDerivedStateFromProps
:在组件接收到新的 props 时被调用,用于根据新的 props 更新状态。render
:在组件挂载时被调用,用于渲染组件的 UI。componentDidMount
:在组件挂载完成后被调用,用于执行一些需要 DOM 操作的任务,例如获取数据或设置定时器。
- 更新阶段:
getDerivedStateFromProps
:在组件接收到新的 props 时被调用,用于根据新的 props 更新状态。shouldComponentUpdate
:在组件接收到新的 props 或 state 时被调用,用于判断组件是否需要重新渲染。render
:在组件需要重新渲染时被调用,用于渲染组件的 UI。getSnapshotBeforeUpdate
:在组件更新之前被调用,用于获取一些组件更新前的信息,例如滚动位置。componentDidUpdate
:在组件更新完成后被调用,用于执行一些需要 DOM 操作的任务,例如更新滚动位置。
- 卸载阶段:
componentWillUnmount
:在组件卸载之前被调用,用于清理一些资源,例如取消定时器或移除事件监听器。
废弃一些生命周期的原因
React 团队在 React 16.8 版本中废弃了一些生命周期钩子函数,例如 componentWillMount
和 componentWillReceiveProps
。废弃这些生命周期钩子函数的原因主要有以下几个:
- 难以理解和使用: 这些生命周期钩子函数的执行顺序和时机比较复杂,这使得它们难以理解和使用。
- 性能问题: 这些生命周期钩子函数可能会导致性能问题,因为它们会在组件更新时被多次调用。
- 不必要的代码: 很多时候,这些生命周期钩子函数中的代码可以被其他生命周期钩子函数或其他方法所替代。
替代方案
对于被废弃的生命周期钩子函数,可以使用以下替代方案:
constructor
可以用来初始化状态和绑定事件。getDerivedStateFromProps
可以用来根据新的 props 更新状态。render
可以用来渲染组件的 UI。componentDidMount
可以用来执行一些需要 DOM 操作的任务,例如获取数据或设置定时器。getSnapshotBeforeUpdate
可以用来获取一些组件更新前的信息,例如滚动位置。componentDidUpdate
可以用来执行一些需要 DOM 操作的任务,例如更新滚动位置。useEffect
可以用来执行一些需要在组件挂载或更新后执行的任务,例如获取数据或设置定时器。
总结
React 生命周期钩子函数可以让你在组件的生命周期中执行一些特定的操作。React 团队在 React 16.8 版本中废弃了一些生命周期钩子函数,并提供了替代方案。新的生命周期钩子函数更易于理解和使用,并且可以提高性能。
关于 react 的优化方法
以下是一些常见的 React 优化方法:
1. 避免不必要的 re-render
- 使用 shouldComponentUpdate: shouldComponentUpdate 是一个生命周期钩子函数,它允许你控制组件是否需要重新渲染。你可以根据 props 和 state 的变化来决定是否需要重新渲染组件。
- 使用 PureComponent: PureComponent 是一个 React 组件类,它会自动比较 props 和 state 的变化,并只在必要时重新渲染组件。
- 使用 memo: memo 是一个高阶组件,它允许你将一个组件包装成一个只在 props 变化时才重新渲染的组件。
- 使用 useMemo 和 useCallback: useMemo 和 useCallback 是两个 React Hook,它们允许你缓存计算结果和回调函数,从而避免不必要的重新计算。
2. 减少组件嵌套
- 拆分大型组件: 将大型组件拆分成多个更小的组件可以提高性能,因为更小的组件更容易被缓存和重用。
- 使用 context: context 可以让你在组件树中传递数据,而无需将数据层层传递下去。这可以减少组件嵌套,提高性能。
3. 优化数据绑定
- 避免使用 v-bind 动态绑定大量数据: 动态绑定大量数据会降低性能,可以使用 computed 属性或方法进行处理。
- 使用 v-model 替代 v-on: 在表单元素中,使用 v-model 比 v-on 更高效。
- 使用事件代理: 对于需要监听大量事件的元素,可以使用事件代理机制,减少事件监听器数量。
4. 其他优化
- 使用 CDN 加载静态资源: 将静态资源如 JavaScript、CSS 文件放在 CDN 上,可以提高页面加载速度。
- 使用代码压缩工具: 使用代码压缩工具可以减小代码体积,提高性能。
- 使用性能分析工具: 使用性能分析工具可以帮助你分析页面性能瓶颈,并进行针对性优化。
对 fiber 的理解
Fiber 是 React 16 中引入的一个新的概念,它代表了 React 组件树中的一个工作单元。Fiber 是一种轻量级的 JavaScript 对象,它包含了组件的 props、state、DOM 元素等信息。
Fiber 的优点
- 更快的渲染性能: Fiber 引入了异步渲染机制,可以将渲染任务拆分成更小的任务,并将其分批执行。这使得 React 可以更快速地渲染组件,并提供更好的用户体验。
- 更易于调试: Fiber 提供了更好的调试工具,可以帮助你更容易地定位和修复性能问题。
- 更易于扩展: Fiber 的设计更加模块化,这使得 React 更易于扩展和定制。
Fiber 的工作原理
Fiber 的工作原理如下:
- React 会创建一个 Fiber 树,其中每个 Fiber 代表一个组件。
- React 会遍历 Fiber 树,并为每个 Fiber 创建一个工作单元。
- React 会将工作单元放入一个队列中,并异步执行它们。
- 当一个工作单元执行完成后,React 会更新 DOM 树。
Fiber 的结构
Fiber 对象包含以下属性:
- type: 组件类型。
- key: 组件的 key 值。
- props: 组件的 props。
- state: 组件的 state。
- dom: 组件的 DOM 元素。
- child: 组件的第一个子 Fiber。
- sibling: 组件的下一个兄弟 Fiber。
- return: 组件的父 Fiber。
总结
Fiber 是 React 16 中引入的一个新的概念,它代表了 React 组件树中的一个工作单元。Fiber 引入了异步渲染机制,可以将渲染任务拆分成更小的任务,并将其分批执行。这使得 React 可以更快速地渲染组件,并提供更好的用户体验。Fiber 的设计更加模块化,这使得 React 更易于扩展和定制。
Fiber 和虚拟 DOM 的区别
Fiber 是 React 16 中引入的一种新的调和引擎(Reconciliation Engine),而虚拟 DOM 是 React 中的一个重要概念。
虚拟 DOM 的主要目的是通过在内存中创建一个虚拟的 DOM 树,来模拟真实的 DOM 结构。它具有以下优点:
- 性能优化:避免了频繁地修改真实 DOM,减少了 DOM 操作的开销。
- 高效更新:能够精确地比较虚拟 DOM 的差异,只更新需要更改的部分。
- 可预测性:使得更新操作更加可控和可预测。
Fiber 相比于之前的调和算法,具有以下优势:
- 支持增量更新:可以在不打断其他任务的情况下进行部分更新。
- 暂停和恢复:允许在需要时暂停和恢复工作。
- 更好的并发处理:提高了 React 在多线程环境下的性能。
总的来说,虚拟 DOM 是 React 的核心概念,用于高效地更新 DOM。而 Fiber 是一种新的调和引擎,提供了更好的性能和并发处理能力。 Fiber 是基于虚拟 DOM 实现的,它进一步优化了 React 的渲染性能和用户体验。
React 元素与组件的区别?
React 元素和组件都是 React 中用来构建 UI 的基本单元,但它们之间存在一些重要的区别。
React 元素
定义:
React 元素是一个轻量级的 JavaScript 对象,它描述了 UI 的一个特定部分。它包含了三个属性:
type
:元素的类型,例如div
、span
或一个自定义组件。props
:元素的属性,例如className
、style
或onClick
。key
:元素的唯一标识符。
创建: React 元素可以使用 JSX 语法或
React.createElement
函数创建。不可变: React 元素是不可变的,这意味着一旦创建,它们就不能被修改。
用途: React 元素用于描述 UI 的结构,并传递给 React 组件进行渲染。
React 组件
- 定义: React 组件是一个 JavaScript 函数或类,它返回一个 React 元素。组件可以包含状态、生命周期方法和事件处理程序。
- 创建: React 组件可以使用 JavaScript 函数或类创建。
- 可变: React 组件是可变的,这意味着它们可以根据状态的变化而重新渲染。
- 用途: React 组件用于封装 UI 逻辑和状态,并可以被组合成更复杂的 UI。
总结
React 元素和组件都是 React 中用来构建 UI 的基本单元,但它们之间存在一些重要的区别。React 元素是轻量级的 JavaScript 对象,它描述了 UI 的一个特定部分。React 组件是 JavaScript 函数或类,它返回一个 React 元素。组件可以包含状态、生命周期方法和事件处理程序。
React 与 Vue 有什么区别?
React 和 Vue 都是流行的 JavaScript 框架,用于构建用户界面。虽然它们都具有许多共同点,但也存在一些重要的区别。
架构
- React: React 是一个声明式的框架,它使用 JSX 语法来描述 UI 的结构。React 组件是纯函数,它们不包含任何状态或生命周期方法。
- Vue: Vue 是一个渐进式的框架,它可以作为库或框架使用。Vue 组件可以是纯函数或包含状态和生命周期方法的类。
数据绑定
- React: React 使用单向数据绑定。数据流从父组件流向子组件,子组件不能直接修改父组件的状态。
- Vue: Vue 使用双向数据绑定。数据流可以双向流动,子组件可以修改父组件的状态。
模板语法
- React: React 使用 JSX 语法,它是一种将 HTML 和 JavaScript 混合在一起的语法。
- Vue: Vue 使用自己的模板语法,它与 HTML 非常相似。
性能
- React: React 的性能通常比 Vue 更高,因为它使用虚拟 DOM 来优化渲染过程。
- Vue: Vue 的性能也很好,但它通常比 React 慢一些。
学习曲线
- React: React 的学习曲线比 Vue 更陡峭,因为它需要学习 JSX 语法和一些其他概念。
- Vue: Vue 的学习曲线比 React 更平缓,因为它更接近于传统的 HTML 和 JavaScript。
生态系统
- React: React 拥有一个庞大而活跃的生态系统,其中包含许多第三方库和工具。
- Vue: Vue 的生态系统也越来越大,但它仍然比 React 的生态系统小一些。
总结
React 和 Vue 都是构建用户界面的优秀框架。React 是一个声明式的框架,它使用 JSX 语法来描述 UI 的结构。Vue 是一个渐进式的框架,它可以作为库或框架使用。React 的性能通常比 Vue 更高,但它的学习曲线也更陡峭。Vue 的学习曲线比 React 更平缓,但它的性能也稍微低一些。最终,选择哪个框架取决于您的具体需求和偏好。
React 和 Vue 的区别?
框架 | React | Vue 2.x |
---|---|---|
类型 | MVVM | MVVM |
响应式 | √ | √ |
组件化 | √ | √ |
脚手架 | Create React App | Vue CLI |
路由 | react-router | vue-router |
状态管理 | react-redux / React Hooks / MobX | vuex |
整体思路 | 函数式、单向数据流 | 声明式、表单双向绑定 |
组件优化 | PureComponent / shouldComponentUpdate | 可理解为自动化 shouldComponentUpdate |
HTML | JSX (结构 & 表现 & 行为融合、完整的 JavaScript / TypeScript 语法支持、先进的开发工具 Lint / 编辑器 Auto 等) | Template(结构 & 表现 & 行为分离、HTML 更友好、开发效率提升、文档学习成本) / JSX |
CSS | CSS 作用域需要额外实现,例如一些 CSS-in-JS 方案(styled-components、styled-jsx),一般需要额外的插件支持语法高亮和提示 | 单文件组件 Style 标签 |
Chrome 开发工具 | react-devtools | vue-devtools |
优势 | 大规模应用程序的鲁棒性(灵活的结构和可扩展性)、适用原生 App、丰富的生态圈、丰富的工具链 | 一站式解决方案、更快的渲染速度和更小的体积 |
除此之外,在语法层面:
- 在复用层面 React 可通过高阶函数、自定义 Hooks 实现。而 Vue 在大部分情况下使用 Mixin。
- Vue 的组件实例有实现自定义事件,父子组件通信可以更解耦。React 万物皆 Props 或者自己实现类似的自定义事件。
- Vue 中可以使用插槽 Slot 分发内容,React 万物皆 Props。
- Vue 中丰富的指令(确实好用,还支持灵活的自定义指令),React 万物皆 JSX。
- Vue 中的计算属性和侦听属性语法糖,React 可在特定的周期函数中进行处理。
- Vue 框架对过渡动画的支持,React 原生没发现该能力。
- Vue 提供一站式服务,React 往往在设计时需要考虑生态应用。
- Vue 全局配置、全局 API 还是挺好用的,比如 Vue.use 可全局在实例中注入对象。
- Vue 中的全局组件非常好用,不需要像 React 那样一遍遍引入组件。
- Vue 的 Template 中使用一些变量数据(例如常量)必须挂载在
this
上(简直蛋疼),React 中的 JSX 万物皆可 JavaScript。 - React Hooks 新颖式概念和语法设计。
- React Fragments 非常棒,Vue 暂时没发现类似的功能(会造成更多的元素嵌套问题)。
- ...
vuex 和 redux 之间的区别?
从实现原理上来说,最大的区别是两点:
Redux使用的是不可变数据,而Vuex
的数据是可变的。Redux
每次都是用新的state
替换旧的state
,而Vuex
是直接修改
Redux在检测数据变化的时候,是通过diff
的方式比较差异的,而Vuex
其实和 Vue 的原理一样,是通过 getter/setter
来比较的(如果看Vuex
源码会知道,其实他内部直接创建一个Vue
实例用来跟踪数据变化)
React 性能优化
React 性能优化是提高 React 应用性能和响应速度的过程。以下是一些常见的 React 性能优化技巧:
使用生产环境构建:在生产环境中使用 React 的优化构建版本,它会进行代码压缩、文件合并和其他优化,减小文件大小,提高加载速度。
使用 React 的最新版本:确保使用 React 的最新稳定版本,因为 React 团队会不断改进性能和修复 bug。
避免不必要的重新渲染:使用 React 的
shouldComponentUpdate
或React.memo
来减少不必要的组件重新渲染。通过比较状态和属性的变化,只在必要时更新组件。列表项的唯一标识:在渲染列表时,为每个列表项提供唯一的
key
属性。这有助于 React 在更新列表时识别新增、删除或移动的项,提高性能。懒加载组件:使用 React 的懒加载(Lazy Loading)功能,按需加载组件。这样可以减少初始加载的代码量,提高首次加载速度。
使用虚拟化列表:对于大型列表或表格,使用虚拟化列表库(如
react-virtualized
或react-window
)可以只渲染可见区域的项,而不是全部渲染。避免频繁的状态更新:合并多个状态更新为一次更新,可以减少渲染次数。使用
setState
的回调函数或useEffect
的依赖数组来处理多个状态更新。使用轻量级的状态管理方案:对于较小规模的应用,考虑使用轻量级的状态管理方案(如 React 的
useState
和useReducer
),而不是引入复杂的状态管理库。优化大型数据集的渲染:对于大型数据集的渲染,可以使用分页、虚拟化或增量加载等技术来提高性能。
使用性能分析工具:使用 React 性能分析工具(如 React DevTools、Chrome 开发者工具的性能面板等)来分析应用的性能瓶颈和优化机会。
这些是一些常见的 React 性能优化技巧,根据具体应用的需求和场景,还可以采用其他优化策略。重要的是在开发过程中时刻关注性能,并进行测试和优化,以提供更好的用户体验。
聊聊 react 中 class 组件和函数组件的区别
在 React 中,有两种主要的组件类型:类组件和函数组件。它们有一些区别,包括语法、状态管理和生命周期方法的处理。
语法: 类组件是使用 ES6 的类语法定义的,继承自 React.Component,并且通过
render
方法返回组件的 JSX。函数组件是使用函数定义的,接收一个 props 对象作为参数,并返回组件的 JSX。类组件示例:
class MyComponent extends React.Component {
render() {
return <div>Hello, Class Component!</div>;
}
}函数组件示例:
function MyComponent(props) {
return <div>Hello, Function Component!</div>;
}状态管理: 在类组件中,可以通过定义和管理组件的状态(state),使用
this.state
和this.setState
方法来更新状态。而在函数组件中,可以使用 React 的 Hooks(如useState
)来管理组件的状态。类组件中的状态管理示例:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
increment() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.increment()}>Increment</button>
</div>
);
}
}函数组件中的状态管理示例:
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}生命周期方法: 类组件提供了一系列的生命周期方法(如
componentDidMount
、componentDidUpdate
、componentWillUnmount
等),可以在不同阶段执行特定的操作。而函数组件可以使用 React 的 Effect Hook(如useEffect
)来实现类似的功能。类组件中的生命周期方法示例:
class MyComponent extends React.Component {
componentDidMount() {
console.log("Component mounted");
}
componentDidUpdate(prevProps, prevState) {
console.log("Component updated");
}
componentWillUnmount() {
console.log("Component will unmount");
}
render() {
return <div>Hello, Class Component!</div>;
}
}函数组件中的生命周期方法示例:
function MyComponent() {
useEffect(() => {
console.log("Component mounted");
return () => {
console.log("Component will unmount");
};
}, []);
useEffect(() => {
console.log("Component updated");
});
return <div>Hello, Function Component!</div>;
}
需要注意的是,从 React 16.8 版本开始,引入了 Hooks,使得函数组件可以拥有状态和生命周期方法的能力,并且被认为是更简洁、可维护的写法。在新的 React 项目中,推荐使用函数组件和 Hooks,除非有特定的需求需要使用类组件。
React Hooks 作用,常用的有哪些?
React Hooks 是 React 16.8 版本引入的一组函数,用于在函数组件中引入状态管理和其他 React 特性。Hooks 的引入使得函数组件具备了类组件的能力,同时提供了更简洁、可重用和可测试的代码编写方式。
React Hooks 的作用包括:
状态管理(State):
useState
Hook 允许在函数组件中添加和管理状态。它接收一个初始状态值,并返回一个状态值和更新该状态值的函数。使用useState
,可以在函数组件中保存和更新局部的状态,避免了使用类组件和 this.state 的繁琐。副作用处理(Effects):
useEffect
Hook 允许在函数组件中执行副作用操作,比如订阅数据、操作 DOM、发送网络请求等。它接收一个副作用函数和一个依赖数组,当依赖数组中的值发生变化时,副作用函数将被执行。使用useEffect
,可以替代类组件的生命周期方法,实现组件挂载、更新和卸载时的操作。上下文访问(Context):
useContext
Hook 允许在函数组件中访问 React 上下文(Context)。它接收一个上下文对象,并返回当前上下文的值。使用useContext
,可以在函数组件中消费上下文,避免了使用类组件和Context.Consumer
的复杂性。引用保存(Ref):
useRef
Hook 允许在函数组件中创建一个可变的引用,并在组件的生命周期中保持引用的一致性。它返回一个可变的 ref 对象,可以在组件的整个生命周期内保持引用的稳定性。使用useRef
,可以在函数组件中保存和访问 DOM 元素、保存定时器的引用等。自定义 Hooks: 自定义 Hooks 是一种在函数组件之间共享逻辑的方式。通过将一些逻辑封装成自定义的 Hook,可以在多个组件中复用该逻辑。自定义 Hooks 通常以
use
开头命名,可以使用其他 Hooks 来构建自定义 Hooks。
除了上述常用的 Hooks,React 还提供了其他一些 Hooks,如 useReducer
(用于管理复杂的状态逻辑)、useCallback
(用于缓存回调函数)、useMemo
(用于缓存计算结果)等。这些 Hooks 可根据具体的需求选择使用,以实现更优雅和高效的函数组件编写。
何时要使用异步组件?如和使用异步组件
使用异步组件(Async Components)可以改善应用程序的性能和用户体验,特别是当应用程序需要加载大型组件或延迟加载某些组件时。
以下是一些使用异步组件的常见情况:
大型组件: 如果你的应用程序包含大型的组件,例如包含大量代码或资源的复杂表单、图表库或地图组件,使用异步组件可以将其延迟加载,避免阻塞整个页面的加载和渲染。这样可以改善初始加载时间,并在需要时按需加载这些组件。
懒加载路由组件: 当你的应用程序使用路由进行页面导航时,某些页面可能非常大或包含许多依赖项。这时使用异步组件可以实现懒加载,只在用户导航到该页面时才加载所需的组件,提高初始加载速度。
条件性加载组件: 有时你可能有一些条件性的组件,只在特定条件下才需要加载。例如,当用户执行某个操作或满足某些条件时,才需要加载某个组件。使用异步组件可以根据需要动态加载这些组件,避免不必要的网络请求和资源占用。
使用异步组件可以通过使用 React 的动态 import()
函数或第三方库(如 react-loadable
或 react-router
的 lazy
函数)来实现。以下是使用 React 动态 import()
函数创建异步组件的示例:
import React, { Suspense } from "react";
// 异步加载组件
const AsyncComponent = React.lazy(() => import("./AsyncComponent"));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent />
</Suspense>
</div>
);
}
在上述示例中,React.lazy()
函数用于创建异步组件,通过传入一个返回动态 import()
的函数来指定要加载的组件。在渲染时,使用 <Suspense>
组件包裹异步组件,并指定一个加载时的占位符(fallback
),在异步组件加载完成之前显示该占位符。
需要注意的是,异步组件的加载是异步的,因此需要处理加载过程中的状态(如显示加载指示器)和加载失败的情况。
React 事件绑定原理
React 并不是将 click 事件绑在该 div 的真实 DOM 上,而是在 document 处监听所有支持的事件,当事件发生并冒泡至 document 处时,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。 另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault。
Virtual Dom 的优势在哪里?
Virtual DOM(虚拟 DOM)是一种在前端开发中用于提高性能和开发效率的技术。它的优势主要体现在以下几个方面:
性能优化: Virtual DOM 通过在内存中构建一个轻量级的、与真实 DOM 结构相对应的虚拟表示,可以减少直接操作真实 DOM 的次数。当数据发生变化时,Virtual DOM 会将新旧虚拟 DOM 进行比较,并计算出最小的 DOM 更新操作,然后批量应用到真实 DOM 中,减少了不必要的 DOM 操作,提高了性能。
跨平台兼容性: 虚拟 DOM 可以在不同的平台上运行,如浏览器、移动端、服务器端等。它抽象了底层的 DOM 操作,使得开发者可以使用统一的 API 来处理 DOM,降低了对底层平台的依赖,提供了更好的跨平台兼容性。
开发效率: 使用 Virtual DOM 可以简化前端开发过程。通过将界面状态映射到虚拟 DOM 上,开发者可以更方便地进行组件化开发,通过声明式的方式描述界面,而无需直接操作 DOM。同时,虚拟 DOM 也提供了一些便捷的工具和方法来处理常见的 DOM 操作,使得开发过程更高效、易于维护。
扩展性和可测试性: 虚拟 DOM 的设计使得它具有良好的扩展性和可测试性。由于虚拟 DOM 与平台无关,可以在不同的环境中进行单元测试、集成测试等。同时,虚拟 DOM 也为开发者提供了灵活的扩展接口,可以自定义组件、指令等,以满足特定的需求。
虚拟 DOM 技术的引入使得前端开发更加高效、灵活和可维护,它已经成为许多流行的前端框架(如 React、Vue 等)的核心原理之一。
React Class 组件有哪些周期函数?分别有什么作用?
constructor()
挂载类组件的时候,先执行构造函数static getDerivedStateFromProps()
会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。render()
渲染真实的 DOM 节点componentDidMount()
会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。
更新:
static getDerivedStateFromProps() 同一次挂载时的 getDerivedStateFromProps() 一致
shouldComponentUpdate() 可以在这里进行性能优化,减少浅层比较
render() 插入真实的DOM节点树上
getSnapshotBeforeUpdate() 能在最近一次渲染中,从之前的DOM拿到一些有用的信息,比如滚动位置等
componentDidUpdate()
当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)
卸载:
componentWillUnmount() 这里是卸载及销毁组件前的调用方法 可以在这里清空一些数据,比如取消网络请求、 componentDidmount中创建的一些数据等等
React 中高阶函数和自定义 Hook 的优缺点?
高阶函数和自定义 Hook 是 React 中用于复用组件逻辑的两种常见方式。它们各自有一些优点和缺点,下面是它们的简要比较:
高阶函数(Higher-Order Functions)的优点:
- 灵活性:高阶函数可以接受任意的组件作为参数,并返回一个新的增强组件。这使得可以在不修改原始组件的情况下,对其进行功能增强和扩展。
- 增强组件:通过高阶函数,可以将一些通用的逻辑、状态或副作用应用到多个组件中,提供了一种组件复用的方式。
- 组件组合:可以通过多次嵌套高阶函数来组合多个增强逻辑,实现更复杂的功能。
高阶函数的缺点:
- 嵌套层级增加:如果使用过多的高阶函数,组件嵌套的层级可能会增加,导致代码结构复杂和可读性降低。
- 命名冲突:当多个高阶函数在同一个组件上应用时,可能出现命名冲突的问题,需要小心处理命名空间和属性传递。
- 难以追踪组件树:高阶函数可能会导致组件树的结构变得复杂,使得在调试和追踪问题时变得困难。
自定义 Hook 的优点:
- 逻辑复用:自定义 Hook 允许将组件逻辑封装到可重用的函数中,使得多个组件可以共享同一段逻辑代码。
- 简化组件:通过自定义 Hook,可以将复杂的组件逻辑拆分成多个可读性更高的自定义 Hook,使组件本身变得更简洁和易于理解。
- 高内聚性:自定义 Hook 可以将相关的状态和副作用组织在一起,提高代码的内聚性和可维护性。
- 纯函数:自定义 Hook 是纯函数,可以在不继承状态的情况下进行测试和重用。
自定义 Hook 的缺点:
- 命名约定:自定义 Hook 的命名约定是以 "use" 开头,这在命名上有一定的限制。
- 依赖管理:自定义 Hook 难以处理对其他 Hook 的依赖管理,需要额外的注意遵循 Hook 的规则。
- 限制:自定义 Hook 主要用于共享状态逻辑,对于其他的非状态逻辑(如 DOM 操作等),可能需要使用其他方式进行复用。
总体而言,高阶函数和自定义 Hook 都是 React 中实现组件逻辑复用的有效方式。选择使用哪种方式取决于具体的需求和代码结构。高阶函数适用于简单的功能增强和组合,而自定义 Hook 则更适合于复用状态逻辑和组件副作用的场景。
简要说明 React Hook 中 useState 和 useEffect 的运行原理?
useState 和 useEffect 是 React Hook 中两个常用的钩子函数,分别用于处理组件的状态和副作用。它们的运行原理如下:
useState:
- useState 是一个函数,它返回一个数组,包含当前状态的值和一个更新状态的函数。
- 当组件第一次渲染时,useState 会创建一个状态变量,将初始值作为状态的初始值。
- 在组件的后续渲染过程中,useState 会返回当前状态的值和更新状态的函数。
- 当调用状态更新函数时,React 会重新渲染组件,并将新的状态值传递给 useState,然后更新组件的状态。
useEffect:
- useEffect 是一个用于处理副作用的钩子函数,比如订阅事件、数据获取、DOM 操作等。
- 在组件渲染完成后,React 会调用 useEffect 中的副作用函数。
- 副作用函数可以返回一个清理函数,用于清理副作用,比如取消订阅、清除定时器等。
- 当组件即将被销毁时,React 会执行清理函数,以确保没有未处理的副作用。
在具体的运行中,React 使用 Fiber 架构来实现 Hook 的工作原理。Fiber 架构是一种用于实现 React 的渲染和调度的算法。通过 Fiber 架构,React 可以以非阻塞的方式处理组件的更新,并且可以中断和恢复渲染过程。
当组件进行更新时,React 会通过 Fiber 架构的调度器决定哪些组件需要更新和重新渲染。对于 useState,React 会通过 Fiber 架构来跟踪组件的状态,并在状态更新时重新渲染相关的组件。对于 useEffect,React 会在组件渲染完成后调用副作用函数,并根据需要执行清理函数。
总结而言,useState 和 useEffect 在 React Hook 中的运行原理是基于 React 的 Fiber 架构,通过状态管理和副作用函数的调度,实现了组件的状态更新和副作用处理。这种机制使得组件的状态管理和副作用处理更加简洁和灵活。
React 如何发现重渲染、什么原因容易造成重渲染、如何避免重渲染?
React 使用 Virtual DOM 和协调算法来检测和触发组件的重新渲染。当组件的状态发生变化时,React 会比较前后两次渲染的 Virtual DOM 树,找出变化的部分,并将只更新变化的部分到实际的 DOM 中,从而实现高效的重渲染。
以下是一些常见的导致组件重渲染的原因:
组件的状态变化:当组件的状态通过
setState
或使用 React Hook 的状态钩子(如 useState)进行更新时,会触发组件的重新渲染。属性的变化:当组件的父组件传递给它的属性发生变化时,组件会重新渲染。
上层组件的重新渲染:如果组件的父组件重新渲染,那么子组件也会重新渲染。
上下文(Context)的变化:当组件依赖的上下文值发生变化时,组件会重新渲染。
引用类型的值变化:如果组件的状态或属性中包含引用类型的值(如对象或数组),当引用发生变化时,会触发组件的重新渲染。
为了避免不必要的组件重渲染,可以采取以下方法:
使用
React.memo
:通过React.memo
包裹组件,可以对组件的输入进行浅层比较,避免不必要的重渲染。使用
shouldComponentUpdate
(对于类组件):在类组件中重写shouldComponentUpdate
方法,手动比较前后两次的状态或属性,决定是否触发重渲染。使用
useMemo
(对于函数组件):使用useMemo
钩子来缓存计算结果,避免在每次渲染时重新计算。避免在渲染过程中创建新的对象或数组:尽量避免在渲染过程中创建新的引用类型的值,可以使用
useState
的函数形式或useReducer
来处理复杂状态。使用合适的数据结构:当需要处理大量数据时,可以使用数据结构(如树状结构)来减少不必要的遍历和比较。
使用上下文优化:通过合理使用上下文,避免将整个组件树重新渲染。
使用分割组件:将大的组件拆分成更小的子组件,使得只有受影响的子组件会重新渲染。
通过以上方法,可以优化组件的性能,避免不必要的重渲染,提高应用的效率和响应性。
React Hook 中 useEffect 有哪些参数,如何检测数组依赖项的变化?
useEffect(setup, dependencies?)
不传参数、空数组、有一个或者多个值得数组、返回一个函数。
useEffect 的第二个参数可用于定义其依赖的所有变量。如果其中一个变量发生变化,则 useEffect 会再次运行。如果包含变量的数组为空,则在更新组件时 useEffect 不会再执行,因为它不会监听任何变量的变更。
React Hook 和闭包有什么关联关系?
React Hook 和闭包之间存在一定的关联关系。在函数组件中使用 React Hook 时,由于 JavaScript 的词法作用域和闭包特性,组件函数形成了闭包,从而实现了状态的保留和更新。
React Hook 的状态钩子(如 useState)使用闭包来保持状态的持久性。当组件函数被调用时,useState 函数会在函数内部创建一个状态变量,并返回当前状态的值和更新状态的函数。由于函数组件形成闭包,状态变量和更新函数会被保留在内存中,即使组件函数执行完毕,状态的值也会在闭包中被保留下来。这样,每次组件重新渲染时,都能够访问到最新的状态值。
例如,以下是一个使用 useState 的示例:
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
在上述代码中,useState 创建了一个名为 count 的状态变量和一个名为 setCount 的更新函数,并将初始值 0 作为状态的初始值。每次组件重新渲染时,useState 都会返回最新的状态值和更新函数,而闭包保证了组件函数中的状态变量和更新函数与每次渲染相关联。
通过闭包机制,React Hook 实现了在函数组件中使用状态和副作用的能力,使得组件的状态管理和副作用处理变得更加简洁和直观。同时,由于闭包的存在,也需要注意在使用 Hook 时正确处理依赖项和避免常见的陷阱,以保证组件行为的正确性。
React 中 useState 是如何做数据初始化的?
在 React 中,useState 是一种用于在函数组件中声明和使用状态的 Hook。useState 接受一个初始值作为参数,并返回一个数组,其中包含当前状态的值和一个更新状态的函数。
当组件首次渲染时,useState 会将初始值作为当前状态的初始值,并返回该初始值作为状态的初始值。在组件的后续渲染中,useState 会忽略初始值参数,而是返回上一次渲染中的状态值作为当前状态的初始值。
这样做的好处是,在组件的每次重新渲染中,都能够保持状态的持久性,而不会因为函数组件的重新调用而丢失状态。这是通过 JavaScript 的闭包机制实现的,函数组件形成闭包,从而在每次重新渲染时能够访问到上一次渲染中的状态。
以下是一个使用 useState 进行数据初始化的示例:
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
// ...
return (
<div>
<p>Count: {count}</p>
{/* ... */}
</div>
);
}
在上述代码中,useState 的初始值为 0,因此在组件首次渲染时,count 的初始值为 0。之后每次重新渲染时,useState 会忽略初始值参数,而是将上一次渲染中的状态值作为当前状态的初始值。
需要注意的是,useState 的初始值只在组件的初始渲染阶段起作用,后续的渲染中不会再使用初始值参数。如果希望根据某个条件动态设置初始值,可以在初始值参数位置使用一个函数,函数的返回值将作为初始值。这样,每次重新渲染时,函数会被调用来计算初始值。
const [count, setCount] = useState(() => {
// 根据某个条件动态设置初始值
return someCondition ? initialValue : otherValue;
});
通过这种方式,可以根据需要灵活地设置初始值,并保持状态的持久性和正确性。
React 应用如何在重新加载页面时保留数据?
当重新加载页面时,React 应用的状态数据会被重置,因为整个应用的状态是存储在内存中的,而不是持久化保存在浏览器中。但是,可以通过一些技术手段来在重新加载页面时保留数据。
下面是几种常见的方法:
使用浏览器的本地存储(LocalStorage 或 SessionStorage):可以将应用的数据存储在浏览器的本地存储中,当页面重新加载时,可以从本地存储中读取数据并还原应用的状态。这种方式适用于需要在多个会话之间保留数据的情况。
使用 URL 参数:将应用的数据编码为 URL 参数,当页面重新加载时,可以从 URL 参数中解析并恢复应用的状态。这种方式适用于需要将数据与页面 URL 关联的情况,比如分享链接。
使用后端存储:将应用的数据存储在后端服务器上,在重新加载页面时从服务器获取数据并还原应用的状态。这通常需要与后端进行数据交互,并确保数据在服务器上持久化存储。
需要注意的是,这些方法都需要开发者自行处理数据的存储和恢复逻辑。在实现时,需要考虑数据的序列化和反序列化、存储方式的选择、数据的安全性等因素。
另外,如果应用的数据需要在多个页面之间进行共享,可以考虑使用 React 的状态管理库(如 Redux、MobX、Context API 等),将数据存储在全局状态中,这样即使页面重新加载,数据仍然可以从状态管理库中获取。
综上所述,保留数据并在重新加载页面时恢复数据需要使用一些技术手段,如浏览器的本地存储、URL 参数、后端存储或状态管理库。具体选择哪种方法取决于应用的需求和架构。
使用 React Hooks 的同时为什么需要使用高阶组件?
在 React 中,Hooks 是一种用于在函数组件中添加状态和其他 React 特性的方式。它们提供了一种简洁、可重用的方式来处理组件的状态和副作用。使用 Hooks 可以使组件的逻辑更加清晰和可维护。
高阶组件(Higher-Order Components,HOC)是一种在 React 中共享组件逻辑的模式。它是一个函数,接受一个组件作为参数,并返回一个新的组件。高阶组件可以用于封装通用的逻辑,例如状态管理、认证、日志记录等。它们提供了一种在不修改原始组件的情况下添加功能的方式。
虽然 Hooks 提供了一种在函数组件中添加状态和其他特性的便捷方式,但有些场景下使用高阶组件仍然是有用的:
兼容类组件和函数组件:Hooks 只能在函数组件中使用,无法在类组件中使用。如果项目中同时使用了类组件和函数组件,并且需要在它们之间共享某些逻辑,可以使用高阶组件来实现这种共享。
包装第三方组件:有时候我们需要对第三方组件进行扩展或定制,但无法直接修改其源代码。这时可以使用高阶组件来包装第三方组件,并添加额外的功能。
组件逻辑复用:某些逻辑在多个组件之间需要共享,而不只是针对一个特定的函数组件。高阶组件可以封装这些共享逻辑,并让多个组件复用它们。
需要注意的是,Hooks 和高阶组件是两种不同的模式,它们可以在不同的场景下使用。使用 Hooks 可以更方便地处理函数组件的状态和副作用,而高阶组件则可以用于共享逻辑和扩展组件的功能。具体使用哪种方式取决于项目的需求和组件的复杂度。
Ajax 请求放在 componentDidMount
里进行处理还是放在componentWillMount
里进行处理比较合适?
在 React 中,componentWillMount
生命周期方法在最新的 React 版本中已被废弃,不再推荐使用。官方建议使用更安全和可预测的方法来处理数据获取和副作用,如 componentDidMount
或 useEffect
。
因此,推荐将 Ajax 请求放在 componentDidMount
中进行处理。componentDidMount
是组件渲染完成并挂载到 DOM 后触发的生命周期方法,适合执行一次性的任务,例如数据获取、订阅事件等。在该方法中发起 Ajax 请求可以确保组件已经完全渲染,并且请求结果可以正确地更新组件状态或触发其他操作。
以下是在 componentDidMount
中处理 Ajax 请求的示例:
class MyComponent extends React.Component {
componentDidMount() {
// 发起 Ajax 请求
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => {
// 处理响应数据,更新组件状态等操作
this.setState({ data });
})
.catch((error) => {
// 处理错误
console.error(error);
});
}
render() {
// 渲染组件
return <div>{/* 组件内容 */}</div>;
}
}
使用 componentDidMount
可以确保在组件挂载完成后再进行数据获取,避免了可能出现的不稳定状态。此外,将 Ajax 请求放在 componentDidMount
中也符合 React 的生命周期流程,使代码更易于理解和维护。
请注意,如果你正在使用函数组件,可以使用 useEffect
钩子来替代 componentDidMount
。useEffect
的用法类似,可以在函数组件中处理 Ajax 请求和其他副作用。
React 在语法层面可以做哪些性能优化?
在 React 中,有几个语法层面的技术和模式可以帮助进行性能优化:
使用合适的组件更新策略:React 提供了
shouldComponentUpdate
生命周期方法和React.memo
高阶组件/React.memo()
Hook,用于控制组件何时重新渲染。通过检查组件的 props 或状态是否发生了实际变化,可以避免不必要的组件重新渲染,从而提高性能。列表渲染优化:在渲染大型列表时,可以使用
key
属性为每个列表项提供稳定的唯一标识。这可以帮助 React 更高效地识别和更新列表项,而不是重新渲染整个列表。使用
React.Fragment
或<>...</>
语法来包装多个子元素,以避免额外的 DOM 嵌套。避免在渲染函数中使用内联函数定义:内联函数定义会导致函数在每次渲染时重新创建,可能会导致子组件的不必要重新渲染。应将内联函数定义移到组件外部,以便在渲染函数之外只创建一次。
使用
React.memo
高阶组件/React.memo()
Hook 来包裹子组件,以避免在父组件重新渲染时触发子组件的不必要重新渲染。使用
React.lazy
和Suspense
实现按需加载组件,以减少初始加载时间。使用
useCallback
和useMemo
钩子来缓存函数和计算结果,避免不必要的重复计算或函数创建。使用虚拟化技术来处理大型数据集的长列表,如
react-virtualized
或react-window
。避免在渲染函数中进行昂贵的计算或操作,以免影响渲染性能。可以将这些计算或操作放在
useEffect
钩子中,只在需要时触发。使用生产环境构建:在生产环境中,可以使用工具如 Webpack 或 Rollup 对代码进行优化、压缩和缩小,以减少文件大小和加载时间。
这些是一些常见的在 React 语法层面进行性能优化的技术和模式。然而,需要根据具体的应用场景和性能瓶颈来选择适合的优化方法。同时,通过性能测试和分析工具,如 Chrome 开发者工具的 Performance 面板,可以帮助确定性能瓶颈并指导优化工作。
深比较和浅比较的区别是什么?
深比较(Deep Comparison)和浅比较(Shallow Comparison)是在编程中常用的两种比较方式,用于确定两个对象或值是否相等。它们的区别在于比较的深度和精确程度。
浅比较是指比较对象或值的引用是否相等。当进行浅比较时,只比较对象或值的内存地址,而不深入比较对象的内部结构或值的具体内容。如果两个对象或值的引用相同,即指向同一块内存地址,那么它们被认为是相等的。在 JavaScript 中,浅比较通常使用 ===
(严格相等)运算符来实现。
以下是浅比较的示例:
const obj1 = { name: "Alice", age: 25 };
const obj2 = { name: "Alice", age: 25 };
console.log(obj1 === obj2); // false,因为 obj1 和 obj2 是两个不同的对象
在上述示例中,尽管 obj1
和 obj2
的属性值相同,但它们是两个不同的对象,因此浅比较返回 false
。
相比之下,深比较是指递归比较对象或值的内部结构和内容。它会递归地比较对象的每个属性或数组的每个元素,以确保它们的值相等。深比较需要比较对象或值的所有层级,因此可能会更加耗时和复杂。在 JavaScript 中,通常需要自定义深比较的逻辑,使用递归或库函数来比较对象的属性或数组的元素。
以下是深比较的示例:
const obj1 = { name: "Alice", age: 25 };
const obj2 = { name: "Alice", age: 25 };
function deepCompare(obj1, obj2) {
// 自定义深比较的逻辑
// 比较 obj1 和 obj2 的属性值是否相等
// ...
}
console.log(deepCompare(obj1, obj2)); // true,因为 obj1 和 obj2 的属性值相同
在上述示例中,deepCompare
函数使用自定义的逻辑来比较 obj1
和 obj2
的属性值是否相等,而不仅仅比较它们的引用。
总结来说,浅比较只比较引用是否相等,而深比较会递归比较对象或值的内部结构和内容。选择使用哪种比较方式取决于具体的需求和数据结构。如果只需要比较对象或值的引用,可以使用浅比较;如果需要比较对象或值的内部结构和内容,需要使用深比较。
你觉得在什么场景下用 Vue 或 React ?
选择使用 Vue 或 React 取决于多个因素,包括项目需求、团队经验和个人偏好。以下是一些常见的场景和考虑因素,可以帮助你选择适合的框架:
使用 Vue 的场景:
- 快速原型开发:Vue 具有简洁的语法和直观的 API,适合快速原型开发和小型项目。
- 小型到中型规模的应用:Vue 在处理中小规模应用方面表现出色,提供了适用于构建交互性前端应用的核心功能。
- 渐进式增强:Vue 的渐进式特性允许逐步采用、集成和优化,可以与现有项目或库逐步集成,而无需全面重写。
- 社区和生态系统:Vue 有一个活跃的社区和丰富的生态系统,提供了许多插件、工具和库,可加速开发过程。
使用 React 的场景:
- 大型和复杂应用:React 的虚拟 DOM 和组件化架构使其在处理大型和复杂应用时表现出色,提供了更好的性能和可维护性。
- 需要更高的定制性:React 提供了更底层的 API 和灵活性,使开发者能够更精确地控制组件行为和渲染过程。
- 移动应用开发:React Native 是一种基于 React 的跨平台移动应用开发框架,可以使用相同的开发模式构建原生移动应用。
- 大型开发团队:React 具有更加严格的组件化和状态管理模式,适合大型开发团队协同工作,并在大型代码库中保持一致性。
需要注意的是,Vue 和 React 都是功能强大的前端框架,都能满足大多数应用的需求。选择哪个框架取决于你的项目需求、团队技能和偏好,以及对应框架的生态系统和社区支持。最重要的是,熟悉和理解所选择框架的基本概念和最佳实践,以便能够充分利用其功能和优势。
React 中受控组件和非受控组件的区别?
在 React 中,受控组件(Controlled Components)和非受控组件(Uncontrolled Components)是两种处理表单元素的方式。
受控组件(Controlled Components): 受控组件是指由 React 组件完全控制的表单元素。在受控组件中,表单元素的值由组件的状态(state)来管理,并通过事件处理函数进行更新。当用户与表单元素交互时,React 组件会监听事件并更新组件的状态,进而更新表单元素的值。这种方式保持了表单元素的值与组件状态的一致性,可以方便地对表单数据进行处理和验证。
示例代码:
class ControlledComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
value: "",
};
}
handleChange(event) {
this.setState({ value: event.target.value });
}
handleSubmit(event) {
event.preventDefault();
// 处理表单提交逻辑
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
type="text"
value={this.state.value}
onChange={this.handleChange}
/>
<button type="submit">Submit</button>
</form>
);
}
}非受控组件(Uncontrolled Components): 非受控组件是指表单元素的值由 DOM 自身管理,React 组件无法控制或修改。在非受控组件中,通过使用
ref
来获取表单元素的引用,然后在需要时直接读取表单元素的值。这种方式相对于受控组件来说,更适合于简单的场景或与第三方库集成时的情况。示例代码:
class UncontrolledComponent extends React.Component {
handleSubmit(event) {
event.preventDefault();
const value = this.inputRef.value;
// 处理表单提交逻辑
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" ref={(ref) => (this.inputRef = ref)} />
<button type="submit">Submit</button>
</form>
);
}
}
总结:
- 受控组件通过组件的状态来管理表单元素的值,所有的值变化都由 React 组件来处理和更新。
- 非受控组件中表单元素的值由 DOM 自身管理,React 组件通过
ref
来获取和读取表单元素的值。 - 受控组件提供了更精确的控制和验证表单数据的能力,适用于复杂的表单场景。
- 非受控组件适用于简单的表单,或者与第三方库集成时的情况。
组件库要做按需加载,觉得应该怎么做?
要实现组件库的按需加载(按需导入),可以采用以下步骤:
使用 ES module 的方式导出组件: 确保你的组件库使用 ES module 的方式导出组件,以便支持按需加载。可以使用类似于以下的语法导出组件:
export { Component1, Component2, ... };
使用工具进行按需加载配置: 为了实现按需加载,可以使用一些工具和插件来进行配置。以下是常用的工具和插件:
- babel-plugin-import:这是一个 Babel 插件,可以根据需要自动导入组件。通过配置该插件,可以在代码中按需导入组件,而不需要手动导入每个组件。
- react-loadable:这是一个用于 React 的高阶组件,可以实现异步加载组件。它可以根据需要动态加载组件,并在加载完成后渲染组件。
- webpack:如果你使用 webpack 进行打包,可以使用 webpack 的代码分割功能(code splitting)来实现按需加载。通过配置 webpack,可以将每个组件打包为独立的文件,然后在需要使用该组件时再进行加载。
配置按需加载规则: 根据你选择的工具和插件,需要进行相应的配置来实现按需加载。具体的配置规则和方式会因工具和插件而异。你需要仔细阅读工具和插件的文档,并按照指导进行配置。
在应用中按需导入组件: 在你的应用中,需要根据需要按需导入组件。根据你的按需加载配置,可以使用类似以下的语法来导入组件:
import { Component1, Component2 } from "your-component-library";
这样,只有在需要使用的组件时,才会将对应的组件代码加载到应用中。
通过以上步骤和配置,你可以实现组件库的按需加载,减小应用的 bundle 大小,并提高应用的加载性能。请注意,按需加载需要根据具体的工具和插件进行配置,因此具体的实现方式可能会有所不同,你需要根据你选择的工具和插件进行相应的配置和使用。
redux 中间件
中间件提供第三方插件的模式,自定义拦截 action -> reducer 的过程。变为 action -> middlewares -> reducer 。这种机制可以让我们改变数据流,实现如异步 action ,action 过 滤,日志输出,异常报告等功能。
常见的中间件:redux-logger:提供日志输出;redux-thunk:处理异步操作;redux-promise: 处理异步操作;actionCreator 的返回值是 promise
简述 flux 思想
Flux 的最大特点,就是数据的"单向流动"。
- 用户访问 View
- View 发出用户的 Action
- Dispatcher 收到 Action,要求 Store 进行相应的更新
- Store 更新后,发出一个"change"事件
- View 收到"change"事件后,更新页面
了解 shouldComponentUpdate 吗
React 虚拟 dom 技术要求不断的将 dom 和虚拟 dom 进行 diff 比较,如果 dom 树比价大, 这种比较操作会比较耗时,因此 React 提供了 shouldComponentUpdate 这种补丁函数,如 果对于一些变化,如果我们不希望某个组件刷新,或者刷新后跟原来其实一样,就可以 使用这个函数直接告诉 React,省去 diff 操作,进一步的提高了效率
在哪个生命周期发起 ajax 请求
在 React 组件中,应该在 componentDidMount 中发起网络请求。这个方法会在组件第 一次“挂载”(被添加到 DOM)时执行,在组件的生命周期中仅会执行一次。更重要的是, 你不能保证在组件挂载之前 Ajax 请求已经完成,如果是这样,也就意味着你将尝试在 一个未挂载的组件上调用 setState,这将不起作用。在 componentDidMount 中发起网络 请求将保证这有一个组件可以更新了。
(在构造函数中)调用 super(props) 的目的是什么
在 super() 被调用之前,子类是不能使用 this 的,在 ES2015 中,子类必须在 constructor 中调用 super()。传递 props 给 super() 的原因则是便于(在子类中)能在 constructor 访问 this.props。
React 使用 use Effect 和 useLayoutEffect 的区别
useEffect 是每次 render 之后都会调用的函数,可以代替之前 class component 中的三个生命周期钩子。
作为 componentDidMount 使用,[]
作为第二个参数;
作为 componentDiUpdate 使用,可以指定依赖;
作为 componentWillUnmount 使用,通过 return 一个函数清除副作用;
以上三种用途可同时存在。
案例
要想知道一个组件什么时候第一次渲染,可以使用 useEffect,第二个参数为空数组,这样只在第一次调用时执行,第二三次不执行。
//使用useEffect之前要先引入
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = useState(0);
const add = () => {
setN((i) => i + 1);
};
// 第一次渲染,只执行这一次,[]要在第二个参数
useEffect(() => {
console.log("这是第一次渲染执行这句话");
}, []);
return (
<div>
n:{n}
<button onClick={add}>+1</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
每次渲染都要执行,只要有一个数据变化了就执行,不用写第二个参数:
useEffect(() => {
console.log("这是第一次渲染执行这句话");
});
当某一个数据变化了才执行:
useEffect(() => {
console.log("n变化了");
}, [n]);
区别
useLayoutEffect 与 useEffect 用法一样,但略有差别:
- useEffect 是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变 DOM),当改变屏幕内容时可能会产生闪烁;
- useLayoutEffect 是改变屏幕像素之前就执行(会推迟页面显示的事件,先改变 DOM 后渲染),不会产生闪烁。
综上:
- useLayoutEffect 总是比 useEffect 先执行;
- useLayoutEffect 里的任务最好影响了 Layout;
- 为了用户体验,优先使用 useEffect(优先渲染)。
mobx 和 redux
两者对比
- redux 将数据保存在单一的 store 中,mobx 将数据分散保存在多个 store 中;
- redux 使用 普通对象 保存数据,需要手动处理变化后的操作;mobx 使用 可观察对象 保存数据,数据变化后自动处理相应的操作;
- redux 使用 不可变状态 ,这意味着状态是只读的,不能直接去修改,而应该返回一个新的状态,同时使用纯函数;mobx 状态是 可以变更的 ,可以直接对其进行修改;
- mobx 相对来说更容易理解,在其中有很多的抽象,mobx 更多的使用面向对象编程的思维;redux 稍显复杂,因为其中函数式的思维比较难以掌握,同时需要借助一系列的中间件来处理异步和副作用;
- mobx 因为有更多的抽象和封装,调试的时候会比较困难,同时结果也难以预测;redux 提供能够进行时间回溯的开发工具,同时其纯函数以及其更少的抽象,让调试变得更加容易。
场景辨析
mobx 更适合数据不复杂的应用:mobx 难以调试,很多状态无法回溯,面对复杂度高的应用往往力不从心。
redux 适合有回溯需求的应用:比如一个画板应用、一个表格应用,很多时候需要撤销、重做的操作,由于 redux 的不可变性,所以天然支持这些操作。
由于面向对象编程特性更加贴近业务开发,所有对于大多数 2b 的业务来说,mobx 更加适合。
当然 mobx 和 redux 并非非此即彼的关系,你可以在项目中使用 redux 作为全局状态管理,用 mobx 作为组件局部状态管理。
Diff 的瓶颈以及 React 的应对
由于 diff 操作本身会带来性能上的损耗,在 React 文档中提到过,即使最先进的算法中,将前后两棵树完全比对的算法复杂度为O(n3)
,其中 n 为树中元素的数量。
如果 React 使用了该算法,那么仅仅一千个元素的页面所需要执行的计算量就是十亿的量级,这无疑是无法接受的。
为了降低算法的复杂度,React 的 diff 会预设三个限制:
- 只对同级元素进行 diff 比对。如果一个元素节点在前后两次更新中跨越了层级,那么 React 不会尝试复用它
- 两个不同类型的元素会产生出不同的树。如果元素由 div 变成 p,React 会销毁 div 及其子孙节点,并新建 p 及其子孙节点
- 开发者可以通过 key 来暗示哪些子元素在不同的渲染下能保持稳定
React 中 key 的作用
<!-- 更新前 -->
<div>
<p key="ka">ka</p>
<h3 key="song">song</he>
</div>
<!-- 更新后 -->
<div>
<h3 key="song">song</h3>
<p key="ka">ka</p>
</div>
如果没有 key,React 会认为 div 的第一个子节点由 p 变成 h3,第二个子节点由 h3 变成 p,则会销毁这两个节点并重新构造。
但是当我们用 key 指明了节点前后对应关系后,React 知道 key === "ka"
的 p 更新后还在,所以可以复用该节点,只需要交换顺序。
key 是 React 用来追踪哪些列表元素被修改、被添加或者被移除的辅助标志。
在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React diff 算法中,React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重新渲染。同时,React 还需要借助 key 来判断元素与本地状态的关联关系。
调用 setState 之后发生了什么
在代码中调用 setState 函数之后,React 会将传入的参数与之前的状态进行合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。在 React 得到元素树之后,React 会计算出新的树和老的树之间的差异,然后根据差异对界面进行最小化重新渲染。通过 diff 算法,React 能够精确制导哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
React 的 setState 是同步的还是异步的
有时表现出同步,有时表现出异步
- setState 只有在 React 自身的合成事件和钩子函数中是异步的,在原生事件和 setTimeout 中都是同步的
- setState 的异步并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的异步。当然可以通过 setState 的第二个参数中的 callback 拿到更新后的结果
- setState 的批量更新优化也是建立在异步(合成事件、钩子函数)之上的,在原生事件和 setTimeout 中不会批量更新,在异步中如果对同一个值进行多次 setState,setState 的批量更新策略会对其进行覆盖,去最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新
React 生命周期函数
挂载阶段
挂载阶段也可以理解为初始化阶段,也就是把我们的组件插入到 DOM 中。
- constructor
- getDerivedStateFromProps
UNSAFE_componentWillMount- render
- (React Updates DOM and refs)
- componentDidMount
constructor
组件的构造函数,第一个被执行。显式定义构造函数时,需要在第一行执行 super(props)
,否则不能再构造函数中拿到 this
。
在构造函数中,我们一般会做两件事:
- 初始化 state
- 对自定义方法进行 this 绑定
getDerivedStateFromProps
是一个静态函数,所以不能在这里使用 this,也表明了 React 官方不希望调用方滥用这个生命周期函数。每当父组件引发当前组件的渲染过程时,getDerivedStateFromProps 都会被调用,这样我们有机会根据新的 props 和当前的 state 来调整一个新的 state。
这个函数会在收到新的 props,调用了 setState 或 forceUpdate 时被调用。
render
React 最核心的方法,class 组件中必须实现的方法。
当 render 被调用时,它会检查 this.props
和 this.state
的变化并返回一下类型之一:
- 原生的 DOM,如 div
- React 组件
- 数组或 Fragment
- Portals(传送门)
- 字符串或数字,被渲染成文本节点
- 布尔值或 null,不会渲染任何东西
componentDidMount
在组件挂载之后立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。这个方法比较适合添加订阅的地方,如果添加了订阅,请记得在卸载的时候取消订阅。
你可以在 componentDidMount 里面直接调用 setState,它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前,如此保证了即使 render 了两次,用户也不会看到中间状态。
更新阶段
更新阶段是指当组件的 props 发生了改变,或者组件内部调用了 setState 或者发生了 forceUpdate,这个阶段的过程包括:
- UNSAFE_componentWillReceiveProps
- getDerivedStateFromProps
- sholdComponentUpdate
- UNSAFE_componentWIllUpdate
- render
- getSnapshotBeforeUpdate
- (React Updates DOM and refs)
- componentDidUpdate
shouldComponentUpdate
它有两个参数,根据此函数的返回值来判断是否重新进行渲染,首次渲染或者是当我们调用了 forceUpdate 时并不会触发此方法,此方法仅用于性能优化。
但是官方提倡我们使用内置的 PureComponent 而不是自己编写 shouldComponentUpdate。
getSnapshotBeforeUpdate
这个生命周期函数发生在 render 之后,在更新之前,给了一个机会去获取 DOM 信息,计算得到并返回一个 snapshot,这个 snapshot 会作为 componentDidUpdate 第三个参数传入。
componentDidUpdate
这个函数会在更新后被立即调用,首次渲染不会执行此方法。在这个函数中我们可以操作 DOM,可以发起请求,还可以 setState,但注意一定要用条件语句,否则会导致无限循环。
卸载阶段
componentWillUnmount
这个生命周期函数会在组件卸载销毁之前被调用,我们可以在这里执行一些清除操作。不要在这里调用 setState,因为组件不会重新渲染。
shouldComponentUpdate 的作用
shouldComponentUpdate 这个方法用来判断是否需要调用 render 方法重新描绘 DOM。因为 DOM 的描绘性能开销很大,如果可以在这个生命周期阶段做出更优化的 DOM diff 算法,可以极大地提升性能。
React 中 ref 的作用
ref 是 React 提供的一种可以安全访问 DOM 元素或者某个组件实例的方式。
在类组件中使用 createRef()
,在函数组件中使用 useRef
。