React
1.React中函数组件和class组件的区别
React 的函数组件和类组件在很多方面都相似,但它们也有一些关键的差异。以下是这两种组件之间的主要区别:
-
定义方式:
-
函数组件: 是简单的 JavaScript 函数,接受 props 为参数,并返回 React 元素。
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
-
类组件: 是 ES6 的类,继承的时候要用到 extends React.Component,至少包含一个名为
render
的方法。class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }
-
-
状态 (State):
-
函数组件: 在 React Hooks 出现之前,函数组件不支持状态。但现在,使用
useState
这个 Hook,函数组件也可以有状态了。 -
类组件: 通过
this.state
和this.setState
方法来使用和更新状态。
-
函数组件: 在 React Hooks 出现之前,函数组件不支持状态。但现在,使用
-
生命周期方法:
-
函数组件: 原本没有生命周期方法,但现在通过使用 Hooks(例如
useEffect
)可以模拟大多数生命周期行为。 -
类组件: 有完整的生命周期方法,如
componentDidMount
,componentDidUpdate
,componentWillUnmount
等。
-
函数组件: 原本没有生命周期方法,但现在通过使用 Hooks(例如
-
this 关键字:
-
函数组件: 不使用
this
。props 作为函数的参数传入。 -
类组件: 必须使用
this
来访问 props、state 和类的方法。
-
函数组件: 不使用
-
React Hooks:
- 函数组件: 可以使用 Hooks。
- 类组件: 不能使用 Hooks。
-
性能和文件大小:
- 函数组件: 通常来说,函数组件由于没有类的额外开销,因此可能稍微快一点,并且生成的组件代码可能更小。
- 类组件: 由于有更多的特性和生命周期方法,可能会有稍微的额外开销。
总的来说,随着 React Hooks 的推出,函数组件变得更加强大,使得开发者能够在没有类的情况下构建完整的应用程序。然而,两者都有其适用的场景,取决于特定的需求和开发者的喜好。
2.hooks模拟生命周期
在函数组件中,我们可以使用 Hooks 来模拟类组件中的生命周期函数。下面我将描述如何使用 useState
和 useEffect
Hooks 来模拟类组件中常见的生命周期函数:
-
componentDidMount
:在类组件中,
componentDidMount
通常用于执行只需要在组件首次渲染后执行的操作,如 API 调用或订阅。使用
useEffect
Hook,你可以模拟此生命周期函数:useEffect(() => { // 这里的代码将在组件首次渲染后执行,类似于 componentDidMount }, []); // 空数组意味着这个 useEffect 只会在组件首次渲染后执行一次
-
componentDidUpdate
:componentDidUpdate
在组件更新后执行,你可以获取到组件更新前的 props 和 state。使用
useEffect
,你可以这样模拟:useEffect(() => { // 这里的代码将在每次组件更新后执行,类似于 componentDidUpdate });
如果你只对某个特定的 prop 或 state 的变化感兴趣,你可以这样:
useEffect(() => { // 当 someProp 发生变化时,这里的代码将执行 }, [someProp]);
-
componentWillUnmount
:类组件中,
componentWillUnmount
用于执行清理操作,如取消 API 请求或移除订阅。使用
useEffect
的返回函数,你可以模拟此生命周期函数:useEffect(() => { // 这里的代码将在组件首次渲染后执行 return () => { // 这里的代码将在组件卸载前执行,类似于 componentWillUnmount }; }, []);
-
模拟
shouldComponentUpdate
:在函数组件中,没有直接的
shouldComponentUpdate
的等价物。但是,你可以使用 React.memo 来包装组件,从而避免不必要的渲染。const MyComponent = React.memo(function MyComponent(props) { // 你的组件代码 });
-
模拟
getSnapshotBeforeUpdate
和componentDidCatch
:到目前为止,这两个生命周期方法没有对应的 Hooks 等价物。你可能需要使用类组件或查找其他解决方案来模拟这两个生命周期方法的功能。
综上所述,使用 Hooks,函数组件可以模拟大多数类组件的生命周期函数,但某些特定的生命周期方法可能需要其他解决方案或仍然需要类组件。
3.React组件传值
在 React 中,组件之间的数据传递主要基于 props 和 state,但也有一些其他的技术和方法来实现数据传递。以下列举了 React 组件之间传值的几种常见方法,并特别强调了子组件向父组件传值的方法:
-
父组件向子组件传值:最常见的数据传递方式。父组件通过属性(props)直接将数据传递给子组件。
<ChildComponent data={this.state.data} />
-
子组件向父组件传值:
-
回调函数:父组件提供一个函数作为 props 传递给子组件,子组件在适当的时候调用这个函数并传递数据。
// 父组件 handleDataFromChild = (data) => { console.log(data); } <ChildComponent sendData={this.handleDataFromChild} /> // 子组件 this.props.sendData(data);
-
回调函数:父组件提供一个函数作为 props 传递给子组件,子组件在适当的时候调用这个函数并传递数据。
-
通过 Context 传值:当你有一些全局的数据(如主题、认证信息等)需要被多个不直接关联的组件访问时,可以使用 Context。它允许你在组件树中的多个层级传递一个值,而不需要手动地在每一层传递 props。
-
兄弟组件之间传值:
- 通过共同的父组件。子组件 A 将值传递给父组件,然后父组件再将这个值通过 props 传递给子组件 B。
- 使用状态管理库(如 Redux、MobX 等)来维护一个全局状态,任何组件都可以从中读取或修改状态。
-
使用 Refs:虽然 refs 主要是为了让父组件可以获取子组件或 DOM 元素的引用,但有时它也可以用来让父组件调用子组件的方法。
-
使用全局事件:例如使用
eventEmitter
的库来触发和监听事件,从而实现组件之间的数据传递。 -
使用状态管理工具:例如 Redux、MobX。这些工具提供了一个集中的数据存储,所有组件都可以从中读取和更新数据。
请注意,虽然有很多方法可以实现组件间的数据传递,但不是所有方法都适用于每一种情况。选择哪种方法应基于你的具体需求和应用的复杂性。一般来说,尽量保持数据流的简单和单向是一个好的做法。
4.react原理fiber调度算法
React Fiber 是 React 16 引入的新的核心算法,旨在提高应用程序的性能和响应性。其目标是通过分片技术实现增量渲染,允许React暂停工作然后稍后继续进行,从而在大型应用程序中实现更平滑的用户体验。
Fiber 为 React 的每个组件都创建了一个数据结构,这个数据结构持有了关于组件的相关信息。这包括了组件的类型(例如函数或类组件)、输入的props、与该组件关联的DOM元素等等。
以下是 Fiber 调度算法的核心概念和工作原理:
-
双缓冲技术: Fiber 使用双缓冲技术来实现快速的状态更新。有两棵树在工作: 当前树和工作中的(或下一个)树。当前的树用于显示界面,而下一个树用于实现变化。一旦下一个树准备好了,React 则会快速切换它们,使下一个树成为当前显示的树。
-
增量渲染: 与一次性完成所有工作不同,React 通过 Fiber 可以将渲染工作分割成小块。每一块都可以单独处理,这使得 React 可以根据优先级处理更重要的更新,例如那些由用户交互引起的。
-
任务的优先级: Fiber 引入了任务的概念,每个任务都有不同的优先级。例如,由用户输入或动画触发的更新有着更高的优先级,而后台数据同步这样的任务优先级较低。
-
暂停、中断和恢复: 一个核心的改进是 Fiber 能够暂停正在进行的渲染,稍后再恢复。这使得 React 可以中断正在进行的渲染以处理更紧急的事情。
总之,React Fiber 是对 React 核心算法的重大改进,它使框架更具响应性,特别是在大型应用程序中,同时为未来的并发模式和其他高级功能奠定了基础。
5.React中的diff算法
React 的 diff
算法通过以下三种优化策略来提高性能,从O(n^3)到O(n):
-
树级比较 (Tree Level):
- React 对两棵树进行逐级比较。对于两个不同类型的元素,React 会认为它们会生成出不同的树,并直接销毁旧树来创建新树。
- 如果是同类型的组件,React 会保留 DOM 节点并仅比较它们的子元素。
- 这种策略意味着当跨越组件边界移动元素时,React 不会尝试复用那些元素。
-
组件级比较 (Component Level):
- 当用户组件被认为是相等的,React 会按照其
key
来判断它们是不是同一个组件。所以,为组件提供一个稳定的key
(如果它们在数组中)可以帮助 React 进行优化。 - 如果组件的类型改变,那么该组件及其所有子组件都会被重新创建。
- 当组件类型相同时,React 会保留组件实例并调用其生命周期方法来确定是否需要更新。
- 当用户组件被认为是相等的,React 会按照其
-
元素级比较 (Element Level):
- 通过为每个子元素分配一个 key,我们可以使算法更加高效。当 key 一致时,React 会认为这两个元素是相同的,只会进行必要的更新。当 key 不一致时,React 会删除旧元素并创建新元素。
-
key
应该是稳定的、预测性的、并且尽可能地是唯一的,以避免无意义的元素重建。
React 的 diff
算法利用了上述策略,通过尽量少的操作来更新真实的 DOM。这种策略在大多数情况下都能提供高效且快速的更新,但是也有可能在某些特定场景下不是最优的。这就是为什么在某些情况下,开发者可能需要优化组件的 shouldComponentUpdate
方法或使用 React.memo
来进一步提高性能。
6.Vue与React不同
开发者不同
1. 响应式原理:
-
Vue: 使用
Object.defineProperty()
进行数据劫持,每个属性被转换为getter和setter,配合watcher
实现数据到视图的响应。 -
React: 主要依赖
setState()
方法更新状态和触发组件重新渲染。还有hooks中的依赖项发生变化
2. 组件写法:
-
Vue:
template
- React: 使用JSX
3. 核心思想:
- Vue: 是一个渐进式的双向绑定的MVVM框架,双向数据流,降低前端开发门槛。
- React: 倡导声明式渲染、组件化和单向数据流。
4. 模板渲染方式:
-
Vue: 在独立模板中使用指令如
v-if
、v-for
等。 - React: 在组件内使用JS逻辑进行模板渲染。
5. 框架本质:
- Vue: 本质上是MVVM框架。
- React: 为前端组件化框架。
7.不要在循环、条件或嵌套函数中调用 Hooks
Hooks 需要在组件的每次渲染中按照完全相同的顺序被调用。如果你在条件或循环中调用 Hook,这一规则就会被打破,因为在某些渲染中 Hook 可能不会被调用。为了确保组件的行为是可预测的,需要确保每次渲染都会以相同的顺序执行所有的 Hooks。
8.useEffect 和useLayoutEffect的区别
useEffect
和 useLayoutEffect
都是 React Hooks,它们都允许你在函数组件内部执行副作用(side effects)。但它们的执行时机有所不同。以下是它们之间的主要区别:
-
执行时机:
- useEffect: 它在浏览器完成布局和绘制之后,在一个延迟事件中被调用。这意味着它是异步的并且不会阻塞浏览器的渲染进程。
- useLayoutEffect: 它的执行时机更早,具体是在浏览器执行绘制之前。因此,它是同步的,会阻塞浏览器的渲染。
-
使用场景:
- useEffect: 适合大多数常见的副作用场景,例如数据获取、订阅或手动更改 DOM 等。
-
useLayoutEffect: 如果你需要在浏览器渲染之前同步地执行某些操作,通常会使用
useLayoutEffect
。例如,如果你要读取 DOM 布局并同步地重新渲染,就需要用useLayoutEffect
。
总之,除非有明确的理由需要在渲染发生之前同步地执行副作用,否则推荐使用 useEffect
。如果你不确定应该选择哪一个,那么 useEffect
是一个更安全的起点。
9. useMemo 和 useCallback
useCallback:
- 目的: 避免组件重新渲染时不必要地创建新的函数。(优化函数)
- 用法:
你有一个父组件和一个子组件,子组件使用了React.memo
或者是PureComponent
来避免不必要的重新渲染。如果父组件传递一个函数给子组件,并且这个函数在父组件的每次渲染时都被重新创建,那么React.memo
或PureComponent
的优化效果就会失效。
这是因为,尽管函数的功能没有改变,但每次都会创建一个新的函数引用,导致子组件认为它接收到了新的props,从而引发子组件的重新渲染。
这就是useCallback
派上用场的地方。通过使用useCallback
,你可以保证,只有当函数的依赖项发生变化时,函数才会被重新创建。这样,你就可以确保子组件不会因为接收到新的函数引用而进行不必要的重新渲染。
useMemo:
- 目的: 记忆昂贵的计算结果,避免每次渲染时的重新计算。(优化计算结果)
- 用法: 当组件中有代价高昂的计算并且不想在每次渲染时都重新执行。
TypeScript (TS) 的理解
-
TypeScript (TS) 的理解:
TypeScript 是 JavaScript 的一个超集,为 JavaScript 增加了静态类型(类型注解、类型推断)、类、接口等特性。其主要目的是增强代码的可读性和可维护性,同时捕获开发过程中可能出现的错误。 -
TypeScript和ES6有什么不同?
- TypeScript包含了所有的ES6功能,并在其之上添加了类型系统。
- ES6是JavaScript的一个版本,而TypeScript是JavaScript的超集。
-
什么是类型注解和类型推断?
- 类型注解:明确地为变量或函数参数指定一个类型。
let name: string = "John";
- 类型推断:不明确指定变量的类型时,TypeScript会使用类型推断来分配类型。
let name = "John"; // TypeScript infers that `name` is of type `string`
- 类型注解:明确地为变量或函数参数指定一个类型。
-
描述TypeScript中的访问修饰符。
- TypeScript中有三个访问修饰符:
public
、private
和protected
。
- TypeScript中有三个访问修饰符:
-
描述
any
、unknown
、never
类型。-
any
: 用于代表任何类型,使用any
后会丧失类型检查的功能。 -
unknown
: 与any
类似,但你必须显式进行类型断言或基于类型检查来使用它。 -
never
: 表示永远不可能存在的值的类型。
-
1.type和interface区别:
在大多数情况下,interface
和 type
是可以互换使用的:
-
相似点:
- 两者都可以描述一个对象或函数的形状。
-
主要区别:
-
声明合并:
interface
在定义时可以合并多次。如果两个interface
有相同的名字,它们会被自动合并成一个。但是,当您使用type
时,您不能有两个相同名称的type
。 -
使用联合 (Union) 和交叉 (Intersection) 类型:
type
有更大的灵活性,可以使用联合和交叉类型,而interface
不可以。 -
implements与extends:类可以实现 (
implements
)interface
,但不能实现 (implements
)type
。 -
计算属性:
type
可以使用计算属性,interface
不可以。
-
声明合并:
总体来说,在大多数情况下,interface
和 type
可以互换使用,但是在一些特殊情况下,它们具有一些细微的差异,需要根据具体情况来选择使用哪个。一般来说,如果需要声明合并、实现接口、继承类等高级类型定义,优先选择使用 interface
。而对于复杂的类型操作,比如联合类型、交叉类型、泛型类型等,type
可能更为灵活和方便。
2.泛型的理解:
泛型是在定义函数、接口或类时,不预先确定方法的具体类型,而是在使用时才决定的一种特性。使用泛型可以创建可重用的组件,组件不仅能够支持当前的数据类型,还能支持未来的数据类型,给予用户更大的灵活性。例如:
function identity<T>(arg: T): T {
return arg;
}
在上面的例子中,T
是一个类型变量,它帮助我们捕获用户提供的类型,例如:number
或 string
,但不限于这些两种。
3.联合 (Union) 和交叉 (Intersection) 类型是什么?请举例
在 TypeScript 中,联合(Union)和交叉(Intersection)类型是两种用于组合多种类型的强大工具。
-
联合类型 (Union Types):
联合类型使用|
符号来表示一个值可以是多种类型之一。举例:
type StringOrNumber = string | number; let myVar: StringOrNumber; myVar = 'Hello'; // 有效,因为 'Hello' 是一个 string 类型 myVar = 123; // 有效,因为 123 是一个 number 类型
使用联合类型时,你只能访问此联合类型的所有类型里共有的成员。
-
交叉类型 (Intersection Types):
交叉类型使用&
符号来组合多个类型,让你可以将多个类型合并为一个类型。举例:
interface Name { name: string; } interface Age { age: number; } type Person = Name & Age; let person: Person = { name: 'Alice', age: 30 };
使用交叉类型,
person
变量必须具有Name
和Age
接口中定义的所有属性。
希望这些示例可以帮助您更好地理解联合类型和交叉类型!
4.implements与extends
类可以实现 (implements
) interface
,但不能实现 (implements
) type
。类可以继承 (extends
) type
如果 type
描述了类的形状。
-
类实现 (
implements
)interface
:interface Movable { move(distance: number): void; } class Car implements Movable { move(distance: number) { console.log(`Car moved ${distance} meters.`); } }
-
类不能实现 (
implements
)type
,但可以继承 (extends
)type
:这个要分两个例子来说明:
-
不能实现一个
type
:type MovableType = { move(distance: number): void; }; // 这里会产生错误,因为类不能实现一个 type class Car implements MovableType { move(distance: number) { console.log(`Car moved ${distance} meters.`); } }
-
但如果
type
描述了类的形状,类可以继承它:type Vehicle = { new (name: string): Vehicle; name: string; drive(): void; }; class Car extends (class implements Vehicle { name: string; constructor(name: string) { this.name = name; } drive() { console.log(`${this.name} drives.`); } }) {}
-
这里的关键在于,type
是一个值的类型别名,而不是一个真正的类型。当 type
描述一个具有构造签名的类型时(如上面的 Vehicle
示例),它事实上是描述了一个类的形状,这时它可以被一个类继承。但在通常情况下,类不能直接实现 type
。
5.计算属性
当谈到 TypeScript 的计算属性,我们通常指的是对象字面量中的计算属性键。与 ES6 中的对象字面量计算属性类似,TypeScript 也允许你使用表达式来定义类型的属性键。
-
使用
type
定义计算属性:const propName = "name"; type MyType = { [propName]: string; // 使用计算属性 age: number; }; let obj: MyType = { name: 'Alice', age: 30 };
-
使用
interface
时无法直接定义计算属性:以下是错误的示例:
const propName = "name"; interface MyInterface { [propName]: string; // 这里会产生错误 age: number; }
然而,需要注意的是,尽管
interface
不能直接定义计算属性,但它们可以定义索引签名来表示某些类型的属性集合,这是一个稍微不同的概念。文章来源:https://www.toymoban.com/news/detail-674395.html
总的来说,如果你需要在类型中使用基于某种计算或常量的属性名称,type
会是更合适的选择。文章来源地址https://www.toymoban.com/news/detail-674395.html
到了这里,关于面试中问:React中函数组件和class组件的区别,hooks模拟生命周期的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!