C# 面试记录
.NET 开发工程师 面试记录
- 一、.NET部分
- 1.下述代码执行后 , 控制台输出的结果是什么 ? 详细解释下原因
- 代码执行结果:
- 详细原因解释:
- 1. 值类型(int)的传递:`UpdateValue(num)`
- 2. 数组(引用类型)的传递:`UpdateArray(nums)`
- 3. 字符串(不可变引用类型)的传递:`UpdateString(str)`
- 总结:
- 2.C# 描述接口和抽象的异同 相同点与不同点
- 核心概念速览
- 相同点 (Similarities)
- 不同点 (Differences)
- 如何选择:接口 vs. 抽象类?
- 使用 **接口** 当:
- 使用 **抽象类** 当:
- 总结比喻
- 3.描述.net 8项目架构下 对应以下 文件的作用 : 1、wwwroot 2、 Program 3 、 依赖项 4 、appsettings.json 5、_Layout.cshtml
- 1. `wwwroot` 文件夹
- 2. `Program.cs` 文件 (取代了之前的 `Startup.cs`)
- 3. `依赖项` (Dependencies)
- 4. `appsettings.json` 文件
- 5. `_Layout.cshtml` 文件
- 总结关系图
- 二、Vue部分
- 4.描述vue中以下指令的作用 : 1、v-bind 2 、 v-model 3、v-for 4、v-if/v-else-if/v-else 5、v-show /v-on
- 1. `v-bind` - 动态绑定属性
- 2. `v-model` - 双向数据绑定
- 3. `v-for` - 循环渲染
- 4. `v-if` / `v-else-if` / `v-else` - 条件渲染
- 5. `v-show` - 条件显示
- 6. `v-on` - 事件监听
- 总结对比
- 5.请描述vue中以下生命周期钩子作用 : 1、onBeforeMount 2、onMounted 3、 onBeforeUpdate4、onUpdate 5 、 onErrorCaptured
- 核心概念:生命周期阶段
- 1. `onBeforeMount`
- 2. `onMounted`
- 3. `onBeforeUpdate`
- 4. `onUpdated`
- 5. `onErrorCaptured`
- 总结与关系图
- 6.项目中 常用的npm、pnpm 、 或yar指令有哪些 ,分别有什么用 ?
- 核心概念
- 通用指令(功能相同,只是命令前缀不同)
- 各包管理器的特色指令
- npm
- yarn (v1 Classic)
- pnpm
- 项目开发中最常用的指令流程
- 如何选择?
- 7.vue如何实现组建间的通信 , 请至少描述两种实现方式
- 方式一:Props / Events (父子组件通信)
- 1. Props (父传子)
- 2. Events (子传父)
- 方式二:Provide / Inject (跨层级组件/祖孙通信)
- 其他重要方式简介
- 三、Sql 部分
- 8.sql : 请先删除姓名 , 年龄重复的记录取得不重复的数据根据姓名、年龄分组 , 再取出每组的Id最大值 , 然后将Id最大值之外的排除
- 步骤分析:
- 方法一:使用子查询和 `IN` clause
- 方法二:使用关联子查询
- 重要提示:先验证再删除!
- 总结
- 9. 现在有以下三张表 , 请解答 :
- 假设
- 方法一:使用关联子查询计算部门平均薪资
- 方法二:使用CTE (公共表表达式) 先计算部门平均薪资
- 总结与推荐
- 后续更新中 ...
一、.NET部分
1.下述代码执行后 , 控制台输出的结果是什么 ? 详细解释下原因
namespace 面试题
{internal class Program{static void UpdateValue(int x) { x = 100; }static void UpdateArray(int[] arr) { arr[0] = 100; }static void UpdateString(string s) { s = "changed"; }static void Main(string[] args){int num = 1;int[] nums = { 1, 2, 3 };string str = "original";UpdateValue(num);UpdateArray(nums);UpdateString(str);Console.WriteLine(num); // 输出 1Console.WriteLine(nums[0]); // 输出 100Console.WriteLine(str); // 输出 originalConsole.ReadKey();}}
}
代码执行结果:
Console.WriteLine(num);
输出:1Console.WriteLine(nums[0]);
输出:100Console.WriteLine(str);
输出:original
详细原因解释:
1. 值类型(int)的传递:UpdateValue(num)
int
是值类型,它在方法中传递的是值的副本。- 当调用
UpdateValue(num)
时,方法内的x
是num
的一个副本(初始值为1)。 - 在方法内修改
x = 100
,只是修改了这个副本的值,而原始变量num
不受影响。 - 因此,
num
仍然为1
。
2. 数组(引用类型)的传递:UpdateArray(nums)
- 数组是引用类型,它在方法中传递的是引用的副本(即内存地址的副本)。
- 当调用
UpdateArray(nums)
时,方法内的arr
是nums
引用的一个副本(指向同一个数组对象)。 - 通过
arr[0] = 100
修改数组元素时,实际上是通过副本引用访问了原始数组对象,并修改了其内容。 - 因此,原始数组
nums[0]
被修改为100
。
3. 字符串(不可变引用类型)的传递:UpdateString(str)
string
是引用类型,但它是不可变的(创建后内容不能修改)。- 当调用
UpdateString(str)
时,方法内的s
是str
引用的一个副本(指向同一个字符串对象)。 - 当执行
s = "changed"
时,并不是修改原始字符串,而是让副本引用s
指向了一个新的字符串对象(“changed”)。 - 原始引用
str
仍然指向原来的字符串对象(“original”),因此不受影响。 - 所以,
str
输出仍然是"original"
。
总结:
- 值类型(如
int
)通过值传递(传递副本),方法内修改不影响原始变量。 - 引用类型(如数组)通过引用传递(传递引用的副本),方法内通过引用修改对象内容会影响原始对象。
- 字符串(不可变引用类型)虽然传递引用副本,但重新赋值(
=
)会指向新对象,而不影响原始引用。
这种差异是由于C#的参数传递机制(默认按值传递)以及值类型和引用类型的本质区别所导致的。
2.C# 描述接口和抽象的异同 相同点与不同点
好的,我们来详细解析C#中接口(Interface) 和抽象类(Abstract Class) 的异同。这是面向对象编程中非常核心的概念。
核心概念速览
- 接口 (Interface): 定义了一份契约(Contract)。它只规定了实现者必须做什么(必须拥有哪些方法、属性、事件等),但不提供任何实现细节。它是一种“Can-Do”关系(例如,
ICanFly
接口意味着实现者“能飞”)。 - 抽象类 (Abstract Class): 定义了一个不完全的类。它既可以包含未实现的成员(抽象成员),也可以包含已实现的成员。它用于表示一种“Is-A”关系(例如,
Animal
抽象类,Dog
是Animal
的一种)。
相同点 (Similarities)
-
都不能被实例化
- 你无法使用
new
关键字直接创建一个接口或抽象类的实例。 - 它们的存在都是为了被其他类继承或实现。
- 你无法使用
-
都可以包含未实现的成员
- 它们都可以声明没有方法体的方法、属性、索引器和事件。这些成员由派生类/实现类来提供具体实现。
-
都支持多态
- 这是它们最重要的共同目标。你可以使用接口或抽象类的类型来引用其派生类/实现类的实例,从而实现基于同一接口或基类的不同行为。
// 使用接口实现多态 ILogger logger = new FileLogger(); // 或 new DatabaseLogger() logger.Log("Message"); // 调用的是具体实现类的方法// 使用抽象类实现多态 Animal animal = new Dog(); // 或 new Cat() animal.MakeSound(); // 调用的是具体派生类的方法
不同点 (Differences)
这是一个更重要的部分,决定了你在设计时应如何选择。下表清晰地列出了它们的核心区别:
特性 | 接口 (Interface) | 抽象类 (Abstract Class) |
---|---|---|
继承机制 | 实现 (Implementation)。一个类可以实现多个接口。 | 继承 (Inheritance)。一个类只能继承一个抽象类(单继承)。 |
成员类型 | 只能包含未实现的成员(方法、属性、事件、索引器)。C# 8.0+ 后可以包含默认实现,但不推荐滥用。 | 既可以包含抽象(未实现)成员,也可以包含已实现的成员(方法、字段、属性等)。 |
访问修饰符 | 成员默认是 public 的,不能使用其他修饰符(如 private , protected , internal )。 | 可以使用各种访问修饰符(public , protected , internal , private 等)来控制成员的可见性。 |
字段(Fields) | 不能包含实例字段和静态字段。 | 可以包含实例字段和静态字段。 |
构造函数 | 没有构造函数和析构函数。 | 有构造函数和析构函数(虽然不能直接实例化,但派生类实例化时会调用)。 |
设计目的 | 定义行为契约。关注“能做什么”(Can-Do)。 (e.g., IEnumerable 定义了“能被遍历”) | 定义类别和共享代码。关注“是什么”(Is-A)。 (e.g., Stream 定义了“是一种流”) |
版本影响 | 向现有接口添加新成员会破坏所有已有的实现类。 | 向抽象类添加已实现的新方法通常不会破坏现有派生类。 |
如何选择:接口 vs. 抽象类?
这是一个非常重要的设计决策。请遵循以下原则:
使用 接口 当:
- 需要多重继承:C#是单继承语言,但一个类可以实现多个接口。这是使用接口最主要的原因。
- 定义不相关对象的行为:如果一些类完全不属于同一类别,但它们需要共享某些行为,使用接口。例如,
Plane
(飞机)和Bird
(鸟)都可以实现IFlyable
(能飞的)接口,但它们显然不属于同一个基类。 - 定义契约而非实现:当你只关心一个类是否实现了某个功能,而不关心它如何实现时。
- 团队协作或构建API:接口是不同模块或团队之间理想的协作契约,它降低了实现之间的耦合度。
使用 抽象类 当:
- 存在明确的“Is-A”关系:多个类共享一个共同的基类,并且它们之间有很多共同的代码和逻辑。抽象类可以完美地提供这些共享代码,避免重复。
- 需要为派生类提供公共的已实现功能:抽象类可以包含字段、构造函数和已实现的方法,这些都可以被派生类直接使用或重写。
- 需要控制成员的访问权限:当你需要
protected
或private
成员时,必须使用抽象类。 - 预计未来会添加新功能且不希望破坏现有代码:在抽象类中添加一个非抽象的新方法是安全的,所有派生类会自动获得此功能。而在接口中添加新成员则会破坏所有实现者。
总结比喻
- 接口就像一份工作职责说明书,它规定了员工(实现类)必须完成的任务,但不关心员工具体怎么做。
- 抽象类就像一个有基础设备和流程的模板车间,它定义了车间的核心功能(已实现方法),但也留了一些必须由具体产品线(派生类)来完成的特定工序(抽象方法)。
在实际项目中,优先考虑使用接口来定义契约以降低耦合度,当确实需要共享代码和实现时,再使用抽象类。两者也经常结合使用,例如,定义一个接口 IRepository
,再提供一个实现了部分通用功能的抽象基类 RepositoryBase
。
3.描述.net 8项目架构下 对应以下 文件的作用 : 1、wwwroot 2、 Program 3 、 依赖项 4 、appsettings.json 5、_Layout.cshtml
好的,在**.NET 8**(特别是使用ASP.NET Core)的项目架构下,这些文件和节点扮演着至关重要的角色。.NET 8统一了Program和Startup,并继续推荐使用依赖注入和配置等现代模式。
以下是它们的作用详细解释:
1. wwwroot
文件夹
- 作用:静态资源根目录。此文件夹用于存放所有可以直接通过浏览器请求的静态文件。
- 详细解释:
- 在ASP.NET Core中,默认只有
wwwroot
文件夹下的文件可以被客户端(如浏览器)直接访问。 - 常见的静态资源包括:
- CSS 文件 (
site.css
) - JavaScript 文件 (
site.js
) - 图片 (
.png
,.jpg
,.ico
) - 字体 文件
- 客户端库(如Bootstrap, jQuery,通常通过LibMan或npm安装后放在这里)
- CSS 文件 (
- 访问方式:如果有一个文件位于
wwwroot/css/site.css
,则可以通过URLhttps://yourdomain.com/css/site.css
直接访问。 - 安全性:该文件夹外的静态文件默认对客户端是不可见的,这提供了额外的安全层。
- 在ASP.NET Core中,默认只有
2. Program.cs
文件 (取代了之前的 Startup.cs
)
- 作用:应用程序的入口点和服务配置中心。这是.NET 5+(包括.NET 8)中最重要的文件,使用最小主机模型。
- 详细解释:
- 应用入口 (Entry Point):
Program.cs
中的Main
方法是应用程序启动时执行的第一个方法。 - 服务注册 (DI Container):使用
var builder = WebApplication.CreateBuilder(args);
创建主机构建器。在builder.Services
(依赖注入容器) 上注册所有应用所需的服务(如MVC控制器、DbContext、自定义服务等)。builder.Services.AddControllersWithViews(); builder.Services.AddDbContext<MyDbContext>(); builder.Services.AddScoped<IMyService, MyService>();
- 中间件管道配置 (Middleware Pipeline):使用
var app = builder.Build();
构建应用后,在此处配置HTTP请求处理管道。中间件的顺序至关重要。app.UseHttpsRedirection(); app.UseStaticFiles(); // 启用wwwroot静态文件服务 app.UseRouting(); app.UseAuthorization(); app.MapControllerRoute(...);
- 它简化和统一了应用程序的配置,将所有设置(服务、中间件、路由)都放在一个文件中。
- 应用入口 (Entry Point):
3. 依赖项
(Dependencies)
- 作用:管理项目的外部引用和NuGet包。它代表了项目运行所依赖的所有外部代码库。
- 详细解释:
- 在Visual Studio的解决方案资源管理器中,这是一个节点,不是物理文件夹。
- 它包含两个主要部分:
- NuGet:列出通过NuGet包管理器安装的所有第三方库(如
EntityFrameworkCore
,Swashbuckle.AspNetCore
(Swagger),Serilog
等)。这些包及其依赖关系在项目文件 (.csproj
) 中定义。 - SDK:指的是你正在使用的.NET SDK本身,它提供了基础类库(如
System.Text.Json
,System.Linq
)。
- NuGet:列出通过NuGet包管理器安装的所有第三方库(如
- 重要性:所有在这里列出的包和SDK都会被编译到你的应用程序中。通过右键单击“依赖项”可以管理NuGet包,添加或删除项目依赖。
4. appsettings.json
文件
- 作用:应用程序的配置文件。用于存储应用在不同环境(开发、生产、测试)下的配置设置。
- 详细解释:
- 基于JSON格式,结构清晰易读。
- 通常包含:
- 数据库连接字符串 (
ConnectionStrings
) - 日志记录配置 (
Logging
) - 应用自定义设置(如API密钥、功能开关、外部服务URL)
- 所有环境通用的设置放在
appsettings.json
中。
- 数据库连接字符串 (
- 环境特定配置:可以有环境特定的版本,如
appsettings.Development.json
和appsettings.Production.json
。应用运行时会自动加载匹配当前环境的文件,并覆盖通用文件中的相同设置。 - 访问方式:在
Program.cs
中,通过builder.Configuration
对象访问这些配置值。配置系统也会自动将这些值绑定到POCO对象上。
5. _Layout.cshtml
文件
- 作用:Razor视图的公共布局页(母版页)。用于定义网站的统一外观、结构和公共元素。
- 详细解释:
- 位于
Views/Shared
文件夹中。 - 它包含了每个页面共有的HTML骨架,如:
<html>
,<head>
,<body>
标签- 引用的通用CSS和JavaScript文件(通常放在
wwwroot
下) - 导航栏 (Navbar)、页眉 (Header)、页脚 (Footer)
- 侧边栏 (Sidebar)
- 占位符:它使用
@RenderBody()
作为占位符。当某个具体视图(如Index.cshtml
)使用此布局时,其内容会渲染在@RenderBody()
的位置。 - 节(Sections):布局页可以定义节(如
@RenderSection("Scripts", required: false)
),允许各个视图页面向布局页的特定位置(如<head>
或body底部)注入额外的内容(如页面特定的CSS或JS)。 - 优点:避免了在每个视图中重复相同的HTML代码,实现了DRY(Don’t Repeat Yourself)原则,易于维护和修改整体布局。
- 位于
总结关系图
启动 (Program.cs)↓ 读取配置
配置 (appsettings.json) → 提供连接字符串等设置↓ 注册服务
依赖项 (NuGet包) → 提供功能实现↓
处理请求 → 中间件管道 (Program.cs中配置)↓使用静态资源 (wwwroot) → 提供CSS, JS, 图片↓
渲染视图 → 使用布局 (_Layout.cshtml) 包裹具体视图内容
这些组件共同协作,构成了一个现代、模块化且可配置的ASP.NET Core应用程序的基础架构。
二、Vue部分
4.描述vue中以下指令的作用 : 1、v-bind 2 、 v-model 3、v-for 4、v-if/v-else-if/v-else 5、v-show /v-on
好的,这是Vue.js中这些核心指令的详细作用说明。指令是Vue模板中带有 v-
前缀的特殊属性,它们为DOM元素应用了特殊的响应式行为。
1. v-bind
- 动态绑定属性
- 作用:单向数据绑定。用于将Vue实例中的数据(data、computed等)动态地绑定到HTML元素的属性(attribute)上。
- 详细解释:
- HTML属性通常是静态的(如
<img src="static.jpg">
)。v-bind
让你可以将属性变成动态的,其值来源于Vue实例的JavaScript数据。 - 当Vue实例中的数据发生变化时,绑定的属性值会自动更新。
- 简写:
:
(冒号)。例如v-bind:href
可以简写为:href
。
- HTML属性通常是静态的(如
- 常见用法:
- 绑定
src
、href
、class
、style
、disabled
等属性。
- 绑定
- 示例:
<!-- 绑定一个属性 --> <img v-bind:src="imageUrl"> <!-- 简写形式 --> <a :href="linkUrl">点击我</a><!-- 动态绑定CSS类 (非常强大) --> <div :class="{ active: isActive, 'text-danger': hasError }"></div>
2. v-model
- 双向数据绑定
- 作用:双向数据绑定。主要在表单输入元素(
<input>
、<textarea>
、<select>
)上创建双向数据绑定。 - 详细解释:
- 它结合了
v-bind
(将数据值绑定到元素的value属性)和v-on:input
(监听用户的输入事件来更新数据)。 - 数据流向是双向的:
- Data → View:当Vue实例中的数据改变时,输入框的值会自动更新。
- View → Data:当用户在输入框中输入内容时,Vue实例中对应的数据也会自动更新。
- 它是语法糖,极大地简化了表单处理。
- 它结合了
- 修饰符:
.lazy
:将input
事件改为change
事件(通常在失去焦点后更新)。.number
:将用户输入自动转换为数字类型。.trim
:自动去除用户输入的首尾空白字符。
- 示例:
<input type="text" v-model="message"> <p>你输入的是: {{ message }}</p><input type="number" v-model.number="age"> <!-- 确保age是数字 --> <textarea v-model.trim="comment"></textarea> <!-- 自动去除首尾空格 -->
3. v-for
- 循环渲染
- 作用:基于源数据多次渲染一个元素或模板。用于渲染列表。
- 详细解释:
- 它需要特殊的语法
item in items
,其中items
是源数据数组(或对象),item
则是被迭代的数组元素的别名。 - 必须为每个迭代项提供一个唯一的
:key
属性(通常使用id),以便Vue能够跟踪每个节点的身份,高效地更新和重用元素。
- 它需要特殊的语法
- 用法:
- 可以遍历数组(
(item, index) in items
)。 - 可以遍历对象的属性(
(value, name, index) in object
)。
- 可以遍历数组(
- 示例:
<!-- 遍历数组 --> <ul><li v-for="(item, index) in items" :key="item.id">{{ index }} - {{ item.name }}</li> </ul><!-- 遍历对象 --> <div v-for="(value, key) in userInfo" :key="key">{{ key }}: {{ value }} </div>
4. v-if
/ v-else-if
/ v-else
- 条件渲染
- 作用:根据表达式的真假值,条件性地渲染一块内容。
- 详细解释:
v-if
:指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回truthy值(真值)的时候被渲染。v-else-if
:提供的是一个“else if”块,可以连续使用。v-else
:为v-if
添加一个“else 块”。它必须紧跟在带v-if
或者v-else-if
的元素之后。- 操作的是DOM元素的存在与否。如果条件为假,元素会从DOM中完全移除(不是隐藏)。
- 示例:
<div v-if="type === 'A'">显示 A </div> <div v-else-if="type === 'B'">显示 B </div> <div v-else>显示其他 </div>
5. v-show
- 条件显示
- 作用:根据条件切换元素的CSS
display
属性。 - 详细解释:
- 用法大致与
v-if
相同:v-show="expression"
。 - 关键区别:
v-show
不管初始条件是什么,元素总是会被渲染并保留在DOM中。它只是简单地通过切换CSS的display: none;
样式来控制元素的显示与隐藏。 - 性能考量:
v-show
有更高的初始渲染开销,但如果需要非常频繁地切换 visibility,则v-show
的性能更好,因为它避免了频繁的DOM添加/移除操作。
- 用法大致与
- 示例:
<h1 v-show="isVisible">你好!Vue!</h1> <!-- 等同于在元素上动态设置 style="display: none;" -->
6. v-on
- 事件监听
- 作用:监听DOM事件,并在触发时运行一些JavaScript代码或调用方法。
- 详细解释:
- 用于响应用户的交互,如点击、键盘输入、鼠标移动等。
- 简写:
@
。例如v-on:click
可以简写为@click
。 - 通常绑定到一个方法(method)或内联的JavaScript语句。
- 修饰符:
.stop
:调用event.stopPropagation()
,阻止事件冒泡。.prevent
:调用event.preventDefault()
,阻止默认行为(如表单提交、链接跳转)。.once
:事件将只触发一次。.enter
、.tab
、.esc
等:只在特定按键上触发事件。
- 示例:
<!-- 绑定一个方法 --> <button v-on:click="greet">打招呼</button> <!-- 简写形式 --> <button @click="greet">打招呼</button><!-- 内联JavaScript语句 --> <button @click="count++">增加 1</button><!-- 使用修饰符 --> <form @submit.prevent="onSubmit">...</form> <!-- 阻止表单默认提交 --> <a @click.stop="doThis">...</a> <!-- 阻止点击事件继续传播 -->
总结对比
指令 | 核心作用 | 关键特点 |
---|---|---|
v-bind (: ) | 单向数据绑定 | 将数据绑定到HTML属性 |
v-model | 双向数据绑定 | 主要用于表单输入,数据视图双向同步 |
v-for | 循环渲染 | 基于数据生成列表,必须使用 :key |
v-if | 条件渲染 | 根据条件创建或销毁元素 |
v-show | 条件显示 | 根据条件切换CSS的 display 属性 |
v-on (@ ) | 事件监听 | 监听用户事件并执行方法或代码 |
5.请描述vue中以下生命周期钩子作用 : 1、onBeforeMount 2、onMounted 3、 onBeforeUpdate4、onUpdate 5 、 onErrorCaptured
好的,这些是 Vue 3 组合式 API (Composition API) 中的生命周期钩子。它们允许你在组件的不同阶段注入自己的代码。以下是它们的详细作用:
核心概念:生命周期阶段
Vue 组件的生命周期可以概括为以下几个主要阶段:
- 初始化 (Initialization):设置响应式数据和事件。
- 挂载 (Mounting):将模板编译渲染成真实DOM,并插入到页面中。
- 更新 (Updating):当响应式数据发生变化时,组件重新渲染和更新DOM。
- 卸载 (Unmounting):组件实例被销毁,并从DOM中移除。
这些钩子就是在这些阶段之间被调用的函数。
1. onBeforeMount
- 作用:在组件被挂载到真实的DOM之前立即执行。
- 触发时机:位于“挂载”阶段的最开始。此时,Vue已经完成了模板的编译和渲染函数的设置,但还没有将生成的DOM节点插入到页面文档中。
- 详细解释:
- 在这个阶段,组件的模板已经编译好了,但生成的DOM元素还只存在于内存中,你无法通过
ref
或原生DOM操作(如document.getElementById
)访问到它们。 - 这是你在渲染之前进行最后准备的时机,但通常很少使用,因为大部分初始化工作可以在
setup()
中完成。
- 在这个阶段,组件的模板已经编译好了,但生成的DOM元素还只存在于内存中,你无法通过
- 常见用途:在服务器端渲染 (SSR) 中,用于执行一些需要在客户端挂载前完成的特定逻辑。
2. onMounted
- 作用:在组件被挂载到真实的DOM之后执行。
- 触发时机:位于“挂载”阶段的最后。此时,Vue已经将编译好的模板内容创建为真实的DOM节点,并将其插入到了指定的父容器中(例如
#app
)。 - 详细解释:
- 这是最常用、最重要的生命周期钩子之一。
- 此时,你可以安全地访问和操作DOM,或者使用
ref
来访问子组件。 - 它也常用于执行需要DOM存在的操作,如初始化图表库 (ECharts, D3.js)、添加事件监听器、发送网络请求获取数据等。
- 常见用途:
- 操作DOM
- 初始化第三方JS库
- 发送Ajax请求 从服务器获取数据以填充组件(注意配合
onUnmounted
进行清理)。
import { onMounted, ref } from 'vue';const chartContainer = ref(null);onMounted(() => {// 现在可以安全地访问DOM元素console.log(chartContainer.value); // 输出实际的DOM元素// 初始化一个图表const chart = echarts.init(chartContainer.value);chart.setOption({...});
});
3. onBeforeUpdate
- 作用:在响应式数据发生变化,导致组件即将重新渲染和更新真实的DOM之前执行。
- 触发时机:位于“更新”阶段的最开始。Vue检测到数据变化,但尚未根据新的数据重新生成虚拟DOM并打补丁 (patch) 到真实DOM上。
- 详细解释:
- 你可以在这个钩子中获取到数据更新前的DOM状态。
- 这个钩子使用相对较少,通常用于在更新前获取一些特定的DOM信息(如当前的滚动位置),以便在更新后恢复它。
- 常见用途:在元素更新前,记录当前的滚动位置或表单元素的状态。
4. onUpdated
- 作用:在响应式数据发生变化,导致组件重新渲染和更新真实的DOM之后执行。
- 触发时机:位于“更新”阶段的最后。此时,Vue已经将新的虚拟DOM差异应用到了真实DOM上,页面视图已经和最新的数据保持同步。
- 详细解释:
- 谨慎使用!因为任何数据的修改在这个钩子里都会再次触发更新,很容易导致无限循环。
- 父组件的
onUpdated
会在其子组件的onUpdated
之后调用。 - 如果你需要在此钩子中改变状态,通常意味着你应该使用计算属性 (computed) 或 侦听器 (watcher) 来代替。
- 常见用途:执行依赖于组件更新后DOM状态的操作,例如在列表项更新后让窗口滚动到某个特定元素。
import { onUpdated, ref } from 'vue';const items = ref([...]);onUpdated(() => {// DOM 现在已经更新了// 但不要在这里修改 `items`,否则会导致再次更新,可能进入循环!
});
5. onErrorCaptured
- 作用:捕获来自后代组件的错误。它是一个错误边界机制。
- 触发时机:当任何后代组件(子、孙、曾孙…)在生命周期钩子、事件处理器或渲染函数中抛出一个错误时被调用。
- 详细解释:
- 它接收三个参数:
(error, instance, info)
。error
:捕获到的错误对象。instance
:抛出错误的组件实例。info
:一个字符串,指出了错误的来源类型(如'lifecycle hook'
,'event handler'
,'render function'
)。
- 它可以返回
false
来阻止错误继续向上传播,或者返回true
让错误继续向上冒泡,可能被更外层的组件捕获。 - 如果全局的
app.config.errorHandler
被定义,捕获到的错误也会被发送给它。
- 它接收三个参数:
- 常见用途:
- 在组件层面优雅地处理错误,例如显示一个友好的错误信息UI,而不是让整个组件树崩溃。
- 将错误日志上报到监控服务。
import { onErrorCaptured, ref } from 'vue';const error = ref(null);onErrorCaptured((err, instance, info) => {error.value = err; // 将错误信息存储起来,用于模板中显示错误UIconsole.error(`Error captured: ${err}\nInfo: ${info}`);// 阻止错误继续向上传播return false;
});
总结与关系图
初始化 (Setup)│
onBeforeMount → DOM未生成,无法访问│
onMounted → ✅ DOM已挂载,可安全操作 (常用)│
┌─ 数据变化 ──── onBeforeUpdate → 更新前,获取旧状态
│ │
│ onUpdated → 更新后,谨慎使用 (易循环)
│
└─ 后代组件出错 ─ onErrorCaptured → 捕获错误,优雅降级
简单记忆:
onMounted
:操作DOM、初始化、请求数据。onUpdated
:更新后操作,但要小心。onErrorCaptured
:抓bug,保平安。onBeforeMount
和onBeforeUpdate
使用场景相对特殊。
6.项目中 常用的npm、pnpm 、 或yar指令有哪些 ,分别有什么用 ?
好的,在现代前端项目中,npm、pnpm 和 yarn 都是常用的包管理器(Package Manager),它们的功能类似,但性能和特性上有所差异。以下是它们最常用的指令及其作用的详细说明。
核心概念
它们的核心作用都是管理项目的依赖(dependencies),即你的项目所依赖的第三方库(如 Vue, React, Lodash, Axios 等)。这些依赖信息被记录在 package.json
文件中。
通用指令(功能相同,只是命令前缀不同)
这些指令在三个管理器中的功能几乎一致,只是语法稍有不同。
作用 | npm | yarn | pnpm | 说明 |
---|---|---|---|---|
初始化项目 | npm init | yarn init | pnpm init | 创建一个新的 package.json 文件,引导你输入项目信息。 |
初始化项目(快速) | npm init -y | yarn init -y | pnpm init -y | 使用默认值快速生成 package.json 。 |
安装所有依赖 | npm install | yarn install | pnpm install | 根据 package.json 中的记录,安装所有依赖包。团队成员拉取项目后必运行。 |
添加生产依赖 | npm install <pkg> | yarn add <pkg> | pnpm add <pkg> | 安装包并将其添加到 package.json 的 dependencies 中(项目运行时需要)。 |
添加开发依赖 | npm install -D <pkg> | yarn add -D <pkg> | pnpm add -D <pkg> | 安装包并将其添加到 package.json 的 devDependencies 中(仅开发/打包时需要,如 ESLint, Webpack)。 |
全局安装 | npm install -g <pkg> | yarn global add <pkg> | pnpm add -g <pkg> | 将包安装到操作系统全局,而不是某个特定项目(如 @vue/cli , create-react-app )。 |
移除依赖 | npm uninstall <pkg> | yarn remove <pkg> | pnpm remove <pkg> | 从 node_modules 和 package.json 中移除一个包。 |
更新依赖 | npm update <pkg> | yarn upgrade <pkg> | pnpm update <pkg> | 更新某个包到最新版本。 |
执行脚本 | npm run <script> | yarn <script> | pnpm run <script> | 运行在 package.json 的 "scripts" 字段中定义的命令(如 dev , build )。 |
各包管理器的特色指令
npm
虽然 npm 是 Node.js 自带的原始包管理器,但其功能也在不断增强。
指令 | 作用 |
---|---|
npm audit | 安全检查。检查项目依赖中是否存在已知的安全漏洞。 |
npm audit fix | 自动修复漏洞。尝试自动升级有漏洞的依赖包来修复问题。 |
npm ci | 清洁安装。用于CI/CD环境,它会根据 package-lock.json 精确安装依赖,速度更快、更严格,能保证环境一致性。 |
npm exec / npx | 执行命令。临时安装并运行一个包的命令(如 npx create-vue@latest )。 |
yarn (v1 Classic)
Yarn 最初是为了解决 npm 早期的性能和一致性问题而诞生的。
指令 | 作用 |
---|---|
yarn why <pkg> | 依赖分析。非常实用的命令,可以显示为什么某个包被安装在了项目中(是哪个包依赖了它)。 |
yarn upgrade-interactive | 交互式更新。提供一个交互式界面,让你可以选择要更新哪些依赖包,非常方便。 |
yarn dlx | 类似于 npx ,临时下载并执行一个包。 |
pnpm
PNPM 以其高效和磁盘空间节省而闻名。它通过硬链接和符号链接在全局存储中管理依赖,避免了重复安装。
指令 | 作用 |
---|---|
pnpm store path | 显示 pnpm 的全局存储(store)目录位置。 |
pnpm store prune | 清理存储。从 store 中删除未被任何项目引用的包,释放磁盘空间。 |
pnpm why <pkg> | 类似于 yarn why ,解释依赖关系。 |
pnpm update -i / pnpm up -i | 交互式更新,类似于 yarn upgrade-interactive 。 |
pnpm add -O <pkg> | 安装可选依赖(optionalDependencies)。 |
项目开发中最常用的指令流程
假设你加入一个新项目,通常会这样操作:
- 克隆代码:
git clone <project-url>
- 安装依赖:
npm install
(或yarn
或pnpm install
) - 启动开发服务器:
npm run dev
(或yarn dev
或pnpm run dev
) - ** coding … **
- 添加新包(例如 Axios):
npm install axios
(或yarn add axios
或pnpm add axios
) - 构建生产版本:
npm run build
(或yarn build
或pnpm run build
)
如何选择?
- npm: 无需额外安装,Node.js 自带,足够稳定和通用。
- yarn: 曾经在性能和确定性上领先,现在依然是许多大型项目的可靠选择。
- pnpm: 当前的首选推荐。它极大地节省了磁盘空间(所有项目共享同一个全局存储),安装速度极快,并且通过严格的 node_modules 结构避免了“幽灵依赖”问题,使得依赖管理更加可靠和安全。
总结来说,这些命令是你作为前端开发者与项目依赖交互的最基本且最常用的工具,熟练掌握它们是项目开发和协作的基础。
7.vue如何实现组建间的通信 , 请至少描述两种实现方式
好的,Vue 组件间通信是核心概念,方式非常多。这里为您详细描述两种最常用且具有代表性的方式:Props / Events(父子通信) 和 Provide / Inject(跨层级/祖孙通信)。
方式一:Props / Events (父子组件通信)
这是最基础、最常用的通信方式,遵循 Vue 的单向数据流原则。
- Props (父 → 子):父组件通过 attribute 的方式将数据传递给子组件。
- Events (子 → 父):子组件通过触发自定义事件,将数据发送给父组件。
1. Props (父传子)
作用:父组件向子组件传递数据。
实现步骤:
- 在子组件中,使用
defineProps
宏(<script setup>
)或props
选项来声明它期望接收的数据。 - 在父组件的模板中,像使用 HTML 属性一样,将数据绑定到子组件的标签上。
示例:
ChildComponent.vue
(子组件)
<!-- 子组件 -->
<template><div><h2>子组件</h2><p>收到父组件的消息: {{ message }}</p><p>收到父组件的数量: {{ count }}</p></div>
</template><script setup>
// 使用 defineProps 来声明接收的 props
const props = defineProps({message: String, // 类型校验count: {type: Number,default: 0 // 默认值}
});
</script>
ParentComponent.vue
(父组件)
<!-- 父组件 -->
<template><div><h1>父组件</h1><!-- 使用 v-bind 动态传递数据给子组件 --><ChildComponent :message="parentMessage" :count="parentCount" /></div>
</template><script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';const parentMessage = ref('Hello from Parent!');
const parentCount = ref(42);
</script>
2. Events (子传父)
作用:子组件向父组件传递数据或通知事件。
实现步骤:
- 在子组件中,使用
defineEmits
宏来声明它要触发的事件。 - 在子组件中,在需要的时候(如按钮点击、输入完成)调用
emit
函数来触发事件,并可传递数据。 - 在父组件的模板中,使用
v-on
或@
来监听子组件触发的事件,并执行父组件中的方法。
示例:
ChildComponent.vue
(子组件)
<!-- 子组件 -->
<template><div><h2>子组件</h2><button @click="sendMessageToParent">点击向父组件发送消息</button><input :value="modelValue" @input="onInput"></div>
</template><script setup>
// 使用 defineEmits 来声明要触发的事件
const emit = defineEmits(['message-sent', 'update:modelValue']);const sendMessageToParent = () => {// 触发 'message-sent' 事件,并传递数据emit('message-sent', 'Hello Parent! from Button');
};const onInput = (event) => {// 触发 'update:modelValue' 事件,用于实现 v-model 双向绑定emit('update:modelValue', event.target.value);
};
</script>
ParentComponent.vue
(父组件)
<!-- 父组件 -->
<template><div><h1>父组件</h1><p>收到子组件的消息: {{ childMessage }}</p><!-- 监听子组件触发的自定义事件 --><ChildComponent @message-sent="handleMessageFromChild"@update:modelValue="parentValue = $event"/><!-- 等同于实现 v-model --><!-- <ChildComponent v-model:modelValue="parentValue" /> --></div>
</template><script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';const childMessage = ref('');
const parentValue = ref('');const handleMessageFromChild = (msg) => {// msg 是子组件传递过来的数据childMessage.value = msg;
};
</script>
总结:Props/Events 是父子组件直接通信的标准方式,简单明了,数据流清晰。
方式二:Provide / Inject (跨层级组件/祖孙通信)
作用:解决深层嵌套组件间(祖→孙,或更远)的通信问题,避免需要将 props 逐层传递的麻烦(称为“prop drilling”)。
- Provide (提供):在祖先组件中,提供(provide)数据。
- Inject (注入):在任何后代组件中,注入(inject)祖先提供的数据。
实现步骤:
- 在祖先组件中,使用
provide()
函数来提供数据。可以提供静态值,也可以提供响应式数据(如ref()
的值)。 - 在任意后代组件中,使用
inject()
函数来注入祖先提供的数据。
示例:
AncestorComponent.vue
(祖先组件)
<!-- 祖先组件 -->
<template><div><h1>祖先组件</h1><ParentComponent /></div>
</template><script setup>
import { ref, provide } from 'vue';
import ParentComponent from './ParentComponent.vue';// 1. 提供静态数据
provide('appTheme', 'dark');// 2. 提供响应式数据 (更常用!)
const userLocation = ref('Beijing');
provide('location', userLocation);const updateLocation = () => {userLocation.value = 'Shanghai';
};
</script>
GrandchildComponent.vue
(孙组件,跳过中间的 ParentComponent)
<!-- 孙组件 (深层嵌套的后代) -->
<template><div><h3>孙组件</h3><p>应用主题: {{ theme }}</p><p>用户位置: {{ location }}</p><button @click="changeLocation">改变位置(会响应式更新)</button></div>
</template><script setup>
import { inject, ref } from 'vue';// 注入祖先提供的数据
// 第一个参数是提供的 key
// 第二个参数是默认值(可选)
const theme = inject('appTheme', 'light'); // 注入静态数据
const location = inject('location'); // 注入响应式 ref// 因为 location 是 ref,修改它会影响所有注入此数据的组件
const changeLocation = () => {location.value = 'Guangzhou';
};
</script>
总结:Provide/Inject 是跨层级通信的利器。它让祖先组件能为其所有后代组件提供“依赖”,而不必关心层级有多深。非常适合管理全局状态(如用户信息、UI主题、首选语言等),但在非全局的普通组件中使用时需要谨慎,因为它会使组件间的耦合关系变得不那么明显。
其他重要方式简介
- Vuex / Pinia (状态管理库):用于复杂应用的全局状态管理。所有组件都可以集中式地存储和获取状态,是解决任意组件间通信的终极方案。Pinia 是当前Vue官方推荐的新标准。
- Event Bus (事件总线):创建一个全局的Vue实例来充当事件中心,用于组件间的事件发布和订阅。在Vue 3和组合式API中,由于其全局性难以跟踪和维护,已不再推荐使用,通常被 Pinia 或 Provide/Inject 替代。
- 模板引用 (
ref
Attribute):父组件通过ref
直接访问子组件的实例或DOM元素,可以直接调用其方法或操作数据。这是一种强制操作,应谨慎使用。
三、Sql 部分
8.sql : 请先删除姓名 , 年龄重复的记录取得不重复的数据根据姓名、年龄分组 , 再取出每组的Id最大值 , 然后将Id最大值之外的排除
ID | Name | Age | Salary |
---|---|---|---|
1 | yzk | 80 | 1000 |
2 | yzk | 80 | 2000 |
3 | tom | 20 | 20000 |
4 | tom | 20 | 20000 |
5 | im | 20 | 20000 |
好的,这是一个非常经典的SQL问题,用于处理重复数据。根据你的要求,我们需要保留每组(姓名、年龄)中ID最大的那条记录,并删除其他重复记录。
步骤分析:
- 分组:按
name
和age
分组。 - 找出最大ID:在每个分组中,找出最大的
id
值。 - 筛选:只选择那些
id
等于其所在分组中最大id
的记录。
方法一:使用子查询和 IN
clause
这是最直观易懂的方法。
DELETE FROM your_table_name
WHERE id NOT IN (SELECT MAX(id)FROM your_table_nameGROUP BY name, age
);
解释:
- 子查询
(SELECT MAX(id) FROM your_table_name GROUP BY name, age)
:这部分先执行。它根据name
和age
分组,并返回每组中最大的id
。对于你的示例数据,它会返回[2, 4, 5]
。 - 主查询
DELETE ... WHERE id NOT IN (...)
:删除那些id
不在刚才子查询结果集([2, 4, 5]
)中的所有记录。
执行后结果:
id | name | age | salary |
---|---|---|---|
2 | yzk | 80 | 2000 |
4 | tom | 20 | 20000 |
5 | im | 20 | 20000 |
方法二:使用关联子查询
这种方法性能通常更好,尤其对于大数据集。
DELETE FROM your_table_name t1
WHERE id < (SELECT MAX(t2.id)FROM your_table_name t2WHERE t1.name = t2.name AND t1.age = t2.age
);
或者更直接的:
DELETE FROM your_table_name t1
WHERE EXISTS (SELECT 1 FROM your_table_name t2WHERE t1.name = t2.name AND t1.age = t2.age AND t1.id < t2.id
);
解释(以第一个关联子查询为例):
- 对于主表
t1
中的每一行,子查询都会执行一次。 - 子查询会找到与
t1
当前行具有相同name
和age
的所有记录中最大的id
。 - 如果当前行的
id
小于 它所在分组中的最大id
,则满足WHERE
条件,这条记录就会被删除。
执行后结果:与方法一完全相同。
重要提示:先验证再删除!
在执行 DELETE
操作前,强烈建议先用 SELECT
语句验证哪些数据会被保留,哪些会被删除,以确保万无一失。
验证查询:
-- 查看将会被删除的记录
SELECT *
FROM your_table_name
WHERE id NOT IN (SELECT MAX(id)FROM your_table_nameGROUP BY name, age
);-- 查看将会被保留的记录 (即每组中id最大的记录)
SELECT *
FROM your_table_name
WHERE id IN (SELECT MAX(id)FROM your_table_nameGROUP BY name, age
);
总结
方法 | 优点 | 缺点 |
---|---|---|
方法一 (NOT IN) | 语法简单,易于理解和阅读 | 如果子查询返回的结果集非常大,NOT IN 的性能可能会下降。 |
方法二 (关联子查询) | 通常性能更好,尤其适合大数据表 | 语法稍微复杂一些。 |
对于你的需求,两种方法都可以完美实现。建议根据数据量的大小和个人习惯进行选择。在生产环境中操作前,请务必做好数据备份。
9. 现在有以下三张表 , 请解答 :
1、Employees(员工表 )
- 字段:EmployeeId(主键),Name,DepartmentId
2、Deparments(部门表) - 字段: DepartmentId(主键),DepartmentName
3、Salaries(薪资表)
字段:SalaryId (主键) , EmployeeId , SalaryAmount , YearMonth
- 请编写sql语句找出当月薪资高于所在部门平均薪资的员工名单 (显示员工姓名、部门名称和薪资)
好的,这是一个典型的SQL问题,涉及到多表连接和子查询(或窗口函数)的使用。我们将使用关联子查询和内连接来解决它。
假设
YearMonth
字段表示年月,格式为 'YYYY-MM'
(例如 '2023-10'
)。我们需要查询的是指定月份的数据,这里我们假设要查询的月份为 '2023-10'
。
方法一:使用关联子查询计算部门平均薪资
这是最直观的方法。思路是:
- 先计算每个部门在指定月份的平均薪资。
- 然后将员工表、部门表、薪资表连接起来。
- 最后筛选出员工薪资大于其部门平均薪资的记录。
SELECT e.Name AS EmployeeName,d.DepartmentName,s.SalaryAmount
FROM Employees e
INNER JOIN Departments d ON e.DepartmentId = d.DepartmentId
INNER JOIN Salaries s ON e.EmployeeId = s.EmployeeId
WHERE s.YearMonth = '2023-10' -- 指定要查询的月份AND s.SalaryAmount > (-- 关联子查询:计算当前员工所在部门在指定月份的平均薪资SELECT AVG(s2.SalaryAmount)FROM Salaries s2INNER JOIN Employees e2 ON s2.EmployeeId = e2.EmployeeIdWHERE e2.DepartmentId = e.DepartmentId -- 与外层查询的部门ID关联AND s2.YearMonth = '2023-10' -- 与外层查询的月份关联)
ORDER BY d.DepartmentName, s.SalaryAmount DESC;
解释:
- 主查询:将三张表连接起来,获取员工姓名、部门名称和其当月的薪资。
- 关联子查询
(SELECT AVG(s2.SalaryAmount)...)
:这是关键部分。- 对于主查询中的每一行(即每一个员工记录),这个子查询都会执行一次。
- 它计算当前员工所在部门 (
e2.DepartmentId = e.DepartmentId
) 在指定月份 (s2.YearMonth = '2023-10'
) 的平均薪资。
- WHERE 条件:主查询的
WHERE
子句确保只处理指定月份的数据,并且只保留那些薪资大于其部门平均薪资的员工记录。
方法二:使用CTE (公共表表达式) 先计算部门平均薪资
这种方法使用 WITH
子句先创建一个临时结果集(部门平均薪资表),然后进行连接和比较,逻辑更清晰,性能也可能更好。
WITH DepartmentAvg AS (-- CTE:先计算出每个部门在指定月份的平均薪资SELECT e.DepartmentId,AVG(s.SalaryAmount) AS AvgSalaryFROM Salaries sINNER JOIN Employees e ON s.EmployeeId = e.EmployeeIdWHERE s.YearMonth = '2023-10'GROUP BY e.DepartmentId
)
SELECT e.Name AS EmployeeName,d.DepartmentName,s.SalaryAmount,da.AvgSalary -- 可选显示,用于对比
FROM Employees e
INNER JOIN Departments d ON e.DepartmentId = d.DepartmentId
INNER JOIN Salaries s ON e.EmployeeId = s.EmployeeId
INNER JOIN DepartmentAvg da ON e.DepartmentId = da.DepartmentId -- 连接部门平均薪资表
WHERE s.YearMonth = '2023-10'AND s.SalaryAmount > da.AvgSalary -- 筛选薪资高于平均值的员工
ORDER BY d.DepartmentName, s.SalaryAmount DESC;
解释:
- CTE (
DepartmentAvg
):首先,它像一个临时表,查询出在'2023-10'
月份,每个部门的平均薪资。 - 主查询:将员工表、部门表、薪资表以及刚才创建的临时平均薪资表
DepartmentAvg
连接起来。 - 筛选:直接比较每个员工的薪资 (
s.SalaryAmount
) 是否大于其部门的平均薪资 (da.AvgSalary
)。
总结与推荐
特性 | 方法一 (关联子查询) | 方法二 (CTE) |
---|---|---|
可读性 | 稍差,逻辑嵌套在内 | 更好,逻辑分步,更清晰 |
性能 | 对于大表可能较慢(每行执行一次子查询) | 通常更好,只需计算一次部门平均值 |
灵活性 | - | CTE的结果可以在查询中多次使用 |
推荐使用CTE (方法二),因为它结构清晰,易于理解和维护,并且在处理大量数据时通常有更好的性能。关联子查询则有助于理解查询的执行逻辑。