2025年03月10日人慧前端面试(外包滴滴)
目录
- 普通函数和箭头函数的区别
- loader 和 plugin 的区别
- webpack 怎么实现分包,为什么要分包
- webpack 的构建流程
- 变量提升
- react 开发中遇到过什么问题
- 什么是闭包
- vue 开发中遇到过什么问题
- vue中的 dep 和 watcher 的依赖收集是什么阶段
- 什么是原型链
- react setState 是同步还是异步的
- react 17 和 18 有什么区别
- react 18 有哪些新特性
- react 常用的 hooks
- useCallback 和 useMemo 的区别
- memo 的用法
- useImperativeHandle 用法
- 浏览器输入 url 发生了什么
- 事件循环
- script 标签的 defer 和 async 的区别
- promise 的 then 和 catch 的区别
- promise 的 all race finally 的区别
- try catch 是同步还是异步的
- instanceof 的原理
- 如何实现继承
- 使用 useHooks 实现一个防抖
- async await 的原理
- 算法题:树形结构转换成列表
- 算法题:数组结构转换成树
- 算法题:最长递增子序列
1. 普通函数和箭头函数的区别
在 JavaScript 中,普通函数和箭头函数是两种不同的定义函数的方式,它们存在多方面的区别,下面为你详细介绍:
1. 语法
- 普通函数:使用
function
关键字来定义,可以有函数名,也可以是匿名函数。
// 具名函数
function add(a, b) {
return a + b;
}
// 匿名函数
const subtract = function(a, b) {
return a - b;
};
- 箭头函数:使用箭头
=>
来定义,语法更加简洁。当只有一个参数时,可以省略括号;当函数体只有一条语句时,可以省略花括号和return
关键字。
// 单个参数
const square = num => num * num;
// 多个参数
const multiply = (a, b) => a * b;
// 无参数
const greet = () => 'Hello!';
// 函数体有多条语句
const calculate = (a, b) => {
const sum = a + b;
return sum * 2;
};
2. this
指向
- 普通函数:
this
的值取决于函数的调用方式,它可以是全局对象(在非严格模式下)、函数本身(使用call
、apply
或bind
方法时)、对象实例(当函数作为对象的方法调用时)等。
const person = {
name: 'John',
sayName: function() {
console.log(this.name);
}
};
person.sayName(); // 输出: John
const sayName = person.sayName;
sayName(); // 在非严格模式下输出: undefined 或全局对象的属性(取决于环境)
- 箭头函数:
this
的值继承自外层函数(即定义时的上下文),不会根据调用方式改变。
const person = {
name: 'John',
sayName: () => {
console.log(this.name);
}
};
person.sayName(); // 输出: undefined,因为 this 指向全局对象
3. arguments
对象
- 普通函数:内部有一个
arguments
对象,它是一个类数组对象,包含了函数调用时传递的所有参数。
function showArgs() {
console.log(arguments);
}
showArgs(1, 2, 3); // 输出: [Arguments] { '0': 1, '1': 2, '2': 3 }
- 箭头函数:没有自己的
arguments
对象,如果需要访问参数,可以使用剩余参数语法。
const showArgs = (...args) => {
console.log(args);
};
showArgs(1, 2, 3); // 输出: [ 1, 2, 3 ]
4. 使用 new
关键字
- 普通函数:可以使用
new
关键字作为构造函数来创建对象实例。
function Person(name) {
this.name = name;
}
const john = new Person('John');
console.log(john.name); // 输出: John
- 箭头函数:不能使用
new
关键字调用,因为它没有自己的this
和prototype
,使用new
调用会抛出错误。
const Person = (name) => {
this.name = name;
};
try {
const john = new Person('John');
} catch (error) {
console.error(error); // 输出: TypeError: Person is not a constructor
}
5. yield
关键字
- 普通函数:可以使用
yield
关键字(在生成器函数中)。
function* generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
console.log(gen.next().value); // 输出: 1
- 箭头函数:不能使用
yield
关键字,因此不能作为生成器函数。
2. loader 和 plugin 的区别
在前端构建工具(如 Webpack)中,loader
和 plugin
是两个非常重要的概念,它们都用于扩展构建过程的功能,但它们的作用和使用方式有明显的区别,下面为你详细介绍:
功能定位
- loader:主要用于对模块的源代码进行转换。它可以将不同类型的文件(如 CSS、图片、TS 等)转换为 Webpack 能够处理的模块,本质上是一个转换器。例如,当你在 JavaScript 代码中引入一个 CSS 文件时,Webpack 本身并不知道如何处理 CSS 文件,这时就需要使用
css-loader
和style-loader
来将 CSS 文件转换为 JavaScript 模块,从而让 Webpack 可以处理。 - plugin:则是在 Webpack 构建流程的特定生命周期节点上执行广泛的任务,用于增强 Webpack 的功能。它可以处理一些全局性的任务,比如代码压缩、资源管理、环境变量注入等。例如,
HtmlWebpackPlugin
可以自动生成 HTML 文件,并将打包后的资源插入到 HTML 中;UglifyJsPlugin
可以对 JavaScript 代码进行压缩。
工作方式
- loader:是链式调用的,多个
loader
可以按照顺序依次对模块进行处理。Webpack 会按照从右到左(或从下到上)的顺序依次调用loader
。例如:
module.exports = {
module: {
rules: [
{
test: /\.css$/,
// 先使用 css-loader 处理 CSS 文件,再使用 style-loader 将 CSS 插入到 DOM 中
use: ['style-loader', 'css-loader']
}
]
}
};
- plugin:通过在 Webpack 配置中实例化插件并添加到
plugins
数组中,Webpack 在构建过程中会根据插件的钩子函数在特定的时机执行插件的逻辑。例如:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
// 实例化 HtmlWebpackPlugin 插件
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
};
使用场景
- loader:
- 处理不同类型的文件,如将 CSS、图片、字体等文件转换为模块。
- 对代码进行预处理,如将 TypeScript 转换为 JavaScript,将 ES6+ 代码转换为 ES5 代码。
- 对文件内容进行转换,如压缩图片、处理 CSS 中的图片路径等。
- plugin:
- 代码优化,如压缩 JavaScript、CSS 代码。
- 资源管理,如生成 HTML 文件、提取公共代码。
- 环境变量注入,如在构建过程中注入不同的环境变量。
- 构建过程监控和报告,如在构建完成后输出构建信息。
示例代码
以下是一个包含 loader
和 plugin
的完整 Webpack 配置示例:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin({
filename: 'styles.css'
})
]
};
在这个示例中,babel-loader
用于将 ES6+ 代码转换为 ES5 代码,css-loader
和 MiniCssExtractPlugin.loader
用于处理 CSS 文件;HtmlWebpackPlugin
用于生成 HTML 文件,MiniCssExtractPlugin
用于将 CSS 提取到单独的文件中。
3. webpack 怎么实现分包,为什么要分包
Webpack 实现分包的方法
1. 多入口配置
- 原理:通过配置多个入口文件,Webpack 会根据这些入口文件分别构建不同的包。
- 示例代码:
// webpack.config.js
const path = require('path');
module.exports = {
// 定义多个入口
entry: {
main: './src/main.js',
vendor: './src/vendor.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
// 使用 [name] 占位符,根据入口名称生成不同的文件名
filename: '[name].[contenthash].js'
}
};
在上述代码中,main
和 vendor
是两个不同的入口,Webpack 会分别对它们进行打包,生成两个不同的文件。
2. 使用 splitChunks
进行代码分割
- 原理:
splitChunks
是 Webpack 内置的一个优化配置项,它可以将公共代码提取到单独的包中,避免重复打包。 - 示例代码:
// webpack.config.js
module.exports = {
// ...其他配置
optimization: {
splitChunks: {
chunks: 'all', // 对所有类型的 chunk 进行分割
minSize: 30000, // 生成 chunk 的最小体积(以字节为单位)
maxSize: 0, // 生成 chunk 的最大体积(0 表示不限制)
minChunks: 1, // 某个模块至少被多少个 chunk 引用才会被分割出来
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 中的模块
name: 'vendors', // 生成的文件名
priority: -10 // 优先级
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
在这个配置中,splitChunks
会将 node_modules
中的模块提取到名为 vendors
的文件中,同时会对其他公共模块进行分割。
3. 动态导入(懒加载)
- 原理:使用 ES6 的动态导入语法(
import()
),可以在需要的时候再加载模块,实现代码的按需加载。 - 示例代码:
// main.js
button.addEventListener('click', async () => {
const { add } = await import('./math.js');
console.log(add(1, 2));
});
当用户点击按钮时,才会加载 math.js
模块,这样可以减少首屏加载的代码量。
为什么要进行分包
1. 减少首屏加载时间
- 当项目越来越大时,打包后的文件会变得非常大,如果一次性加载整个文件,会导致首屏加载时间过长,影响用户体验。通过分包,可以将不常用的代码或按需加载的代码分离出来,只在需要的时候加载,从而减少首屏加载的代码量,提高页面加载速度。
2. 提高缓存利用率
- 不同的包可能有不同的更新频率。例如,
node_modules
中的第三方库通常不会频繁更新,而业务代码可能会经常修改。将第三方库和业务代码分开打包,当业务代码更新时,用户只需要重新下载业务代码包,而第三方库包可以继续使用缓存,从而减少用户的下载量,提高缓存利用率。
3. 便于代码维护和管理
- 分包可以将不同功能的代码分离到不同的文件中,使代码结构更加清晰,便于开发人员进行维护和管理。例如,可以将公共组件、工具函数等提取到单独的包中,方便复用和修改。
4. webpack 的构建流程
Webpack 是一个强大的模块打包工具,其构建流程可以大致分为以下几个阶段:
1. 初始化阶段
- 启动配置解析:Webpack 启动时,会读取项目根目录下的
webpack.config.js
文件(也可以通过命令行参数指定其他配置文件),解析其中的配置信息,包括入口文件、输出路径、加载器(loader)、插件(plugin)等。 - 创建编译器对象:根据解析后的配置信息,Webpack 会创建一个
Compiler
对象,这个对象是 Webpack 的核心,它包含了整个构建过程的配置和方法,负责统筹和管理整个构建流程。
2. 编译阶段
- 入口模块分析:从配置文件中指定的入口文件开始,Webpack 会调用
Compiler
对象的compile
方法开始编译。首先会对入口模块进行解析,识别模块中的导入语句(如import
或require
)。 - 模块递归解析:根据入口模块中的导入语句,Webpack 会递归地解析所有依赖的模块。在解析每个模块时,会根据配置中定义的规则,使用相应的加载器(loader)对模块进行处理。例如,如果遇到
.css
文件,会使用css-loader
和style-loader
进行处理;如果遇到.js
文件,可能会使用babel-loader
进行转换。 - 生成抽象语法树(AST):对于 JavaScript 模块,Webpack 会将其源代码解析为抽象语法树(AST),通过分析 AST 可以更准确地识别模块中的依赖关系和语法结构。然后根据 AST 对模块进行转换和优化。
3. 模块构建阶段
- 应用加载器:在解析每个模块的过程中,Webpack 会按照配置中
module.rules
里定义的规则,依次应用相应的加载器对模块进行处理。加载器可以对模块的源代码进行转换,比如将 CSS 代码转换为 JavaScript 模块,将 TypeScript 代码转换为 JavaScript 代码等。 - 生成模块代码:经过加载器处理后,每个模块会生成最终的 JavaScript 代码,这些代码会被封装成一个模块对象,包含模块的 ID、依赖信息和模块代码本身。
4. 打包阶段
- 模块合并与优化:Webpack 会根据模块之间的依赖关系,将所有模块合并成一个或多个包(chunk)。在合并过程中,Webpack 会进行一些优化操作,如去除重复的代码、压缩代码体积等。
- 应用插件:在打包过程中,Webpack 会触发一系列的钩子(hook),插件可以监听这些钩子并在特定的时机执行相应的任务。例如,
HtmlWebpackPlugin
会在打包完成后生成 HTML 文件,并将打包后的资源插入到 HTML 中;UglifyJsPlugin
会对 JavaScript 代码进行压缩。
5. 输出阶段
- 生成文件:经过打包和优化后,Webpack 会将最终的包(chunk)写入到配置文件中指定的输出路径。输出的文件可以是 JavaScript 文件、CSS 文件、图片文件等,具体取决于配置和模块的类型。
- 构建完成:当所有文件都成功写入到输出路径后,Webpack 会输出构建完成的信息,整个构建流程结束。
示例代码(简单的 Webpack 配置)
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口文件
entry: './src/index.js',
// 输出配置
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
// 模块规则
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
// 插件配置
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
};
通过以上步骤,Webpack 可以将项目中的各种模块打包成最终的静态资源文件,方便在浏览器中使用。
5. 变量提升
变量提升(Hoisting)是 JavaScript 中的一个重要概念,它允许在变量声明之前就可以访问该变量,但变量的值在声明之前是 undefined
。下面将从变量提升的原理、var
、let
和 const
声明变量时的提升情况以及函数声明提升等方面详细介绍。
原理
在 JavaScript 中,代码的执行分为两个阶段:编译阶段和执行阶段。在编译阶段,JavaScript 引擎会将变量和函数的声明提升到当前作用域的顶部,但是变量的赋值操作不会被提升。这意味着在代码执行之前,变量和函数已经在内存中被创建好了,只不过变量的值还未被赋值。
var
声明的变量提升
使用 var
声明的变量会发生变量提升,在变量声明之前可以访问该变量,但值为 undefined
。
console.log(num); // 输出: undefined
var num = 10;
console.log(num); // 输出: 10
上述代码在编译阶段,var num
声明被提升到当前作用域的顶部,执行阶段先执行 console.log(num)
,此时 num
已经被声明但未赋值,所以输出 undefined
,接着执行 num = 10
进行赋值,最后再次输出 num
时,值为 10
。
let
和 const
声明的变量提升
let
和 const
也会发生变量提升,但它们存在暂时性死区(Temporal Dead Zone,TDZ)。在变量声明之前访问 let
或 const
声明的变量会导致 ReferenceError
。
// console.log(num); // 报错: ReferenceError: Cannot access 'num' before initialization
let num = 10;
console.log(num); // 输出: 10
在编译阶段,let num
声明被提升,但在执行到 let num = 10
之前,num
处于暂时性死区,不能被访问。
函数声明提升
函数声明也会发生提升,这意味着可以在函数声明之前调用该函数。
sayHello(); // 输出: Hello!
function sayHello() {
console.log('Hello!');
}
在编译阶段,函数声明 function sayHello() {...}
被提升到当前作用域的顶部,所以在函数声明之前可以调用该函数。
函数表达式和箭头函数提升
函数表达式和箭头函数不会像函数声明那样进行提升,因为它们本质上是变量赋值,遵循变量提升的规则。
// sayHello(); // 报错: TypeError: sayHello is not a function
var sayHello = function() {
console.log('Hello!');
};
上述代码中,var sayHello
声明被提升,但在赋值之前 sayHello
的值为 undefined
,所以在赋值之前调用 sayHello
会报错。
总结
var
声明的变量会提升,声明前访问值为undefined
。let
和const
声明的变量会提升,但存在暂时性死区,声明前访问会报错。- 函数声明会提升,可以在声明前调用。
- 函数表达式和箭头函数遵循变量提升规则。
6. react 开发中遇到过什么问题
在 React 开发中,开发者常常会遇到各种不同类型的问题,以下为你详细介绍常见问题及相应的解决办法:
1. 状态管理问题
- 问题描述
- 状态不同步:在函数组件里使用
useState
更新状态时,由于状态更新是异步的,可能会出现状态不同步的状况。比如在一个点击事件里多次调用状态更新函数,新状态可能不会立即生效。 - 复杂状态管理:当应用的状态变得复杂,例如嵌套层级深或者多个组件共享状态时,单纯使用
useState
会让代码难以维护和调试。
- 状态不同步:在函数组件里使用
- 解决办法
- 对于状态不同步问题,可以使用函数式更新。例如:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prevCount => prevCount + 1);
};
return (
<button onClick={handleClick}>
Count: {count}
</button>
);
}
- 对于复杂状态管理,可以采用 `useReducer` 或者状态管理库,像 Redux、MobX 等。
2. 组件渲染问题
- 问题描述
- 不必要的重新渲染:由于组件的
props
或者state
发生变化,会触发组件的重新渲染。有时候,即使组件的实际数据没有改变,也可能因为浅比较或者函数引用的变化而重新渲染,从而影响性能。 - 渲染顺序问题:在处理异步数据时,可能会出现组件渲染顺序不符合预期的情况,例如在数据还未加载完成时就进行渲染,导致页面显示异常。
- 不必要的重新渲染:由于组件的
- 解决办法
- 针对不必要的重新渲染,可以使用
React.memo
对函数组件进行包裹,它会对组件的props
进行浅比较,只有当props
发生变化时才会重新渲染组件。示例如下:
- 针对不必要的重新渲染,可以使用
import React from 'react';
const MyComponent = React.memo((props) => {
return <div>{props.data}</div>;
});
- 对于渲染顺序问题,可以使用条件渲染,确保在数据加载完成后再进行渲染。例如:
import React, { useState, useEffect } from 'react';
function DataComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => setData(data));
}, []);
if (!data) {
return <div>Loading...</div>;
}
return <div>{data}</div>;
}
3. 生命周期与副作用问题
- 问题描述
- 副作用未清理:在使用
useEffect
时,如果副作用函数返回了一个清理函数,但没有正确调用该清理函数,可能会导致内存泄漏或者出现意外的行为,例如定时器未清除、订阅未取消等。 - 依赖项错误:
useEffect
的依赖项数组设置不正确,可能会导致副作用函数在不需要执行的时候执行,或者在需要执行的时候没有执行。
- 副作用未清理:在使用
- 解决办法
- 确保在
useEffect
中返回清理函数,并在组件卸载或者依赖项变化时执行清理操作。例如:
- 确保在
import React, { useEffect } from 'react';
function TimerComponent() {
useEffect(() => {
const timer = setInterval(() => {
console.log('Timer tick');
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return <div>Timer component</div>;
}
- 仔细检查 `useEffect` 的依赖项数组,确保只包含必要的依赖项。如果不需要依赖项,可以传入空数组;如果依赖项发生变化时需要重新执行副作用函数,就将这些依赖项添加到数组中。
4. 事件处理问题
- 问题描述
- 事件绑定问题:在类组件中,事件处理函数的
this
指向可能会出现问题,默认情况下this
不会自动绑定到组件实例上。 - 事件传递问题:在嵌套组件中传递事件处理函数时,可能会因为函数引用的变化导致不必要的重新渲染。
- 事件绑定问题:在类组件中,事件处理函数的
- 解决办法
- 在类组件中,可以使用箭头函数或者在构造函数中绑定
this
。例如:
- 在类组件中,可以使用箭头函数或者在构造函数中绑定
import React, { Component } from 'react';
class MyComponent extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Button clicked');
}
render() {
return <button onClick={this.handleClick}>Click me</button>;
}
}
- 对于事件传递问题,可以使用 `useCallback` 对事件处理函数进行记忆化处理,确保函数引用不变。例如:
import React, { useCallback } from 'react';
function ParentComponent() {
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
return <ChildComponent onClick={handleClick} />;
}
5. 路由问题
- 问题描述
- 路由匹配问题:在使用 React Router 时,可能会出现路由匹配不正确的情况,例如路由路径配置错误或者路由优先级设置不合理。
- 路由传参问题:在路由之间传递参数时,可能会遇到参数丢失或者参数解析错误的问题。
- 解决办法
- 仔细检查路由路径的配置,确保路径格式正确,并且合理设置路由的优先级。例如:
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
);
}
- 使用 React Router 提供的路由传参和解析方法,例如使用 `useParams` 来获取路由参数。示例如下:
import { BrowserRouter as Router, Routes, Route, useParams } from 'react-router-dom';
function User() {
const { id } = useParams();
return <div>User ID: {id}</div>;
}
function App() {
return (
<Router>
<Routes>
<Route path="/users/:id" element={<User />} />
</Routes>
</Router>
);
}
7. 什么是闭包
闭包的定义
闭包是指有权访问另一个函数作用域中的变量的函数。简单来说,即使一个函数执行完毕并返回,其内部定义的变量也不会被销毁,而是会被另一个函数引用,这个引用了其他函数作用域变量的函数就形成了闭包。
闭包的形成条件
- 函数嵌套:在一个函数内部定义另一个函数。
- 内部函数引用外部函数的变量:内部函数使用了外部函数作用域中的变量。
代码示例理解闭包
示例一:基本闭包形式
function outer() {
let message = 'Hello, Closure!';
function inner() {
console.log(message);
}
return inner;
}
let closureFunction = outer();
closureFunction();
在这个例子中,outer
函数内部定义了 inner
函数,inner
函数引用了 outer
函数中的 message
变量。当调用 outer
函数时,它返回了 inner
函数,赋值给 closureFunction
。此时,尽管 outer
函数已经执行完毕,但由于 inner
函数(也就是 closureFunction
)仍然引用着 message
变量,所以 message
变量不会被销毁。当调用 closureFunction
时,它可以正常访问并输出 message
的值。
示例二:使用闭包实现计数器
function createCounter() {
let count = 0;
return {
increment: function () {
count++;
return count;
},
decrement: function () {
count--;
return count;
},
getCount: function () {
return count;
}
};
}
let counter = createCounter();
console.log(counter.getCount());
console.log(counter.increment());
console.log(counter.increment());
console.log(counter.decrement());
在 createCounter
函数中,定义了一个局部变量 count
,并返回一个包含三个方法的对象。这三个方法(increment
、decrement
、getCount
)都形成了闭包,因为它们都引用了 createCounter
函数作用域中的 count
变量。即使 createCounter
函数执行结束,count
变量也会一直存在于内存中,并且可以通过返回对象的方法进行操作。
闭包的作用
- 读取函数内部的变量:在函数外部可以通过闭包访问和操作函数内部的变量。
- 让变量的值始终保持在内存中:闭包可以让这些变量不被垃圾回收机制回收,持续存在于内存中。
- 实现数据的封装和隐藏:可以通过闭包创建私有变量和方法,外部无法直接访问这些变量和方法,只能通过闭包提供的接口进行操作。
闭包可能带来的问题
- 内存泄漏:由于闭包会持有对外部变量的引用,导致这些变量无法被垃圾回收。如果闭包使用不当,会使一些不再使用的变量一直占用内存,造成内存泄漏。
- 性能问题:闭包的创建和使用会带来一定的性能开销,尤其是在频繁创建闭包的情况下。
8. vue 开发中遇到过什么问题
在 Vue 开发过程中,开发者可能会遇到各种各样的问题,以下为你详细介绍常见问题及对应的解决办法:
1. 响应式问题
问题描述
- 对象属性添加或删除时不更新视图:Vue 的响应式系统是基于 Object.defineProperty() 实现的,当你在已经创建的实例上动态添加一个新的属性或者删除一个属性时,Vue 无法检测到这些变化,从而不会更新视图。
- 数组变异方法使用不当:Vue 对数组的一些方法进行了重写,如
push()
、pop()
等,但使用length
直接修改数组长度或者通过索引直接修改数组元素时,Vue 可能无法检测到变化。
解决办法
- 对象属性添加或删除:使用
Vue.set()
(Vue 2)或this.$set()
(组件内部)来添加响应式属性,使用Vue.delete()
或this.$delete()
来删除响应式属性。在 Vue 3 中,可以使用reactive
和toRefs
等新特性来处理响应式数据。
// Vue 2 示例
import Vue from 'vue';
const vm = new Vue({
data: {
user: {
name: 'John'
}
}
});
// 添加响应式属性
Vue.set(vm.user, 'age', 25);
// 删除响应式属性
Vue.delete(vm.user, 'name');
- 数组操作:尽量使用 Vue 重写过的数组方法来修改数组。如果需要通过索引修改数组元素,可以使用
Vue.set()
或this.$set()
。
// Vue 2 示例
this.$set(this.array, index, newValue);
2. 组件通信问题
问题描述
- 父子组件通信混乱:在复杂的组件结构中,父子组件之间的
props
传递和$emit
事件触发可能会变得混乱,导致数据流向不清晰。 - 跨级组件通信困难:当需要在祖孙组件之间进行通信时,使用
props
层层传递或者$emit
层层触发会使代码变得复杂且难以维护。
解决办法
- 父子组件通信:遵循单向数据流原则,确保
props
只用于父组件向子组件传递数据,$emit
用于子组件向父组件发送事件。可以使用v-bind
和v-on
语法糖来简化代码。
<!-- 父组件 -->
<template>
<ChildComponent :message="parentMessage" @childEvent="handleChildEvent" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
parentMessage: 'Hello from parent'
};
},
methods: {
handleChildEvent() {
console.log('Received event from child');
}
}
};
</script>
- 跨级组件通信:可以使用事件总线(Event Bus)、Vuex(状态管理库)或
provide
和inject
(Vue 2)、provide
和inject
配合reactive
(Vue 3)来实现跨级组件通信。
// 事件总线示例
// event-bus.js
import Vue from 'vue';
export const eventBus = new Vue();
// 发送事件的组件
import { eventBus } from './event-bus.js';
eventBus.$emit('message', 'Hello from sibling');
// 接收事件的组件
import { eventBus } from './event-bus.js';
export default {
created() {
eventBus.$on('message', (message) => {
console.log(message);
});
}
};
3. 生命周期钩子使用问题
问题描述
- 钩子函数执行顺序混乱:在复杂的组件嵌套和异步操作场景下,可能会出现生命周期钩子函数执行顺序不符合预期的情况,导致数据初始化、DOM 操作等出现问题。
- 钩子函数内异步操作处理不当:在生命周期钩子函数中进行异步操作时,如果没有正确处理回调和状态更新,可能会导致页面闪烁、数据不一致等问题。
解决办法
- 理解生命周期钩子顺序:深入理解 Vue 的生命周期钩子函数的执行顺序,根据业务需求选择合适的钩子函数进行操作。例如,在
created
钩子中进行数据初始化,在mounted
钩子中进行 DOM 操作。 - 异步操作处理:在生命周期钩子函数中进行异步操作时,可以使用
async/await
或Promise
来处理异步逻辑,确保数据更新和 DOM 渲染的一致性。
<template>
<div>{{ data }}</div>
</template>
<script>
export default {
data() {
return {
data: null
};
},
async created() {
try {
const response = await fetch('https://api.example.com/data');
this.data = await response.json();
} catch (error) {
console.error(error);
}
}
};
</script>
4. 路由问题
问题描述
- 路由跳转问题:在使用 Vue Router 进行路由跳转时,可能会遇到路由跳转失败、页面刷新、路由参数丢失等问题。
- 路由守卫使用不当:路由守卫可以用来控制路由的访问权限,但如果使用不当,可能会导致页面无法正常访问或者权限控制失效。
解决办法
- 路由跳转:确保路由配置正确,使用
router.push()
或router.replace()
进行路由跳转。如果需要传递参数,可以使用params
或query
。
// 路由跳转示例
this.$router.push({ name: 'Home', params: { id: 1 } });
- 路由守卫:根据业务需求合理使用全局守卫、路由独享守卫和组件内守卫。例如,在全局前置守卫中进行登录验证。
// 全局前置守卫示例
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth && !isAuthenticated()) {
next({ name: 'Login' });
} else {
next();
}
});
5. 性能问题
问题描述
- 组件渲染性能差:在处理大量数据或复杂组件时,可能会出现组件渲染缓慢、页面卡顿等性能问题。
- 频繁的响应式更新:如果响应式数据频繁变化,会导致 Vue 不断进行依赖收集和更新操作,影响性能。
解决办法
- 虚拟列表:对于大量数据的列表渲染,可以使用虚拟列表技术,只渲染当前可见区域的数据,减少 DOM 节点数量,提高渲染性能。
- 计算属性和监听器优化:合理使用计算属性和监听器,避免不必要的计算和监听。计算属性会缓存结果,只有在依赖项变化时才会重新计算。
<template>
<div>{{ fullName }}</div>
</template>
<script>
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
};
},
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
};
</script>
9. vue中的 dep 和 watcher 的依赖收集是什么阶段
在 Vue.js 里,Dep
(依赖)和 Watcher
(观察者)的依赖收集主要发生在两个关键阶段:初始化渲染阶段和数据更新阶段。下面详细介绍这两个阶段的依赖收集过程。
初始化渲染阶段
流程概述
当 Vue 实例创建并首次渲染时,会触发模板编译,在这个过程中会对模板中的表达式进行求值,从而触发 Dep
和 Watcher
的依赖收集。
详细步骤
- 创建
Watcher
对象:在 Vue 实例初始化时,会为每个需要响应式更新的地方(如模板中的表达式、计算属性等)创建一个Watcher
对象。以渲染Watcher
为例,它负责监听数据变化并更新 DOM。 - 进入依赖收集阶段:当
Watcher
对象创建后,会将自身设置为当前正在收集依赖的Watcher
,即Dep.target = watcher
。 - 访问响应式数据:在模板渲染过程中,会访问数据对象的属性。由于 Vue 的响应式原理是基于
Object.defineProperty()
或Proxy
实现的,当访问这些属性时,会触发其getter
方法。 - 依赖收集:在属性的
getter
方法中,会检查Dep.target
是否存在。如果存在,说明有Watcher
正在收集依赖,此时会将该Watcher
添加到当前属性对应的Dep
对象的subs
(订阅者列表)中,表示该Watcher
依赖于这个属性。同时,当前属性对应的Dep
对象也会被添加到Watcher
的deps
列表中。 - 完成依赖收集:当模板渲染完成后,会将
Dep.target
置为null
,表示本次依赖收集结束。
代码示例
// 简化的 Dep 类
class Dep {
constructor() {
this.subs = [];
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target);
Dep.target.addDep(this);
}
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
Dep.target = null;
// 简化的 Watcher 类
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.expOrFn = expOrFn;
this.cb = cb;
this.deps = [];
this.get();
}
get() {
Dep.target = this;
const value = this.vm[this.expOrFn];
Dep.target = null;
return value;
}
addDep(dep) {
this.deps.push(dep);
}
update() {
const value = this.get();
this.cb(value);
}
}
// 简化的 Vue 响应式数据
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
dep.depend();
return val;
},
set(newVal) {
if (val !== newVal) {
val = newVal;
dep.notify();
}
}
});
}
// 示例使用
const vm = {
message: 'Hello, Vue!'
};
defineReactive(vm, 'message', vm.message);
const updateDOM = (value) => {
console.log(`DOM updated with value: ${value}`);
};
const watcher = new Watcher(vm, 'message', updateDOM);
数据更新阶段
流程概述
当响应式数据发生变化时,会触发其 setter
方法,进而通知所有依赖于该数据的 Watcher
进行更新。
详细步骤
- 数据更新:当修改响应式数据的属性时,会触发该属性的
setter
方法。 - 通知依赖:在
setter
方法中,会调用该属性对应的Dep
对象的notify()
方法,该方法会遍历subs
列表,依次调用每个Watcher
的update()
方法。 Watcher
更新:Watcher
的update()
方法会重新计算表达式的值,并根据新值更新 DOM 或执行其他回调函数。
代码示例(基于上述示例继续)
// 修改数据
vm.message = 'New message';
在这个示例中,当修改 vm.message
的值时,会触发 message
属性的 setter
方法,进而调用 Dep
对象的 notify()
方法,通知 Watcher
进行更新。Watcher
会重新计算 message
的值,并调用 updateDOM
函数更新 DOM。
综上所述,Dep
和 Watcher
的依赖收集在初始化渲染阶段建立数据与 Watcher
之间的依赖关系,在数据更新阶段利用这些依赖关系实现响应式更新。
10. 什么是原型链
原型链是JavaScript中实现继承和对象属性查找的一种机制。以下是关于原型链的详细介绍:
原型的概念
在JavaScript中,每个对象都有一个原型(prototype
)。原型也是一个对象,它可以包含一些属性和方法。当访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript引擎就会去它的原型对象中查找。
原型链的形成
- 所有的对象都默认从
Object.prototype
继承属性和方法。例如,toString()
、valueOf()
等方法就是从Object.prototype
继承来的。 - 当创建一个函数时,JavaScript会自动为这个函数添加一个
prototype
属性,这个属性指向一个对象,称为该函数的原型对象。当使用构造函数创建一个新对象时,新对象的__proto__
属性(也称为原型链指针)会指向构造函数的原型对象。这样就形成了一条链,从新对象开始,通过__proto__
不断指向它的原型对象,直到Object.prototype
,这条链就是原型链。
原型链的作用
- 实现继承:通过原型链,一个对象可以继承另一个对象的属性和方法。例如,定义一个
Animal
构造函数,再定义一个Dog
构造函数,让Dog
的原型指向Animal
的实例,这样Dog
的实例就可以继承Animal
的属性和方法。 - 属性和方法的共享:多个对象可以共享原型对象上的属性和方法,节省内存空间。比如,所有数组对象都共享
Array.prototype
上的push()
、pop()
等方法。
示例代码
// 定义一个构造函数
function Person(name) {
this.name = name;
}
// 在构造函数的原型上添加方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
// 创建一个Person的实例
const person1 = new Person('John');
// 访问实例的属性和方法,先在实例本身查找,找不到就去原型上查找
person1.sayHello(); // 输出 "Hello, my name is John"
console.log(person1.__proto__ === Person.prototype); // 输出 true
在这个例子中,person1
是 Person
构造函数的实例,它的 __proto__
属性指向 Person.prototype
。当调用 person1.sayHello()
时,由于 person1
本身没有 sayHello
方法,JavaScript会沿着原型链在 Person.prototype
上找到该方法并执行。
11. react setState 是同步还是异步的
在 React 中,setState
的行为既可以表现为异步,也可以表现为同步,这取决于调用 setState
的场景。下面详细分析不同场景下 setState
的同步和异步特性。
异步场景
情况描述
在 React 的合成事件和生命周期函数中,setState
是异步执行的。这意味着调用 setState
后,并不会立即更新 state
的值,而是会将更新操作放入一个队列中,等到合适的时机(如批量更新)再统一处理。
示例代码
import React, { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出 0,因为 setState 是异步的,此时 state 还未更新
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
export default Counter;
原因解释
这种异步设计主要是为了性能优化,React 会将多次 setState
操作合并成一次更新,减少不必要的渲染,提高性能。例如,在一个事件处理函数中多次调用 setState
,React 会将这些更新操作批量处理,只进行一次重新渲染。
获取更新后的状态
如果需要在 setState
更新后执行某些操作,可以使用 setState
的第二个参数,它是一个回调函数,会在 state
更新完成后执行。
this.setState({ count: this.state.count + 1 }, () => {
console.log(this.state.count); // 输出更新后的 count 值
});
同步场景
情况描述
在原生事件、setTimeout
、setInterval
等异步回调函数中,setState
是同步执行的。这是因为这些场景脱离了 React 的合成事件系统,React 不会对其进行批量更新处理。
示例代码
import React, { Component } from 'react';
class SyncCounter extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出 1,因为 setState 是同步的,state 已经更新
}, 1000);
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
</div>
);
}
}
export default SyncCounter;
原因解释
在这些异步回调函数中,React 无法提前收集更新操作进行批量处理,所以会立即执行 setState
并更新 state
,表现为同步行为。
综上所述,setState
的同步和异步特性取决于调用它的上下文环境,理解这一点对于正确处理 state
更新和编写稳定的 React 应用非常重要。
在 React 18 中,setState
的基本行为和之前版本类似,但由于 React 18 引入了自动批量更新(Automatic Batching),使得 setState
的异步表现范围有所扩大,下面为你详细介绍:
异步情况
1. 合成事件和生命周期函数
和之前版本一样,在 React 的合成事件(如 onClick
、onChange
等)以及生命周期函数(在 React 18 里函数组件使用 Hooks 替代了部分生命周期函数)中,setState
依然是异步执行的。
import React, { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
// 输出 0,因为 setState 是异步的,此时 state 还未更新
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
export default Counter;
2. React 18 的自动批量更新
在 React 18 中,自动批量更新功能得到了增强。之前在原生事件、setTimeout
、setInterval
等异步回调函数中,setState
是同步执行的,但在 React 18 里,只要是在 React 的事件处理流程中(即使是嵌套的异步操作),setState
都会被批量处理,表现为异步。
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(c => c + 1);
setCount(c => c + 1);
console.log(count);
// 输出 0,因为 React 18 自动批量更新,state 还未更新
}, 0);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
export default App;
在上述代码中,虽然 setCount
是在 setTimeout
中调用的,但由于 React 18 的自动批量更新机制,这两个 setCount
操作会被批量处理,在 console.log(count)
执行时,count
的值还未更新。
同步情况
如果你确实需要同步更新 state
,可以使用 flushSync
函数(该函数在 ReactDOM 包中)。flushSync
会强制 React 立即应用更新并同步渲染。
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
function SyncApp() {
const [count, setCount] = useState(0);
const handleClick = () => {
flushSync(() => {
setCount(count + 1);
});
console.log(count);
// 输出更新后的值,因为使用 flushSync 强制同步更新了
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
export default SyncApp;
综上所述,React 18 中 setState
(在函数组件中是 setState
对应的 setX
函数)的异步特性范围更广,主要是因为自动批量更新机制的增强,而需要同步更新时可以借助 flushSync
函数。
12. react 17 和 18 有什么区别
React 18 是 React 框架的一个重要版本更新,与 React 17 相比,在多个方面存在显著区别,下面从新特性、性能优化、行为变化等维度为你详细介绍:
1. 新特性
自动批量更新(Automatic Batching)
- React 17:在 React 17 中,批量更新仅在 React 事件处理函数中生效。像在
setTimeout
、Promise
等异步回调中调用setState
或者useState
的更新函数时,不会进行批量处理,每次调用都会触发重新渲染。
// React 17 示例
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(c => c + 1);
setCount(c => c + 1);
// 会触发两次重新渲染
}, 0);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
export default App;
- React 18:React 18 引入了自动批量更新机制,无论在 React 事件处理函数、
setTimeout
、Promise
等异步回调中,只要是在同一个事件循环中,多次调用setState
或者useState
的更新函数都会进行批量处理,只触发一次重新渲染,从而提升性能。
// React 18 示例
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(c => c + 1);
setCount(c => c + 1);
// 只会触发一次重新渲染
}, 0);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
export default App;
新的根渲染 API
- React 17:使用
ReactDOM.render
方法来渲染应用的根组件。
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
- React 18:引入了新的
createRoot
API 来渲染根组件,ReactDOM.render
仍然可用,但已被标记为旧版 API。createRoot
支持新的并发特性。
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
并发特性
- React 17:不支持并发渲染特性,渲染过程是同步且阻塞的,在渲染大型组件树时可能会导致页面卡顿。
- React 18:引入了并发特性,如并发渲染、时间切片等。并发渲染允许 React 在渲染过程中暂停、继续或中断渲染任务,优先处理更紧急的任务,从而提升用户体验,尤其是在处理复杂 UI 时。
2. 性能优化
Suspense 改进
- React 17:
Suspense
主要用于处理异步数据加载,功能相对有限。 - React 18:
Suspense
得到了进一步改进,能够更好地与并发渲染配合,在组件树中可以更灵活地处理加载状态,并且可以在数据加载过程中进行部分渲染,避免长时间的白屏。
3. 行为变化
严格模式(Strict Mode)
- React 17:严格模式会在开发环境下对组件进行额外的检查,但不会影响组件的实际行为。
- React 18:在严格模式下,React 会对组件进行更严格的检查,并且会在开发环境下额外调用一次组件的
useEffect
清理函数和useLayoutEffect
清理函数,以帮助开发者发现副作用中的潜在问题。
4. 事件委托
React 17**:事件委托是绑定到 document
上的。
React 18**:事件委托默认绑定到根节点上,这有助于减少全局事件监听器的数量,提高性能和安全性。
13. react 18 有哪些新特性
React 18 引入了一系列重要的新特性,旨在提升开发体验、增强性能和支持并发渲染等,以下是对这些新特性的详细介绍:
1. 自动批量更新(Automatic Batching)
在 React 18 之前,批量更新仅在 React 事件处理函数中生效。而在 React 18 中,无论在 React 事件处理函数、setTimeout
、Promise
等异步回调中,只要是在同一个事件循环中,多次调用 setState
或者 useState
的更新函数都会进行批量处理,只触发一次重新渲染,从而提升性能。
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(c => c + 1);
setCount(c => c + 1);
// 在 React 18 中只会触发一次重新渲染
}, 0);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
export default App;
2. 新的根渲染 API
React 18 引入了 createRoot
作为新的根渲染 API,替代了旧的 ReactDOM.render
。createRoot
支持新的并发特性,并且与旧的渲染方式相比,提供了更好的性能和灵活性。
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
3. 并发特性
- 并发渲染(Concurrent Rendering):允许 React 在渲染过程中暂停、继续或中断渲染任务,优先处理更紧急的任务。例如,当用户与页面交互时,React 可以暂停当前的渲染任务,先响应用户操作,之后再继续完成渲染,从而提升用户体验。
- 时间切片(Time Slicing):将大型渲染任务分割成多个小任务,在多个帧中依次执行,避免长时间阻塞主线程,使得页面在渲染过程中仍然保持响应。
4. Suspense 改进
- 支持过渡效果:
Suspense
现在可以更好地与并发渲染配合,在组件树中可以更灵活地处理加载状态,并且可以在数据加载过程中进行部分渲染,避免长时间的白屏。还可以通过useTransition
或startTransition
来实现平滑的过渡效果。
import React, { Suspense, useState, useTransition } from 'react';
const SomeComponent = React.lazy(() => import('./SomeComponent'));
function App() {
const [isPending, startTransition] = useTransition();
const [showComponent, setShowComponent] = useState(false);
const handleClick = () => {
startTransition(() => {
setShowComponent(true);
});
};
return (
<div>
<button onClick={handleClick}>Load Component</button>
{isPending ? <p>Loading...</p> : null}
<Suspense fallback={<p>Loading...</p>}>
{showComponent && <SomeComponent />}
</Suspense>
</div>
);
}
export default App;
5. 新的 Hooks
- useId:用于生成全局唯一的 ID,适用于在服务端渲染(SSR)环境中生成唯一的 ID,避免客户端和服务端生成的 ID 不一致的问题。
import React, { useId } from 'react';
function MyComponent() {
const id = useId();
return <label htmlFor={id}>Input: <input id={id} /></label>;
}
- useSyncExternalStore:用于订阅外部数据源,确保在数据变化时组件能够正确更新。它是一个通用的 API,可用于订阅任何外部数据源,如浏览器的
localStorage
、WebSocket
等。 - useInsertionEffect:这是一个新的副作用钩子,它在 DOM 插入之前执行,主要用于插入 CSS 样式等操作,确保在渲染之前样式已经准备好。
6. 严格模式(Strict Mode)增强
在严格模式下,React 会对组件进行更严格的检查,并且会在开发环境下额外调用一次组件的 useEffect
清理函数和 useLayoutEffect
清理函数,以帮助开发者发现副作用中的潜在问题。
7. 事件委托变化
React 18 中事件委托默认绑定到根节点上,而不是像之前版本那样绑定到 document
上。这有助于减少全局事件监听器的数量,提高性能和安全性。
14. react 常用的 hooks
在 React 中,Hooks 是一种特殊的函数,它可以让你在不编写 class
的情况下使用 state
以及其他 React 特性。以下是一些常用的 React Hooks:
基础 Hooks
useState
- 功能:用于在函数组件中添加状态(
state
)。它返回一个包含两个元素的数组,第一个元素是当前状态的值,第二个元素是用于更新该状态的函数。 - 示例:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
useEffect
- 功能:用于处理副作用操作,比如数据获取、订阅、手动修改 DOM 等。它类似于
class
组件中的componentDidMount
、componentDidUpdate
和componentWillUnmount
生命周期方法的组合。 - 示例:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
return () => {
// 清理操作,比如取消订阅
};
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
useContext
- 功能:用于在组件树中共享数据,避免通过
props
层层传递数据。它接收一个上下文对象(React.createContext
的返回值)并返回该上下文的当前值。 - 示例:
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme === 'dark' ? 'black' : 'white' }}>
I am styled by theme context!
</button>
);
}
function App() {
return (
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
);
}
额外 Hooks
useReducer
- 功能:是
useState
的替代方案,适合处理复杂的状态逻辑,特别是当状态更新依赖于之前的状态时。它接收一个reducer
函数和初始状态,并返回当前状态以及一个dispatch
函数。 - 示例:
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
useCallback
- 功能:返回一个记忆化的回调函数,用于避免因函数重新创建而导致子组件不必要的重新渲染。它接收一个回调函数和一个依赖项数组,只有当依赖项发生变化时,才会返回一个新的函数。
- 示例:
import React, { useState, useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<Child onClick={handleClick} />
</div>
);
}
function Child({ onClick }) {
return <button onClick={onClick}>Click me</button>;
}
useMemo
- 功能:返回一个记忆化的值,用于避免重复计算。它接收一个计算函数和一个依赖项数组,只有当依赖项发生变化时,才会重新计算该值。
- 示例:
import React, { useState, useMemo } from 'react';
function App() {
const [count, setCount] = useState(0);
const expensiveValue = useMemo(() => {
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
return sum + count;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Expensive Value: {expensiveValue}</p>
</div>
);
}
useRef
- 功能:返回一个可变的
ref
对象,其.current
属性被初始化为传入的参数。主要用于获取 DOM 节点、保存一些不需要触发重新渲染的值等。 - 示例:
import React, { useRef } from 'react';
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
这些是 React 中常用的 Hooks,它们能帮助开发者更高效地编写函数组件,处理状态、副作用等各种场景。
15. useCallback 和 useMemo 的区别
在 React 中,useCallback
和 useMemo
都是用于优化性能的 Hooks,它们在功能和使用场景上有一些区别,下面为你详细介绍:
1. 基本语法
import React, { useCallback, useMemo } from 'react';
// useCallback 语法
const memoizedCallback = useCallback(
() => {
// 回调函数的逻辑
doSomething(a, b);
},
[a, b], // 依赖项数组
);
// useMemo 语法
const memoizedValue = useMemo(() => {
// 计算一个值
return computeExpensiveValue(a, b);
}, [a, b]); // 依赖项数组
2. 功能区别
useCallback
useCallback
用于缓存函数。它返回一个记忆化的回调函数,只有当依赖项数组中的值发生变化时,才会返回一个新的函数实例。- 主要用于优化子组件的性能,避免因为父组件重新渲染而导致传递给子组件的回调函数重新创建,从而避免子组件不必要的重新渲染。
useMemo
useMemo
用于缓存计算结果。它返回一个记忆化的值,只有当依赖项数组中的值发生变化时,才会重新计算这个值。- 主要用于优化计算密集型的操作,避免在每次组件渲染时都进行高开销的计算。
3. 使用场景区别
useCallback
- 当你将一个回调函数传递给一个依赖于引用相等性来避免不必要渲染的子组件时,使用
useCallback
。例如,当子组件使用React.memo
进行包裹时,父组件传递的回调函数如果没有使用useCallback
进行记忆化,即使子组件的props
没有变化,也可能会因为回调函数的引用发生变化而重新渲染。
- 当你将一个回调函数传递给一个依赖于引用相等性来避免不必要渲染的子组件时,使用
import React, { useCallback, useState } from 'react';
// 使用 React.memo 包裹子组件
const ChildComponent = React.memo(({ onClick }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
// 使用 useCallback 记忆化回调函数
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<ChildComponent onClick={handleClick} />
</div>
);
};
export default ParentComponent;
useMemo
- 当你有一个计算密集型的操作,并且这个操作在每次组件渲染时都可能会被重复执行时,使用
useMemo
。例如,计算一个大型数组的总和或过滤一个大型列表。
- 当你有一个计算密集型的操作,并且这个操作在每次组件渲染时都可能会被重复执行时,使用
import React, { useMemo, useState } from 'react';
const ParentComponent = () => {
const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
// 使用 useMemo 记忆化计算结果
const sum = useMemo(() => {
console.log('Calculating sum...');
return numbers.reduce((acc, num) => acc + num, 0);
}, [numbers]);
return (
<div>
<p>Sum: {sum}</p>
<button onClick={() => setNumbers([...numbers, numbers.length + 1])}>
Add number
</button>
</div>
);
};
export default ParentComponent;
4. 总结
useCallback
缓存的是函数,用于优化子组件的性能,避免不必要的重新渲染。useMemo
缓存的是计算结果,用于优化计算密集型的操作,避免重复计算。
虽然它们的功能有所不同,但核心都是通过记忆化来减少不必要的计算和渲染,从而提高 React 应用的性能。
16. memo 的用法
在 React 中,memo
是一个高阶组件,用于优化函数组件的性能,避免组件在 props
没有发生变化时进行不必要的重新渲染。以下是关于 memo
的详细用法:
基本语法
import React from 'react';
// 定义一个普通的函数组件
const MyComponent = (props) => {
return (
<div>{props.message}</div>
);
};
// 使用 React.memo 包裹组件
const MemoizedComponent = React.memo(MyComponent);
export default MemoizedComponent;
在上述代码中,React.memo
接收一个函数组件作为参数,并返回一个新的组件 MemoizedComponent
。这个新组件会对传入的 props
进行浅比较,如果 props
没有发生变化,组件将不会重新渲染。
浅比较机制
React.memo
默认使用浅比较来判断 props
是否发生变化。浅比较只会比较对象或数组的引用,而不会深入比较它们的属性或元素。例如:
import React, { useState } from 'react';
const ChildComponent = React.memo(({ data }) => {
console.log('ChildComponent rendered');
return <div>{data}</div>;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
const data = 'Some data';
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ChildComponent data={data} />
</div>
);
};
export default ParentComponent;
在这个例子中,每次点击按钮时,ParentComponent
会重新渲染,但由于 data
的引用没有发生变化,ChildComponent
不会重新渲染。
自定义比较函数
如果你需要更复杂的比较逻辑,可以为 React.memo
提供一个自定义的比较函数作为第二个参数。这个比较函数接收两个参数:prevProps
和 nextProps
,并返回一个布尔值,表示 props
是否相等。
import React from 'react';
const MyComponent = (props) => {
return (
<div>{props.message}</div>
);
};
const arePropsEqual = (prevProps, nextProps) => {
// 自定义比较逻辑
return prevProps.message === nextProps.message;
};
const MemoizedComponent = React.memo(MyComponent, arePropsEqual);
export default MemoizedComponent;
在上述代码中,arePropsEqual
函数会比较 prevProps.message
和 nextProps.message
是否相等。如果相等,组件将不会重新渲染。
注意事项
- 仅适用于函数组件:
React.memo
只能用于函数组件,对于类组件,可以使用shouldComponentUpdate
生命周期方法来实现类似的性能优化。 - 状态变化:
React.memo
只比较props
,不会阻止组件因为自身状态的变化而重新渲染。 - 浅比较的局限性:由于默认使用浅比较,对于嵌套对象或数组,即使其内部元素发生了变化,但引用没有改变,组件也不会重新渲染。在这种情况下,需要使用自定义比较函数或确保传递给组件的
props
引用发生变化。
17. useImperativeHandle 用法
useImperativeHandle
是 React 提供的一个 Hook,它主要用于自定义使用 ref
时暴露给父组件的实例值。通常在使用 forwardRef
把 ref
从父组件传递到子组件后,使用 useImperativeHandle
可以更精细地控制子组件向父组件暴露的内容。下面详细介绍其用法。
基本语法
import React, { useImperativeHandle, forwardRef } from 'react';
const ChildComponent = forwardRef((props, ref) => {
// 定义一些内部状态或方法
const [count, setCount] = React.useState(0);
const increment = () => setCount(count + 1);
// 使用 useImperativeHandle 自定义暴露给父组件的实例值
useImperativeHandle(ref, () => ({
// 这里定义暴露给父组件的属性和方法
increment: increment,
getCount: () => count
}));
return <div>Child Component Count: {count}</div>;
});
const ParentComponent = () => {
const childRef = React.createRef();
const handleClick = () => {
// 通过 ref 调用子组件暴露的方法
childRef.current.increment();
console.log('Current count:', childRef.current.getCount());
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleClick}>Increment Child Count</button>
</div>
);
};
export default ParentComponent;
代码解释
forwardRef
包裹子组件:forwardRef
是 React 提供的一个高阶组件,用于将ref
从父组件传递到子组件。在上述代码中,ChildComponent
被forwardRef
包裹,这样它就可以接收ref
作为第二个参数。useImperativeHandle
的使用:- 它接收三个参数,第一个参数是
ref
,也就是从父组件传递过来的ref
;第二个参数是一个函数,该函数返回一个对象,这个对象就是要暴露给父组件的实例值;第三个参数是可选的依赖项数组,类似于useEffect
的依赖项数组,当依赖项发生变化时,useImperativeHandle
会重新执行。 - 在上述例子中,
useImperativeHandle
暴露了increment
方法和getCount
方法给父组件。
- 它接收三个参数,第一个参数是
- 父组件使用
ref
调用子组件方法:在ParentComponent
中,使用React.createRef()
创建了一个ref
对象childRef
,并将其传递给ChildComponent
。当点击按钮时,通过childRef.current
调用子组件暴露的increment
方法和getCount
方法。
使用场景
- 访问子组件的 DOM 节点:当需要在父组件中直接操作子组件的 DOM 节点时,可以使用
useImperativeHandle
暴露一些与 DOM 操作相关的方法,如滚动、聚焦等。 - 触发子组件的特定行为:父组件需要触发子组件内部的一些特定行为,而这些行为又不适合通过
props
来传递时,可以使用useImperativeHandle
暴露相应的方法。
注意事项
- 谨慎使用:使用
ref
进行父子组件通信会破坏组件之间的封装性,使得组件之间的耦合度增加,因此应该谨慎使用,优先考虑通过props
和state
进行数据传递。 ref
的更新:当useImperativeHandle
的依赖项发生变化时,暴露给父组件的实例值会更新。但要注意避免不必要的更新,以免影响性能。
18. 浏览器输入 url 发生了什么
当在浏览器中输入 URL 并按下回车键后,会发生一系列复杂的过程,主要包括以下几个阶段:
域名解析
- 浏览器首先检查自身的缓存,看是否有该域名对应的 IP 地址。如果缓存中存在,则直接使用该 IP 地址进行连接;如果缓存中没有,则向操作系统的域名系统(DNS)缓存发起查询。若操作系统的 DNS 缓存也没有相关记录,就会向本地配置的 DNS 服务器发送 DNS 查询请求。DNS 服务器会根据域名查找对应的 IP 地址,并将结果返回给浏览器。
建立连接
- 浏览器与目标服务器通过 TCP 协议建立连接。这个过程通常需要经过三次握手:首先,浏览器向服务器发送一个 SYN 数据包,请求建立连接;服务器收到后,返回一个 SYN - ACK 数据包,表示同意建立连接;最后,浏览器再发送一个 ACK 数据包,完成连接的建立。
发送请求
- 连接建立后,浏览器会根据用户输入的 URL 和相关操作,构建 HTTP 请求消息。请求消息包含请求方法(如 GET、POST 等)、请求头字段(如 User - Agent、Accept 等)以及请求体(如果有)等信息。然后,浏览器将请求消息发送给服务器。
服务器处理请求
- 服务器接收到浏览器发送的请求后,会根据请求的内容进行相应的处理。服务器可能会查询数据库、执行脚本、处理业务逻辑等,以生成响应数据。
发送响应
- 服务器处理完请求后,会构建 HTTP 响应消息。响应消息包含状态码(如 200 OK、404 Not Found 等)、响应头字段(如 Content - Type、Content - Length 等)以及响应体(即请求的资源内容,如 HTML 页面、图片、视频等)。服务器将响应消息发送回浏览器。
浏览器渲染页面
- 浏览器接收到响应消息后,会根据响应的内容进行渲染。如果响应的是 HTML 页面,浏览器会首先解析 HTML 代码,构建文档对象模型(DOM)。然后,根据 CSS 样式信息,构建渲染树(Render Tree)。渲染树确定了页面上每个元素的样式和位置。接着,浏览器会根据渲染树进行布局计算,确定每个元素在页面上的实际位置和大小。最后,浏览器会将渲染树绘制到屏幕上,呈现出用户看到的页面。在渲染过程中,浏览器还可能会根据需要加载其他资源,如图片、脚本、样式表等。
连接关闭
- 当页面加载完成或者用户与页面的交互结束后,浏览器与服务器之间的连接可能会被关闭。这可以通过 HTTP 协议中的连接头字段来控制,例如设置
Connection: close
表示连接完成后立即关闭,或者设置Connection: keep - alive
表示保持连接一段时间,以便后续的请求可以复用该连接,提高性能。
19. 事件循环
事件循环(Event Loop)是 JavaScript 实现异步编程的核心机制,它使得 JavaScript 可以在单线程的情况下处理异步操作,广泛应用于浏览器环境和 Node.js 环境。下面从原理、浏览器环境、Node.js 环境等方面为你详细介绍事件循环。
基本概念
JavaScript 是单线程的,这意味着同一时间只能执行一个任务。为了处理异步操作(如网络请求、定时器等),JavaScript 引入了事件循环机制。事件循环的核心工作是不断地从任务队列中取出任务并执行,以此实现异步操作的有序执行。
相关概念
- 调用栈(Call Stack):是一个后进先出(LIFO)的数据结构,用于存储函数调用的信息。当调用一个函数时,会将该函数的执行上下文压入调用栈;函数执行完毕后,其执行上下文从调用栈中弹出。
- 任务队列(Task Queue):也称为消息队列,用于存储异步任务完成后要执行的回调函数。任务队列分为宏任务队列(MacroTask Queue)和微任务队列(MicroTask Queue)。
- 宏任务(MacroTask):常见的宏任务包括
setTimeout
、setInterval
、setImmediate
(Node.js 环境)、I/O 操作
、UI 渲染
等。 - 微任务(MicroTask):常见的微任务包括
Promise.then
、MutationObserver
、process.nextTick
(Node.js 环境)等。
- 宏任务(MacroTask):常见的宏任务包括
浏览器环境中的事件循环
其工作流程如下:
- 执行同步代码:JavaScript 引擎首先会执行全局作用域中的同步代码,将同步函数依次压入调用栈并执行,执行完毕后弹出调用栈。
- 处理微任务队列:当调用栈为空时,JavaScript 引擎会检查微任务队列。如果微任务队列中有任务,会依次将微任务从队列中取出并压入调用栈执行,直到微任务队列为空。
- 处理宏任务队列:微任务队列处理完毕后,JavaScript 引擎会从宏任务队列中取出一个宏任务,将其压入调用栈执行。执行完一个宏任务后,会再次检查微任务队列,重复步骤 2。
- 循环执行:不断重复步骤 2 和步骤 3,形成一个循环,这就是事件循环。
以下是一个简单的示例代码:
console.log('1. 同步代码开始');
// 宏任务
setTimeout(() => {
console.log('4. setTimeout 回调函数执行');
}, 0);
// 微任务
Promise.resolve().then(() => {
console.log('3. Promise.then 回调函数执行');
});
console.log('2. 同步代码结束');
上述代码的执行顺序为:
1. 同步代码开始
2. 同步代码结束
3. Promise.then 回调函数执行
4. setTimeout 回调函数执行
Node.js 环境中的事件循环
Node.js 的事件循环与浏览器环境有所不同,它有六个阶段:
- timers(定时器阶段):执行
setTimeout
和setInterval
的回调函数。 - pending callbacks(待定回调阶段):执行一些系统操作的回调函数,如 TCP 连接错误等。
- idle, prepare(闲置、准备阶段):仅供内部使用。
- poll(轮询阶段):检索新的 I/O 事件,执行 I/O 相关的回调函数。如果没有其他任务,会在此阶段等待。
- check(检查阶段):执行
setImmediate
的回调函数。 - close callbacks(关闭回调阶段):执行一些关闭操作的回调函数,如
socket.on('close', ...)
。
Node.js 事件循环的执行流程如下:
- 进入事件循环,首先执行同步代码。
- 同步代码执行完毕后,依次进入各个阶段,处理相应的任务。
- 在每个阶段结束时,会检查微任务队列,将微任务依次取出并执行,直到微任务队列为空。
- 不断循环执行各个阶段,直到所有任务处理完毕。
以下是一个 Node.js 环境下的示例代码:
console.log('1. 同步代码开始');
// 宏任务:setImmediate
setImmediate(() => {
console.log('4. setImmediate 回调函数执行');
});
// 宏任务:setTimeout
setTimeout(() => {
console.log('3. setTimeout 回调函数执行');
}, 0);
// 微任务
process.nextTick(() => {
console.log('2. process.nextTick 回调函数执行');
});
console.log('5. 同步代码结束');
在 Node.js 环境中,由于 setTimeout
和 setImmediate
的执行顺序取决于代码的执行环境和事件循环的时机,所以上述代码的执行顺序可能会有所不同。但 process.nextTick
作为微任务,会在当前阶段结束时立即执行。
20. script 标签的 defer 和 async 的区别
在 HTML 中,<script>
标签用于引入外部脚本文件或直接嵌入 JavaScript 代码。defer
和 async
是 <script>
标签的两个属性,它们主要用于控制脚本的加载和执行时机,下面详细介绍它们的区别。
加载与执行机制
无 defer
和 async
属性
当 <script>
标签没有 defer
或 async
属性时,浏览器会按照文档的顺序依次解析 HTML,当遇到 <script>
标签时,会立即停止 HTML 解析,开始下载并执行脚本,执行完毕后再继续解析 HTML。这种方式可能会导致页面渲染延迟,特别是当脚本文件较大或网络状况不佳时。
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Without defer or async</title>
<!-- 遇到此脚本会立即下载并执行,阻塞 HTML 解析 -->
<script src="script.js"></script>
</head>
<body>
<p>这是一个段落。</p>
</body>
</html>
defer
属性
- 加载时机:带有
defer
属性的<script>
标签会在 HTML 解析的同时异步下载脚本文件,但不会立即执行。 - 执行时机:脚本会在 HTML 解析完成后,
DOMContentLoaded
事件触发之前按照脚本在文档中出现的顺序依次执行。 - 适用场景:适用于需要在文档解析完成后执行,但又不希望阻塞 HTML 解析的脚本,比如操作 DOM 的脚本。
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>With defer</title>
<!-- 异步下载,HTML 解析完成后按顺序执行 -->
<script src="script1.js" defer></script>
<script src="script2.js" defer></script>
</head>
<body>
<p>这是一个段落。</p>
</body>
</html>
async
属性
- 加载时机:带有
async
属性的<script>
标签会在 HTML 解析的同时异步下载脚本文件。 - 执行时机:脚本下载完成后会立即执行,执行时会阻塞 HTML 解析,并且脚本的执行顺序不保证与文档中出现的顺序一致,取决于脚本的下载完成时间。
- 适用场景:适用于独立的、不依赖于文档中其他脚本和 DOM 结构的脚本,比如第三方统计脚本、广告脚本等。
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>With async</title>
<!-- 异步下载,下载完成后立即执行,不保证顺序 -->
<script src="script1.js" async></script>
<script src="script2.js" async></script>
</head>
<body>
<p>这是一个段落。</p>
</body>
</html>
总结
属性 | 加载方式 | 执行时机 | 执行顺序 | 是否阻塞 HTML 解析 | 适用场景 |
---|---|---|---|---|---|
无 defer 和 async | 同步 | 下载完成后立即执行 | 按文档顺序 | 是 | 对执行顺序要求严格且依赖文档结构的脚本 |
defer | 异步 | HTML 解析完成后,DOMContentLoaded 事件触发之前 | 按文档顺序 | 否 | 需要在文档解析完成后执行,且对顺序有要求的脚本 |
async | 异步 | 下载完成后立即执行 | 不保证顺序 | 执行时阻塞 | 独立的、不依赖文档结构和其他脚本的脚本 |
21. promise 的 then 和 catch 的区别
在 JavaScript 中,Promise
是一种用于处理异步操作的对象,它有三种状态:pending
(进行中)、fulfilled
(已成功)和 rejected
(已失败)。then
和 catch
是 Promise
对象提供的两个重要方法,它们在功能和使用场景上存在明显区别,下面为你详细介绍。
语法
// then 方法语法
promise.then(onFulfilled, onRejected);
// catch 方法语法
promise.catch(onRejected);
其中,onFulfilled
是 Promise
成功时执行的回调函数,onRejected
是 Promise
失败时执行的回调函数。
功能区别
then
方法
- 处理成功结果:
then
方法的主要作用是处理Promise
成功(状态变为fulfilled
)时的结果。它接收一个回调函数作为第一个参数,当Promise
成功时,这个回调函数会被调用,并且会将Promise
的成功结果作为参数传递给它。 - 处理失败结果(可选):
then
方法还可以接收第二个可选的回调函数,用于处理Promise
失败(状态变为rejected
)时的情况。不过,通常更推荐使用catch
方法来专门处理失败情况,这样代码的可读性会更好。
catch
方法
- 专门处理失败结果:
catch
方法是then(null, onRejected)
的语法糖,它只接收一个回调函数作为参数,用于处理Promise
失败时的情况。当Promise
的状态变为rejected
时,catch
方法中的回调函数会被调用,并且会将Promise
的错误原因作为参数传递给它。
使用场景区别
then
方法
- 链式调用处理多个异步操作:
then
方法可以进行链式调用,用于依次处理多个异步操作。每个then
方法都会返回一个新的Promise
对象,这样可以方便地将多个异步操作串联起来,实现复杂的异步流程控制。
function asyncOperation1() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Result of asyncOperation1');
}, 1000);
});
}
function asyncOperation2(result) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(result + ' -> Result of asyncOperation2');
}, 1000);
});
}
asyncOperation1()
.then((result1) => {
console.log(result1);
return asyncOperation2(result1);
})
.then((result2) => {
console.log(result2);
});
- 处理成功结果并进行后续操作:当需要对
Promise
的成功结果进行进一步处理时,使用then
方法。例如,对成功返回的数据进行格式化、展示等操作。
catch
方法
- 统一处理错误:在链式调用中,
catch
方法通常放在最后,用于统一处理整个链式操作中可能出现的任何错误。这样可以避免在每个then
方法中都编写错误处理逻辑,提高代码的可维护性。
function asyncOperation() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Something went wrong'));
}, 1000);
});
}
asyncOperation()
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error('Error:', error.message);
});
- 处理单个
Promise
的失败情况:当只关注一个Promise
的失败情况时,使用catch
方法可以使代码更加清晰。
总结
then
方法主要用于处理Promise
的成功结果,也可以处理失败结果,但通常更适合用于链式调用和对成功结果进行后续操作。catch
方法专门用于处理Promise
的失败情况,通常放在链式调用的最后,用于统一捕获和处理错误。
22. promise 的 all race finally 的区别
在 JavaScript 中,Promise.all
、Promise.race
和 Promise.prototype.finally
都是用于处理 Promise
的重要方法,它们的功能和使用场景有所不同,下面为你详细介绍它们的区别。
1. Promise.all
- 功能:
Promise.all
方法接收一个可迭代对象(通常是数组)作为参数,该可迭代对象中的每个元素都是一个Promise
。它会返回一个新的Promise
,这个新Promise
会在所有输入的Promise
都成功解决(resolved)时才会解决,解决的值是一个包含所有输入Promise
解决值的数组,且数组中元素的顺序与输入的Promise
顺序一致。如果其中任何一个Promise
被拒绝(rejected),则新Promise
会立即被拒绝,并携带第一个被拒绝的Promise
的错误信息。 - 示例代码:
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); // 输出: [1, 2, 3]
})
.catch((error) => {
console.error(error);
});
const promise4 = Promise.resolve(4);
const promise5 = Promise.reject(new Error('Error in promise5'));
const promise6 = Promise.resolve(6);
Promise.all([promise4, promise5, promise6])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.error(error.message); // 输出: Error in promise5
});
2. Promise.race
- 功能:
Promise.race
方法同样接收一个可迭代对象(通常是数组)作为参数,该可迭代对象中的每个元素都是一个Promise
。它会返回一个新的Promise
,这个新Promise
会在输入的Promise
中第一个解决(resolved)或拒绝(rejected)时就立即解决或拒绝,其解决值或拒绝原因就是第一个完成的Promise
的解决值或拒绝原因。 - 示例代码:
const promise7 = new Promise((resolve) => {
setTimeout(() => {
resolve('Promise 7 resolved');
}, 200);
});
const promise8 = new Promise((resolve) => {
setTimeout(() => {
resolve('Promise 8 resolved');
}, 100);
});
Promise.race([promise7, promise8])
.then((value) => {
console.log(value); // 输出: Promise 8 resolved
})
.catch((error) => {
console.error(error);
});
const promise9 = new Promise((reject) => {
setTimeout(() => {
reject(new Error('Error in promise9'));
}, 150);
});
const promise10 = new Promise((resolve) => {
setTimeout(() => {
resolve('Promise 10 resolved');
}, 250);
});
Promise.race([promise9, promise10])
.then((value) => {
console.log(value);
})
.catch((error) => {
console.error(error.message); // 输出: Error in promise9
});
3. Promise.prototype.finally
- 功能:
finally
是Promise
实例的方法,它接收一个回调函数作为参数。无论Promise
最终是解决(resolved)还是拒绝(rejected),这个回调函数都会被执行。finally
方法返回一个新的Promise
,其状态和解决值/拒绝原因与原Promise
相同。 - 示例代码:
const promise11 = new Promise((resolve) => {
setTimeout(() => {
resolve('Promise 11 resolved');
}, 200);
});
promise11
.then((value) => {
console.log(value); // 输出: Promise 11 resolved
})
.finally(() => {
console.log('This will be executed whether the promise is resolved or rejected');
});
const promise12 = new Promise((reject) => {
setTimeout(() => {
reject(new Error('Error in promise12'));
}, 200);
});
promise12
.catch((error) => {
console.error(error.message); // 输出: Error in promise12
})
.finally(() => {
console.log('This will be executed whether the promise is resolved or rejected');
});
总结
Promise.all
用于并行处理多个Promise
,并等待所有Promise
都成功解决。Promise.race
用于并行处理多个Promise
,只要有一个Promise
解决或拒绝就立即返回结果。Promise.prototype.finally
用于在Promise
完成(解决或拒绝)后执行一些清理或后续操作,无论Promise
的最终状态如何。
23. try catch 是同步还是异步的
try...catch
本身是同步的语法结构,但它在处理同步代码和异步代码时的表现有所不同,下面为你详细介绍:
处理同步代码
try...catch
能够很好地捕获同步代码中抛出的异常。当 try
块中的同步代码抛出异常时,程序会立即跳转到 catch
块中执行,并且可以在 catch
块中处理该异常。
示例代码:
try {
// 同步代码,故意抛出异常
throw new Error('这是一个同步异常');
} catch (error) {
console.error('捕获到同步异常:', error.message);
}
代码解释:在上述代码中,try
块里抛出了一个同步异常,catch
块会立即捕获该异常并输出错误信息,整个过程是同步执行的。
处理异步代码
- 无法直接捕获异步操作中的异常:对于使用回调函数或
Promise
进行的异步操作,try...catch
不能直接捕获其中抛出的异常。因为异步操作会在主线程执行完毕后,在事件循环中执行,而try...catch
语句在同步代码执行时就已经完成了,所以无法捕获后续异步操作中的异常。
示例代码:
try {
setTimeout(() => {
// 异步操作中抛出异常
throw new Error('这是一个异步异常');
}, 1000);
} catch (error) {
console.error('捕获到异常:', error.message);
}
代码解释:在这个例子中,try...catch
无法捕获 setTimeout
回调函数中抛出的异常,因为 setTimeout
的回调是异步执行的,当异常抛出时,try...catch
语句已经执行完毕。
- 处理
Promise
异步异常:对于Promise
异步操作,需要使用.catch()
方法来捕获异常,而不是try...catch
。不过,如果使用async/await
语法,就可以使用try...catch
来捕获Promise
中的异常,因为async/await
让异步代码看起来更像同步代码。
使用 .catch()
捕获 Promise
异常示例:
const asyncOperation = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Promise 异步异常'));
}, 1000);
});
asyncOperation
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error('捕获到 Promise 异常:', error.message);
});
使用 async/await
和 try...catch
捕获 Promise
异常示例:
async function asyncFunction() {
try {
const asyncOperation = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Promise 异步异常'));
}, 1000);
});
await asyncOperation;
} catch (error) {
console.error('使用 async/await 和 try...catch 捕获到异常:', error.message);
}
}
asyncFunction();
总结
try...catch
本身是同步的语法结构,它可以直接捕获同步代码中的异常,但对于异步代码,需要根据具体情况采用不同的异常处理方式。对于使用回调函数的异步操作,try...catch
无法直接捕获异常;对于 Promise
异步操作,通常使用 .catch()
方法,使用 async/await
时可以使用 try...catch
来捕获异常。
24. instanceof 的原理
在 JavaScript 中,instanceof
是一个二元运算符,用于检查一个对象是否是某个构造函数的实例,或者是否是该构造函数原型链上的某个对象的实例。下面详细介绍其原理。
基本语法
object instanceof constructor
其中,object
是要检查的对象,constructor
是构造函数。该运算符返回一个布尔值,如果 object
是 constructor
的实例,则返回 true
,否则返回 false
。
实现原理
instanceof
的原理是通过检查对象的原型链来判断该对象是否是某个构造函数的实例。具体步骤如下:
- 获取
object
的原型(即object.__proto__
)。 - 获取
constructor
的原型(即constructor.prototype
)。 - 比较
object
的原型是否等于constructor
的原型,如果相等,则返回true
。 - 如果不相等,则继续检查
object
的原型的原型(即object.__proto__.__proto__
),直到找到相等的原型或者到达原型链的末尾(即object.__proto__
为null
)。
示例代码及解释
function Person(name) {
this.name = name;
}
const person = new Person('John');
// 使用 instanceof 检查
console.log(person instanceof Person); // 输出: true
// 手动模拟 instanceof 的实现
function myInstanceOf(object, constructor) {
// 获取构造函数的原型
const prototype = constructor.prototype;
// 获取对象的原型
let objProto = object.__proto__;
while (objProto) {
if (objProto === prototype) {
return true;
}
// 继续检查原型链的上一层
objProto = objProto.__proto__;
}
return false;
}
console.log(myInstanceOf(person, Person)); // 输出: true
在上述代码中,首先定义了一个构造函数 Person
,并创建了一个 Person
的实例 person
。然后使用 instanceof
运算符检查 person
是否是 Person
的实例,结果为 true
。接着,手动实现了一个 myInstanceOf
函数,该函数的逻辑与 instanceof
运算符的原理相同,通过不断比较对象的原型链和构造函数的原型,最终判断对象是否是构造函数的实例。
注意事项
instanceof
只能用于检查对象是否是某个构造函数的实例,对于基本数据类型(如number
、string
、boolean
等),instanceof
通常不会返回预期的结果,因为基本数据类型不是对象。
const num = 10;
console.log(num instanceof Number); // 输出: false
- 如果
constructor
不是一个函数,instanceof
会抛出一个TypeError
异常。
综上所述,instanceof
运算符的核心原理是通过遍历对象的原型链,检查是否存在与构造函数原型相等的原型,从而判断对象是否是该构造函数的实例。
25. 如何实现继承
在 JavaScript 中,实现继承有多种方式,下面为你详细介绍几种常见的继承实现方法:
1. 原型链继承
原理:通过让子类的原型指向父类的实例,从而实现子类继承父类的属性和方法。
示例代码:
// 父类构造函数
function Parent() {
this.name = 'parent';
this.colors = ['red', 'blue', 'green'];
}
// 父类方法
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类构造函数
function Child() {}
// 子类的原型指向父类的实例
Child.prototype = new Parent();
// 创建子类实例
const child1 = new Child();
const child2 = new Child();
child1.colors.push('yellow');
console.log(child1.colors); // 输出: ['red', 'blue', 'green', 'yellow']
console.log(child2.colors); // 输出: ['red', 'blue', 'green', 'yellow']
child1.sayName(); // 输出: parent
缺点:
- 多个子类实例会共享父类实例的引用类型属性,一个子类实例修改该属性会影响其他子类实例。
- 创建子类实例时,无法向父类构造函数传递参数。
2. 构造函数继承
原理:在子类构造函数中通过 call()
、apply()
或 bind()
方法调用父类构造函数,将父类的属性和方法复制到子类实例中。
示例代码:
// 父类构造函数
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
// 子类构造函数
function Child(name) {
Parent.call(this, name);
}
// 创建子类实例
const child1 = new Child('child1');
const child2 = new Child('child2');
child1.colors.push('yellow');
console.log(child1.colors); // 输出: ['red', 'blue', 'green', 'yellow']
console.log(child2.colors); // 输出: ['red', 'blue', 'green']
console.log(child1.name); // 输出: child1
缺点:
- 只能继承父类构造函数中的属性和方法,无法继承父类原型上的属性和方法。
- 每个子类实例都会复制一份父类的属性和方法,造成内存浪费。
3. 组合继承
原理:结合了原型链继承和构造函数继承的优点,通过原型链继承父类原型上的属性和方法,通过构造函数继承父类构造函数中的属性和方法。
示例代码:
// 父类构造函数
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
// 父类方法
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类构造函数
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// 子类的原型指向父类的实例
Child.prototype = new Parent();
// 修复子类原型的 constructor 属性
Child.prototype.constructor = Child;
// 创建子类实例
const child1 = new Child('child1', 18);
const child2 = new Child('child2', 20);
child1.colors.push('yellow');
console.log(child1.colors); // 输出: ['red', 'blue', 'green', 'yellow']
console.log(child2.colors); // 输出: ['red', 'blue', 'green']
child1.sayName(); // 输出: child1
缺点:父类构造函数会被调用两次,一次是在创建子类原型时,一次是在子类构造函数中。
4. 寄生组合继承
原理:在组合继承的基础上进行优化,通过 Object.create()
方法创建一个以父类原型为原型的新对象,作为子类的原型,避免了父类构造函数的重复调用。
示例代码:
// 父类构造函数
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
// 父类方法
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类构造函数
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// 寄生组合继承的核心代码
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 实现继承
inheritPrototype(Child, Parent);
// 创建子类实例
const child1 = new Child('child1', 18);
const child2 = new Child('child2', 20);
child1.colors.push('yellow');
console.log(child1.colors); // 输出: ['red', 'blue', 'green', 'yellow']
console.log(child2.colors); // 输出: ['red', 'blue', 'green']
child1.sayName(); // 输出: child1
5. class
和 extends
(ES6 语法)
原理:ES6 引入了 class
关键字和 extends
关键字,提供了更简洁的语法来实现继承。
示例代码:
// 父类
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
sayName() {
console.log(this.name);
}
}
// 子类
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
// 创建子类实例
const child1 = new Child('child1', 18);
const child2 = new Child('child2', 20);
child1.colors.push('yellow');
console.log(child1.colors); // 输出: ['red', 'blue', 'green', 'yellow']
console.log(child2.colors); // 输出: ['red', 'blue', 'green']
child1.sayName(); // 输出: child1
以上就是 JavaScript 中常见的几种继承实现方式,每种方式都有其优缺点,在实际开发中可以根据具体需求选择合适的继承方式。
26. 使用 useHooks 实现一个防抖
在 React 中,你可以使用自定义 Hook 来实现防抖功能。防抖是一种限制函数调用频率的技术,它会在用户停止触发事件一段时间后才执行函数,避免函数在短时间内被频繁调用。以下是使用自定义 Hook 实现防抖的详细步骤和示例代码:
实现思路
- 创建自定义 Hook:创建一个名为
useDebounce
的自定义 Hook,该 Hook 接收两个参数:要防抖的函数和防抖的延迟时间。 - 使用
useRef
保存定时器:使用useRef
来保存定时器的 ID,以便在后续的渲染中可以清除定时器。 - 使用
useCallback
封装防抖函数:使用useCallback
来创建一个防抖函数,该函数会在用户停止触发事件一段时间后才执行传入的函数。
示例代码
import React, { useRef, useCallback } from 'react';
// 自定义 Hook 实现防抖
function useDebounce(callback, delay) {
// 使用 useRef 保存定时器的 ID
const timerRef = useRef(null);
// 使用 useCallback 封装防抖函数
const debouncedCallback = useCallback((...args) => {
// 如果定时器已经存在,则清除它
if (timerRef.current) {
clearTimeout(timerRef.current);
}
// 设置新的定时器
timerRef.current = setTimeout(() => {
// 执行传入的回调函数
callback(...args);
// 执行完函数后将定时器 ID 置为 null
timerRef.current = null;
}, delay);
}, [callback, delay]);
return debouncedCallback;
}
// 示例组件
function App() {
const handleInputChange = (event) => {
console.log('Input value:', event.target.value);
};
// 使用自定义 Hook 实现防抖
const debouncedHandleInputChange = useDebounce(handleInputChange, 500);
return (
<div>
<input
type="text"
placeholder="Type something..."
onChange={debouncedHandleInputChange}
/>
</div>
);
}
export default App;
代码解释
-
useDebounce
自定义 Hook:timerRef
:使用useRef
创建一个可变的 ref 对象,用于保存定时器的 ID。debouncedCallback
:使用useCallback
创建一个防抖函数。在该函数内部,首先检查timerRef.current
是否存在,如果存在则清除定时器。然后设置一个新的定时器,在延迟时间后执行传入的回调函数,并将timerRef.current
置为null
。- 返回
debouncedCallback
作为防抖后的函数。
-
App
组件:handleInputChange
:定义一个处理输入框变化的函数。debouncedHandleInputChange
:使用useDebounce
自定义 Hook 对handleInputChange
函数进行防抖处理,延迟时间为 500 毫秒。- 将
debouncedHandleInputChange
作为onChange
事件的处理函数绑定到输入框上。
注意事项
- 由于
useDebounce
使用了useCallback
,所以传入的callback
和delay
发生变化时,才会重新创建防抖函数。 - 防抖函数会在用户停止触发事件
delay
毫秒后才执行,避免了函数的频繁调用。
通过以上步骤,你就可以使用自定义 Hook 实现防抖功能。
27. async await 的原理
async/await
是 ES2017 引入的用于处理异步操作的语法糖,它建立在 Promise
的基础之上,目的是让异步代码在写法上更接近同步代码,提高代码的可读性和可维护性。下面详细介绍其原理。
基本语法
async
用于声明一个异步函数,该函数会返回一个 Promise
对象。await
只能在 async
函数内部使用,它会暂停 async
函数的执行,等待 Promise
被解决(resolved)或拒绝(rejected),并返回 Promise
的解决值。
async function asyncFunction() {
const result = await new Promise((resolve) => {
setTimeout(() => {
resolve('异步操作完成');
}, 1000);
});
console.log(result);
}
asyncFunction();
原理剖析
1. async
函数的返回值
async
函数总是返回一个 Promise
对象。如果函数内部直接返回一个值,该值会被包装在一个已解决的 Promise
中;如果函数内部抛出异常,该异常会被包装在一个已拒绝的 Promise
中。
async function returnValue() {
return '直接返回的值';
}
async function throwError() {
throw new Error('抛出的异常');
}
returnValue().then((value) => {
console.log(value); // 输出: 直接返回的值
});
throwError().catch((error) => {
console.error(error.message); // 输出: 抛出的异常
});
2. await
的作用机制
await
表达式会暂停 async
函数的执行,等待右侧的 Promise
被解决或拒绝。具体步骤如下:
- 暂停执行:当
async
函数执行到await
表达式时,函数会暂停执行,让出线程控制权,允许其他代码继续执行。 - 等待
Promise
结果:await
会等待右侧的Promise
被解决或拒绝。如果Promise
被解决,await
表达式会返回Promise
的解决值;如果Promise
被拒绝,await
会抛出该拒绝原因。 - 恢复执行:当
Promise
被解决或拒绝后,async
函数会从暂停的位置继续执行。
async function asyncExample() {
console.log('开始执行');
const result = await new Promise((resolve) => {
setTimeout(() => {
resolve('异步结果');
}, 1000);
});
console.log(result);
console.log('继续执行');
}
asyncExample();
在上述代码中,asyncExample
函数执行到 await
时会暂停,等待 Promise
被解决。1 秒后,Promise
被解决,await
返回解决值,函数从暂停的位置继续执行。
3. 基于生成器(Generator)和自动执行器的实现
从底层原理来看,async/await
可以通过生成器(Generator)和自动执行器来模拟实现。生成器是一种特殊的函数,它可以暂停和恢复执行。
function run(gen) {
const iterator = gen();
function iterate(iteration) {
if (iteration.done) return iteration.value;
const promise = iteration.value;
promise.then((result) => {
return iterate(iterator.next(result));
}).catch((error) => {
return iterate(iterator.throw(error));
});
}
return iterate(iterator.next());
}
function asyncOperation() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('异步操作结果');
}, 1000);
});
}
function* asyncGenerator() {
try {
const result = yield asyncOperation();
console.log(result);
} catch (error) {
console.error(error);
}
}
run(asyncGenerator);
在上述代码中,run
函数是一个自动执行器,它会自动迭代生成器,并处理 Promise
的解决和拒绝。asyncGenerator
是一个生成器函数,它使用 yield
暂停执行,等待 Promise
的结果。
总结
async/await
是基于 Promise
实现的语法糖,它利用 async
函数返回 Promise
对象和 await
暂停函数执行的特性,让异步代码的编写更像同步代码。底层可以通过生成器和自动执行器来模拟实现。
28. 算法题:树形结构转换成列表
以下为你介绍几种使用 JavaScript 实现树形结构转换为列表的方法,包含深度优先搜索(DFS)和广度优先搜索(BFS)两种不同的遍历策略。
1. 深度优先搜索(DFS)实现
深度优先搜索会沿着树的深度遍历树的节点,尽可能深地搜索树的分支。当节点所在的边都已被探寻过,搜索将回溯到发现该节点的那条边的起始节点。
// 示例树形结构
const tree = {
value: 1,
children: [
{
value: 2,
children: [
{
value: 4,
children: []
}
]
},
{
value: 3,
children: []
}
]
};
function treeToListDFS(tree) {
const list = [];
function dfs(node) {
list.push(node);
if (node.children && node.children.length > 0) {
node.children.forEach(child => dfs(child));
}
}
dfs(tree);
return list;
}
const resultDFS = treeToListDFS(tree);
console.log(resultDFS);
代码解释:
- 定义了
treeToListDFS
函数,内部声明了dfs
递归函数用于遍历节点。 dfs
函数会先将当前节点添加到list
中,若该节点存在子节点,则递归调用dfs
函数处理每个子节点。- 最后返回转换后的列表。
2. 广度优先搜索(BFS)实现
广度优先搜索是按照树的层次依次访问节点,先访问根节点,然后依次访问根节点的所有子节点,再依次访问这些子节点的子节点,以此类推。
function treeToListBFS(tree) {
const list = [];
const queue = [tree];
while (queue.length > 0) {
const currentNode = queue.shift();
list.push(currentNode);
if (currentNode.children && currentNode.children.length > 0) {
queue.push(...currentNode.children);
}
}
return list;
}
const resultBFS = treeToListBFS(tree);
console.log(resultBFS);
代码解释:
- 定义了
treeToListBFS
函数,使用一个队列queue
来辅助遍历。 - 初始时将根节点加入队列,然后在队列不为空的情况下,取出队首节点添加到
list
中,并将其所有子节点加入队列。 - 不断重复这个过程,直到队列为空,最后返回转换后的列表。
3. 支持自定义处理节点的 DFS 实现
有时候你可能需要对节点进行一些额外的处理,比如只提取节点的某些属性,可以通过传入一个回调函数来实现。
function treeToListWithCallbackDFS(tree, callback = (node) => node) {
const list = [];
function dfs(node) {
const processedNode = callback(node);
list.push(processedNode);
if (node.children && node.children.length > 0) {
node.children.forEach(child => dfs(child));
}
}
dfs(tree);
return list;
}
// 示例:只提取节点的 value 属性
const resultWithCallback = treeToListWithCallbackDFS(tree, (node) => node.value);
console.log(resultWithCallback);
代码解释:
treeToListWithCallbackDFS
函数接收两个参数,一个是树形结构tree
,另一个是可选的回调函数callback
。- 在
dfs
函数中,使用callback
对节点进行处理后再添加到列表中。 - 这样可以根据具体需求灵活处理节点。
29. 算法题:数组结构转换成树
将数组结构转换成树是一个常见的编程需求,通常数组中的每个元素代表树的一个节点,节点之间通过特定的关联字段(如 id
和 parentId
)来确定父子关系。以下是使用 JavaScript 实现的详细步骤和示例代码。
实现思路
- 创建映射表:遍历数组,以每个节点的
id
为键,将节点存储在一个映射表(对象)中,方便后续查找。 - 构建树结构:再次遍历数组,根据每个节点的
parentId
在映射表中查找其父节点。如果找到父节点,则将当前节点添加到父节点的children
属性中;如果没有找到父节点,则将当前节点视为根节点。
示例代码
// 示例数组
const array = [
{ id: 1, parentId: null, name: 'Root' },
{ id: 2, parentId: 1, name: 'Child 1' },
{ id: 3, parentId: 1, name: 'Child 2' },
{ id: 4, parentId: 2, name: 'Grandchild 1' }
];
function arrayToTree(arr) {
// 创建一个映射表,用于快速查找节点
const nodeMap = {};
// 存储根节点
const rootNodes = [];
// 第一步:将所有节点存储到映射表中
arr.forEach(node => {
nodeMap[node.id] = { ...node, children: [] };
});
// 第二步:构建树结构
arr.forEach(node => {
const currentNode = nodeMap[node.id];
const parentId = node.parentId;
if (parentId === null) {
// 如果 parentId 为 null,则该节点为根节点
rootNodes.push(currentNode);
} else {
const parentNode = nodeMap[parentId];
if (parentNode) {
// 将当前节点添加到父节点的 children 数组中
parentNode.children.push(currentNode);
}
}
});
return rootNodes;
}
// 调用函数进行转换
const tree = arrayToTree(array);
console.log(tree);
代码解释
- 创建映射表:使用
forEach
方法遍历数组,将每个节点存储在nodeMap
中,并为每个节点添加一个空的children
属性。 - 构建树结构:再次使用
forEach
方法遍历数组,对于每个节点,根据其parentId
在nodeMap
中查找其父节点。如果parentId
为null
,则将该节点视为根节点,添加到rootNodes
数组中;否则,将该节点添加到其父节点的children
数组中。 - 返回结果:最终返回
rootNodes
数组,其中包含了所有的根节点,形成了树结构。
复杂度分析
- 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组的长度。需要遍历数组两次。
- 空间复杂度: O ( n ) O(n) O(n),主要用于存储映射表和树结构。
30. 算法题:最长递增子序列
最长递增子序列(Longest Increasing Subsequence,LIS)问题是在一个给定的无序整数数组中,找到一个最长的严格递增子序列的长度。以下为你介绍几种用 JavaScript 实现最长递增子序列的方法。
方法一:动态规划
思路
- 定义一个数组
dp
,其中dp[i]
表示以第i
个元素结尾的最长递增子序列的长度。 - 对于每个元素
nums[i]
,遍历其前面的所有元素nums[j]
(j < i
),如果nums[j] < nums[i]
,则说明可以将nums[i]
接在以nums[j]
结尾的递增子序列后面,更新dp[i]
的值。 - 最后,找出
dp
数组中的最大值,即为最长递增子序列的长度。
代码实现
function lengthOfLIS(nums) {
if (nums.length === 0) return 0;
const n = nums.length;
// 初始化 dp 数组,每个元素初始值为 1
const dp = new Array(n).fill(1);
for (let i = 1; i < n; i++) {
for (let j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
// 更新 dp[i] 的值
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
// 找出 dp 数组中的最大值
let maxLength = 0;
for (let i = 0; i < n; i++) {
maxLength = Math.max(maxLength, dp[i]);
}
return maxLength;
}
// 示例用法
const nums = [10, 9, 2, 5, 3, 7, 101, 18];
console.log(lengthOfLIS(nums));
复杂度分析
- 时间复杂度:
O
(
n
2
)
O(n^2)
O(n2),其中
n
n
n 是数组的长度。需要嵌套两层循环来填充
dp
数组。 - 空间复杂度:
O
(
n
)
O(n)
O(n),主要用于存储
dp
数组。
方法二:贪心 + 二分查找
思路
- 维护一个数组
tail
,其中tail[i]
表示长度为i + 1
的递增子序列的末尾元素的最小值。 - 遍历数组
nums
,对于每个元素num
,使用二分查找在tail
数组中找到第一个大于等于num
的位置pos
。 - 如果
pos
等于tail
数组的长度,说明可以将num
接在当前最长递增子序列的后面,将num
添加到tail
数组的末尾;否则,将tail[pos]
更新为num
。 - 最后,
tail
数组的长度即为最长递增子序列的长度。
代码实现
function lengthOfLIS(nums) {
if (nums.length === 0) return 0;
const n = nums.length;
const tail = [nums[0]];
for (let i = 1; i < n; i++) {
const num = nums[i];
if (num > tail[tail.length - 1]) {
// 如果 num 大于 tail 数组的最后一个元素,直接添加到 tail 数组末尾
tail.push(num);
} else {
// 使用二分查找找到第一个大于等于 num 的位置
let left = 0, right = tail.length - 1;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (tail[mid] < num) {
left = mid + 1;
} else {
right = mid;
}
}
// 更新 tail[left] 的值
tail[left] = num;
}
}
return tail.length;
}
// 示例用法
const nums = [10, 9, 2, 5, 3, 7, 101, 18];
console.log(lengthOfLIS(nums));
复杂度分析
- 时间复杂度:
O
(
n
l
o
g
n
)
O(n log n)
O(nlogn),其中
n
n
n 是数组的长度。对于每个元素,需要使用二分查找在
tail
数组中找到合适的位置,二分查找的时间复杂度为 O ( l o g n ) O(log n) O(logn)。 - 空间复杂度:
O
(
n
)
O(n)
O(n),主要用于存储
tail
数组。在最坏情况下,tail
数组的长度可能达到 n n n。