一、VUE
1、vue2生命周期
| 阶段名称 | 钩子函数 | 触发时机 | 用途 | 注意 |
|---|---|---|---|---|
| 创建前 | beforeCreate |
组件实例初始化之前 | 插件开发中的初始化任务 | 无法访问 data 和 methods
|
| 创建后 | created |
数据观测、计算属性、方法已初始化,但 DOM 未生成 | 异步请求数据(如 API 调用)、初始化非 DOM 操作 | 避免操作 DOM(需等待 mounted) |
| 挂载前 | beforeMount |
模板编译完成,虚拟 DOM 尚未渲染为真实 DOM | 渲染前对状态的最后修改 | 极少使用 |
| 挂载后 | mounted |
实例已挂载到 DOM,可访问 this.$el
|
操作 DOM、集成第三方库(如图表初始化) | 使用 this.$nextTick() 确保子组件渲染完成 |
| 更新前 | beforeUpdate |
数据变化后,虚拟 DOM 重新渲染前 | 获取更新前的 DOM 状态(如保存滚动位置) | 避免直接修改数据 |
| 更新后 | updated |
虚拟 DOM 重新渲染并应用更新后 | 执行依赖新 DOM 的操作(如调整布局) | 修改数据可能导致无限循环 |
| 销毁前 | beforeDestroy |
实例销毁前,仍完全可用 | 清理定时器、解绑事件、取消订阅(防止内存泄漏) | 需手动清理非 Vue 管理的资源 |
| 销毁后 | destroyed |
实例销毁后,所有指令和事件监听器已移除 | 执行最终清理操作 | 实例的所有绑定已解除 |
2、Vue3 与 Vue2 生命周期对比详解
1. 钩子函数命名规范
- Vue3:生命周期钩子统一添加
on前缀(如onMounted),需显式引入后使用。 - Vue2:直接使用选项式 API 中的钩子(如
mounted)。
2. beforeCreate 和 created 合并
- Vue3:通过
setup()函数替代这两个阶段,初始化逻辑直接写在setup中。 - Vue2:分别使用
beforeCreate和created钩子。
3. 卸载阶段语义化更名
| Vue2 钩子 | Vue3 钩子 | 行为描述 |
|---|---|---|
beforeDestroy |
onBeforeUnmount |
组件卸载前触发 |
destroyed |
onUnmounted |
组件卸载完成时触发 |
4. 新增调试钩子
-
onRenderTracked: 追踪响应式依赖的收集过程(开发模式) -
onRenderTriggered: 追踪数据变更触发的重新渲染(开发模式)
5. API 引入方式
- Vue3:需从
vue显式引入钩子函数:import { onMounted, onUpdated } from 'vue'
6. 完整生命周期对照表
| 阶段 | Vue2 钩子 | Vue3 钩子 |
|---|---|---|
| 初始化 | beforeCreate |
setup() 替代 |
created |
setup() 替代 | |
| 挂载 | beforeMount |
onBeforeMount |
mounted |
onMounted | |
| 更新 | beforeUpdate |
onBeforeUpdate |
updated |
onUpdated | |
| 销毁 | beforeDestroy |
onBeforeUnmount |
destroyed |
onUnmounted | |
| 调试 | - | onRenderTracked |
| - | onRenderTriggered |
7. 代码示例对比
Vue2 选项式 API
export default {
created() {
console.log('数据观测/事件初始化完成')
},
mounted() {
console.log('DOM 渲染完成')
},
beforeDestroy() {
console.log('实例销毁前清理操作')
}
}
Vue3 组合式 API
import { onMounted, onBeforeUnmount } from 'vue'
export default {
setup() {
// 替代 created
console.log('响应式数据初始化')
onMounted(() => {
console.log('DOM 挂载完成')
})
onBeforeUnmount(() => {
console.log('组件卸载前清理')
})
}
}
3、Vue 的父组件和子组件生命周期钩子函数执行顺序?
- 加载渲染过程: 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子mounted -> 父 mounted
- 子组件更新过程:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
- 父组件更新过程:父 beforeUpdate -> 父 updated
- 销毁过程:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
4、OptionsAPI(选项式 API) 与 ***positionAPI(组合式 API)
Options API
-
在Vue3之前,我们主要使用的是选项式API(Options API)。这种API的设计方式是基于对象的,我们将一个Vue实例的各个部分拆分成不同的选项,如data、methods、***puted、watch等,并在创建Vue实例时将它们作为选项传入。
-
选项式API的优点在于其结构清晰、易于理解和上手。每个选项都有其明确的职责,开发者只需关注自己需要实现的功能,而无需过多关心Vue内部的运行机制。这种开发方式对于小型到中型的应用来说是非常高效的。然而,随着应用规模的扩大和复杂度的增加,选项式API也暴露出了一些问题。当组件的逻辑变得复杂时,代码会变得难以维护和理解。由于数据和逻辑被分散在多个选项中,很难一眼看出它们之间的关系。此外,对于复用逻辑代码也存在一定的困难,因为逻辑代码往往与特定的data和methods紧密耦合。。Options API 的特点包括:
易于上手:Options API 的结构清晰,容易理解和学习,适合初学者入门。 逻辑分离:不同功能的代码被分离到不同的选项中,使得代码更易维护和阅读。 依赖注入:通过 this 上下文可以方便地访问到组件的属性和方法。 -
示例:
export default { data() { return { count: 0 }; }, methods: { increment() { this.count++; } }, mounted() { console.log('Mounted'); } }
***positionAPI
-
组合式API是 Vue.js 3.x 中引入的新特性,旨在解决选项式API 在复杂组件中难以维护的问题。组合式API允许将组件的逻辑按照功能相关性放在一起,而不是按照选项分散组织。组合式API 的特点包括:
逻辑复用:可以将逻辑抽取为可复用的函数,更方便地在不同组件之间共享逻辑。 代码组织:将相关逻辑放在一起,使得组件更加清晰和易于维护。 更好的类型推断:由于函数可以提供更多信息,TypeScript 在使用 ***position API 时能够提供更好的类型推断。 -
示例:
import { ref, onMounted } from 'vue'; export default { setup() { const count = ref(0); const increment = () => count.value++; onMounted(() => console.log('Mounted')); return { count, increment }; } } -
举个栗子:
-
选项式 API 就像你家里整理东西的抽屉:
每个抽屉专门放一类东西(比如一个抽屉放袜子,一个放证件)。
缺点:如果你想找一套衣服(上衣+裤子),得挨个翻不同的抽屉。
代码中:数据在 data 抽屉,方法在 methods 抽屉,生命周期在 mounted 抽屉…同一功能的代码分散在各处。 -
组合式 API 就像你收拾行李:
直接把一套衣服(上衣+裤子+袜子)叠好放一个包里,要用时整个包拿走。
代码中:同一个功能的所有代码(数据、方法、生命周期)都集中写在一起,方便维护和复用。
额外好处:你可以把常用的行李包(比如洗漱包)做好,以后出门直接复用,不会和其他行李搞混。
-
对比
Options类型的 API,数据、方法、计算属性等,集中在:data、methods、***puted中的,若想改动一个需求,就需要分别修改:data、methods、***puted,不便于维护和复用。
***position 可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。
5、vue3 setup
在 Vue3 中,setup 函数是一个新引入的概念,它代替了之前版本中的 data、***puted、methods 等选项。setup 是 Vue 3 组合式 API 的“大本营”,用来集中写组件的核心逻辑(数据、方法、生命周期等)。至于为什么用它,是因为它告别选项式 API 的代码分散问题,让同一功能的代码“扎堆”写在一起,方便维护和复用!。在setup中不用写 this.所有数据通过变量名直接访问。
以下是setup的特点:
- 更灵活的组织逻辑:setup 函数可以将相关逻辑按照功能进行组织,使得组件更加清晰和易于维护。不再受到 Options API 中选项的限制,可以更自由地组织代码。
- 逻辑复用:可以将逻辑抽取为可复用的函数,并在 setup 函数中进行调用,实现逻辑的复用,避免了在 Options API 中通过 mixins 或混入对象实现逻辑复用时可能出现的问题。
- 更好的类型推断:由于 setup 函数本身是一个普通的 JavaScript 函数,可以更好地与 TypeScript 配合,提供更好的类型推断和代码提示。
- 更好的响应式处理:setup 函数中可以使用 ref、reactive 等函数创建响应式数据,可以更方便地处理组件的状态,实现数据的动态更新。
- 更细粒度的生命周期钩子:setup 函数中可以使用 onMounted、onUpdated、onUnmounted 等函数注册组件的生命周期钩子,可以更细粒度地控制组件的生命周期行为。
- 更好的代码组织:setup 函数将组件的逻辑集中在一个地方,使得代码更易读、易维护,并且可以更清晰地看到组件的整体逻辑。
两种写法
1. 选项式写法(传统)
<script>
import { ref } from 'vue';
export default {
setup() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
}
}
</script>
2. <script setup> 语法糖写法
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => count.value++;
</script>
6、vue3 setup语法糖
直接在script标签中添加setup属性就可以直接使用setup语法糖了。
使用setup语法糖后,不用写setup函数,组件只需要引入不需要注册,属性和方法也不需要再返回,所有在 <script setup> 顶层声明的变量函数自动暴露给模板。
- 示例:
<template>
<my-***ponent @click="func" :numb="numb"></my-***ponent>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import my***ponent from '@/***ponent/my***ponent.vue';
//此时注册的变量或方法可以直接在template中使用而不需要导出
const numb = ref(0);
let func = ()=>{
numb.value++;
}
</script>
setup语法糖中新增的api
- defineProps:子组件接收父组件中传来的props
- defineEmits:子组件调用父组件中的方法
- defineExpose:子组件暴露属性,可以在父组件中拿到
defineProps
父组件代码
<template>
<my-***ponent @click="func" :numb="numb"></my-***ponent>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import my***ponent from '@/***ponents/my***ponent.vue';
const numb = ref(0);
let func = ()=>{
numb.value++;
}
</script>
子组件代码
<template>
<div>{{numb}}</div>
</template>
<script lang="ts" setup>
import {defineProps} from 'vue';
defineProps({
numb:{
type:Number,
default:NaN
}
})
</script>
defineEmits
子组件代码
<template>
<div>{{numb}}</div>
<button @click="onClickButton">数值加1</button>
</template>
<script lang="ts" setup>
import {defineProps,defineEmits} from 'vue';
defineProps({
numb:{
type:Number,
default:NaN
}
})
const emit = defineEmits(['addNumb']);
const onClickButton = ()=>{
//emit(父组件中的自定义方法,参数一,参数二,...)
emit("addNumb");
}
</script>
父组件代码
<template>
<my-***ponent @addNumb="func" :numb="numb"></my-***ponent>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import my***ponent from '@/***ponents/my***ponent.vue';
const numb = ref(0);
let func = ()=>{
numb.value++;
}
</script>
defineExpose
子组件代码
<template>
<div>子组件中的值{{numb}}</div>
<button @click="onClickButton">数值加1</button>
</template>
<script lang="ts" setup>
import {ref,defineExpose} from 'vue';
let numb = ref(0);
function onClickButton(){
numb.value++;
}
//暴露出子组件中的属性
defineExpose({
numb
})
</script>
父组件代码
<template>
<my-***p ref="my***ponent"></my-***p>
<button @click="onClickButton">获取子组件中暴露的值</button>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import my***p from '@/***ponents/my***ponent.vue';
//注册ref,获取组件
const my***ponent = ref();
function onClickButton(){
//在组件的value属性中获取暴露的值
console.log(my***ponent.value.numb) //0
}
//注意:在生命周期中使用或事件中使用都可以获取到值,
//但在setup中立即使用为undefined
console.log(my***ponent.value.numb) //undefined
const init = ()=>{
console.log(my***ponent.value.numb) //undefined
}
init()
onMounted(()=>{
console.log(my***ponent.value.numb) //0
})
</script>
7、在 Vue3 中引入组件主要有 全局注册 和 局部注册 两种方式,以下是具体实现和对比:
手动引入组件(非自动注册)
1. 全局注册(Global Registration)
在 main.ts 中一次性注册全局组件,适用于高频使用的公共组件(如按钮、弹窗)。
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
// 导入组件
import MyButton from '@/***ponents/MyButton.vue'
import MyModal from '@/***ponents/MyModal.vue'
const app = createApp(App)
// 全局注册组件
app.***ponent('MyButton', MyButton)
app.***ponent('MyModal', MyModal)
app.mount('#app')
- 特点:
全局可用,任何模板中直接使用 标签
适合基础组件,但可能导致打包体积冗余
局部注册(Local Registration)
在单个 .vue 文件中按需引入,适用于低频或专用组件。
1.使用 Options API(传统写法)
<!-- Parent***ponent.vue -->
<template>
<Child***ponent />
</template>
<script>
import Child***ponent from './Child***ponent.vue'
export default {
***ponents: { Child***ponent } // 局部注册
}
</script>
2.使用 <script setup> 语法糖(推荐)
<!-- Parent***ponent.vue -->
<template>
<Child***ponent />
</template>
<script setup>
// 直接导入即可使用,无需显式注册
import Child***ponent from './Child***ponent.vue'
</script>
- 特点:
组件仅在当前文件中可用
避免全局污染,更利于 Tree-shaking 优化
自动注册组件(Auto Registration)
1. 使用 Vite 的 Glob 导入(推荐)
动态扫描 ***ponents 目录下的所有 .vue 文件,批量全局注册。
// src/***ponents/auto-register.ts
import { App } from 'vue'
export default {
install(app: App) {
// 匹配 ***ponents 目录下所有 .vue 文件
const modules = import.meta.glob('@/***ponents/**/*.vue', { eager: true })
Object.entries(modules).forEach(([path, module]) => {
// 从文件路径提取组件名(如 MyButton.vue -> MyButton)
const name = path.split('/').pop()?.replace('.vue', '') || ''
app.***ponent(name, (module as any).default)
})
}
}
// main.ts 中调用
import autoRegister from '@/***ponents/auto-register'
app.use(autoRegister)
命名规则:
MyButton.vue → <my-button>(推荐小写短横线命名)
强制命名规范:可在注册逻辑中添加 PascalCase 转换
2. 使用 unplugin-vue-***ponents 插件(按需自动注册)
通过插件自动识别模板中的组件并动态导入(类似 Uniapp 的 Easy***)。
// vite.config.ts
import ***ponents from 'unplugin-vue-***ponents/vite'
export default defineConfig({
plugins: [
***ponents({
// 指定扫描目录(默认 src/***ponents)
dirs: ['src/***ponents'],
// 生成类型声明文件(支持TS)
dts: 'src/***ponents.d.ts'
})
]
})
- 特点:
无需手动导入,直接在模板中使用<My***ponent>
自动生成类型声明,完美支持 TypeScript
最佳实践选择
| 场景 | 推荐方案 |
|---|---|
| 高频基础组件(如按钮、输入框) | 全局手动注册 或 unplugin 插件 |
| 低频专用组件 | 局部注册 + <script setup>
|
| UI 库组件(如 Element Plus) | unplugin 插件 + 按需导入 |
| 旧项目迁移 | Vite Glob 自动注册 |
8、Vue2 和 Vue3 的区别
- 响应式原理:Vue2 使用 Object.defineProperty,Vue3 改用 Proxy(支持数组和深层对象监听)。
- API 设计:Vue3 引入 ***position API(逻辑复用更灵活),Vue2 使用 Options API。
- 性能优化:Vue3 的虚拟 DOM 更高效,支持 Tree-shaking(减少打包体积)。
- 生命周期:部分钩子重命名(如 beforeDestroy → beforeUnmount)。
- 新特性:Fragment(多根节点)、Teleport(传送组件)、Suspense(异步组件加载)。
- 全局 API:Vue3 通过 createApp 创建实例,避免全局污染。
9、Vue2/Vue3 全家桶
Vue2:
核心库:Vue.js
路由:Vue Router
状态管理:Vuex
构建工具:Vue CLI
Vue3:
核心库:Vue.js
路由:Vue Router
状态管理:Pinia(官方推荐,替代 Vuex)
构建工具:Vite 或 Vue CLI。
10、 Vue2 不能监听数组下标原因
- Vue 2 用的是 Object.defineProperty 劫持数据实现数据视图双向绑定。
- Object.defineProperty 是可以劫持数组的
const arr = [1, 2, 3, 4];
Object.keys(arr).forEach(function(key) {
Object.defineProperty(arr, key, {
get: function() {
console.log('key:' + key)
},
set: function(value) {
console.log('value:' + value)
}
});
});
arr[1];
arr[2] = 4;
- 真实情况:是 Object.defineProperty 可以劫持数组而 vue2 没有用来劫持数组。
- 原因:Object.defineProperty 是属性级别的劫持,如果按上面代码的方式去劫持数组,随着数组长度增加,会有很大的性能损耗,导致框架的性能不稳定,因此vue2 放弃一定的用户便捷性,提供了 $set 方法去操作数组,以最大程度保证框架的性能稳定。
11、vue 的通讯方式
通讯用于组件间数据传递与共享,vue 提供了多种方式解决该问题。
- vue中8种常规的通信方案:
通过 props 传递
通过 $emit 触发自定义事件
使用 ref
EventBus
$parent 或$root
attrs 与 listeners
Provide 与 Inject
Vuex
- 组件间通信的分类可以分成以下:
父子关系的组件数据传递选择 props 与 $emit进行传递,也可选择ref
兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递
祖先与后代组件数据传递可选择attrs与listeners或者 Provide与 Inject
复杂关系的组件数据传递可以通过vuex存放共享的变量
11、vue3 主流的通讯方式
defineProps、defineEmits、defineExpose、Pinia
12、为什么 vue 中的 data 是一个 function 而不是普通 object?
因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
13、watch 和 ***puted 有什么区别?
- ***puted:
计算属性: ***puted是用于创建计算属性的方式,它依赖于Vue的响应式系统来进行数据追踪。当依赖的数据发生变化时,计算属性会自动重新计算,而且只在必要时才重新计算。
缓存: 计算属性具有缓存机制,只有在它依赖的数据发生变化时,计算属性才会重新计算。这意味着多次访问同一个计算属性会返回相同的结果,而不会重复计算。
无副作用: 计算属性应当是无副作用的,它们只是基于数据的计算,并不会修改数据本身。
用于模板中: 计算属性通常用于模板中,以便在模板中显示派生数据。
必须同步:只对同步代码中的依赖响应。
- watch:
监听数据: watch用于监视数据的变化,你可以监视一个或多个数据的变化,以执行自定义的响应操作。
副作用操作: watch中的回调函数可以执行副作用操作,例如发送网络请求、手动操作DOM,或执行其他需要的逻辑。
不缓存: watch中的回调函数会在依赖数据变化时立即被触发,不会像***puted那样具有缓存机制。
用于监听数据变化: watch通常用于监听数据的变化,而不是用于在模板中显示数据。
支持异步:在检测数据变化后,可进行同步或异步操作。
我自己的理解watch 当数据变化后需要触发外部动作(如接口请求、DOM 操作)或处理异步任务时,而***puted 当需要实时同步计算且结果需直接显示时(如购物车总价、表单验证)
14、谈谈 ***puted 的机制,缓存了什么?
Vue.js 中的 ***puted 属性确实具有缓存机制,这个缓存机制实际上是指对计算属性的值进行了缓存。当你在模板中多次访问同一个计算属性时,Vue.js只会计算一次这个属性的值,然后将结果缓存起来,以后再次访问时会直接返回缓存的结果,而不会重新计算。
假设你有一个计算属性 fullName,它依赖于 firstName 和 lastName 两个响应式数据。当你在模板中使用 {{ fullName }} 来显示全名时,Vue.js会自动建立依赖关系,并在 firstName 或lastName发生变化时,自动更新fullName的值,然后将新的值渲染到页面上。
我的理解如果 ***puted 所依赖的响应式数据(如 data 中的属性或其他 ***puted 属性)没有发生变化,则无论多少次访问该 ***puted 属性,直接返回上一次的缓存值,不会重新计算,如果依赖发生变化了那么就重新计算。
15、为什么 ***puted 不支持异步
这个是 vue 设计层面决定的,***puted 的定义是,“依赖值改变***puted值就会改变”,所以这里必须是同步的,否则就可能 “依赖值改变但***puted值未改变了”,一旦***puted 支持异步,***puted 就违背定义了,会变得不稳定。相反,watch 的定义是,“监控的数据改变后,它做某件事”,那 watch 在监听变化后,做同步异步都可以,并不违背定义。
16、vue3 中 ref 和 reactive 的区别
1. 处理的数据类型不同
-
ref:
-
适合处理基本类型(数字、字符串、布尔值)。
-
也能处理对象或数组。
const num = ref(0); // 数字 ✅ (但要用 num .value) const obj = ref({ a: 1 }); // 对象 ✅(但要用 obj.value.a)
-
-
reactive:
-
只能处理对象或数组(不能直接处理基本类型)。
const state = reactive({ count: 0 }); // 对象 ✅ const list = reactive([1, 2, 3]); // 数组 ✅ const num = reactive(0); // ❌ 错误!
-
2. 使用方式不同
-
ref:
-
在 JS 中必须用 .value 访问或修改值。
-
在模板中自动解包,不用写 .value。
// JS 中 const count = ref(0); count.value = 1; // ✅ 修改值<!-- 模板中 --> <div>{{ count }}</div> <!-- 直接写 count,不用 .value -->
-
-
reactive:
-
直接访问属性,不用 .value。
const state = reactive({ count: 0 }); state.count = 1; // ✅ 直接修改属性
-
3. 如何保持响应性?
-
ref:
-
解构时会丢失响应性(比如 const { value } = count)。
-
但可以传递整个 ref 给其他函数或组件,保持响应性。
// 父组件 import { ref } from 'vue'; // 定义一个 ref const count = ref(0); // 定义一个函数,接收整个 ref function increment(counter) { counter.value++; // 直接修改 ref 的 value } // 调用函数,传递整个 ref increment(count); console.log(count.value); // 1 ✅(值被修改,且保持响应性)
-
-
reactive:
-
解构也会丢失响应性!
const state = reactive({ count: 0 }); const { count } = state; // ❌ count 不再响应式! -
解决办法:用 toRefs 转换
const { count } = toRefs(state); // ✅ count 是 ref,保持响应式
-
4. 替换对象时的区别
-
ref:可以直接替换整个对象。
const obj = ref({ a: 1 }); obj.value = { a: 2 }; // ✅ 替换整个对象 -
reactive:不能直接替换整个对象!
const state = reactive({ a: 1 }); state = { a: 2 }; // ❌ 错误!会破坏响应性 -
一句话总结
- 用 ref:处理基本类型,或想统一写法时(不怕写 .value)。
- 用 reactive:处理对象/数组,且想直接操作属性(不想写 .value)。
-
举个栗子 🌰
-
计数器(基本类型)→ 用 ref
const count = ref(0); const add = () => count.value++; -
表单对象(多个属性)→ 用 reactive
const form = reactive({ name: '小明', age: 18, submit() { /* 提交逻辑 */ } });
-
17、vue3 区分 ref 和 reactive 的原因
-
- 因为「基本类型」和「对象」的响应式实现方式不同
- 基本类型(数字、字符串等)本身是“不可变的”,Vue3 想要监听它的变化,必须把它包成一个对象(比如 { value: 0 }),这就是 ref 的由来。
- 对象本身是“可变的”,Vue3 可以直接用 Proxy 代理它的属性变化,所以直接用 reactive 处理更简单。
-
简单说:
- ref 是给「单个值」穿个马甲(包成对象),强行让它能变。
- reactive 是直接给「对象」装个监听器(Proxy),监听属性变化。
-
- 为了开发体验更灵活
- ref 的 .value 虽然麻烦,但统一了写法(不管数据是简单值还是对象,都用 .value 操作),适合简单场景。
- reactive 不用写 .value,直接操作属性,适合复杂对象(比如表单、配置项)。
-
举个栗子:
- 如果只有 reactive,处理一个数字也得写成 reactive({ value: 0 }),反而更啰嗦。
- 如果只有 ref,操作对象属性时一直要写 .value.xxx,代码会很难看。
-
- 避免开发者踩坑
- 基本类型用 reactive 会报错:比如 reactive(0) 直接无效,强制你用 ref,防止错误使用。
- 对象用 ref 需要写 .value:提醒你这是个响应式对象,避免和普通对象混淆。
-
类比:
- 就像药盒上贴标签,告诉你“这是外用药”还是“内服药”,防止用错。
-
一句话总结
- Vue3 区分 ref 和 reactive,是因为基本类型和对象的响应式实现原理不同,同时让开发者能根据场景选择更顺手的写法,少写 bug,多摸鱼 🐟。
18、vue3 为什么要用 proxy 替换 Object.defineproperty
Vue 3 在设计上选择使用 Proxy 替代 Object.defineProperty 主要是为了提供更好的响应性和性能。
Object.defineProperty 是在 ES5 中引入的属性定义方法,用于对对象的属性进行劫持和拦截。Vue 2.x 使用 Object.defineProperty 来实现对数据的劫持,从而实现响应式数据的更新和依赖追踪。
-
Object.defineProperty只能对已经存在的属性进行劫持,无法拦截新增的属性和删除的属性。这就意味着在 Vue 2.x 中,当你添加或删除属性时,需要使用特定的方法(Vue.set 和 Vue.delete)来通知 Vue 响应式系统进行更新。这种限制增加了开发的复杂性。 -
Object.defineProperty的劫持是基于属性级别的,也就是说每个属性都需要被劫持。这对于大规模的对象或数组来说,会导致性能下降。因为每个属性都需要添加劫持逻辑,这会增加内存消耗和初始化时间。 - 相比之下,Proxy 是 ES6 中引入的元编程特性,可以对整个对象进行拦截和代理。Proxy 提供了更强大和灵活的拦截能力,可以拦截对象的读取、赋值、删除等操作。Vue 3.x 利用 Proxy 的特性,可以更方便地实现响应式系统。
- 使用 Proxy 可以解决 Object.defineProperty 的限制问题。它可以直接拦截对象的读取和赋值操作,无需在每个属性上进行劫持。这样就消除了属性级别的劫持开销,提高了初始化性能。另外,Proxy 还可以拦截新增属性和删除属性的操作,使得响应式系统更加完备和自动化。
19、Vue 与 React 的区别
- 设计理念:
Vue:渐进式框架,内置路由/状态管理。
React:库性质,依赖社区生态(如 React Router/Redux)。
语法:Vue 用模板,React 用 JSX。
响应式:Vue 自动追踪依赖,React 是状态驱动需手动 setState 或使用 Hooks。
打包体积:Vue3 更小(Tree-shaking),React + React DOM 约 40KB+(gzip)。
20、Vue Router 3.x Hash vs History 模式
- Hash 模式:
URL 带 #,通过 hashchange 监听路由变化。
无需后端支持,兼容性好。 - History 模式:
基于 history.pushState,URL 更简洁。
需服务器配置(如 Nginx 的 try_files $uri $uri/ /index.html)。
21、Vue2 的 $nextTick
- 作用:在下次 DOM 更新循环后执行回调,用于获取更新后的 DOM。
- 原理:基于微任务(如 Promise.then)或宏任务(如 setTimeout)实现异步队列。
this.$nextTick(() => {
// DOM 已更新,可以安全操作
const element = document.getElementById('my-element');
console.log(element.offsetHeight);
});
22、Vue2 数组变更刷新
- 限制:
直接通过索引修改(如 arr = 1)或修改长度(arr.length = 0)不会触发视图更新。 - 解决方案:
使用变异方法:push、pop、splice 等。
Vue.set(arr, index, newValue) 或 this. s e t ( a r r , i n d e x , n e w V a l u e ) 。或者使用 t h i s . set(arr, index, newValue)。或者使用this. set(arr,index,newValue)。或者使用this.forceUpdate强制刷新
23、watch 怎么深度监听对象变化?为什么要深度监听默认监听不行吗?
-
设置deep: true来启用深度监听
watch: { myObject: { handler(newVal, oldVal) { console.log('对象发生变化'); }, deep: true, // 设置 deep 为 true 表示深度监听 } } -
问题所在:默认监听的局限性
- 对象深层属性变化无法被检测
export default { data() { return { user: { name: '张三', address: { city: '北京', street: '朝阳路' } } } }, watch: { user: { handler(newVal) { console.log('user 变化了'); } // 默认情况下,只有 user 被整体替换时才会触发 } }, methods: { updateUserCity() { // ❌ 这里不会触发 watch! this.user.address.city = '上海'; // ✅ 只有这种方式会触发 // this.user = { ...this.user, address: { ...this.user.address, city: '上海' } }; } } } ``` 2. 数组元素变化无法被检测 ```javascript export default { data() { return { list: ['a', 'b', 'c'] } }, watch: { list: { handler(newVal) { console.log('list 变化了'); } // 默认情况下,数组元素变化不会触发 } }, methods: { updateArray() { // ❌ 这些都不会触发 watch! this.list[0] = 'x'; // 修改元素 this.list.length = 0; // 修改长度 // ✅ 只有这些会触发 // this.list.push('d'); // Vue 重写的数组方法 // this.list = ['x', 'y']; // 整体替换 } } }
解决方案:深度监听
-
开启深度监听
export default { data() { return { user: { name: '张三', profile: { age: 25, hobbies: ['篮球', '音乐'] } } } }, watch: { user: { handler(newVal, oldVal) { console.log('user 或其嵌套属性变化了'); // 现在无论修改 user.name 还是 user.profile.age 都会触发 }, deep: true, // ⭐ 关键:开启深度监听 immediate: true // 可选:立即执行一次 } }, methods: { testDeepWatch() { // ✅ 所有这些现在都会触发 watch: this.user.name = '李四'; this.user.profile.age = 30; this.user.profile.hobbies.push('阅读'); } } } -
深度监听的工作原理
// 简化版原理:Vue 会递归遍历对象的所有属性 function enableDeepWatch(obj, watcher) { // 遍历对象的所有属性 for (let key in obj) { if (obj.hasOwnProperty(key)) { const value = obj[key]; // 如果是对象或数组,递归设置响应式 if (typeof value === 'object' && value !== null) { // 为嵌套属性也创建依赖收集 defineReactive(value); enableDeepWatch(value, watcher); // 递归 } } } }
实际应用场景
场景1:表单的复杂嵌套数据
export default {
data() {
return {
formData: {
basic: {
name: '',
age: ''
},
contact: {
phone: '',
address: {
province: '',
city: ''
}
}
}
}
},
watch: {
formData: {
handler(newVal) {
// 监听整个表单的任何变化,用于自动保存等
this.autoSave();
},
deep: true,
immediate: true
}
}
}
场景2:监控复杂配置对象
export default {
data() {
return {
chartConfig: {
title: { text: '图表', show: true },
xAxis: { data: [] },
yAxis: { type: 'value' },
series: [{ data: [], type: 'line' }]
}
}
},
watch: {
chartConfig: {
handler() {
// 配置的任何变化都重新渲染图表
this.renderChart();
},
deep: true
}
}
}
性能考虑和替代方案
问题:深度监听可能性能开销大
export default {
watch: {
hugeObject: {
handler() {
// 如果 hugeObject 很大,深度监听会递归所有属性
},
deep: true // ⚠️ 对大对象可能有性能问题
}
}
}
解决方案:精确监听
export default {
watch: {
// 方案1:只监听特定嵌套属性
'user.profile.age'(newVal) {
console.log('年龄变化:', newVal);
},
// 方案2:监听多个关键路径
'user.name': function(newVal) {
console.log('姓名变化:', newVal);
},
// 方案3:使用计算属性作为中介
userBasicInfo() {
// 在 ***puted 中返回需要监听的部分
console.log('用户基本信息变化');
}
},
***puted: {
userBasicInfo() {
return {
name: this.user.name,
age: this.user.profile.age
};
}
}
}
总结
-
为什么 watch 需要深度监听?
-
引用类型特性:对象和数组是引用类型,直接修改嵌套属性不会触发 setter
-
响应式系统限制:Vue 2 默认只监听属性值的直接变化
-
实际业务需求:复杂数据结构的任何变化都需要被感知
-
开发体验:避免手动处理深层数据变化的繁琐工作
使用建议:
-
小到中等规模对象:直接使用 deep: true
-
大型对象或性能敏感场景:使用精确监听路径
-
数组变化:考虑使用 Vue 重写的数组方法或 deep: true
24、vue2 删除数组用 delete 和 Vue.delete 有什么区别?
delete:
-
delete是JavaScript的原生操作符,用于删除对象的属性。当你使用delete删除数组的元素时,元素确实会被删除,但数组的长度不会改变,被删除的元素将变成undefined。 -
delete操作不会触发Vue的响应系统,因此不会引起视图的更新。
const arr = [1, 2, 3];
delete arr[1]; // 删除元素2
// 现在 arr 变成 [1, empty, 3]
Vue.delete:
-
Vue.delete是Vue 2提供的用于在响应式数组中删除元素的方法。它会将数组的长度缩短,并触发Vue的响应系统,确保视图与数据同步。 - 使用
Vue.delete来删除数组元素,Vue会正确追踪更改,并在视图中删除相应的元素。
const arr = [1, 2, 3];
Vue.delete(arr, 1); // 删除元素2
// 现在 arr 变成 [1, 3]
25、Vue3.0 编译做了哪些优化?
- 静态树提升(Static Tree Hoisting): Vue 3.0 引入了静态树提升优化,它通过分析模板并检测其中的静态部分,将静态节点提升为常量,减小渲染时的开销。可显著降低渲染函数的复杂性,减少不必要的运行时开销。
- 源码优化: Vue 3.0 在编译器的源码生成方面进行了优化,生成的代码更加精简和高效。这有助于减小构建后的包的体积,提高运行时性能。
- Patch Flag: Vue 3.0 引入了 Patch Flag,它允许 Vue 在渲染时跳过不需要更新的节点,从而进一步提高性能。Patch Flag 为 Vue 提供了一种方法来跟踪哪些节点需要重新渲染,以及哪些节点可以被跳过。
- Diff 算法优化: Vue 3.0 使用了更高效的Virtual DOM diff算法,与Vue 2相比,减少了不必要的虚拟节点创建和比对,提高了渲染性能。
- 模板嵌套内联: Vue 3.0 允许在模板中内联子组件的模板,从而避免了运行时编译。这有助于减小构建后的包的大小,提高初始化性能。
- 模板块提取: Vue 3.0 允许在编译时将模板块提取到独立的模块中,这有助于代码分割和按需加载,从而减小初始化时需要加载的代码量。
- 更好的类型支持: Vue 3.0 支持更好的类型推断,借助TypeScript等类型检查工具,可以提供更好的开发体验和更强的类型安全性。
26、问题:Vue3.0 新特性 —— ***position API 与 React.js 中 Hooks 的异同点
相似点:
-
核心目标一致:解决「逻辑复用」难题
- Vue ***position API:通过 useXXX() 函数(比如 useCounter())把相关逻辑打包成块,哪里需要就“搬”到哪里。
- React Hooks:通过自定义 Hook(比如 useCounter())复用逻辑,直接插到组件里就能用。
说白了:两者都能像拼乐高一样,把代码块随意组合复用,告别“复制粘贴”大法。
-
告别「类组件」,拥抱函数式
- Vue:用 setup() 函数替代 data、methods 等选项,所有逻辑写成函数。
- React:函数组件 + Hooks 取代类组件,不用再写 this 和生命周期方法。
说白了:以前用类写的复杂组件,现在都能用函数搞定,代码更简洁,脑子更清醒。
-
状态管理:让数据跟着逻辑走
- Vue:用 ref、reactive 定义响应式数据,数据一变,视图自动更新。
- React:用 useState、useReducer 管理状态,状态变化触发组件重新渲染。
说白了:两者都让“数据驱动视图”,把数据和操作数据的逻辑放在一起,不再东一块西一块。
-
副作用处理:集中管理「搞事情」的代码
- Vue:用 watch、watchEffect 监听数据变化,处理副作用(比如调接口)。
- React:用 useEffect 统一处理副作用(比如订阅、调接口)。
说白了:以前散落在生命周期里的“搞事情”代码(如 ***ponentDidMount),现在都能集中管理,一目了然。
-
代码组织:把「相关的东西」放一起
- Vue:在 setup() 里,可以把“用户登录”、“表单验证”等逻辑各自打包成块。
- React:在函数组件里,用多个 Hooks 把“计数”、“动画”等逻辑拆分成独立单元。
说白了:以前按选项(data、methods)分类,现在按功能分类,改代码不用上下乱跳。
一句话总结:
***position API 和 React Hooks 就像麦当劳和肯德基,虽然做法不同(响应式 vs 状态驱动),但核心目标都是让开发者吃上更香(复用逻辑)、更爽(代码清晰)的汉堡(写代码)!
不同点:
-
响应式原理不同
-
Vue ***position API:
基于“响应式系统”,数据变化自动触发更新。
(比如用 ref、reactive 定义数据,修改时视图自动跟着变,不用手动触发。) -
React Hooks:
基于“状态 + 副作用”,数据变化需要手动触发重新渲染。
(比如用 useState 定义数据,改完数据后,React 会自动重新执行组件函数来更新视图,但依赖闭包,容易遇到“过期值”问题。)
-
-
代码组织逻辑不同
-
Vue ***position API:
在 setup 函数里,可以把相关逻辑的变量、方法、计算属性等写在一起,像一个乐高积木块。
(比如把用户登录的逻辑集中在一个 useAuth 函数里,清晰隔离。) -
React Hooks:
逻辑分散在多个 useState、useEffect 中,需要靠开发者自己拆分组合。
(比如一个功能可能要用到多个 useEffect,代码容易分散在不同位置。)
-
-
对生命周期的依赖不同
-
Vue ***position API:
生命周期钩子(如 onMounted)可以直接写在 setup 里,但更多时候不需要关心生命周期,因为响应式系统自动跟踪依赖。
(比如一个数据变了,用到它的视图会自动更新,不用手动监听。) -
React Hooks:
重度依赖 useEffect 来模拟生命周期(如组件挂载、更新、卸载),需要手动管理依赖数组。
(比如忘记写依赖项,可能导致闭包问题,拿到旧值。)
-
-
条件限制不同
-
Vue ***position API:
没有条件限制,可以在任何地方写逻辑。
(比如在 if 语句里定义 ref,完全没问题。) -
React Hooks:
必须遵守“不能在条件、循环、嵌套函数中调用 Hooks”的规则。
(比如在 if 里写 useState,React 会直接报错。)
-
-
复用逻辑的方式不同
-
Vue ***position API:
通过组合函数(如 useXXX())返回响应式数据和方法,直接使用即可。
(复用逻辑像拼积木,拿来就能用。) -
React Hooks:
通过自定义 Hook(如 useXXX())返回状态和方法,但每次调用 Hook 会创建独立的状态。
(复用逻辑时,每个组件实例的状态是隔离的。)
-
一句话总结
Vue ***position API 像自动挡汽车,响应式系统帮你处理依赖和更新,代码可以自由组织;
React Hooks 像手动挡汽车,灵活但需要自己管理状态更新和副作用,还要遵守严格的规则。
选哪个?看你是喜欢省心(Vue)还是追求极致控制(React)!
26、Vue-Router 3.x hash模式 与 history模式 的区别
Hash 模式(默认):利用 #号使用 hash 来模拟一个完整的 URL,如:http://xxx.***/#/path/to/route。
History 模式:利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法来完成 URL 跳转而无须重新加载页面。服务端增加一个覆盖所有情况的候选页面,如果 URL 匹配不到任何资源,则返回这个页面。
27、什么是虚拟dom
- “在Vue的响应式系统中,数据变化会触发组件级别的重新渲染。如果一个组件包含大量DOM节点,比如1000个dom,当我只修改了期中一个dom的数据时,也会导致整个组件重新生成,响应式更新只能更改到组件层级无法精准找到是那个dom发生变化。所以就有了虚拟DOM,虚拟dom就是在初始渲染时生成了一个用JS 对象 创建虚拟 DOM,当有数据改变的时候又会生成新的虚拟 DOM 树,新旧dom通过Diff 算法进行对比,找出差异(可以通过设置key来提高对比速度减少无意义对比),对比完成后将新的内容一次提交更新真实dom避免频繁操作dom造成回流和重绘,浪费性能,还有就是如果dom树对比发现新旧节点的标签类型或组件类型不同时就会变成直接销毁旧子树(解除旧节点的 事件监听 和 数据绑定、递归移除旧子树对应的 真实 DOM 节点、触发组件 生命周期钩子),新树替换旧树根据新虚拟 DOM 节点递归生成 真实 DOM 节点设置新节点的 属性(如 class、style)和 事件监听(如 addEventListener),由于出现销毁和重新创建所以会造成高额的开销,也会触发回流和重绘。通过这些操作就实现了从’组件级别’的更新粒度,细化到了’具体DOM节点’级别的更新粒度,大大提升了性能。还有就是通过虚拟”
- Vue3 的虚拟 DOM 通过 算法优化(双端 Diff、Patch Flag)、静态提升、事件侦听器缓存 等策略,显著降低了渲染开销和内存占用,同时结合 Fragments 支持 和 Tree Shaking 优化了开发体验。这些改进使得 Vue3 在复杂应用场景下(如动态列表、高频交互)的渲染性能远超 Vue2
28、算法优化、静态提升、事件侦听器缓存、Fragments、Tree Shaking
一、算法优化
-
1.双端 Diff 算法:
双端指针策略:对比新旧虚拟 DOM 时,同时从首尾向中间遍历,减少非必要节点比较次数,提升动态列表渲染效率。
最长递增子序列算法:针对动态子节点顺序调整场景,通过数学方法计算最小移动次数,避免全量重建。 -
2.Patch Flag(静态标记)
在编译阶段标记动态属性(如文本、class、style),Diff 时仅对比带标记的节点,跳过全量遍历。
例如:动态文本节点标记为 1/* TEXT */,仅需检查文本内容变化。
二、静态提升(HoistStatic)
- 原理:将模板中无动态绑定的静态节点(如固定文本、无响应式数据的元素)提取为常量,避免每次渲染重复创建。
效果: - 内存占用降低:静态节点仅初始化一次,后续复用;
- 减少计算开销:跳过 Diff 流程中的静态节点对比。
三、事件侦听器缓存(CacheHandlers)
- 机制:对动态绑定的事件处理函数(如 @click)进行缓存,避免每次渲染生成新函数对象。
- 优势:减少内存消耗和垃圾回收(GC)压力;避免因函数引用变化触发不必要的子组件更新。
四、Fragments 支持
- 功能:允许组件模板包含多个根节点(如 ),无需外层包裹冗余元素。
- 意义:简化布局结构,提升代码可读性和灵活性。
五、Tree Shaking 优化
- 实现:虚拟 DOM 相关代码模块化,构建时通过静态分析剔除未使用的功能(如未启用的过渡动画)。
- 效果:减少最终打包体积,提升应用加载速度。
29、vue3中如何引入react18封装的组件呢
方法:将 React 组件封装为 Web ***ponents
步骤 1:创建 React Web ***ponent 封装器
使用 @lit/react 或自定义封装方法将 React 组件转换为 Web ***ponent。
# 创建 React 项目(如果尚未创建)
npx create-react-app my-react-***ponent --template typescript
cd my-react-***ponent
npm install @web***ponents/web***ponentsjs @lit/react
// src/ReactCounter.tsx
import React, { useState } from 'react'
import { create***ponent } from '@lit/react'
import { html, css, LitElement } from 'lit'
// 1. 创建 Lit 元素封装 React 组件
class ReactCounterWrapper extends LitElement {
static styles = css`
div { border: 1px solid blue; padding: 10px; }
`
@property({ type: Number }) count = 0
@eventOptions({}) private _onIncrement!: () => void
render() {
return html`
<div>
<button @click=${() => this.dispatchEvent(new CustomEvent('increment'))}>
React Count: ${this.count}
</button>
</div>
`
}
}
// 2. 将 React 组件与 Lit 元素绑定
const ReactCounter = create***ponent({
react: React,
elementClass: ReactCounterWrapper,
tagName: 'react-counter',
events: {
onIncrement: 'increment'
}
})
export default ReactCounter
步骤 2:构建为独立 JS 文件
配置构建工具(如 Vite)生成浏览器兼容的包:
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: 'src/ReactCounter.tsx',
formats: ['es'],
fileName: 'react-counter'
}
}
})
构建命令:
vite build
将生成的 dist/react-counter.js 复制到 Vue 项目的 public 目录。
步骤 3:在 Vue 中引入 Web ***ponent
<!-- Vue***ponent.vue -->
<template>
<div>
<react-counter
ref="reactCounterRef"
:count="count"
@increment="handleIncrement"
/>
<p>Vue 中的计数:{{ count }}</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const count = ref(0)
const reactCounterRef = ref(null)
// 确保 Web ***ponent 加载完成
onMounted(() => {
if (reactCounterRef.value) {
// 动态更新属性
reactCounterRef.value.count = count.value
}
})
// 监听事件
const handleIncrement = () => {
count.value += 1
}
</script>
<!-- 在入口文件引入 JS -->
<script>
// main.js
import './public/react-counter.js'
</script>
30、pinia和vuex有什么区别?
- Pinia 和 Vuex 均为 Vue 的状态管理工具,核心区别在于:Pinia 专为 Vue 3 设计,采用模块化架构,允许直接修改状态且无需 mutations,原生支持 TypeScript 并简化了异步操作(仅用 actions 统一处理),体积更小(约 1KB);而 Vuex 基于全局单例模式,严格区分同步(mutations)和异步(actions),对 TypeScript 支持较弱,体积较大(约 10KB),更适合 Vue 2 或需要严格数据流控制的大型项目。
31、vue3为什么使用pinia?
- Pinia 作为 Vue 官方新一代状态管理工具,专为 Vue 3 设计,彻底简化了状态管理流程——通过移除 Vuex 中繁琐的 mutations、统一用 actions 处理同步/异步操作,原生深度集成 TypeScript 实现开箱即用的类型推断,同时依托 ***position API 实现更直观的响应式状态管理。其模块化架构天然规避命名空间冲突,体积仅 1KB(远小于 Vuex 的 10KB),完美适配 Vue 3 的轻量化与高效渲染特性,成为现代 Vue 应用开发的首选方案。
32、proxy能监听基础类型吗?
- 不能。Proxy 无法直接监听基本类型(如数字、字符串、布尔值),这是由 JavaScript 语言本身的特性决定的。
33、vue3 组合式api的响应原理
-
组合式 API 的原理 = 函数化逻辑组织方式 + 响应式系统依赖收集机制(Proxy + effect)
-
函数化逻辑组织方式
白话:就是把复杂的仓库管理流程,拆分成一个个独立的、功能明确的“工具包”。 以前的方式(选项式 API):就像把所有工具胡乱堆在一个大箱子里。螺丝刀、锤子、图纸混在一起。 你想找“包裹入库”的流程,得在“方法区”找到扫码枪,再去“数据区”找包裹列表,很麻烦。 现在的方式(组合式 API):你买了几个工具盒! 一个工具盒叫 use包裹管理(),里面只放:包裹列表数据、扫码枪、贴标签机。 另一个工具盒叫 use车辆调度(),里面只放:卡车数据、分配卡车的逻辑、装车规划图。 这样做的好处是: 功能集中:所有关于“包裹”的东西都在一个盒子里,一目了然。 可以复用:另一个仓库需要同样的“包裹管理”功能?直接把 use包裹管理() 这个工具盒拿过去用就行了! 灵活组合:你想建一个新仓库,需要“包裹管理”和“车辆调度”功能?把这两个工具盒拿过来拼在一起,仓库就搭好了! 所以,“函数化逻辑组织方式”就是:用函数的形式,把相关的数据和操作逻辑打包成一个独立的、可复用、可随意组合的“工具盒”。 -
响应式系统依赖收集机制(Proxy + effect)
白话:就是让仓库里的物品和显示屏上的数据“自动同步更新”。 现在你的“包裹管理”工具盒里有一个“包裹列表”(数据)和一个“实时显示屏”(effect,副作用)。 它是如何实现自动化的呢? Proxy(代理)- 给包裹贴上智能标签 你不是直接操作原始的包裹,而是给每个包裹都套上一个智能包装(Proxy)。这个智能包装自带“监控功能”。 依赖收集 / track(追踪)- 记录谁关心这个包裹 当你的实时显示屏(effect) 启动时,它需要读取“包裹列表”来显示信息。 只要它一读取某个包裹(比如“读取包裹A的数量”),包裹上的智能标签(Proxy) 就立刻记录下来:“哦豁!这个显示屏依赖我!得记在小本本上。” 触发更新 / trigger(触发)- 包裹一变,自动通知 现在,你通过扫码枪新入库了一个包裹(修改了数据)。智能标签(Proxy)的监控立刻被触发! 它马上翻开小本本:“都有谁依赖我来着?哦,是那个实时显示屏!” 然后它自动打电话(trigger) 给那个显示屏:“喂!我变了,你快更新一下!” 显示屏收到通知,就自动刷新最新的数据了。 所以,这个机制的核心就是: Proxy:给数据装上“监控器”。 effect:需要依赖数据才能运行的东西(比如显示屏、自动打包机)。 track:数据被读取时,监控器就记下“谁用了我”。 trigger:数据被修改时,监控器就通知“所有用了我的人”更新。 数据访问 → 触发 get(Proxy)→ 调用 track → 建立effect记录依赖。 数据修改 → 触发 set (Proxy)→ 调用 trigger → 通知所有建立依赖的 effect 更新 ┌─────────┐ 读取数据 ┌─────────┐ 调用 track ┌─────────┐ │ effect │ -----------> │ Proxy │ -----------> │ track │ │ (副作用)│ │ (代理层) │ │(依赖收集)│ └─────────┘ └─────────┘ └─────────┘ ^ │ │ │ │ 修改数据 │ 记录依赖关系 │ ↓ ↓ │ ┌─────────┐ 调用 trigger ┌─────────┐ │ │ Proxy │ -----------> │ trigger │ └────────────────────│ (代理层) │ │(更新触发)│ effect重新执行 └─────────┘ └─────────┘
-
终极总结:两者合二为一
组合式 API = 用“工具盒”组织代码 + 让工具盒里的数据“自动化”
-
你用 use包裹管理() 和 use车辆调度() 这些“工具盒”(函数)把逻辑整理得清清楚楚。(函数化组织)
-
每个工具盒里的数据(包裹列表、卡车信息)都是智能的、被监控的(Proxy)。
-
依赖于这些数据的部分(界面、计算器)都是自动同步的(effect)。数据一变,它们自动更新,你完全不用手动操作。
这样一来,你就能轻松搭建出功能清晰、高度自动化、且易于复用的复杂应用了!这就是组合式API的魅力所在。
34、vue的订阅发布有哪些
响应式系统:reactive/ref 内部基于 track/trigger 实现依赖收集与更新。
组件通信:props/emit、provide/inject。
全局通信:事件总线(EventBus/mitt)、状态管理(Vuex/Pinia)。
工具函数:watch/watchEffect 用于订阅数据变化。
35、track/trigger 是如何实现的
track(追踪、收集依赖)
白话: “你用了我的数据,我得拿小本本记下来,以后我好找你。”
什么时候发生? 当你读取(使用)一个响应式数据(比如 console.log(state.count) 或在模板里用了 {{ state.count }})的时候。
发生了什么? Vue 会立刻在背后偷偷执行 track 操作。它有一个“全局小本本”,会记录下:
哪个数据被读取了(比如 state.count)
是谁读取了它(比如当前正在运行的组件渲染函数、或者一个 watchEffect 函数)
记下来干嘛? 这样 Vue 就知道这个数据(state.count)和这个函数(比如渲染函数)之间存在一种 “依赖” 关系。
trigger(触发、通知更新)
白话: “我变了!小本本上所有用过我的人,你们赶紧去更新!”
什么时候发生? 当你修改一个响应式数据(比如 state.count = 5)的时候。
发生了什么? Vue 会立刻在背后执行 trigger 操作。它马上翻出之前的“全局小本本”,查找:
所有依赖了这个数据的地方(比如之前记录的那个渲染函数)
找到后干嘛? Vue 会自动执行所有找到的函数。这意味着组件会重新渲染、watchEffect 会重新运行,视图也就自动更新了。
它们是怎么实现的?(超级简化版)
用 Proxy 挖陷阱:当你用 reactive() 包裹一个对象时,Vue 会用 Proxy 把它包起来。这个 Proxy 设置了“陷阱”(拦截器),专门抓你的“获取”(get)和“修改”(set)操作。
get 陷阱里调用 track:只要你一读取某个属性(obj.count),get 陷阱就触发,立马执行 track(target, 'count'),把当前正在运行的函数(依赖)记到 count 名下。
set 陷阱里调用 trigger:只要你一修改某个属性(obj.count = 5),set 陷阱就触发,立马执行 trigger(target, 'count'),去小本本里找到所有记在 count 名下的函数,挨个执行一遍。
总结:
track 是在 get(读)的时候,记下谁依赖了我。
trigger 是在 set(写)的时候,通知所有依赖我的人更新。
这就是 Vue 响应式魔法背后的核心机制,一切都是自动的!
为什么不能只用 Proxy 的 get/set,还需要 track/trigger?
-
"因为仅仅通过 key 无法建立精确的 effect-属性 依赖关系:
-
缺少执行上下文:在 Proxy 的 get 中,你不知道当前是哪个 effect 在读取属性
-
需要动态依赖收集:effects 可能有条件地依赖不同的属性
-
处理嵌套 effects:需要维护 effect 执行栈来处理嵌套场景
-
track 和 trigger 通过维护 activeEffect 和依赖映射表,解决了这些问题:
-
track:在属性读取时,记录"当前活跃的 effect 依赖这个属性"
-
trigger:在属性修改时,查找"所有依赖这个属性的 effects"并执行
这种设计让 Vue 3 能够智能地管理复杂的依赖关系,实现精确的响应式更新。"
这样的解释说明了为什么响应式系统需要比简单的 Proxy get/set 更复杂的机制!
track
track 函数的核心作用是在读取响应式数据时,建立数据与副作用函数(effect)之间的依赖关系。
// 全局的依赖存储仓库,用于存储所有响应式对象的依赖关系
const targetMap = new WeakMap()
export function track(target: object, key: unknown) {
// 如果当前没有活跃的 effect 或者不允许追踪,直接返回
if (!activeEffect || !shouldTrack) return
// 1. 获取 target 对应的 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
// 如果 target 还没有对应的 depsMap,就创建一个并存入 targetMap
targetMap.set(target, (depsMap = new Map()))
}
// 2. 获取 key 对应的 dep 集合
let dep = depsMap.get(key)
if (!dep) {
// 如果 key 还没有对应的 dep 集合,就创建一个 Set 并存入 depsMap
depsMap.set(key, (dep = new Set()))
}
// 3. 将当前活跃的 effect 添加到 dep 中
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
// 同时,将 dep 添加到 effect 的依赖列表中,用于后续清理
activeEffect.deps.push(dep)
}
}
关键点解析:
targetMap 结构:WeakMap<Target, Map<Key, Set>>,这是一个三层嵌套结构
activeEffect:全局变量,指向当前正在执行的副作用函数
依赖关系:当 effect 执行时,内部访问的响应式数据都会通过 track 与这个 effect 建立联系
trigger
trigger 函数的核心作用是在响应式数据变化时,找出所有依赖这个数据的 effect,并重新执行它们。
export function trigger(
target: object,
key: unknown,
type: TriggerOpTypes = TriggerOpTypes.SET
) {
// 1. 获取 target 对应的 depsMap
const depsMap = targetMap.get(target)
if (!depsMap) {
// 如果 target 没有被追踪过,直接返回
return
}
// 2. 收集需要触发的 effects
const effects: ReactiveEffect[] = []
// 添加直接关联的依赖
const dep = depsMap.get(key)
if (dep) {
effects.push(...dep)
}
// 处理数组的特殊情况
if (type === TriggerOpTypes.ADD && isArray(target)) {
// 数组添加元素时,需要触发 length 属性的依赖
const lengthDep = depsMap.get('length')
if (lengthDep) {
effects.push(...lengthDep)
}
}
// 3. 执行所有收集到的 effects
for (const effect of effects) {
if (effect !== activeEffect) { // 避免循环触发
if (effect.scheduler) {
// 如果有调度器,通过调度器执行
effect.scheduler()
} else {
// 否则直接执行
effect.run()
}
}
}
}
关键点解析:
性能优化:通过 effect !== activeEffect 避免在 effect 中修改自身依赖的数据导致的无限循环
调度器:effect.scheduler 允许自定义 effect 的执行时机,这是实现批量更新的基础
完整的工作流程示例
为了帮你更好地理解,这里有一个简化的示例展示它们如何配合工作:
// 1. 创建响应式对象
const state = reactive({ count: 0 })
// 2. 创建 effect
effect(() => {
console.log('Count:', state.count) // 读取 state.count,触发 track
})
// 3. 修改数据
state.count++ // 触发 trigger
执行流程:
-
effect 执行,activeEffect 指向这个函数
-
执行 console.log(state.count),触发 Proxy 的 get 陷阱
-
get 陷阱调用 track(target, ‘count’),建立依赖关系
-
state.count++ 触发 Proxy 的 set 陷阱
-
set 陷阱调用 trigger(target, ‘count’)
-
trigger 找到所有依赖 count 的 effects 并执行
设计思想总结
理解 track 和 trigger 的关键在于明白 Vue 3 响应式系统的核心思想:
-
精确依赖追踪:不像 Vue 2 那样递归追踪整个对象,Vue 3 只追踪实际被访问的属性
-
懒收集依赖:只有真正在 effect 中被访问的属性才会被追踪
-
高效更新:数据变化时,只重新执行真正依赖这个数据的 effect
这就是为什么 Vue 3 的响应式系统在性能和内存占用上都有显著提升。track 和 trigger 虽然代码简单,但它们背后的设计思想确实很精妙!
35、Vite vs Webpack 核心区别
36、Vite的底层组件
Vite底层主要依赖以下组件:
-
ESBuild - 用于依赖预构建,由Go编写,编译速度极快
-
Rollup - 用于生产环境打包,提供优秀的Tree-shaking
-
Koa - 开发服务器基于Koa框架
-
原生ES模块(ESM) - 利用浏览器原生支持实现按需加载
37、Vue2响应式原理:Object.defineProperty
-
首先gtter这个环节是如何产生的
- 我在data中定义一个 a:‘’,项目启动的时候会循环data里的变量 通过 Object.defineProperty 把它变成响应式数据,并为它创建一个 Dep(依赖管理器),当有地方使用这个数据a了就会产生一个watcher观察这个数据,因为使用了a那么就是触发触发Object.defineProperty里的getter,在getter里会把该数据的观察者watcher添加到dep中。
- dep是依赖收集器,如果a是报纸的话,那么dap就是订阅名单,可以在里面找到每个订阅的人
- watcher是订阅人,只不过一份报纸给多个人看,当我报纸更新的时候,通过dep去找到每个订阅的人告诉他们要更新报纸了
启动项目初始化流程
``` 应用启动 ↓ new Vue() 实例化 ↓ 初始化 data ↓ 遍历所有属性 → Object.defineProperty ← 只在这里执行! ↓ 建立 getter/setter ↓ 响应式系统就绪 ✅ ↓ ┌─────────────────────────────────────────┐ │ 运行时数据更新 │ │ │ │ vm.name = 'new value' → 触发 setter │ │ console.log(vm.name) → 触发 getter │ │ this.count++ → 触发 getter + setter │ └─────────────────────────────────────────┘ ```数据初始化
data() { return { a: '' // ← 只是一个普通数据 } }Vue 通过 Object.defineProperty 把它变成响应式数据,并为它创建一个 Dep(依赖管理器),但此时还没有 Watcher!
创建 Watcher 的时机
Watcher 是在使用数据的地方创建的:
情况1:在模板中使用
<template> <div>{{ a }}</div> <!-- 这里使用了 a --> </template>Vue 会为这个组件创建一个渲染 Watcher
情况2:在计算属性中使用
***puted: { ***putedA() { return this.a + '!'; // 这里使用了 a } }Vue 会为这个计算属性创建一个 计算属性 Watcher
完整的正确流程
// 步骤1:初始化数据 data: { a: '' } ↓ // Vue 为 a 创建响应式getter/setter + Dep Object.defineProperty(data, 'a', { get() { if (Dep.target) { // 如果有Watcher正在运行 dep.depend(); // 把这个Watcher加入到a的Dep中 } return value; }, set(newVal) { value = newVal; dep.notify(); // 通知所有Watcher更新 } }) // 步骤2:组件渲染,创建渲染Watcher new Watcher(***ponent, update***ponent); ↓ // Watcher执行渲染函数 update***ponent() { // 这里读取了 this.a } ↓ // 触发 a 的 getter ↓ // 此时 Dep.target = 渲染Watcher ↓ // a 的 getter 把【渲染Watcher】加入到【a的Dep】中更准确的比喻
-
数据 (a):像是一个出版社
-
Dep:出版社的订阅者名单
-
Watcher:像是读者
-
使用数据的地方:像是读者下单订阅的动作
总结
-
✅ 数据有自己的 Dep
-
✅ 使用数据的地方创建 Watcher
-
✅ Watcher 执行时触发数据的 getter
-
✅ getter 把当前 Watcher 加入到数据的 Dep 中
-
怎么修改了set试图就跟着刷新了
我的理解是 ,数据变化触发object.defineproperty的set,if (newVal === value) return;判断是否是该set是就触发dep.notify(),在notify里循环调用每个Watcher 的.update(),执行在Watcher 里调用run执行内部get方法执行 update***ponent()生成新虚拟DOM → 对比差异 → 更新真实DOM
1. 你修改数据
this.a = 'new value';2. 触发 Object.defineProperty 的 setter
set(newVal) { if (newVal === value) return; value = newVal; dep.notify(); // 【关键】通知所有依赖这个数据的 Watcher }3. Dep 通知所有 Watcher
class Dep { notify() { for (let i = 0; i < this.subs.length; i++) { this.subs[i].update(); // 调用每个 Watcher 的 update 方法 } } }4. Watcher 执行更新
class Watcher { update() { // 对于渲染 Watcher,这会触发重新渲染 this.run(); } run() { const value = this.get(); // 重新执行 getter 函数 // 对于渲染 Watcher,getter 就是 update***ponent } get() { Dep.target = this; const value = this.getter.call(this.vm); // 【关键】执行渲染函数! Dep.target = null; return value; } }5. 重新渲染组件
// 渲染 Watcher 的 getter 就是 update***ponent function update***ponent() { // 1. 生成新的虚拟DOM const vnode = vm._render(); // 2. 对比新旧虚拟DOM,更新真实DOM vm._update(vnode); }完整的链条
你修改 this.a = 'new value' ↓ 触发 a 的 setter ↓ dep.notify() ↓ 遍历所有订阅了 a 的 Watcher,调用 watcher.update() ↓ Watcher 执行 this.get() ↓ 执行 update***ponent() // 重新渲染! ↓ 生成新虚拟DOM → 对比差异 → 更新真实DOM ↓ 视图更新完成!关键点说明
为什么修改 set 视图就刷新了?
因为 setter 中调用了 dep.notify(),这个方法:
-
找到所有依赖这个数据的 Watcher(之前通过 getter 收集的)
-
通知它们执行更新
-
对于渲染 Watcher,更新就是重新执行渲染函数
-
重新渲染 = 生成新虚拟DOM + 更新真实DOM
举个例子
// 初始状态 data: { message: 'Hello' } template: `<div>{{ message }}</div>` // 流程: 1. 渲染时,渲染Watcher读取 this.message,被收集到 message 的 Dep 中 2. 你执行:this.message = 'World' 3. message 的 setter 被触发 → dep.notify() → 找到渲染Watcher → 调用 watcher.update() → 执行 update***ponent() → 重新读取 this.message (现在是 'World') → 生成新的虚拟DOM → 更新真实DOM显示 'World'所以本质上:修改 set → 触发通知 → 找到依赖的 Watcher → 重新执行渲染函数 → 视图更新!
-
二、react
1、react是什么?
- React 是一个用于构建用户界面的 JavaScript 库,由 Facebook(现 Meta)开发并开源。它专注于通过组件化的方式高效构建动态、交互式的 Web 和移动应用界面。以下是 React 的核心特点和应用场景:
2、React 的核心特性是什么?
- 虚拟DOM、组件化、单向数据流、JSX 语法、声明式语法。
3、虚拟DOM
- React 在内存中维护一个轻量级的虚拟 DOM,当数据变化时,先更新虚拟 DOM,再通过对比(Diffing Algorithm)找出实际需要更新的部分,最后高效更新真实 DOM。这减少了直接操作 DOM 的性能损耗。
4、组件化
- 将界面拆分为独立、可复用的组件(如按钮、表单、页面等),通过组合组件构建复杂 UI。
- 组件可管理自身状态(数据)和逻辑,提升代码复用性和可维护性。
5、单向数据流
- 单向数据流是前端框架(如 React、Vue 等)中常见的一种数据传递模式,核心思想是数据只能按照单一方向流动,通常从父组件传递到子组件,且子组件不能直接修改父组件的数据。这种设计使得数据流更清晰、可预测,便于调试和维护。
6、JSX 语法
-
允许在 JavaScript 中直接编写类似 HTML 的代码,使组件结构更直观。例如:
function Button() { return <button className="primary">点击我</button>; }
7、声明式语法
- 我的理解声明式语法就是将命令式语法进行了优化升级,我们不用将每一步都写清楚比如我们之前创建一个dom需要一下这些操作document.getElementById,然后document.createElement在document.appendChild这样一步一步的去把每一步写清楚。
对比“命令式 vs 声明式”
-
命令式(How):
代码直接操作 DOM,详细描述每一步。
例子(原生 JavaScript 实现点击计数)// 1. 找到按钮和显示区域 const button = document.getElementById('btn'); const text = document.getElementById('count'); let count = 0; // 2. 监听点击事件,手动更新 DOM button.addEventListener('click', () => { count++; text.innerText = `点了 ${count} 次`; // 手动修改 DOM }); -
声明式(What):
代码描述“UI 应该长什么样”,状态变化时 React 自动更新 DOM。
例子(React 实现点击计数):function Counter() { const [count, setCount] = useState(0); // 声明状态 return ( <button onClick={() => setCount(count + 1)}> 点了 {count} 次 {/* React 自动更新 */} </button> ); }
React 声明式的核心体现”
-
用 JSX 描述 UI:
直接写类似 HTML 的结构,比如 点了 {count} 次,而不是拼接字符串或操作 DOM。 -
状态驱动视图:
更新数据(如 setCount)后,React 自动计算如何高效更新 DOM。
你只需关心“数据是什么”,不用手动操作 DOM。 -
虚拟 DOM 的抽象层:
React 内部通过虚拟 DOM 对比(Diffing)找出变化部分,再批量更新真实 DOM,隐藏了具体操作步骤。
声明式的好处”
- 代码更简洁:不用写 document.getElementById、element.appendChild 这类繁琐操作。
- 可维护性更强:UI 和逻辑绑定在组件内,改代码时不用全局搜索 DOM 操作。
- 性能优化自动化:React 的 Diff 算法会自动跳过不必要的 DOM 更新。
一句话总结”
声明式语法本质上就是把命令式语法中那些繁琐的底层操作(How),封装成更简洁的描述性表达(What),让开发者不用再手动处理每一步细节。
-
JSX 是语法糖,让写 React 元素更直观
-
声明式 是思想,关注描述最终结果而非具体步骤
-
React 使用 JSX 来实现声明式 UI 开发
-
你可以不用 JSX 但依然保持声明式,只是代码会更冗长
没有 JSX 的声明式”
即使不用 JSX,React 仍然可以是声明式的:
// 使用 React.createElement 的声明式
function Counter() {
const [count, setCount] = useState(0);
return React.createElement(
'div',
null,
React.createElement('p', null, 'Count: ', count),
React.createElement(
'button',
{ onClick: () => setCount(count + 1) },
'Increment'
)
);
}
8、react全家桶
- React Router、Redux、Ant Design、Webpack
8、类组件 vs 函数组件的区别?
- 生命周期、状态管理(类用 this.state,函数用 useState)、性能优化方式。
9、什么是 JSX?它的作用是什么?
- JSX 是 JavaScript 的语法扩展,允许在 JavaScript 代码中编写类似 HTML 的结构,JSX 本身无法被浏览器直接执行,需通过工具(如 Babel)编译为标准的 JavaScript 代码。编译后的代码(React 元素)由 React 库处理,最终生成页面内容。
10、受控组件(Controlled ***ponent)和非受控组件(Uncontrolled ***ponent)的区别?
- 受控组件:表单数据由 React 组件的 state 完全控制,输入元素的 value 属性直接绑定到 state,并通过 onChange 事件同步更新状态;
- 由 DOM 自身管理,通过 ref 直接操作 DOM 节点获取值。初始值可通过 defaultValue 或 defaultChecked 设置,但后续修改不依赖 React 状态。
11、为什么列表渲染需要 key?
- key 帮助 React 识别元素的变化,优化虚拟 DOM 的 Diff 算法效率。
12、state 和 props 的区别?
- state 是组件内部管理的可变数据,props 是父组件传递给子组件的只读数据。
13、如何实现父子组件通信?
- 父传子:通过 props;子传父:父组件通过 props 传递回调函数给子组件调用。
14、组件的生命周期方法?
React组件的生命周期可以分为三个阶段:挂载阶段、更新阶段和卸载阶段。
- 挂载阶段包括constructor、render、***ponentDidMount等方法,用于初始化组件、渲染到真实DOM和处理副作用。
- 更新阶段包括should***ponentUpdate、render、***ponentDidUpdate等方法,用于控制组件的重新渲染和处理更新后的副作用。
- 卸载阶段包括***ponentWillUnmount方法,用于清理组件产生的副作用和资源。
15、如何实现兄弟组件或跨层级组件通信?
状态提升(Lifting State Up)、Context API、Redux 等状态管理库。
16、什么是状态提升(Lifting State Up)?
- 将多个组件需要共享的状态提升到它们的最近公共父组件中管理。
17、Context API 是什么?
Context 直接让数据穿透组件层级,实现跨组件共享。
const UserContext = React.createContext(null);
function App() {
const [user, setUser] = useState({ name: 'Alice' });
return (
<UserContext.Provider value={user}>
<Navbar />
</UserContext.Provider>
);
}
function Navbar() {
const user = useContext(UserContext);
return <div>欢迎回来, {user.name}</div>;
}
如果在某个组件中改变了值那么其他组件通过useContext 订阅的组件也会改变
import React, { useState, useContext, useMemo } from 'react';
// 1. 创建 Context
const ThemeContext = React.createContext({
mode: 'light',
toggleTheme: () => {}, // 占位函数,避免未提供 Provider 时出错
});
// 2. 定义 App 组件(顶层 Provider)
function App() {
// 管理主题状态
const [theme, setTheme] = useState({ mode: 'light' });
// 定义切换主题的函数
const toggleTheme = () => {
setTheme(prev => ({
mode: prev.mode === 'light' ? 'dark' : 'light',
}));
};
// 优化:使用 useMemo 避免每次渲染生成新对象
const themeValue = useMemo(
() => ({
mode: theme.mode,
toggleTheme,
}),
[theme.mode] // 仅在 theme.mode 变化时重新生成对象
);
// 提供 Context 数据
return (
<ThemeContext.Provider value={themeValue}>
<Toolbar />
<PageContent />
</ThemeContext.Provider>
);
}
// 3. 子组件 Toolbar(展示当前主题,含切换按钮)
function Toolbar() {
// 消费 Context 数据
const { mode, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ background: mode === 'light' ? '#fff' : '#333', padding: 20 }}>
<h3>Toolbar - 当前主题: {mode}</h3>
<button onClick={toggleTheme}>切换主题</button>
</div>
);
}
// 4. 子组件 PageContent(展示主题相关内容)
function PageContent() {
// 消费 Context 数据
const { mode } = useContext(ThemeContext);
return (
<div style={{
background: mode === 'light' ? '#f0f0f0' : '#222',
color: mode === 'light' ? '#000' : '#fff',
padding: 20,
marginTop: 10
}}>
<h2>页面内容</h2>
<p>当前主题模式: {mode}</p>
</div>
);
}
export default App;
18、Redux
Redux 是一个用于管理 JavaScript 应用状态的可预测化状态容器,最初是为 React 设计的,但也可用于其他框架(如 Vue、Angular)或纯 JavaScript 应用。它的核心目标是让应用的状态管理更清晰、可维护且可追踪。
为什么需要 Redux?
- 在复杂的前端应用中,组件间的状态(如用户登录信息、页面数据、UI 状态等)可能需要在多个组件间共享或传递。传统的组件间通信(如 props 层层传递)会变得繁琐且难以维护。Redux 通过集中管理全局状态,解决了这类问题。
Redux 的三大核心原则
-
单一数据源 (Single Source of Truth)
- 整个应用的状态存储在一个唯一的 Store(对象树)中。
- 便于调试和跟踪状态变化。
-
状态是只读的 (State is Read-Only)
- 不能直接修改状态,必须通过 Action(一个描述发生了什么的对象)来触发变更。
-
使用纯函数修改状态 (Changes via Pure Functions)
- Reducer 是一个纯函数,接收旧状态和 Action,返回新状态。
- 保证状态变化的可预测性。
Redux 的核心概念
4. Store
- 存储全局状态的容器,通过 createStore(reducer) 创建。
- 提供 getState() 获取当前状态,dispatch(action) 触发状态变更,subscribe(listener) 监听变化。
-
Action
-
一个普通 JavaScript 对象,必须包含 type 字段描述操作类型,例如:
{ type: 'ADD_TODO', text: 'Learn Redux' }
-
-
Reducer
-
根据 Action 的类型处理状态变更。例如:
function todoReducer(state = [], action) { switch (action.type) { case 'ADD_TODO': return [...state, { text: action.text }]; default: return state; } }
-
-
Middleware(可选)
- 扩展 Redux 的功能,例如处理异步操作(常用 redux-thunk 或 redux-saga)。
Redux 工作流程
- 触发 Action:用户操作或事件(如点击按钮)触发一个 Action。
- 派发 Action:调用 dispatch(action) 将 Action 发送到 Store。
- 执行 Reducer:Store 调用 Reducer,传入当前状态和 Action,生成新状态。
- 更新视图:Store 保存新状态,并通知所有订阅状态的组件重新渲染。
适用场景
- 组件需要共享大量状态。
- 需要跟踪状态变更历史(如实现撤销/重做)。
- 复杂的异步数据流管理。
我自己封装的redux
reducers.js
// reducers.js
const CLEAR_PAGE_PARAM = 'CLEAR_PAGE_PARAM';
const initialState = {
userInfo: '',
pageListQuery: {},
menuInfo: [],
selectedPage: {},
tabsList: [],
isLoading:false,
pageParam: {},
openMenuKeys:[],
permissions:{},
isDelPage:false,
unorganizedMenuData:[],
tabsAndPageChange:''
};
const permissionsReducer = (state = initialState.permissions, action) => {
const handlers = {
'SET_PERMISSIONS': () => action.payload,
'RESET_PERMISSIONS': () => initialState.permissions,
};
return handlers[action.type] ? handlers[action.type]() : state;
};
const userReducer = (state = initialState.userInfo, action) => {
const handlers = {
'SET_USER_INFO': () => action.payload,
'RESET_USER_INFO': () => initialState.userInfo,
};
return handlers[action.type] ? handlers[action.type]() : state;
};
const openMenuKeysReducer = (state = initialState.openMenuKeys, action) => {
const handlers = {
'SET_OPEN_MENU_KEYS': () => action.payload,
'RESET_OPEN_MENU_KEYS': () => initialState.openMenuKeys,
};
return handlers[action.type] ? handlers[action.type]() : state;
};
const pageListQueryReducer = (state = initialState.pageListQuery, action) => {
const handlers = {
'SET_PAGE_LIST_QUERY': () => action.payload,
'RESET_PAGE_LIST_QUERY': () => initialState.pageListQuery,
};
return handlers[action.type] ? handlers[action.type]() : state;
};
const menuInfoReducer = (state = initialState.menuInfo, action) => {
const handlers = {
'SET_MENU_INFO': () => action.payload,
'RESET_MENU_INFO': () => initialState.menuInfo,
};
return handlers[action.type] ? handlers[action.type]() : state;
};
const selectedPageReducer = (state = initialState.selectedPage, action) => {
const handlers = {
'SET_SELECTED_PAGE': () => action.payload,
'RESET_SELECTED_PAGE': () => initialState.selectedPage,
};
return handlers[action.type] ? handlers[action.type]() : state;
};
const tabsListReducer = (state = initialState.tabsList, action) => {
const handlers = {
'SET_TABS_LIST': () => action.payload,
'RESET_TABS_LIST': () => initialState.tabsList,
};
return handlers[action.type] ? handlers[action.type]() : state;
};
const isLoadingReducer = (state = initialState.isLoading, action) => {
switch (action.type) {
case 'SHOW_LOADING':
return true;
case 'HIDE_LOADING':
return false;
default:
return state;
}
};
const isDelPageReducer = (state = initialState.isDelPage, action) => {
switch (action.type) {
case 'TRUE_DEL_PAGE':
return true;
case 'FALSE_DEL_PAGE':
return false;
default:
return state;
}
};
const pageParamReducer = (state = initialState.pageParam, action) => {
const handlers = {
'SET_PAGE_PARAM': () => action.payload,
'RESET_PAGE_PARAM': () => initialState.pageParam,
'CLEAR_PAGE_PARAM': () => {
const newState = { ...state };
delete newState[action.payload]; // 假设 payload 是要删除的键名
return newState;
},
};
return handlers[action.type] ? handlers[action.type]() : state;
};
const unorganizedMenuDataReducer = (state = initialState.unorganizedMenuData, action) => {
const handlers = {
'SET_UNORGANIZED_MENU_INFO': () => action.payload,
'RESET_UNORGANIZED_MENU_INFO': () => initialState.unorganizedMenuData,
};
return handlers[action.type] ? handlers[action.type]() : state;
};
const tabsAndPageChangeReducer = (state = initialState.tabsAndPageChange, action) => {
const handlers = {
'SET_TABS_PAGE_CHANGE': () => action.payload,
'RESET_TABS_PAGE_CHANGE': () => initialState.tabsAndPageChange,
};
return handlers[action.type] ? handlers[action.type]() : state;
};
function clearPageParam(paramKey) {
return {
type: CLEAR_PAGE_PARAM,
payload: paramKey,
};
}
export {
clearPageParam, isDelPageReducer, isLoadingReducer, menuInfoReducer, openMenuKeysReducer, pageListQueryReducer, pageParamReducer, permissionsReducer, selectedPageReducer, tabsAndPageChangeReducer, tabsListReducer, unorganizedMenuDataReducer, userReducer
};
在store.js引入reducers.js,做持久化配置。
// store.js
import { ***bineReducers, configureStore } from '@reduxjs/toolkit';
import { persistReducer, persistStore } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import {
isDelPageReducer,
isLoadingReducer,
menuInfoReducer,
openMenuKeysReducer,
pageListQueryReducer,
pageParamReducer,
permissionsReducer,
selectedPageReducer,
tabsAndPageChangeReducer,
tabsListReducer,
unorganizedMenuDataReducer,
userReducer
} from './reducers';
const rootReducer = ***bineReducers({
userInfo: userReducer,
pageListQuery: pageListQueryReducer,
menuInfo: menuInfoReducer,
selectedPage: selectedPageReducer,
tabsList: tabsListReducer,
isLoading:isLoadingReducer,
pageParam:pageParamReducer,
openMenuKeys:openMenuKeysReducer,
permissions:permissionsReducer,
isDelPage:isDelPageReducer,
unorganizedMenuData:unorganizedMenuDataReducer,
tabsAndPageChange:tabsAndPageChangeReducer
});
//持久化配置
const persistConfig = {
key: 'root',// 存储的 key
storage,// 存储方式
blacklist: ['isLoading'],// 不持久化
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
const persistor = persistStore(store);
export { persistor, store };
- 在index.js中引入store,通过 包裹整个应用,react-redux 会利用 React 的 Context API 将 Redux 的 store 对象传递给所有子组件,任何子组件(如 及其内部组件)都可以通过 useSelector 或 connect 访问全局状态。
- 使用 redux-persist 库时, 会在应用启动前从本地存储(如 localStorage)加载已保存的状态,并合并到 Redux Store 中。
import { ConfigProvider, message } from 'antd';
import zh*** from 'antd/locale/zh_***';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-***';
import updateLocale from 'dayjs/plugin/updateLocale';
import { default as React } from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import App from './App';
import './index.scss';
import reportWebVitals from './reportWebVitals';
import { persistor, store } from './store';
//antd 时间组件中文
dayjs.extend(updateLocale);
dayjs.updateLocale('zh-***');
// 全局配置 message
message.config({
maxCount: 3,// 最大显示数, 超过限制时,最早的消息会被自动关闭
prefixCls: 'my-message',
});
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ConfigProvider locale={zh***}>
<App />
</ConfigProvider>
</PersistGate>
</Provider>
);
reportWebVitals();
19、什么是hook?
React Hooks 是 React 16.8 版本引入的一种特性,它允许开发者在函数组件中使用状态(state)、生命周期方法(lifecycle methods)等 React 特性,而无需编写 class 组件。Hooks 旨在简化组件逻辑、提高代码复用性,并解决 class 组件中常见的代码冗余和逻辑分散问题。
我自己的理解就是在React16.8版本将之前不好用的方法进行了替换成了新方法升级并且又用了语法糖的形式进行了封装让我们更好的调用和开发
- React Hooks 与 16.8 之前版本定义变量的核心区别
-
状态变量的定义方式
-
16.8 之前(类组件):
必须通过 this.state 定义状态变量,且只能在类组件中使用。class Example extends React.***ponent { constructor(props) { super(props); this.state = { count: 0 }; // 状态需集中定义在对象中:ml-citation{ref="7" data="citationList"} } } -
Hooks(函数组件):
使用 useState 直接定义单个状态变量,无需包裹在对象中。function Example() { const [count, setCount] = useState(0); // 独立声明状态变量:ml-citation{ref="1,5" data="citationList"} }
-
-
变量更新的方式
-
16.8 之前:
this.setState({ count: this.state.count + 1 }); // 需要手动合并对象:ml-citation{ref="7" data="citationList"} -
Hooks:
通过 useState 返回的 setter 函数直接更新变量,且更新是独立的。setCount(count + 1); // 直接赋值,无需合并对象:ml-citation{ref="5,7" data="citationList"}
-
总结
Hooks 通过函数式的方式,解决了类组件中状态分散、逻辑复用困难、闭包陷阱等问题,同时简化了代码结构并提升了可维护性
19、react 常用的hook有哪些
-
useState
-
用途:在函数定义状态。
-
示例:
const [count, setCount] = useState(0);
-
-
useEffect
-
用途:处理副作用。
-
示例:
依赖数组设为空数组,组件挂载时执行(仅一次)
useEffect(() => { init(); }, []);依赖数组不为空数组,监听数组内容变化,每次变化都会执行
useEffect(() => { const updatedPageListQuery = { ...pageListQuery, banquetCumulativeIncentiveList }; dispatch({ type: 'SET_PAGE_LIST_QUERY', payload: updatedPageListQuery }); }, [banquetCumulativeIncentiveList]);
useEffect适合执行不需要在浏览器布局更新之前同步进行的操作,如数据请求、订阅事件等
-
-
useLayoutEffect
-
用途:与 useEffect 类似,但会在 DOM 更新后同步执行(适合需要直接操作 DOM 的场景)。
-
示例:
useLayoutEffect(() => { measureDOMNode(); }, []);useLayoutEffect适合执行需要在浏览器布局更新之前同步进行的操作,如优化布局、控制动画、计算DOM尺寸等
-
-
useMemo
-
用途:缓存计算结果,避免重复计算。
-
示例:
const memoizedValue = useMemo(() => ***puteExpensiveValue(a, b), [a, b]);useMemo 接收两个参数一个函数和一个依赖项数组,当依赖项发生变化时,useMemo会重新执行该函数并返回新的计算结果,没有发生变化则返回上一次的计算结果,所以useMemo也有缓存机制,useMemo类似vue的***puted。
-
-
useCallback
-
用途:缓存函数引用,避免子组件不必要的重新渲染。
-
示例1:
const handleClick = useCallback(() => { doSomething(a, b); }, [a, b]);useCallback 接收两个参数:一个是要缓存的函数,另一个是依赖项数组。依赖项数组用于指定哪些变量发生变化时,缓存函数需要重新生成。当依赖项发生变化时,useCallback 会自动重新生成缓存函数。
- 示例2:
import React, { useState, useCallback } from 'react'; function Counter() { const [count, setCount] = useState(0); const increment = useCallback(() => { setCount((prevCount) => prevCount + 1); }, []);// 依赖项为空,函数仅在组件挂载时创建一次 const decrement = useCallback(() => { setCount((prevCount) => prevCount - 1); }, []);// 依赖项为空,函数仅在组件挂载时创建一次 return ( <> <button onClick={decrement}>-</button> <span>{count}</span> <button onClick={increment}>+</button> </> ); } export default Counter;- 缓存生效:由于依赖项为空,increment 函数在组件首次渲染时创建后,后续无论组件如何重新渲染,increment 都会返回同一个函数引用。
- 点击行为:每次点击按钮时,实际执行的都是 首次渲染时创建的缓存函数。
-
-
useRef
-
用途:若子组件是 原生 HTML 标签(如
<div>、<input>),父组件可直接通过 useRef 获取其 DOM 节点: -
示例:
// 父组件 import { useRef } from 'react'; function Parent() { const childInputRef = useRef(null); const focusChildInput = () => { childInputRef.current.focus(); // ✅ 操作子组件 DOM }; return ( <div> <Child ref={childInputRef} /> <button onClick={focusChildInput}>聚焦子组件输入框</button> </div> ); } // 子组件(原生 input) const Child = React.forwardRef((props, ref) => { return <input ref={ref} />; // ✅ 转发 ref 到原生元素 }); -
用途:若子组件是 自定义组件(Class ***ponent),父组件可通过 useRef 获取其实例,并调用其方法
-
示例:
//父组件 const areaRef = useRef(null); areaRef.current.setAreaIds(vo.activitySchemeRuleVo.districts) <AreaSelectModal isDisabled={parentData.pageType === 'detail'} ref={areaRef} districtFlag={true} isRequired={true} /> //子组件 /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable no-unreachable */ import React, { forwardRef, useImperativeHandle, useState } from 'react'; import './AreaSelectModal.scss'; const AreaSelectModal = forwardRef(({ districtFlag, isRequired, isDisabled }, ref) => { const [areaIds, setAreaIds] = useState([]); const getAreaIds = () => { return areaIds; }; //传递给父组件的 ref 对象 useImperativeHandle(ref, () => ({ getAreaIds })); return ( <div style={{ display: 'flex', alignItems: 'center', width: '100%' }}> <label className={`name ${isRequired ? 'is-required' : ''}`}>地区:</label> </div> ); }); export default AreaSelectModal; -
20、useMemo 和 useCallback 的区别?
- useMemo 缓存计算结果,useCallback 缓存函数本身,用于性能优化。
21、为什么 Hooks 不能写在条件语句或循环中?
一、底层原理:Hooks 的链表管理机制
1. React 如何追踪 Hooks?
React 内部通过链表结构管理 Hooks。每次组件渲染时,Hooks 必须按照完全相同的顺序被调用,React 才能将每个 Hook 与链表中的对应节点正确关联。
2. 伪代码模拟实现
let hooks = []; // 存储所有 Hook 状态的数组
let currentIndex = 0; // 当前 Hook 的索引
function render***ponent() {
currentIndex = 0; // 每次渲染前重置索引
// 执行组件函数...
}
function useState(initialValue) {
if (!hooks[currentIndex]) {
hooks[currentIndex] = initialValue; // 初始化状态
}
const state = hooks[currentIndex];
const setState = (newValue) => {
hooks[currentIndex] = newValue; // 更新状态
};
currentIndex++; // 索引递增,指向下一个 Hook
return [state, setState];
}
- 关键点:Hooks 的调用顺序直接依赖 currentIndex 的递增逻辑。顺序或次数变化会导致索引错位。
二、问题场景:条件语句和循环的破坏性
1. 条件语句破坏调用顺序
function ***ponent({ condition }) {
if (condition) {
const [name, setName] = useState(""); // 第一次渲染调用
}
const [count, setCount] = useState(0); // 第二次渲染时,若 condition 为 false,此 Hook 的索引会错位
}
- 结果:
- 当 condition 从 true 变为 false 时,count 会错误地读取 name 的状态。
- React 抛出错误:Rendered fewer hooks than expected。
2. 循环导致调用次数不一致
function ***ponent({ items }) {
const values = [];
for (let i = 0; i < items.length; i++) {
const [value, setValue] = useState(items[i]); // 每次渲染 Hook 数量随 items 长度变化
values.push(value);
}
// 如果 items.length 变化,后续 Hooks 全部错乱
}
- 结果:
- 循环次数变化时,React 无法正确匹配链表中的 Hook 节点。
- React 抛出错误:Hooks must be called in the exact same order。
三、解决方案:保证调用顺序和次数
1. 始终在顶层调用 Hooks
- 规则:Hooks 必须在函数组件的顶层(不在任何条件、循环或嵌套函数中)调用。
- 正确示例:
function ***ponent() {
const [count, setCount] = useState(0); // ✅ 顶层调用
useEffect(() => {}); // ✅ 顶层调用
}
2. 将条件逻辑移至 Hook 内部
- 场景:需要根据条件执行副作用或动态计算值。
- 正确示例:
// 条件执行副作用
useEffect(() => {
if (condition) {
// 条件满足时执行操作 ✅
}
}, [condition]); // 依赖数组控制条件
// 动态计算初始值
const initialCount = props.mode === "edit" ? 10 : 0;
const [count, setCount] = useState(initialCount); // ✅ 提前计算参数
3. 使用 useMemo/useCallback 封装动态逻辑
- 场景:依赖外部条件的复杂计算。
- 正确示例:
const expensiveValue = useMemo(() => {
return condition ? calculateA() : calculateB();
}, [condition]); // ✅ 缓存计算结果
四、总结与扩展
1. 核心规则
- 调用顺序和次数必须严格一致:这是 React Hooks 设计的核心约束。
- 底层实现依赖链表结构:顺序错位会导致状态管理完全崩溃。
2. 扩展思考
-
为什么 Class 组件没有此限制?
→ Class 组件通过 this.state 集中管理状态,而函数组件通过 Hooks 分散管理,依赖调用顺序的稳定性。 -
Hooks 的设计取舍
→ 牺牲部分灵活性(如动态条件),换取更好的逻辑复用和代码可读性。
3. 最佳实践
- 静态调用:所有 Hooks 在组件顶层声明,不嵌套在任何逻辑块中。
- 动态参数:通过依赖数组(useEffect)或提前计算(useState)实现动态性。
- 最终结论:遵守 Hooks 的调用规则是避免不可预测 Bug 的核心前提,理解其底层机制有助于写出更健壮的 React 代码。
22、如何自定义hook?为什么要自定义hook普通函数不行吗?
- 如何自定义hook
-
将可复用的逻辑封装为函数,函数名以 use 开头,内部可调用其他 Hooks。
-
实例
const useCounter = (initialValue = 0) => { const [count, setCount] = useState(initialValue); const increment = () => setCount(c => c + 1); return { count, increment }; };
-
- 为什么要自定义hook
- 在 React 中,普通函数和自定义 Hook 虽然都能封装逻辑,但它们的核心区别在于 能否访问 React 特性(如状态、生命周期) 和 对组件生命周期的绑定方式。
| 对比维度 | 普通函数 | 自定义 Hook |
|---|---|---|
| 访问 React 特性 | ❌ 无法使用 useState, useEffect 等 Hook | ✅ 可直接使用所有 React Hook |
| 状态管理 | ❌ 只能通过参数传递或闭包临时保存状态(易污染) | ✅ 通过 useState/useReducer 管理独立、隔离的状态 |
| 生命周期绑定 | ❌ 无法感知组件挂载/更新/销毁 | ✅ 可通过 useEffect 绑定组件生命周期 |
| 作用域隔离 | ❌ 多次调用共享同一作用域(可能互相干扰) | ✅ 每次调用 Hook 都有独立作用域(闭包机制) |
| 代码复用场景 | 纯计算、数据转换等无副作用逻辑 | 涉及状态、副作用、生命周期的组件逻辑 |
23、React 18 的新特性有哪些?
- 并发模式(Concurrent Mode)、自动批处理(Automatic Batching)、Suspense 支持服务端渲染等。
24、什么是错误边界(Error Boundary)?如何实现?
- 错误边界(Error Boundary)是 React 中用于捕获子组件树中的 JavaScript 错误,并显示备用 UI 的组件。它防止因局部组件错误导致整个应用崩溃,提升用户体验。
- 通过 static getDerivedStateFromError() 和 ***ponentDidCatch() 捕获子组件的错误(仅类组件)。
25、React 服务端渲染(SSR)的原理是什么?
- React 服务端渲染(SSR)的原理是通过在服务器端将 React 组件渲染为 HTML 字符串,直接发送给客户端,以提升首屏加载速度和 SEO 优化。
26、React Fiber 的作用是什么?
- 新的协调算法,支持任务分片和中断/恢复,以实现并发模式下的高性能渲染。
27、解释 React 的协调(Reconciliation)过程。
- 通过 Diff 算法对比新旧虚拟 DOM,生成最小化的真实 DOM 更新。
28、如何实现一个防抖(Debounce)的搜索输入框?
- 使用 useEffect 和 setTimeout 延迟 API 请求,清理函数中取消定时器。
29、如何优化长列表的性能?
- 使用虚拟滚动库(如 react-window 或 react-virtualized),仅渲染可见区域内容。
30、如何处理组件间的复杂状态逻辑?
- 使用 Redux(单向数据流)或 Context API + useReducer 组合。
31、如何实现路由守卫(如登录验证)?为什么要实现路由守卫?
-
如何实现路由守卫(如登录验证)
在 React Router 中使用高阶组件或自定义
<Route>包装逻辑。
方案 1:封装高阶组件拦截路由(适合简单场景)// src/guards/AuthGuard.jsx import { Navigate, useLocation } from 'react-router-dom'; const AuthGuard = ({ children }) => { const location = useLocation(); const isAuthenticated = localStorage.getItem('token'); // 实际项目建议用状态管理 if (!isAuthenticated) { // 记录来源路径,登录后自动跳回 return <Navigate to="/login" state={{ from: location }} replace />; } return children; }; // 使用示例 <Route path="/dashboard" element={ <AuthGuard> <Dashboard /> </AuthGuard> } />方案 2:全局路由布局拦截(推荐,统一管理)
// src/layouts/AuthLayout.jsx import { Outlet, Navigate } from 'react-router-dom'; const AuthLayout = () => { const isAuthenticated = checkAuthToken(); // 自定义校验逻辑 return isAuthenticated ? <Outlet /> : <Navigate to="/login" replace />; }; // 路由配置 <Routes> <Route path="/login" element={<Login />} /> <Route element={<AuthLayout />}> {/* 所有子路由受保护 */} <Route path="/dashboard" element={<Dashboard />} /> <Route path="/profile" element={<Profile />} /> </Route> </Routes>方案 3:动态权限路由表(适合复杂权限系统)
// src/routes.js const routes = [ { path: '/login', element: <Login />, isPublic: true, }, { path: '/admin', element: <AdminPanel />, requiredRole: 'admin', // 需要管理员权限 }, ]; // 动态渲染路由 const Router = () => { const { role } = useUser(); // 从全局状态获取用户角色 return ( <Routes> {routes.map((route) => { if (route.isPublic) { return <Route key={route.path} {...route} />; } // 权限校验 if (route.requiredRole && role !== route.requiredRole) { return <Route key={route.path} path={route.path} element={<Forbidden />} />; } return <Route key={route.path} {...route} />; })} </Routes> ); }; -
为什么要实现路由守卫?
| 场景 | 问题风险 | 路由守卫的作用 |
|---|---|---|
| 用户未登录访问 /profile | 敏感数据泄露 | 强制跳转到登录页 |
| 普通用户访问 /admin | 越权操作 | 根据角色权限拦截路由 |
| 页面直接输入URL 访问 | 绕过前端按钮逻辑,直接进入未授权页面 | 统一权限校验入口 |
| JWT Token 过期 | 发起无效请求,后端返回 401 | 自动检测 Token 有效性并刷新/跳转 |
32、React 的优缺点是什么?
- 优点:组件化、生态丰富、性能优化手段多;缺点:学习曲线陡峭、频繁的版本更新。
33、如何调试 React 应用?
- 浏览器调试工具、console.log、错误边界
34、如何在react中使用vue3封装的组件
方法:使用Web ***ponents封装Vue组件
步骤1:将Vue组件转换为自定义元素
在Vue项目中,使用defineCustomElement将组件包装成自定义元素。
// MyVue***ponent.vue
<script>
import { defineCustomElement } from 'vue'
export default {
name: 'MyVueElement',
props: {
initialCount: Number
},
data() {
return { count: this.initialCount }
},
methods: {
increment() {
this.count++
// 通过CustomEvent传递事件
this.dispatchEvent(new CustomEvent('count-changed', { detail: this.count }))
}
},
// 样式封装在Shadow DOM中
styles: [`
button { color: red; }
`]
}
// 转换为自定义元素
const MyVueElement = defineCustomElement(MyVue***ponent)
customElements.define('my-vue-element', MyVueElement)
</script>
步骤2:构建Vue组件为独立JS文件
使用Vite或Vue CLI构建组件,生成可在浏览器中使用的JS文件。
示例Vite配置:
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: 'src/MyVue***ponent.vue',
formats: ['es']
}
}
})
运行构建命令:
vite build
将生成的JS文件(如dist/MyVue***ponent.js)复制到React项目的public目录。
步骤3:在React中引入自定义元素
在React入口文件(如index.js)中导入JS文件:
// index.js
import './public/MyVue***ponent.js'
步骤4:在React组件中使用自定义元素
通过ref监听事件并传递属性:
import React, { useRef, useEffect, useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const elementRef = useRef(null)
useEffect(() => {
const element = elementRef.current
if (element) {
// 设置初始属性
element.initialCount = 0
// 监听事件
const handleEvent = (e) => setCount(e.detail)
element.addEventListener('count-changed', handleEvent)
return () => element.removeEventListener('count-changed', handleEvent)
}
}, [])
// 更新属性示例(可选)
useEffect(() => {
if (elementRef.current) {
elementRef.current.initialCount = count
}
}, [count])
return (
<div>
<my-vue-element ref={elementRef} />
<p>React中的计数:{count}</p>
</div>
)
}
export default App
三、UNIAPP
1、什么是 UniApp?它有什么特点?
UniApp 是一个基于 Vue.js 的跨平台应用开发框架,可以使用 Vue.js 的开发语法编写一次代码,然后通过编译生成可以在多个平台(包括iOS、Android、H5 等)上运行的应用。UniApp 具有以下特点:
跨平台:开发者可以使用相同的代码基底构建多个平台的应用,避免了针对不同平台的重复开发。
高性能:UniApp 在运行时使用原生渲染技术,具有接近原生应用的性能表现。
开放生态:UniApp 支持原生插件和原生能力的扩展,可以调用设备的硬件功能和第三方原生 SDK。
开发便捷:UniApp 提供了丰富的组件和开发工具,简化了应用开发和调试的流程。
2、 请解释 UniApp 中的生命周期钩子函数及其执行顺序。
在 UniApp 中,每个页面和组件都有一系列的生命周期钩子函数,用于在特定的时机执行代码。以下是 UniApp 中常用的生命周期钩子函数及其执行顺序:
onLoad:页面/组件加载时触发。
onShow:页面/组件显示在前台时触发。
onReady:页面/组件初次渲染完成时触发。
onHide:页面/组件被隐藏在后台时触发。
onUnload:页面/组件被销毁时触发。
执行顺序为:onLoad -> onShow -> onReady -> onHide -> onUnload。
3、请解释 UniApp 中的全局组件和页面组件的区别。
在 UniApp 中,全局组件和页面组件是两种不同类型的组件。
- 全局组件:在 App.vue 中注册的组件,可以在应用的所有页面和组件中使用。可以通过 Vue.***ponent 方法进行全局注册。
- 页面组件:每个页面都有自己的组件,用于描述页面的结构和交互。页面组件只在当前页面有效,不能在其他页面中直接使用,但可以通过组件引用的方式进行复用。
4、请解释 UniApp 中的条件编译是如何工作的。
UniApp 中的条件编译允许开发者根据不同的平台或条件编译指令来编写不同的代码。在编译过程中,指定的平台或条件将会被处理,并最终生成对应平台的可执行代码。条件编译通过在代码中使用 #ifdef、#ifndef、#endif 等指令进行控制。例如,可以使用 #ifdef H5 来编写只在 H5 平台生效的代码块。
5、请解释 UniApp 中的跨平台兼容性问题和解决方案。
- 使用条件编译:根据不同的平台,编写对应平台的代码,使用条件编译指令来控制代码块的执行。
- 使用平台 API:UniApp 提供了一些平台 API,可以通过条件编译指令来使用特定平台的功能和能力。
- 样式适配:不同平台的样式表现可能有差异,使用 uni-app-plus 插件中的 upx2px 方法来进行样式适配,使得在不同平台上显示一致。
- 原生扩展:使用原生插件和扩展来调用设备的原生功能和第三方 SDK,以解决特定平台的需求。
6、uniApp中如何进行数据绑定?
可以使用双花括号{{}}进行数据绑定,将数据动态展示在页面上
<template>
<view>
<text>{{ message }}</text>
</view>
</template>
<script>
export default {
data() {
return {
message: 'Hello uniApp'
};
}
};
</script>
7、uniApp中如何发送网络请求?
可以使用uni.request方法发送网络请求,通过设置url、method、data等参数来实现不同的请求
uni.request({
url: 'https://api.example.***/data',
method: 'GET',
su***ess: (res) => {
console.log(res.data);
},
fail: (err) => {
console.error(err);
}
});
8、uniApp中如何进行数据缓存?
可以使用uni.setStorageSync方法进行数据缓存,将数据存储到本地缓存中。
// 存储数据到本地缓存
uni.setStorageSync('key', 'value');
// 从本地缓存中读取数据
const data = uni.getStorageSync('key');
console.log(data); // 输出:value
9、uniApp中如何使用组件?
可以在页面中引入组件,并在***ponents属性中注册组件,然后在页面中使用。
<template>
<view>
<my-***ponent></my-***ponent>
</view>
</template>
<script>
import my***ponent from '@/***ponents/my***ponent.vue';
export default {
***ponents: {
my***ponent
}
};
</script>
10、uniApp中如何实现下拉刷新和上拉加载更多?
可以使用uni.onPullDownRefresh方法实现下拉刷新,使用uni.onReachBottom方法实现上拉加载更多。
// 在页面的onPullDownRefresh方法中实现下拉刷新
onPullDownRefresh() {
// 执行刷新操作
console.log('下拉刷新');
// 刷新完成后调用uni.stopPullDownRefresh()方法停止刷新
uni.stopPullDownRefresh();
}
// 在页面的onReachBottom方法中实现上拉加载更多
onReachBottom() {
// 执行加载更多操作
console.log('上拉加载更多');
}
11、uniApp中如何获取用户地理位置信息?
可以使用uni.getLocation方法获取用户的地理位置信息。
uni.getLocation({
su***ess: (res) => {
console.log(res.latitude, res.longitude);
},
fail: (err) => {
console.error(err);
}
});
12、uniApp中如何进行微信支付?
可以使用uni.requestPayment方法进行微信支付,通过设置支付参数来实现支付功能。
uni.requestPayment({
provider: 'wxpay',
timeStamp: '1234567890',
nonceStr: 'abcdefg',
package: 'prepay_id=1234567890',
signType: 'MD5',
paySign: 'abcdefg',
su***ess: (res) => {
console.log(res);
},
fail: (err) => {
console.error(err);
}
});
13、uniApp中如何进行音频的播放和控制?
可以使用uni.createInnerAudioContext方法创建音频实例,通过调用实例的方法来实现音频的播放和控制。
// 创建音频实例
const audio = uni.createInnerAudioContext();
// 设置音频资源
audio.src = 'http://example.***/audio.mp3';
// 播放音频
audio.play();
// 暂停音频
audio.pause();
// 停止音频
audio.stop();
14、uniApp中如何进行图片的懒加载?
可以使用uni.lazyLoadImage组件实现图片的懒加载,将图片的src属性设置为需要加载的图片地址。
<template>
<view>
<uni-lazy-load-image src="http://example.***/image.jpg"></uni-lazy-load-image>
</view>
</template>
<script>
export default {
***ponents: {
'uni-lazy-load-image': '@/***ponents/uniLazyLoadImage.vue'
}
};
</script>
15、uniApp中如何获取设备信息?
可以使用uni.getSystemInfo方法获取设备信息,包括设备型号、操作系统版本等。
uni.getSystemInfo({
su***ess: (res) => {
console.log(res.model, res.system);
},
fail: (err) => {
console.error(err);
}
});
16、uniApp中如何实现页面间的数据传递?
可以使用uni.navigateTo方法的url参数中添加query参数来实现页面间的数据传递或者将参数写入App.vue里globalData中。
// 页面A跳转到页面B,并传递参数
uni.navigateTo({
url: '/pages/detail/detail?id=123'
});
// 在页面B中获取传递的参数
export default {
onLoad(options) {
console.log(options.id); // 输出:123
}
};
17、uniApp中如何实现图片预览功能?
可以使用uni.previewImage方法实现图片预览功能,通过设置urls参数来指定要预览的图片地址。
uni.previewImage({
urls: ['http://example.***/image1.jpg', 'http://example.***/image2.jpg']
});
18、uniApp中如何实现页面的分享功能?
可以使用uni.showShareMenu方法开启页面的分享功能,使用uni.onShareAppMessage方法设置分享的标题、路径等。
// 开启页面的分享功能
uni.showShareMenu();
// 设置分享的标题、路径等
uni.onShareAppMessage(() => {
return {
title: '分享标题',
path: '/pages/index/index'
};
});
19、uniApp中如何实现页面的转发功能?
可以使用uni.share方法实现页面的转发功能,通过设置title、path等参数来指定转发的标题和路径。
uni.share({
title: '转发标题',
path: '/pages/index/index'
});
20、uniApp中如何实现页面的分享到朋友圈功能?
// 开启页面的分享功能
uni.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
});
// 设置分享的标题、路径等
uni.onShareAppMessage(() => {
return {
title: '分享标题',
path: '/pages/index/index'
};
});
uni.onShareTimeline(() => {
return {
title: '分享标题',
path: '/pages/index/index'
};
});
21、 UniApp 打包方式?
HBuilderX 云打包:一键生成安卓(APK)或 iOS(IPA)包,需配置证书。
ios离线打包:通过HB生成离线打包文件,再讲生成好的文件放到xcode中进行打包
22、安卓打包需要配置什么?
安卓打包需要在manifest.json-->Android云打包权限配置-->额外添加的权限 添加使用到的权限
23、UniApp app获取地理位置的方式有哪些?
1. 直接使用uni.getLocation根据手机GPS获取
2. app通过第三方获取需要再manifest.json–>App模块配置–> Geolocation(定位)开启定位然后配置对应定位公司的ios和安卓的key(key是第三方平台获取)
3. ios上架商城需要增加相对应的配置manifest.json–>app权限–>IOS隐私信息访向的许可描述–>访问位置(NSLocationAlwaysAndWhenInUseUsageDescription)增加描述该应用需要你的地理位置,以便为你提供当前位置信息,才可以上架商城
24、UniApp微信小程序获取地理位置?
微信小程序需要先配置manifest.json-->微信小程序配置-->勾选位置接口,填入描述,然后使用uni.getLocation会出现弹窗
25、 UniApp ios上架商城需要做什么?
Uniapp是一款跨平台的开发工具,可以让开发者使用一份代码同时在多个平台上运行。苹果上架是开发者将Uniapp开发的应用程序发布到苹果商店的过程。
一、开发前准备
在进行Uniapp苹果上架之前,需要先准备好以下工作:
-
注册苹果开发者账号
在苹果商店上架应用程序需要先注册苹果开发者账号,注册后需要缴纳99美元的年费。注册成功后,开发者就可以登录到苹果开发者平台,创建应用程序和证书等。
-
创建应用程序
在苹果开发者平台上创建应用程序,需要填写应用程序的名称、描述、图标等信息。创建成功后,可以获得应用程序的Bundle ID,这是应用程序的唯一标识符。
-
获取证书
在苹果开发者平台上获取证书,用于对应用程序进行签名。需要先创建一个证书签名请求,然后将该请求提交给苹果开发者平台,最后下载证书安装到开发者电脑上。
-
配置应用程序
在Uniapp开发环境中,需要对应用程序进行配置,包括应用程序名称、Bundle ID、图标等信息。在配置完成后,可以进行本地测试。
二、打包应用程序
- 在开发项目的时候配置好manifest.json–>app权限–>IOS隐私信息访向的许可描述配置相对应权限的描述
- 在开发项目的时候配置好manifest.json–>基础配置–>appid、应用名称、应用描述、版本号
- 在开发项目的时候配置好manifest.json–>App图标配置–>各尺寸图标
在完成应用程序开发后我们已经配置好iOS,需要将其打包成ipa文件,以便上传到苹果商店。打包应用程序的步骤如下:
-
在HBuilderX中选择iOS平台,配置包名(Bundle ID)与证书文件,完成打包。包名需与苹果开发者后台创建的App ID完全一致。
-
进行打包操作,生成ipa文件。
-
将ipa文件上传到苹果商店。
三、上传应用程序
在上传应用程序之前,需要先完成以下准备工作:
-
在苹果开发者平台上创建App Store Connect应用程序。
-
在App Store Connect中填写应用程序的名称、描述、关键字等信息。
-
在App Store Connect中上传应用程序的ipa文件。
上传应用程序的步骤如下:
-
登录到App Store Connect。
-
选择“我的应用程序”,然后选择要上传的应用程序。
-
在“版本”选项卡中,选择要上传的应用程序版本。
-
在“构建”选项卡中,上传应用程序的ipa文件。
-
在“上架”选项卡中,填写应用程序的价格、分类等信息。
-
提交应用程序审核。
四、审核应用程序
苹果商店会对上传的应用程序进行苹果appstore审核,以确保应用程序符合苹果商店的规定和标准。审核的过程可能需要几天甚至几周的时间,需要耐心等待。
在审核过程中,若应用程序存在问题,苹果商店会发送邮件通知开发者,并提供修改建议。开发者需要根据邮件中的提示进行修改,然后重新提交应用程序审核。
五、上架应用程序
当应用程序通过审核后,苹果商店会将其上架。在应用程序上架后,用户就可以在苹果商店中搜索并下载应用程序了。
总结
以上就是Uniapp苹果上架的流程。开发者需要先进行开发前准备,然后打包应用程序并上传到苹果商店,最后进行审核和上架。对于初次上架的开发者来说,这个过程可能会比较繁琐,但如果按照规定和标准操作,就可以顺利完成应用程序的上架。
25、 UniApp 安卓上架商城需要做什么?
一、资质与材料准备
- 企业资质
- 需提供企业营业执照、对公账户信息(含开户许可证)及公章。
- 软件著作权证书(软著名称需与APP名称一致)。
- 隐私政策文件
隐私政策需包含:权限使用说明(如IMEI、MAC地址等)、用户数据收集范围及用途、服务协议链接等。
需在三个位置展示:首次启动弹窗、登录页面、设置页。
二、应用配置与打包
- manifest.json配置
- 配置应用名称、图标(需提供512×512像素主图标及多尺寸启动图)、权限配置(如微信登录、分享、摄像头、地理位置等)。
启用原生隐私政策弹窗:勾选“使用原生隐私政策提示框”,自动生成androidPrivacy.json文件。
- 打包APK
- 在HBuilderX中选择“发行-云打包”,输入包名(反向域名格式,如***.***pany.app),选择证书类型(推荐云端证书)。
- 勾选“正式包”并选择打包方式(传统打包或快速安心打包)。
三、提交应用市场审核
- 注册开发者账号
- 主流市场(华为、小米、OPPO等)需单独注册企业账号,部分需付费。
- 填写应用信息
- 基本信息:名称、分类、简介、关键词、技术支持链接等。
- 上传安装包(APK)、应用截图(2张以上,含功能演示)、版权证明(软著扫描件)。
- 隐私合规性审核
- 确保隐私政策内容完整,权限声明与功能匹配,避免因违规收集信息被驳回。
- 提供测试账号(如需登录功能)及适配说明。
四、注意事项
- 版本号规范:遵循递增规则(如1.0.0→1.0.1),不可重复或降级。
- 隐私弹窗强制要求:未配置原生弹窗或政策内容缺失会导致审核失败。
- 多市场适配:不同商城对截图尺寸、隐私政策格式可能有差异,需针对性调整。
以上流程覆盖资质准备、应用配置、打包发布及审核要求,适用于华为、小米等主流安卓应用商城
26、uniapp、微信小程序、vue他们的关系
一、uni-app与Vue的关系
-
技术栈继承
-
uni-app基于Vue.js开发,继承了Vue的语法特性(如数据绑定、组件化开发、生命周期等)。
在H5端支持完整的Vue语法,但在App和小程序端不支持部分语法(如vue-router、涉及DOM操作的第三方插件)。
开发模式统一 -
开发者可通过Vue的单文件组件(.vue)编写代码,实现跨平台兼容。
-
二、uni-app与微信小程序的关联
-
规范对齐
- 组件标签:uni-app的标签规范更接近微信小程序(如使用
<view>替代<div>)。 - API能力:JS API设计参考微信小程序规范(如uni.navigateTo对应小程序的wx.navigateTo)。
- 生命周期:在Vue生命周期的基础上,扩展了微信小程序的完整生命周期(如onLoad、onShow等)。
- 组件标签:uni-app的标签规范更接近微信小程序(如使用
-
跨平台特性
- uni-app通过条件编译,可将同一套代码编译为微信小程序、H5、App等多端应用,减少重复开发成本。
三、三者的技术整合与差异
-
开发语言差异
- Vue:使用标准HTML标签、CSS预处理器及原生JavaScript。
- 微信小程序:依赖特有语法(WXML/WXSS)及封闭的API生态。
- uni-app:整合Vue开发模式,屏蔽平台差异,实现多端统一编译。
-
功能限制对比
- 数据绑定:Vue使用:前缀(如:src),微信小程序用双括号({{}}),而uni-app在小程序端遵循后者规范。
- 列表渲染:Vue通过v-for实现,微信小程序用wx:for,uni-app兼容两种语法。
-
生态定位
- Vue:专注Web端单页应用开发。
- 微信小程序:聚焦微信生态内的轻量级应用。
- uni-app:以Vue为基础,扩展为跨端开发框架,覆盖小程序、App、H5等多场景。
总结
uni-app本质是基于Vue技术栈的跨端框架,通过规范对齐和条件编译,实现与微信小程序的深度兼容,同时保留Vue的开发体验。三者关系可概括为:uni-app = Vue语法 + 小程序规范 + 跨端编译能力。
四、微信小程序
1、小程序的架构是什么样的?
小程序分为webview和appService两部分,其中webview主要用来展现UI,appService有来处理业务逻辑、数据及接口调用,它们在两个进程中运行,通过系统层JSBridge实现通信,完成UI的渲染、事件的处理。
2、什么是WXML和WXSS
WXML(WeiXin Markup Language)是小程序的标记语言,用于构建页面结构。WXSS(WeiXin Style Sheets)是小程序的样式表语言,类似于CSS。
3、小程序的生命周期有哪些?
小程序的生命周期包括onLaunch(小程序初始化)、onShow(小程序显示)、onHide(小程序隐藏)、onError(错误处理)等。
4、WXML与标准的HTML的区别?
WXML与HTML都是用来描述页面的结构;都由标签、属性等构成;但标签名字不同,且小程序标签更少,单一标签更多;WXML多了一些wx:if这样的属性以及{{}}这样的表达式;WXML仅能在微信小程序开发者工具中预览,而HTML可以在浏览器内预览;组件封装不同,WXML对组件进行了重新封装;小程序运行在JS Core内,没有DOM树和window对象,无法使用window对象和document对象。
5、WXSS和CSS的异同?
WXSS和CSS都是用来描述页面的样式;WXSS具有CSS的大部分特性,但也做了一些扩充和修改;WXSS新增了尺寸单位rpx,是响应式像素,可以根据屏幕宽度进行自适应;WXSS仅支持部分CSS选择器;WXSS提供全局样式与局部样式;WXSS不支持window和dom文档流。
6、小程序页面间有哪些传递数据的方法?
在app.js中使用全局变量实现数据传递;给元素添加data-*属性来传递值,然后通过e.currentTarget.dataset或onload的param参数获取;通过设置id的方法标识来传值,通过e.currentTarget.id获取设置的id的值,然后通过设置全局对象的方式来传递数值;页面跳转或重定向时,在navigator中使用url带参数传递数据;使用组件模板template传递参数;使用缓存传递参数;使用数据库传递参数。
7、小程序的双向绑定和Vue哪里不一样?
-
微信小程序的双向绑定本质是单向数据流,与Vue的响应式双向绑定核心差异在于:
- Vue:通过v-model和响应式系统(如Object.defineProperty/Proxy)实现自动双向同步,数据修改直接触发视图更新;
- 小程序:仅通过{{}}语法实现视图层数据单向绑定,数据修改需手动调用setData显式传递到视图层,而表单控件等“双向绑定”实际是语法糖(如model:value底层仍需绑定事件+setData更新数据),本质仍是单向流+事件触发的组合模式,无自动依赖追踪。
8、小程序如何实现下拉刷新?
在app.json或page.json中配置enablePullDownRefresh:true;在page里用onPullDownRefresh函数,在下拉刷新时执行;在下拉函数执行时发起数据请求,请求返回后,调用wx.stopPullDownRefresh停止下拉刷新的状态。
9、bindtap和catchtap的区别?
bindtap不会阻止冒泡事件,catchtap可以阻止冒泡。
10、微信小程序与H5的区别?
运行环境不同(小程序在微信运行,H5在浏览器运行);开发成本不同(H5需要兼容不同的浏览器);获取系统权限不同(系统级权限可以和小程序无缝衔接);应用在生产环境的运行流畅度不同(H5需不断对项目优化来提高用户体验)。
11、微信小程序原理是什么?
微信小程序采用JavaScript、WXML、WXSS三种技术进行开发。从技术上讲和现有的前端开发差不多,但深入挖掘又有所不同。JavaScript的代码是运行在微信App中的,并非运行在浏览器中,因此一些H5技术的应用需要微信App提供对应的API支持。WXML是微信自己基于XML语法开发的,只能使用微信提供的现有标签。WXSS具有CSS的大部分特性,但并不是所有的都支持。微信的架构是数据驱动的架构模式,UI和数据是分离的,所有的页面更新都需要通过对数据的更改来实现。小程序分为webview和appService两部分,其中webview主要用来展现UI,appService有来处理业务逻辑、数据及接口调用,它们在两个进程中运行,通过系统层JSBridge实现通信,完成UI的渲染、事件的处理。
12、分析微信小程序的优劣势?
优势包括无需下载、打开速度快、开发成本低、为用户提供良好的安全保障、服务请求快等。劣势包括依托微信不能开发后台管理功能、大小限制不能超过2M、不能打开超过5个层级的页面、样式单一等。
13、小程序有哪些文件类型?
WXML(模板文件)、WXSS(样式文件)、JS(脚本逻辑文件)、JSON(配置文件)
14、简述微信小程序页面的生命周期函数?
onLoad:页面加载时触发;onReady:页面初次渲染完成时触发;onShow:页面显示时触发;onHide:页面隐藏时触发;onUnload:页面卸载时触发。
15、小程序如何更新页面中的值?
通过调用this.setData()方法来更新页面中的值。
16、如何实现登录数据的持久化?
可以使用本地存储(如wx.setStorageSync和wx.getStorageSync)或缓存(如wx.setStorageSync和wx.getStorageSync)来实现登录数据的持久化。
17、微信小程序和app有什么不同之处?
微信小程序无需下载安装即可使用,而app需要下载安装;微信小程序更轻量级,占用空间小;app的功能和性能通常比小程序更强大。
18、微信小程序如何关联微信公众号?
需要在微信公众号后台进行相关配置,并获取必要的信息(如AppID、AppSecret等),然后在小程序中进行相应的设置和调用。
19、webview中的页面怎么跳转回小程序?
先在管理后台配置域名白名单,然后引入jweixin-1.3.2.js(https://res.wx.qq.***/open/js/jweixin-1.3.0.js),最后使用wx.miniProgram.navigateTo方法进行跳转。
20、微信小程序如何实现分页加载数据?
可以通过滚动监听(如onReachBottom事件)来检测用户是否滚动到页面底部,然后发起网络请求加载更多数据,并更新页面内容。
21、微信小程序如何获取用户的位置信息?
可以使用wx.getLocation接口来获取用户的地理位置信息,但需要注意获取用户授权。
22、小程序中的图片如何实现懒加载?
可以使用标签的lazy-load属性来实现图片的懒加载,或者通过第三方库来实现。
23、页面生命周期
| 生命周期函数 | 触发时机 | 典型使用场景 |
|---|---|---|
| onLoad(options) | 页面加载时触发(一个页面只触发一次) | 接收页面参数(options.query),初始化页面数据 |
| onShow() | 页面显示/切入前台时触发 | 更新动态数据(如实时位置、计时器) |
| onReady() | 页面初次渲染完成时触发(一个页面只触发一次) | 操作页面 DOM(需在此后调用 wx.createSelectorQuery) |
| onHide() | 页面隐藏/切入后台时触发 | 暂停页面动画、停止视频播放 |
| onUnload() | 页面卸载时触发(关闭页面或重定向到其他页面) | 清除定时器、取消订阅消息 |
五、JS
1、闭包
闭包就是在一个函数A内部定义一个新的函数B,并且这个新函数B,调用了在函数A内定义的变量,并且在函数A外部被调用时返回函数A的内部变量,这就形成了闭包
用途:封装私有变量、防抖节流等。
风险:闭包会使得函数内部的变量在函数执行后仍然缓存于内存中,直到没有任何引用指向闭包。如果不注意管理闭包,可能会导致内存泄漏问题。
-
第一种:
function createCache() { const cache = {}; // 外部函数作用域的变量 return { set(key, value) { cache[key] = value; }, get(key) { return cache[key]; } }; } const myCache = createCache(); myCache.set('token', 'abc123'); // 数据存储在闭包内的 cache 对象中 console.log(myCache.get('token')); // 输出 'abc123'原理:cache 变量被闭包内部方法引用,即使 createCache() 执行完毕,cache 仍缓存于内存中。
效果:通过闭包隔离作用域,实现类似私有缓存的机制,避免全局变量污染
解决内存泄漏:function createCache() { const cache = {}; return { set(key, value) { cache[key] = value; }, get(key) { return cache[key]; }, delete(key) { delete cache[key]; }, // 删除单个键 clear() { Object.keys(cache).forEach(key => delete cache[key]); }, // 清空所有 size() { return Object.keys(cache).length; } // 查看大小 }; } const myCache = createCache(); myCache.set('token', 'abc123'); myCache.delete('token'); // 手动删除 myCache.clear(); // 清空缓存 -
第二种:
function createXXX(){ const big = 'xxxxxxx'; const small = 'xxxx'; function s1(){ console.log(big); // 实际使用big } function s2(){ return small; // 实际返回small } return s2; } const xxx = createXXX();我们可以看到s2是被使用的,那么s2就不会被销毁,small在s2里面也使用了所以也不会销毁,但是big呢,他的s1没有被使用所以big会不会被销毁呢?
答案是:也不会销毁
原因:
因为词法环境是共享的,这两个闭包都指向同一个词法环境所以s1没有被使用这个闭包会销毁,但是s2还在使用切内部调用了small所以big也不会销毁这样理解对吗 -
常见题:
for(var i=0;i<10;i++){ setTimeout(function(){ console.info(i) }) }答案:
- console打印的都是10
问题原因:
-
var 是函数作用域,整个循环共享同一个 i
-
当 setTimeout 回调执行时,循环已结束,i 的值已经是 10
解决方案:
- 使用闭包:
for(var i=0; i<10; i++) { (function(j) { setTimeout(function() { console.info(j) // 输出0-9 }) })(i) }- 使用 let:
for(let i=0; i<10; i++) { setTimeout(function() { console.info(i) // 输出0-9 }) }
2、原型链
-
原型链的本质是对象间通过
__proto__属性串联形成的链式关系,访问属性时,沿着这条链向上查找:对象 → 原型 → 原型的原型 → null。例如,若A继承B,则A的实例会通过A.prototype.__proto__链接到B.prototype,A实例 →A.prototype→B.prototype→Object.prototype→null的链条- 递归查找
-
JavaScript 引擎内部是通过 递归查找 来实现逐层向上查找的。
内部实现原理(简化版)
想象 JavaScript 引擎有一个这样的查找函数:function findProperty(obj, propertyName) { // 1. 先在当前对象自身查找 if (obj.hasOwnProperty(propertyName)) { return obj[propertyName]; } // 2. 如果自身没有,就找原型对象 const prototype = obj.__proto__; if (prototype) { // 3. 递归调用:在原型对象上继续查找 return findProperty(prototype, propertyName); } // 4. 找到原型链顶端还没找到,返回 undefined return undefined; }具体查找过程
// 实际例子 const arr = [1, 2, 3]; // 当访问 arr.toString() 时: // 第1步:arr 自身有 toString 吗? ❌ // 第2步:找 arr.__proto__ (Array.prototype),有 toString 吗? ❌ // 第3步:找 Array.prototype.__proto__ (Object.prototype),有 toString 吗? ✅ // 找到!返回 Object.prototype.toString关键机制
-
每个对象都有 proto:指向它的原型
-
原型也是对象:所以原型也有自己的 proto
-
递归查找:不断通过 proto 向上找,直到找到或到达 null
面试一句话:
- JavaScript 引擎通过 proto 指针递归向上查找,形成"原型链"的搜索机制
-
-
- 递归查找
-
__proto__: 每个对象(包括函数对象和普通对象)都具有的属性,它指向对象的原型,也就是它的父对象。用于实现原型链,当你访问一个对象的属性时,如果对象本身没有这个属性,JavaScript 引擎会沿着原型链(通过__proto__属性)向上查找, 直到找到属性或到达原型链的顶部(通常是Object.prototype)。主要用于对象之间的继承,它建立了对象之间的原型关系。 -
prototype:函数对象(构造函数)特有属性,每个函数对象都有一个prototype属性,它是一个对象。通常用于定义共享的属性和方法,可以被构造函数创建的实例对象所继承。可以在构造函数的 prototype 上定义方法,以便多个实例对象共享这些方法, 从而节省内存。主要用于原型继承,它是构造函数和实例对象之间的链接,用于共享方法和属性。
-
示例
// 1. 创建基础构造函数 function Animal(name) { this.name = name; } // 2. 在原型上添加方法 Animal.prototype.speak = function() { console.log(`${this.name} makes a noise.`); }; // 3. 创建子类构造函数 function Dog(name, breed) { Animal.call(this, name); // 调用父类构造函数 this.breed = breed; } // 4. 继承父类原型 (关键步骤) Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; // 修复构造函数指向 // 5. 扩展子类原型方法 Dog.prototype.bark = function() { console.log(`${this.name} (${this.breed}) barks: Woof!`); }; // 6. 创建实例 const myDog = new Dog('Buddy', 'Golden Retriever'); // ---------- 原型链验证 ---------- // 实例 -> Dog.prototype -> Animal.prototype -> Object.prototype -> null console.log(myDog.__proto__ === Dog.prototype); // true console.log(Dog.prototype.__proto__ === Animal.prototype); // true console.log(Animal.prototype.__proto__ === Object.prototype);// true console.log(Object.prototype.__proto__); // null // 7. 方法调用演示 myDog.speak(); // "Buddy makes a noise." (继承自Animal) myDog.bark(); // "Buddy (Golden Retriever) barks: Woof!" // 8. 覆盖父类方法 Dog.prototype.speak = function() { console.log(`${this.name} overrides speak!`); Animal.prototype.speak.call(this); // 调用父类方法 }; myDog.speak(); // 输出: // "Buddy overrides speak!" // "Buddy makes a noise." // 9. 类型检查验证 console.log(myDog instanceof Dog); // true console.log(myDog instanceof Animal); // true console.log(myDog instanceof Object); // true console.log(Object.prototype.toString.call(myDog)); // [object Object]
总结
- 原型链的指向是单向的、逐级向上的,通过
__proto__串联。 - 所有对象最终指向
Object.prototype,而Object.prototype指向null。 - 构造函数和函数对象通过
Function.prototype关联到原型链中。
2.1 那原型链真正的作用是什么呢就为了把api共享出来吗
原型链的真正作用远不止共享API,它是JavaScript面向对象编程的基石。让我用最简单的方式说明:
核心作用一:实现真正的继承
-
没有原型链(每个实例都创建自己的方法副本)
function PersonWithoutPrototype(name) { this.name = name; // 在构造函数内部定义方法,每个实例都会有自己的方法副本 this.sayHello = function() { console.log(`Hello, I'm ${this.name}`); }; } // 创建两个实例 const person1 = new PersonWithoutPrototype('Alice'); const person2 = new PersonWithoutPrototype('Bob'); // 两个实例的 sayHello 方法是不同的函数对象 console.log(person1.sayHello === person2.sayHello); // false缺点:每个实例都包含一个独立的 sayHello 方法,如果创建很多实例,会消耗更多内存,因为每个方法都是新创建的。
- 使用原型链(所有实例共享原型上的方法)
function PersonWithPrototype(name) { this.name = name; } // 将方法定义在原型上 PersonWithPrototype.prototype.sayHello = function() { console.log(`Hello, I'm ${this.name}`); }; // 创建两个实例 const person1 = new PersonWithPrototype('Alice'); const person2 = new PersonWithPrototype('Bob'); // 两个实例的 sayHello 方法是同一个函数对象 console.log(person1.sayHello === person2.sayHello); // true优点:无论创建多少个实例,它们都共享同一个 sayHello 方法,节省内存。
核心作用二:内存高效的方法共享
-
没有原型链:每个实例都有方法的副本
function Dog(name) { this.name = name; // 每次创建实例时,都会创建一个新的bark函数 this.bark = function() { console.log(`${this.name} says woof!`); }; } const dog1 = new Dog('Buddy'); const dog2 = new Dog('Max'); // 两个实例的bark方法是不同的函数对象 console.log(dog1.bark === dog2.bark); // false在这个例子中,每创建一个新的Dog实例,都会在内存中创建一个新的bark函数。如果有成千上万个实例,就会创建成千上万个函数,它们的功能相同,但却占用了不同的内存空间。
-
有原型链:所有实例共享原型上的方法
function Dog(name) { this.name = name; } // 将bark方法定义在原型上 Dog.prototype.bark = function() { console.log(`${this.name} says woof!`); }; const dog1 = new Dog('Buddy'); const dog2 = new Dog('Max'); // 两个实例的bark方法是同一个函数对象 console.log(dog1.bark === dog2.bark); // true在这个例子中,bark方法只存在一个,存储在Dog.prototype上。所有通过Dog构造函数创建的实例都可以通过原型链访问到这个方法。这样,无论创建多少个实例,bark方法在内存中只存在一份,极大地节省了内存。
核心作用三:动态扩展和修改
-
没有原型链(动态扩展困难)
在没有原型链的情况下,如果我们使用工厂函数或构造函数内定义方法,那么每个对象都有自己的方法副本。当我们需要添加一个新方法时,我们必须逐个修改每个对象,这显然是非常低效的。// 使用构造函数内定义方法的方式 function Car(model, year) { this.model = model; this.year = year; this.drive = function() { console.log(`Driving the ${this.model}`); }; } // 创建两个实例 const car1 = new Car('Toyota', 2020); const car2 = new Car('Honda', 2021); // 现在,我们想为所有Car实例添加一个名为`stop`的新方法。 // 由于没有原型链,我们只能逐个实例添加,或者修改构造函数,但修改构造函数不会影响已创建的实例。 // 方法1:逐个实例添加(不现实,如果实例很多的话) car1.stop = function() { console.log(`Stopping the ${this.model}`); }; car2.stop = function() { console.log(`Stopping the ${this.model}`); }; // 方法2:修改构造函数,但已经创建的实例不会受到影响 Car.prototype.stop = function() { console.log(`Stopping the ${this.model}`); }; // 但是,由于car1和car2在创建时并没有通过原型链继承,所以它们不会得到这个新方法。 // 测试 car1.stop(); // 只有car1有stop方法(如果用了方法1) car2.stop(); // 如果用了方法1,car2也有。但如果只修改了构造函数原型,则car2没有。 // 实际上,在没有原型链的情况下,我们无法通过修改一个共享的地方来影响所有实例。 -
使用原型链(动态扩展容易)
使用原型链,我们可以通过修改构造函数的原型对象来动态添加方法,所有实例(包括已经创建和将来创建的)都会自动拥有这个方法。
function Car(model, year) {
this.model = model;
this.year = year;
}
// 将方法定义在原型上
Car.prototype.drive = function() {
console.log(`Driving the ${this.model}`);
};
// 创建两个实例
const car1 = new Car('Toyota', 2020);
const car2 = new Car('Honda', 2021);
// 现在,我们想为所有Car实例添加一个名为`stop`的新方法。
// 只需要在原型上添加即可
Car.prototype.stop = function() {
console.log(`Stopping the ${this.model}`);
};
// 测试
car1.stop(); // "Stopping the Toyota"
car2.stop(); // "Stopping the Honda"
// 甚至新创建的实例也会拥有这个方法
const car3 = new Car('Ford', 2022);
car3.stop(); // "Stopping the Ford"
面试一句话总结:
-
原型链的三大核心作用:
-
实现继承 - 让对象之间有关联关系
-
内存优化 - 方法共享,避免重复创建
-
动态扩展 - 运行时修改和增强对象能力
-
-
没有原型链,JavaScript就没有:
-
真正的面向对象继承
-
高效的内存使用
-
灵活的动态语言特性
-
所以原型链不是"为了共享API",而是JavaScript对象系统的根本机制!
2.2 js原型链,vue2是如何使用的原型链,vue3又是如何模拟的原型链
JavaScript 原型链 API 完整对应关系
一、原型操作 API(直接操作原型链)
| API | 作用 | 使用场景 |
|---|---|---|
| Object.create(proto) | 创建指定原型的对象 | 纯对象继承 |
| Object.getPrototypeOf(obj) | 获取对象的原型 | 调试、反射 |
| Object.setPrototypeOf(obj, proto) | 设置对象的原型 | 动态修改继承 |
| instanceof | 检查构造函数prototype是否在原型链 | 类型检查 |
| prototype | 函数的原型对象 | 定义共享方法 |
| constructor | 指向构造函数 | 实例溯源 |
| proto 对象的原型 | (已废弃) | 历史代码 |
二、原型检查 API
| API | 作用 | 返回值 |
|---|---|---|
| obj.hasOwnProperty(key) | 检查自身属性(非原型链) | boolean |
| key in obj | 检查整个原型链是否存在 | boolean |
三、内置对象原型方法对应关系
| 原型方法 | 日常写法 | 作用 |
|---|---|---|
| Array.prototype.map() | arr.map(callback) | 数组映射 |
| Array.prototype.filter() | arr.filter(callback) | 数组过滤 |
| Array.prototype.reduce() | arr.reduce(callback) | 数组累加 |
| Array.prototype.forEach() | arr.forEach(callback) | 数组遍历 |
| Array.prototype.push() | arr.push(item) | 数组追加 |
| Array.prototype.pop() | arr.pop() | 数组弹出 |
| Array.prototype.slice() | arr.slice(start, end) | 数组切片 |
| Array.prototype.splice() | arr.splice(start, count) | 数组删除/插入 |
| Array.prototype.includes() | arr.includes(item) | 数组包含检查 |
| Array.prototype.find() | arr.find(callback) | 数组查找 |
| String.prototype.slice() | str.slice(start, end) | 字符串切片 |
| String.prototype.substring() | str.substring(start, end) | 字符串子串 |
| String.prototype.split() | str.split(separator) | 字符串分割 |
| String.prototype.replace() | str.replace(search, replace) | 字符串替换 |
| String.prototype.toLowerCase() | str.toLowerCase() | 转小写 |
| String.prototype.toUpperCase() | str.toUpperCase() | 转大写 |
| String.prototype.trim() | str.trim() | 去除空格 |
| String.prototype.includes() | str.includes(search) | 字符串包含 |
| Number.prototype.toFixed() | num.toFixed(digits) | 数字固定小数 |
| Number.prototype.toString() | num.toString(radix) | 数字转字符串 |
| Map.prototype.get() | map.get(key) | Map获取值 |
| Map.prototype.set() | map.set(key, value) | Map设置值 |
| Map.prototype.has() | map.has(key) | Map检查键 |
| Set.prototype.add() | set.add(value) | Set添加值 |
| Set.prototype.has() | set.has(value) | Set检查值 |
| Object.prototype.toString() | obj.toString() | 对象转字符串 |
| Object.prototype.valueOf() | obj.valueOf() | 对象原始值 |
| Function.prototype.call() | func.call(thisArg, …args) | 函数调用 |
| Function.prototype.apply() | func.apply(thisArg, args) | 函数调用 |
| Function.prototype.bind() | func.bind(thisArg) | 函数绑定 |
四、特殊使用场景对应关系
| 场景 | 原型写法 | 日常替代方案 |
|---|---|---|
| 扩展内置对象 | Array.prototype.last = function(){} | 工具函数模块 |
| 处理类数组对象 | Array.prototype.map.call(arrayLike) | Array.from(arrayLike).map() |
| 方法借用 | Array.prototype.join.call(obj) | 直接操作对象 |
| 安全检查 | Object.prototype.hasOwnProperty.call(obj, key) | Object.hasOwn(obj, key) |
面试总结:
-
知道原理:所有内置方法都通过原型链共享
-
日常使用:直接用简洁写法 arr.map()、str.slice()
-
特殊场景:才需要直接操作 Array.prototype
Vue 2 如何使用原型链
核心机制:组件继承体系
// 所有组件都继承自 Vue 构造函数
const Sub = Vue.extend(Base***ponent)
Sub.prototype.__proto__ === Vue.prototype // true
// 全局 API 挂载
Vue.prototype.$http = axios
Vue.prototype.$router = router
// 组件内使用
this.$http.get('/api') // 通过原型链找到 Vue.prototype.$http
-
特点:
-
真正的 JavaScript 原型链
-
组件实例 this 直接继承 Vue.prototype
-
Mixins 通过原型链合并方法
-
Vue 3 如何模拟原型链
核心机制:Proxy 代理查找
// 模拟原型链查找
instance.proxy = new Proxy(instance, {
get(target, key) {
// 1. 本地数据 → 2. Props → 3. 全局属性
if (key in target.data) return target.data[key]
if (key in appContext.config.globalProperties) {
return appContext.config.globalProperties[key]
}
}
})
// 全局属性配置
app.config.globalProperties.$http = axios
// 组件内使用
const { proxy } = getCurrentInstance()
proxy.$http.get('/api') // 通过模拟原型链找到
-
特点:
-
不是真正的原型链,是查找模拟
-
通过 Proxy 实现"链式查找"效果
-
更灵活,可控制查找顺序
-
-
面试一句话总结
Vue 2 用真实原型链实现组件继承,Vue 3 用 Proxy 模拟原型链查找,都是为了实现全局属性共享。
2.3 为什么要有原型链?原型链继承 vs 传统面向对象继承?
问题一:为什么要有原型链?
-
节省内存
// 坏:每人带一本字典 // 好:全班共用一本字典 -
方便扩展
// 给爸爸买新东西,儿子马上就能用 -
灵活继承
// 想换爸爸?可以! // 想认干爹?也可以! -
一句话总结:
原型链让对象能"借用"别人的属性和方法,不用自己都带一份。
问题二:原型链 vs 传统面向对象
-
传统面向对象(如Java)
// 像"复印机" class Animal { // 模板 void eat() { } // 方法定义 } class Dog extends Animal { // 复印并扩展 void bark() { } // 新增内容 } Dog dog = new Dog(); // 复印出一份特点:每个对象都是独立副本,修改模板不影响已创建对象
-
JavaScript原型链
// 像"家族链" function Animal() {} // 祖先 Animal.prototype.eat = function() {} // 家族技能 function Dog() {} // 后代 Dog.prototype = Object.create(Animal.prototype) // 认祖归宗 const dog = new Dog(); // 家族成员特点:对象间有血缘关系,祖先新增技能,后代自动获得
-
核心区别
| 方面 | 传统面向对象 | JavaScript原型链 |
|---|---|---|
| 继承方式 | 类继承(复制) | 原型继承(委托) |
| 内存使用 | 每个实例带全套方法 | 方法共享,节省内存 |
| 灵活性 | 编译时确定,较死板 | 运行时可变,很灵活 |
| 扩展性 | 修改类不影响现有实例 | 修改原型影响所有实例 |
实际例子
// 传统面向对象:每人发一本说明书
// JavaScript原型链:全家共用一本说明书,谁需要谁看
// 后来发现说明书有错:
// - 传统:要收回所有人的书重新发
// - JavaScript:改一本就行了,大家自动看到最新版
一句话总结:
传统面向对象是"复制继承",JavaScript原型链是"委托继承"
2.4 javaScript 中实现继承的方式
-
原型链继承(最基本的方式)
在 JavaScript 的原型继承机制中,方法和属性的定义位置决定了它们在内存中的存储方式和访问特性:定义在prototype原型对象上的属性和方法会被所有实例共享,在内存中仅存在一份副本,所有实例都通过原型链访问这同一份资源,这种设计特别适合存储不变的方法和常量数据。
Animal.prototype.species = 'Animal'; Animal.prototype.speak = function() { console.log(`${this.name} makes a sound`); };定义在子构造函数中的属性和方法则会在每次实例化时创建独立的副本,每个实例都拥有自己专属的属性存储空间,彼此之间互不影响,这种设计适合存储实例特有的状态和数据。
// 子类构造函数 - 定义实例特有的属性 function Dog(name, breed, colors) { // 在构造函数中定义实例属性,每个实例都有自己独立的副本 this.name = name; this.breed = breed; this.colors = colors || ['black', 'white']; }这种区分确保了方法共享的内存效率与实例状态隔离的数据完整性之间的平衡,是原型继承模式的核心设计原则。
例子:
// 父类构造函数 function Animal() { // 构造函数内部不定义属性,或者定义一些基础共享属性 } // 父类原型上的共享属性和方法 Animal.prototype.species = 'Animal'; Animal.prototype.speak = function() { console.log(`${this.name} makes a sound`); }; // 子类构造函数 - 定义实例特有的属性 function Dog(name, breed, colors) { // 在构造函数中定义实例属性,每个实例都有自己独立的副本 this.name = name; this.breed = breed; this.colors = colors || ['black', 'white']; } // 原型链继承 Dog.prototype = new Animal(); Dog.prototype.constructor = Dog; // 子类方法挂在原型上 Dog.prototype.bark = function() { console.log(`${this.name} barks!`); };// 创建实例,每个实例有自己的属性 const dog1 = new Dog('Buddy', 'Golden Retriever', ['golden']); const dog2 = new Dog('Max', 'Poodle', ['white', 'brown']); // 每个实例有自己独立的属性 console.log(dog1.name); // "Buddy" console.log(dog1.breed); // "Golden Retriever" console.log(dog1.colors); // ['golden'] console.log(dog2.name); // "Max" console.log(dog2.breed); // "Poodle" console.log(dog2.colors); // ['white', 'brown'] // 修改一个实例的属性不会影响其他实例 dog1.colors.push('dark golden'); console.log(dog1.colors); // ['golden', 'dark golden'] console.log(dog2.colors); // ['white', 'brown'] - 不受影响 // 仍然可以访问原型上的共享属性和方法 console.log(dog1.species); // "Animal" - 来自原型 dog1.speak(); // "Buddy makes a sound" - 使用实例的name dog1.bark(); // "Buddy barks!" - 使用实例的name -
构造函数继承(经典继承)
在子类构造函数中调用父类构造函数,使用 call 或 apply。-
选择依据:
-
参数明确且固定时用 call
-
参数是数组或数量动态时用 apply
-
构造方法通过 call/apply 实现父类属性实例化和传参
特征:
✅ 使用 Animal.call(this, name) 实现属性继承 ✅ 每个实例有独立的属性副本 ✅ 引用类型属性不会共享 ❌ 无法访问父类prototype原型上的方法/** * 父类 Animal * @param {string} name - 动物名称 * name 参数 根据call或apply变化 */ function Animal(name) { // 实例属性 - 每个实例都有自己独立的副本 this.name = name; this.colors = ['black', 'white']; } /** * 父类原型方法 - 所有实例共享同一个方法 */ Animal.prototype.speak = function() { console.log(`${this.name} makes a sound`); }; /** * 子类 Dog * @param {string} name - 狗的名字 * @param {string} breed - 狗的品种 */ function Dog(name, breed) { // 1. 构造方法继承 - 继承父类的实例属性 // 方法一:使用 call (第一个是 this 值,后面是任意数量的参数) Animal.call(this, name); // 等价于:在这个 Dog 实例的上下文中执行 Animal 构造函数 // 相当于:this.name = name; this.colors = ['black', 'white']; // 方法二:使用 apply (参数以数组形式传递) // Animal.apply(this, [name]); // 如果 Animal 构造函数有多个参数,可以使用 apply // 例如:Animal.apply(this, [name, age, weight]); // 子类自己的实例属性 this.breed = breed; } function Dog(name, breed) { // 纯构造函数继承 Animal.call(this, name); // 关键代码 this.breed = breed; } // ========== 测试代码 ========== // 测试 const dog1 = new Dog('Buddy', 'Golden'); const dog2 = new Dog('Max', 'Poodle'); dog1.colors.push('brown'); console.log(dog1.colors); // ['black', 'white', 'brown'] console.log(dog2.colors); // ['black', 'white'] - 不受影响! // 问题:无法继承原型上的方法 // dog1.speak(); // 报错:dog1.speak is not a function缺点:
只能继承实例属性,无法继承prototype原型上的方法
方法都在构造函数中定义,每次创建实例都要创建方法,函数复用就无从谈起
-
-
组合继承(最常用的经典方式)
结合原型链继承和构造函数继承,是 JavaScript 中最常用的继承模式。/** * 父类 Animal * @param {string} name - 动物名称 * name 参数 根据call或apply变化 */ function Animal(name) { // 实例属性 - 每个实例都有自己独立的副本 this.name = name; this.colors = ['black', 'white']; } /** * 父类原型方法 - 所有实例共享同一个方法 */ Animal.prototype.speak = function() { console.log(`${this.name} makes a sound`); }; /** * 子类 Dog * @param {string} name - 狗的名字 * @param {string} breed - 狗的品种 */ function Dog(name, breed) { // 1. 构造方法继承 - 继承父类的实例属性 // 方法一:使用 call (第一个是 this 值,后面是任意数量的参数) Animal.call(this, name); // 等价于:在这个 Dog 实例的上下文中执行 Animal 构造函数 // 相当于:this.name = name; this.colors = ['black', 'white']; // 方法二:使用 apply (参数以数组形式传递) // Animal.apply(this, [name]); // 如果 Animal 构造函数有多个参数,可以使用 apply // 例如:Animal.apply(this, [name, age, weight]); // 子类自己的实例属性 this.breed = breed; } // 2. 原型链继承 - 继承父类的原型方法 /** * 设置 Dog 的原型为 Animal 的一个实例 * 这样 Dog 实例就能通过原型链访问 Animal 原型上的方法 */ Dog.prototype = Object.create(Animal.prototype); /** * 修复 constructor 指向 * 因为上面一行重写了 Dog.prototype,导致 constructor 指向了 Animal * 这里需要手动修复,让 constructor 正确指向 Dog */ Dog.prototype.constructor = Dog; /** * 子类自己的原型方法 */ Dog.prototype.bark = function() { console.log(`${this.name} barks loudly!`); }; // ========== 测试代码 ========== console.log("=== 创建实例 ==="); const dog1 = new Dog('Buddy', 'Golden Retriever'); const dog2 = new Dog('Max', 'Poodle'); console.log("=== 实例属性测试 ==="); console.log('dog1:', dog1.name, dog1.breed, dog1.colors); console.log('dog2:', dog2.name, dog2.breed, dog2.colors); console.log("=== 属性独立性测试 ==="); dog1.colors.push('brown'); // 修改 dog1 的 colors console.log('dog1.colors 添加 brown 后:', dog1.colors); // ['black', 'white', 'brown'] console.log('dog2.colors 不受影响:', dog2.colors); // ['black', 'white'] - 确实独立 console.log("=== 方法继承测试 ==="); dog1.speak(); // "Buddy makes a sound" - 继承的父类方法 dog2.speak(); // "Max makes a sound" - 继承的父类方法 dog1.bark(); // "Buddy barks loudly!" - 子类自己的方法 dog2.bark(); // "Max barks loudly!" - 子类自己的方法 console.log("=== 原型链验证 ==="); console.log('dog1 能访问 Animal.prototype.speak:', dog1.speak === Animal.prototype.speak); // true console.log('dog1 的 constructor:', dog1.constructor.name); // Dog console.log('dog1 是 Dog 的实例:', dog1 instanceof Dog); // true console.log('dog1 是 Animal 的实例:', dog1 instanceof Animal); // true console.log("=== call vs apply 补充示例 ==="); // 假设有一个需要多个参数的父类 function MultiParamAnimal(name, species, age) { this.name = name; this.species = species; this.age = age; } function MultiParamDog(name, species, age, breed) { // 使用 call - 适合参数明确且数量不多的情况 // MultiParamAnimal.call(this, name, species, age); // 使用 apply - 适合参数以数组形式存在或参数数量动态的情况 MultiParamAnimal.apply(this, [name, species, age]); this.breed = breed; } const multiDog = new MultiParamDog('Rex', 'Canine', 3, 'German Shepherd'); console.log('多参数继承示例:', multiDog.name, multiDog.species, multiDog.age, multiDog.breed);优点:
实例属性独立,不共享
可以继承原型方法
可以向父类构造函数传参
缺点:
调用了两次父类构造函数
-
原型式继承
基于已有对象创建新对象,不自定义类型。const animal = { colors: ['black', 'white'], speak: function() { console.log(`${this.name} makes a sound`); } }; // 方式1:Object.create() (推荐) const dog1 = Object.create(animal); dog1.name = 'Buddy'; dog1.breed = 'Golden'; // 方式2:自定义实现(兼容旧浏览器) function createObject(obj) { function F() {} F.prototype = obj; return new F(); } const dog2 = createObject(animal); dog2.name = 'Max'; dog2.breed = 'Poodle'; // 测试 dog1.speak(); // "Buddy makes a sound" dog1.colors.push('brown'); console.log(dog2.colors); // ['black', 'white', 'brown'] - 共享问题依然存在适用场景:
不需要创建构造函数,只想让一个对象与另一个对象保持类似 现代 JavaScript 中常用于对象之间简单继承 -
寄生式继承
在原型式继承的基础上,增强对象。function createDog(original, name, breed) { // 基于 original 创建新对象 const clone = Object.create(original); // 增强对象 clone.name = name; clone.breed = breed; clone.bark = function() { console.log(`${this.name} barks!`); }; return clone; } const animal = { colors: ['black', 'white'], speak: function() { console.log(`${this.name} makes a sound`); } }; const dog1 = createDog(animal, 'Buddy', 'Golden'); const dog2 = createDog(animal, 'Max', 'Poodle'); dog1.speak(); // "Buddy makes a sound" dog1.bark(); // "Buddy barks!"缺点:
方法不能复用,跟构造函数模式类似
-
寄生组合式继承(最理想的继承方式)
这是最完美的继承方式,结合了各种模式的优点。function inheritPrototype(child, parent) { // 创建父类原型的副本 const prototype = Object.create(parent.prototype); // 修复 constructor 指向 prototype.constructor = child; // 设置子类原型 child.prototype = prototype; } // 父类 function Animal(name) { this.name = name; this.colors = ['black', 'white']; } Animal.prototype.speak = function() { console.log(`${this.name} makes a sound`); }; // 子类 function Dog(name, breed) { // 继承实例属性 Animal.call(this, name); this.breed = breed; } // 继承原型方法(关键步骤) inheritPrototype(Dog, Animal); // 子类自己的方法 Dog.prototype.bark = function() { console.log(`${this.name} barks!`); }; // 测试 const dog1 = new Dog('Buddy', 'Golden'); const dog2 = new Dog('Max', 'Poodle'); dog1.speak(); // "Buddy makes a sound" dog1.bark(); // "Buddy barks!" dog1.colors.push('brown'); console.log(dog1.colors); // ['black', 'white', 'brown'] console.log(dog2.colors); // ['black', 'white'] - 不受影响优点:
只调用一次父类构造函数 避免在子类原型上创建不必要的属性 原型链保持不变 -
ES6 Class 继承(现代方式)
ES6 引入了 class 和 extends 关键字,让继承更加清晰易懂。class Animal { constructor(name) { this.name = name; this.colors = ['black', 'white']; } speak() { console.log(`${this.name} makes a sound`); } } class Dog extends Animal { constructor(name, breed) { super(name); // 必须调用 super() this.breed = breed; } // 方法自动添加到原型上 bark() { console.log(`${this.name} barks!`); } // 重写父类方法 speak() { super.speak(); // 调用父类方法 console.log(`${this.name} barks loudly!`); } } // 测试 const dog1 = new Dog('Buddy', 'Golden'); const dog2 = new Dog('Max', 'Poodle'); dog1.speak(); // "Buddy makes a sound" // "Buddy barks loudly!" dog1.colors.push('brown'); console.log(dog1.colors); // ['black', 'white', 'brown'] console.log(dog2.colors); // ['black', 'white'] - 不受影响 // 静态方法继承 class Animal { static isAnimal(obj) { return obj instanceof Animal; } } class Dog extends Animal { static isDog(obj) { return obj instanceof Dog; } } console.log(Dog.isAnimal(new Dog())); // true console.log(Animal.isDog(new Animal())); // false-
ES6 Class 继承的特点:
-
语法更加清晰易懂
-
内置 super 关键字调用父类方法
-
支持静态方法继承
-
底层实现仍然是原型链
-
必须使用 new 调用
-
类中定义的方法都是不可枚举的
-
-
总结
1.现代项目:直接使用 ES6 class 和 extends
2.需要兼容旧环境:使用寄生组合式继承
3.简单对象继承:使用 Object.create()
4.学习理解:从组合继承开始,理解原型链机制
2.5 什么是构造函数
什么是构造函数
构造函数是 JavaScript 中用于创建和初始化对象的特殊函数。
基本概念
定义
-
构造函数是一个普通的 JavaScript 函数,但有以下特点:
-
通常首字母大写(命名约定)
-
使用 new 关键字调用
-
用于创建特定类型的对象
-
基本语法
// 定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 使用 new 调用构造函数创建实例
const person1 = new Person('Alice', 25);
const person2 = new Person('Bob', 30);
构造函数的工作原理
当使用 new 调用构造函数时,发生以下步骤:
function Person(name, age) {
// 1. 隐式创建新对象:const this = {}
// 2. 设置原型链:this.__proto__ = Person.prototype
this.name = name; // 3. 初始化属性
this.age = age;
// 4. 隐式返回 this:return this
}
构造函数的特征
-
创建实例
function Car(brand, year) { this.brand = brand; this.year = year; } const myCar = new Car('Toyota', 2020); console.log(myCar.brand); // "Toyota" -
每个实例独立
const car1 = new Car('Honda', 2019); const car2 = new Car('Ford', 2021); car1.brand = 'BMW'; // 只修改 car1,不影响 car2 console.log(car2.brand); // "Ford" -
与原型配合使用
function Animal(name) { this.name = name; // 实例特有属性 } // 共享方法放在原型上 Animal.prototype.speak = function() { console.log(`${this.name} makes a sound`); }; const dog = new Animal('Buddy'); dog.speak(); // "Buddy makes a sound"
构造函数的类型
-
自定义构造函数
function Product(name, price) { this.name = name; this.price = price; } const product = new Product('Laptop', 999); -
内置构造函数
// JavaScript 提供的内置构造函数 const obj = new Object(); const arr = new Array(); const str = new String('hello'); const num = new Number(42); const date = new Date(); 3. 类构造函数(ES6) javascript class Student { constructor(name, grade) { this.name = name; this.grade = grade; } } const student = new Student('John', 'A');
构造函数的验证
检查构造函数
function Person(name) {
this.name = name;
}
const person = new Person('Alice');
console.log(person.constructor === Person); // true
console.log(person instanceof Person); // true
构造函数 vs 普通函数
| 特性 | 构造函数 | 普通函数 |
|---|---|---|
| 调用方式 | new Person() | person() |
| this 指向 | 新创建的对象 | 调用上下文 |
| 返回值 | 隐式返回 this | 显式返回或 undefined |
| 命名约定 | 首字母大写 | 首字母小写 |
总结
-
构造函数是:
-
用于创建对象实例的特殊函数
-
通过 new 关键字调用
-
负责初始化实例属性
-
与原型对象配合实现继承和方法共享
-
JavaScript 面向对象编程的基础构建块
-
构造函数提供了创建具有相同结构和行为的多个对象的模板,是 JavaScript 中实现"类"概念的传统方式。
2.6 new操作符具体做了什么
-
创建一个空的对象
-
将空对象的原型,指向于构造函数的原型
-
将空对象作为构造函数的上下文(改变this指向)
-
对构造函数有返回值的处理判断
-
如果构造函数返回对象,则返回该对象
function Person(name) { this.name = name; // 返回一个对象 return { custom: '我是返回的对象', name: '被覆盖的名字' }; } const person = new Person('张三'); console.log(person); // { custom: '我是返回的对象', name: '被覆盖的名字' } console.log(person instanceof Person); // false - 不是Person的实例! -
如果构造函数返回原始值,则忽略返回值,返回新创建的对象
function Person(name) { this.name = name; // 返回原始值(字符串、数字、布尔值等) return '我是字符串'; // 被忽略 // 或者 return 123; // 或者 return true; } const person = new Person('张三'); console.log(person); // Person { name: '张三' } - 原始值被忽略 console.log(person instanceof Person); // true -
构造函数没有返回值(或返回 undefined),返回新创建的对象
function Person(name) { this.name = name; // 没有return语句,相当于返回undefined } const person = new Person('张三'); console.log(person); // Person { name: '张三' } console.log(person instanceof Person); // true
例子1
function Fun( age , name ){ this.age = age; // 执行:obj.age = 18 this.name = name;/ 执行:obj.name = '张三' // return 1111; // 执行:返回 11111 // return {a:1}; // 执行:返回 {a:1} } function create( fn , ...args ){ //1.创建一个空的对象 // 此时 obj = {} var obj = {}; // 或者使用 var obj = Object.created( {} ) //2. 将空对象的原型,指向于构造函数的原型 // 此时 obj 的原型指向 Fun.prototype,但 obj 自身还是空对象 {} Object.setPrototypeOf( obj , fn.prototype); //3. 将空对象作为构造函数的上下文(改变this指向) // 在 Fun 函数内部,this 指向 obj: //obj 已经变成了 { age: 18, name: '张三' } //result 的值是 如果Fun没有return 值就是 undefined 有就是11111 或 {a:1} var result = fn.apply( obj , args ); //4. 对构造函数有返回值的处理判断 // result = undefined 或者 11111 或者 {a:1} // undefined instanceof Object → false // 11111 instanceof Object → false // {a:1} instanceof Object → true // false 所以返回 obj,即 { age: 18, name: '张三' } // true所以返回 obj,即 {a:1} return result instanceof Object ? result : obj; } console.info( create( Fun , 18 , '张三' ) ); -
3、同步队列 微任务 宏任务的执行顺序
总体执行流程:
- JavaScript 的事件循环机制遵循以下顺序:同步代码(主线程) → 微任务队列 → 宏任务队列 → 重复循环
具体规则如下::
- 同步代码:作为第一个宏任务(主线程任务)优先执行。
- 微任务队列:同步代码执行完毕后,立即清空所有微任务(包括执行过程中新生成的微任务)
- 宏任务队列:微任务清空后,从宏任务队列中取出下一个宏任务执行,并重复上述流程(我的理解: 宏队列里的宏任务是一个一个执行,微队列里的微任务是一次执行所有微任务全部清空, 每次执行完一个宏队列里的宏任务后就肯定会跟着执行一次清空微队列里的微任务,清空微队列后然后继续执行第二个宏任务重复上述操作)
4、什么是js隐藏类
const obj1 = {
a: 1
}
const obj2 = {
a: 1
}
const obj3 = {
a: 1
}
const obj1 = {
a: 1
}
const obj2 = {
b: 1
}
const obj3 = {
c: 1
}
// 测试代码
console.time('a');
for (let i = 0; i < 1000000; ++i) {
const obj = {};
obj['a'] = i;
}
console.timeEnd('a');
console.time('b');
for (let i = 0; i < 1000000; ++i) {
const obj = {};
obj[`${i}`] = i;
}
console.timeEnd('b');
第一个代码块要比第二个运行速度快,这是因为多个属性顺序一致的 JS 对象,会重用同一个隐藏类,减少 new Class 的开销,
JavaScript 的隐藏类(Hidden Class)是 V8 引擎用于优化对象属性访问的核心机制。
5、以下哪段代码效率更高(数组 - 快速模式 / 字典模式)
const arr1 = [];
for (let i = 0; i < 10000000; ++i) {
arr1[i] = 1;
}
const arr2 = [];
arr2[10000000 - 1] = 1;
for (let i = 0; i < 10000000; ++i) {
arr2[i] = 1;
}
// 测试代码
console.time('a');
const arr1 = [];
for (let i = 0; i < 10000000; ++i) {
arr1[i] = 1;
}
console.timeEnd('a');
console.time('b');
const arr2 = [];
arr2[10000000 - 1] = 1;
for (let i = 0; i < 10000000; ++i) {
arr2[i] = 1;
}
console.timeEnd('b');
- 第一个代码块的效率更高,利用了数组的 快速模式
- “数组从 0 到 length-1 无空洞” ,会进入快速模式,存放为 array。
- “数组中间有空洞”,会进入字典模式,存放为 HashMap。
6、如何判断 object 为空
常用方法:
- Object.keys(obj).length === 0
- JSON.stringify(obj) === ‘{}’
- for in 判断
严谨的方法:
- Reflect.ownKeys(obj).length === 0;
7、== 和 === 的区别
==比较的是值,===比较的是值和类型
8、作用域链如何延长
闭包
9、如何解决异步回调地狱
- Promise
- await/async
10、不同类型宏任务的优先级
浏览器中:用户交互事件宏任务和网络请求回调宏任务通常优先于定时器宏任务
11、javascript 变量在内存中的堆栈存储
JavaScript 变量在内存中的存储方式分为 栈(Stack) 和 堆(Heap),区别如下:
栈存储:原始类型:
- 存储内容:
number、string、boolean、null、undefined、Symbol、BigInt - 值直接存储在栈内存中,大小固定,访问速度快。
- 变量赋值时,复制的是值的副本,修改互不影响。
堆存储:引用类型:
- 存储内容:
Object、Array、Function、Date等。 - 值存储在堆内存中,大小不固定,栈中仅存储指向堆的内存地址(指针)
- 变量赋值时,复制的是地址,多个变量可能指向同一对象
特殊场景:
- 函数内部变量若被闭包引用,会被提升到堆中,避免函数执行后栈内存释放
12、JS 单线程设计的目的
JavaScript 采用单线程设计的核心目的是简化并发编程复杂度并确保浏览器环境稳定。作为脚本语言,它最初需频繁操作 DOM,而多线程同时修改 DOM 会导致不可预测的渲染错误(如样式冲突)。单线程模型天然避免了多线程的竞态条件(Race Condition),开发者无需处理锁、同步等复杂问题,代码执行顺序更直观,降低错误概率。
单线程通过事件循环(Event Loop) 实现高效并发:主线程执行同步代码,异步任务(如网络请求、定时器)由浏览器或 Node.js 底层多线程处理,完成后将回调推入任务队列。事件循环按优先级调度宏任务(如用户点击)和微任务(如 Promise),实现非阻塞异步操作,保障主线程高响应性。例如,AJAX 请求不会阻塞页面交互。
- 单线程的局限性通过其他方案弥补:
- 长任务阻塞:拆分为微任务或用 Web Worker 在后台线程执行计算;
- 无法利用多核:Node.js 通过 cluster 模块多进程扩展,浏览器通过 Web Worker 分工。
这种设计权衡了开发效率与运行性能,既避免 DOM 操作冲突,又通过异步机制支持高并发 I/O,成为 Web 和服务端(Node.js)的高效解决方案。
13、如何判断 javascript 的数据类型
-
typeof 操作符: 可以用来确定一个值的基本数据类型,返回一个表示数据类型的字符串。
typeof 42; // "number" typeof "Hello"; // "string" typeof true; // "boolean" typeof undefined; // "undefined" typeof null; // "object" (这是 typeof 的一个常见的误解) typeof [1, 2, 3]; // "object" typeof { key: "value" }; // "object" typeof function() {}; // "function"
注意,typeof null 返回 “object” 是历史遗留问题,不是很准确。
-
Object.prototype.toString: 用于获取更详细的数据类型信息。
Object.prototype.toString.call(42); // "[object Number]" Object.prototype.toString.call("Hello"); // "[object String]" Object.prototype.toString.call(true); // "[object Boolean]" Object.prototype.toString.call(undefined); // "[object Undefined]" Object.prototype.toString.call(null); // "[object Null]" Object.prototype.toString.call([1, 2, 3]); // "[object Array]" Object.prototype.toString.call({ key: "value" }); // "[object Object]" Object.prototype.toString.call(function() {}); // "[object Function]" -
instanceof 操作符: 用于检查对象是否属于某个类的实例。
var obj = {}; obj instanceof Object; // true var arr = []; arr instanceof Array; // true function Person() {} var person = new Person(); person instanceof Person; // true -
Array.isArray:用于检查一个对象是否是数组。
Array.isArray([1, 2, 3]); // true Array.isArray("Hello"); // false
14、var、let、const
var、let 和 const 是 JavaScript 声明变量的关键字,核心区别有三点:
- 作用域:var 为函数级作用域,let/const 为块级作用域(如 {} 内有效)。
- 变量提升:var 声明会提升到作用域顶部(值为 undefined),let/const 存在暂时性死区(声明前不可访问)。
- 可变性:var 可重复声明(和重新赋值,let不能在同一作用于中重复声明可以重新赋值,const 声明后不可重新赋值(基本类型),但引用类型内部属性可修改。
- 实践建议:优先用 const,需修改变量时用 let,避免使用 var。
15、如何判断对象相等
较为常用:JSON.stringify(obj1) === JSON.stringify(obj2)
16、null 和 undefined 的区别
undefined
- 当声明了一个变量但未初始化它时,它的值为 undefined。
- 当访问对象属性或数组元素中不存在的属性或索引时,也会返回 undefined。
- 当函数没有返回值时,默认返回 undefined。
- 如果函数的参数没有传递或没有被提供值,函数内的对应参数的值为 undefined。
let x;
console.log(x); // undefined
const obj = {};
console.log(obj.property); // undefined
function exampleFunc() {}
console.log(exampleFunc()); // undefined
function add(a, b) {
return a + b;
}
console.log(add(2)); // NaN
null
- null 是一个特殊的关键字,表示一个空对象指针。
- 它通常用于显式地指示一个变量或属性的值是空的,null 是一个赋值的操作,用来表示 “没有值” 或 “空”。
- null 通常需要开发人员主动分配给变量,而不是自动分配的默认值。
- null 是原型链的顶层:所有对象都继承自Object原型对象,Object原型对象的原型是null。
const a = null;
console.log(a); // null
const obj = { a: 1 };
const proto = obj.__proto__;
console.log(proto.__proto__); // null
18、创建函数的几种方式
声明式函数
function sayHello() {
console.log("Hello, World!");
}
sayHello(); // 调用函数
函数表达式
var sayHi = function() {
console.log("Hi there!");
};
sayHi(); // 调用函数
// 匿名函数表达式
var greet = function(name) {
console.log("Hello, " + name);
};
greet("Alice"); // 调用函数
箭头函数
const add = (a, b) => a + b;
console.log(add(2, 3)); // 输出 5
匿名函数
setTimeout(function() {
console.log("This is an anonymous function.");
}, 1000);
19、Promise是什么?
-
将「原本嵌套混乱的回调」变成「线性的步骤链」
-
让「有前后依赖关系的异步操作」按明确顺序执行
-
但「不同Promise链之间」还是异步并行的
简单说:Promise让「相关的异步操作」排队执行,让「不相关的异步操作」继续并行
我的理解是我需要部分函数按顺序执行就是用promise.then把他们变成链式的,promise以外的函数或代码还是保持之前的运行顺序会异步。
20、promise 和 await/async 的关系
async/await是Promise的语法糖 ,它是 ES8引入的特性,可以解决回掉地狱。await 让 async 函数内部的代码按顺序执行,当代码执行到 await 时,async 函数会暂停执行,并将控制权返回给外部代码,外部代码可以继续执行。等到 await 后面的 Promise 完成,async 函数会恢复执行,继续执行 await 后面的代码。
21、防抖节流的区别?
- 一、防抖(Debounce)的正确理解
- 核心:事件触发后开启一个定时器,若在定时器结束前再次触发事件,重置定时器;只有最后一次事件触发后等待时间结束才会执行函数。
- 行为:
-
连续触发时,函数执行被无限推迟,直到事件停止触发。
-
例如:用户连续点击按钮,若设置防抖延迟为 300ms,只有最后一次点击的 300ms 后才会执行。
// 经典防抖实现(延迟执行最后一次) function debounce(fn, delay) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }
-
- 二、节流(Throttle)的正确理解
- 核心:事件触发后,函数会立即执行一次,随后进入“冷却时间”,在冷却时间内忽略后续所有触发,直到冷却结束才允许再次执行。
- 行为:
-
连续触发时,函数以固定频率执行,但第一次触发会立即执行(或延迟执行,取决于实现)。
-
例如:用户连续点击按钮,若设置节流间隔为 300ms,第一次点击立即生效,后续点击在 300ms 内无效,300ms 后的第一次点击再次生效。
// 经典节流实现(立即执行第一次,忽略后续触发直到冷却结束) function throttle(fn, interval) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= interval) { fn.apply(this, args); lastTime = now; } }; }
-
22、如何实现长列表的优化?
长列表优化的核心是通过虚拟化渲染仅展示可视区域内容(配合动态加载减少初始负载),使用绝对定位或CSS Transform避免布局重排,结合数据分块加载和对象池复用控制内存消耗,并借助Web Worker预处理数据与滚动节流提升交互流畅度,最终实现10万级数据量下仍保持60fps的滚动性能,典型方案如React的react-window或原生实现动态计算渲染区间。
<!DOCTYPE html>
<html>
<head>
<style>
#container {
height: 100vh;
overflow-y: auto;
position: relative;
border: 1px solid #***c;
}
.item {
position: absolute;
width: 100%;
box-sizing: border-box;
padding: 12px;
border-bottom: 1px solid #eee;
transform: translateZ(0);
will-change: transform;
transition: transform 0.2s ease;
}
.placeholder {
background: #f5f5f5;
color: #999;
}
.loader {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
color: white;
padding: 8px 16px;
border-radius: 20px;
display: none;
}
</style>
</head>
<body>
<div id="container"></div>
<div class="loader">Loading...</div>
<script>
class VirtualScroll {
constructor({
container,
itemHeight = 50,
chunkSize = 2000,
buffer = 8
}) {
this.container = container;
this.itemHeight = itemHeight;
this.chunkSize = chunkSize;
this.buffer = buffer;
// 状态管理
this.data = new Map(); // 已加载的数据块
this.visibleItems = new Set();// 当前可见项索引
this.nodePool = []; // DOM节点池
this.currentPage = 0; // 当前加载页数
this.totalItems = 100000; // 总数据量
this.isLoading = false; // 加载状态
this.lastScrollTop = 0; // 上次滚动位置
this.scrollDelta = 0; // 滚动速度检测
this.init();
this.loadNextChunk();
}
init() {
// 创建滚动容器
this.scroller = document.createElement('div');
this.scroller.style.height = `${this.itemHeight * this.totalItems}px`;
this.container.appendChild(this.scroller);
// 事件监听
this.container.addEventListener('scroll', this.handleScroll.bind(this));
this.container.addEventListener('wheel', this.handleWheel.bind(this));
}
handleScroll() {
const scrollTop = this.container.scrollTop;
this.scrollDelta = scrollTop - this.lastScrollTop;
this.lastScrollTop = scrollTop;
// 动态调整缓冲区
const dynamicBuffer = Math.min(
this.buffer * 3,
this.buffer + Math.floor(Math.abs(this.scrollDelta) / this.itemHeight)
);
requestAnimationFrame(() => {
this.updateVisibleItems(dynamicBuffer);
// 预加载方向检测
const direction = this.scrollDelta > 0 ? 1 : -1;
this.preloadChunks(direction);
});
}
handleWheel(e) {
// 根据滚轮速度调整预加载
this.scrollDelta = e.deltaY;
}
preloadChunks(direction) {
const currentChunk = Math.floor(this.currentPage * this.chunkSize);
const viewportEnd = this.lastScrollTop + this.container.clientHeight;
// 预测需要加载的区块
const predictPosition = viewportEnd + (direction * this.container.clientHeight);
const predictPage = Math.floor(predictPosition / (this.chunkSize * this.itemHeight));
if (!this.data.has(predictPage) && !this.isLoading) {
this.loadNextChunk();
}
}
async loadNextChunk() {
this.isLoading = true;
document.querySelector('.loader').style.display = 'block';
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 300));
// 生成模拟数据
const data = Array.from({length: this.chunkSize}, (_, i) => ({
id: i + (this.currentPage * this.chunkSize),
content: `Item ${i + (this.currentPage * this.chunkSize)} - ${Math.random().toString(36).substr(2, 5)}`
}));
this.data.set(this.currentPage, data);
this.currentPage++;
this.isLoading = false;
document.querySelector('.loader').style.display = 'none';
// 强制更新视图
this.updateVisibleItems();
}
getVisibleRange(buffer) {
const scrollTop = this.container.scrollTop;
const start = Math.max(0, Math.floor(scrollTop / this.itemHeight) - buffer);
const end = Math.min(
this.totalItems - 1,
start + Math.ceil(this.container.clientHeight / this.itemHeight) + buffer * 2
);
return [start, end];
}
updateVisibleItems(buffer = this.buffer) {
const [start, end] = this.getVisibleRange(buffer);
// 回收不可见节点
this.recycleNodes(start, end);
// 渲染可见项
this.renderVisibleItems(start, end);
}
recycleNodes(start, end) {
this.visibleItems.forEach(index => {
if (index < start || index > end) {
const node = this.scroller.querySelector(`[data-index="${index}"]`);
if (node) {
node.style.display = 'none';
this.nodePool.push(node);
this.visibleItems.delete(index);
}
}
});
}
renderVisibleItems(start, end) {
for (let i = start; i <= end; i++) {
if (!this.visibleItems.has(i)) {
const page = Math.floor(i / this.chunkSize);
const data = this.data.get(page);
let content = '<div class="placeholder">Loading...</div>';
if (data) {
const itemData = data[i % this.chunkSize];
content = itemData ? itemData.content : '<div class="placeholder">Data missing</div>';
}
const node = this.getNode(i);
node.innerHTML = content;
node.style.transform = `translateY(${i * this.itemHeight}px)`;
node.style.display = 'block';
this.visibleItems.add(i);
}
}
}
getNode(index) {
const reusedNode = this.nodePool.find(n => !n.style.display || n.style.display === 'none');
if (reusedNode) {
reusedNode.dataset.index = index;
return reusedNode;
}
const newNode = document.createElement('div');
newNode.className = 'item';
newNode.dataset.index = index;
this.scroller.appendChild(newNode);
return newNode;
}
}
// 初始化虚拟滚动
new VirtualScroll({
container: document.getElementById('container'),
itemHeight: 60,
chunkSize: 2000,
buffer: 8
});
</script>
</body>
</html>
23、箭头函数是什么?
箭头函数是没有自己的作用域,它的this是继承的外层作用域,它也没有prototype属性,也不能作为构造函数
24、JS map是做什么的
当你需要对一个数组进行遍历和转换,并得到一个新数组时 -> 使用数组方法 map()。
场景:处理数据、格式化内容、提取属性、渲染列表(在 React 中非常常见)。
当你需要一种更强大的键值对数据结构,并且键可能不是字符串时 -> 使用 Map 集合。
场景:需要将复杂对象(如 DOM 节点)关联到一些元数据时,或者需要保证键的插入顺序时。
25、浏览器缓存
“浏览器缓存是一个多层次的系统,目的是用最快的速度提供资源。
最快的是内存缓存 (Memory Cache),它把当前会话的资源存在内存里,速度极快,但关闭标签页就会清空,是第一道关卡。
最核心和常用的是HTTP磁盘缓存 (Disk Cache),它通过服务器返回的 Cache-Control、ETag 等头信息策略化地控制缓存,将资源持久化在硬盘上,是性能优化的主要手段。
此外还有由开发者控制的 Service Worker 缓存,它允许我们编写脚本实现精细的缓存策略,是实现离线应用(PWA) 的关键。
最后,在 HTTP/2 协议下,还有一个推送缓存 (Push Cache),它是服务器主动推送给浏览器的资源临时存放地,生命周期很短,并且在所有缓存中优先级最低,作为整个缓存机制的一个补充。”
缓存的四个简单层次(由快到慢)
你可以把浏览器缓存想象成一个四层过滤系统,浏览器按顺序检查:
-
内存缓存 (Memory Cache)
在哪:电脑内存里。特点:速度最快,但关掉标签页就没了。
存什么:刚看过的图片、刚用过的脚本等临时东西。
怎么看:在浏览器开发者工具的“***work”面板里,看到 (from memory cache) 就是它。
-
Service Worker 缓存
特点:由网站开发者控制的“高级缓存”。可以用来让网站离线也能访问。怎么工作:像一个代理,能拦截网络请求,决定是从网络拿数据还是直接从缓存里给你。
-
HTTP 缓存 (Disk Cache) - 你最需要知道的
在哪:电脑硬盘上。特点:关了浏览器也不会丢,是持久化的缓存。
怎么控制:由服务器通过响应头(如 Cache-Control)告诉浏览器这个文件该存多久。
强缓存:在有效期内,浏览器根本不联系服务器,直接用本地缓存。
协商缓存:缓存过期后,浏览器会问服务器“文件变没变”,没变就继续用旧的(省流量),变了就下载新的。
怎么看:看到 (from disk cache) 或状态码 304 就是它在工作。
-
Push Cache
特点:HTTP/2 协议的特性,比较少见。是服务器提前预测你可能需要某个资源,在你还没请求时就主动“推”给你的缓存。优先级最低,只有上面都找不到时才会用它。
六、CSS
1、CSS 中选择器的优先级,权重计算方式
内联样式(1000)、id(100)、class(10)、元素选择器、(1)伪元素(1)
2、响应式布局
- 使用媒体查询(Media Queries)
- 流式布局百分比宽度
- 弹性布局 Flex 布局
3、重绘和回流
重绘(Repaint) 和 回流(Reflow,又称重排) 是浏览器渲染引擎更新页面时的关键步骤,直接影响网页性能。
重绘:改变颜色阴影等,不会改变页面的大小,消耗资源小
回流:改变布局、宽高、字体大小会改变页面大小,会导致浏览器重新计算布局,消耗资源大
触发了回流一定会触发重绘
4、浏览器渲染流程
回流(Reflow) → 重绘(Repaint) → 合成(***posite)
回流会触发完整的渲染流水线:计算样式(Style) → 布局(Layout) → 绘制(Paint) → 合成(***posite)
重绘跳过布局阶段:计算样式(Style) → 绘制(Paint) → 合成(***posite)
5、什么是Margin 塌陷?如何解决?BFC 是什么? 怎么触发?
- margin塌陷问题:两个相邻的div他们之间的margin都是200px,我们希望得到的他们两个间距400px,但是实际上他们的间距只有200px,这是因为会CSS 的外边距合并规则margin会重叠且取重叠部分的更大值,如果希望间隔 3400px,可为每个 div 触发 BFC。
- BFC定义:全称叫块级格式化上下文 (Block Formatting Context),一个独立的渲染区域,有自己的渲染规则,与外部元素不会互相影响。
- BFC触发方式:
- 设置了 float 属性(值不为 none)
- 设置了 position 属性为 absolute 或 fixed
- 设置了 display 属性为 inline-block
- 设置了 overflow 属性(值不为 visible)
6、渐进增强(progressive enhancement)和优雅降级(graceful degradation)
- 渐进增强和优雅降级就是让页面能保证在所有浏览器都能访问且正常使用,在低版本的设备上保证正常显示和功能的正常使用即可,在高版本的设备上可以展示更多复杂的效果增加用户的体验。
7、CSS 盒子模型
css盒子模型包含内容,内边距,边框,外边距
8、Less 和 SCSS 的区别
Less(Leaner Style Sheets)和 SCSS(Sassy CSS)都是CSS预处理器,它们添加了一些功能和语法糖来帮助开发人员更轻松地管理和组织样式代码。
语法:
- Less: Less 使用较少的特殊字符,例如,变量定义以@开头,Mixin以.开头,选择器嵌套使用&等。
- SCSS: SCSS采用类似于CSS的语法,使用大括号{}和分号;来定义块和分隔属性。
特性:
- Less: Less提供了一些常见的CSS功能,如变量、嵌套、Mixin等,但在某些高级功能方面不如SCSS强大。
- SCSS: SCSS具有更丰富的功能集,包括控制指令、函数、循环等,因此在某些情况下更强大。
扩展名:
- Less: Less文件的扩展名通常为.less。
- SCSS: SCSS文件的扩展名通常为.scss。
9、px,rpx,vw,vh,rem,em 的区别
px(像素):
- 相对单位,代表屏幕上的一个基本单位,逻辑像素。
- 不会根据屏幕尺寸或分辨率自动调整大小。
- 在高分辨率屏幕上可能显得很小。
rpx(微信小程序单位):
- 主要用于微信小程序开发。
- 是相对单位,基于屏幕宽度进行缩放。
- 可以在不同设备上保持一致的布局。
vw(视窗宽度单位):
- 相对单位,表示视窗宽度的百分比。
- 1vw等于视窗宽度的1%。
- 用于创建适应不同屏幕宽度的布局。
vh(视窗高度单位):
- 相对单位,表示视窗高度的百分比。
- 1vh等于视窗高度的1%。
- 用于创建根据屏幕高度进行布局调整的效果。
rem(根元素单位):
- 相对单位,基于根元素的字体大小。
- 1rem等于根元素的字体大小。
- 可用于实现相对大小的字体和元素,适合响应式设计。
em(字体相对单位):
- 相对单位,基于当前元素的字体大小。
- 1em等于当前元素的字体大小。
- 通常用于设置相对于父元素的字体大小。
10、box-sizing 的作用
-
box-sizing: content-box:
当设置box-sizing: content-box;时,元素的宽度和高度仅包括内容区域,边框和内边距会额外增加到总宽度和总高度上。这意味着,如果内容区域的宽度为100px,加上20px的内边距和10px的边框,元素的总宽度将为140px(100px内容 + 20px内边距 + 10px边框)。 -
box-sizing: border-box:
当设置box-sizing: border-box;时,元素的宽度和高度包括内容区域、内边距和边框。这意味着,即使加上边框和内边距,元素的总宽度和高度也不会改变。例如,如果设置一个元素的宽度为100px,内边距为20px,边框为10px,使用border-box后,元素的实际宽度仍然是100px,内边距和边框会被压缩到内容区域内。
11、css透明度设置三种方法?
-
opacity 属性
设置元素的整体透明度(包括其内容及子元素),取值范围 0(完全透明)到 1(完全不透明)。.element { opacity: 0.5; /* 半透明 */ }特点:
影响整个元素(包括子元素)。
值小于 1 时,元素会创建一个新的层叠上下文(可能影响性能)。 -
RGBA 颜色模式
通过 rgba() 函数设置颜色的透明度,仅影响当前颜色属性(如背景、边框等)。.element { background-color: rgba(255, 0, 0, 0.5); /* 半透明红色背景 */ color: rgba(0, 0, 0, 0.8); /* 文字 80% 不透明 */ }参数:
rgba(红, 绿, 蓝, alpha),其中 alpha 范围 0(透明)到 1(不透明)。
特点:
只影响当前颜色,不改变子元素透明度。
适用于背景、边框、文字等需要局部透明的场景。 -
HSLA 颜色模式
类似 RGBA,但使用 HSL(色相、饱和度、亮度)模式定义颜色,并添加透明度。.element { background-color: hsla(120, 100%, 50%, 0.3); /* 半透明绿色背景 */ }参数:
hsla(色相, 饱和度%, 亮度%, alpha),alpha 范围同上。
特点:
语法更符合人类对颜色的直观感知(如调整亮度比 RGB 更直观)。
同样只影响当前颜色,不涉及子元素。
对比总结
| 方法 | 作用范围 | 适用场景 |
|---|---|---|
| opacity | 整个元素(含子元素) | 整体淡入淡出、遮罩效果 |
| rgba() | 当前颜色属性(如背景、边框) | 背景透明但文字不透明、局部颜色透明 |
| hsla() | 同上,但使用 HSL 颜色模式 | 需要直观调整颜色亮度和透明度的场景 |
12、html和小程序图片变形如何解决?
一、HTML(Web 端)解决方案
1. 使用 <img> 标签 + CSS
通过 object-fit 控制图片填充方式:
<img src="image.jpg" class="image" />
.image {
width: 100%; /* 容器宽度 */
height: 300px; /* 容器高度 */
object-fit: cover; /* 保持比例填充容器(可能裁剪) */
}
2. 背景图片 + background-size
通过 background-image 和 background-size 控制:
<div class="background-image"></div>
.background-image {
width: 100%;
height: 300px;
background-image: url("image.jpg");
background-size: cover; /* 或 contain */
background-position: center;
}
3. 固定宽高比容器
使用 aspect-ratio 或 padding 技巧:
/* 方法1:aspect-ratio */
.container {
width: 100%;
aspect-ratio: 16/9; /* 16:9 宽高比 */
}
/* 方法2:经典 padding 百分比 */
.container {
width: 100%;
padding-top: 56.25%; /* 16:9 比例 (9/16 * 100%) */
position: relative;
}
.container img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
二、小程序(微信)解决方案
1. 使用 image 组件的 mode 属性
<image
src="image.jpg"
mode="aspectFill"
style="width: 100%; height: 300rpx;"
/>
常用 mode 值:
-
aspectFill:保持比例填充容器(可能裁剪)。 -
aspectFit:保持比例完整显示(可能留白)。 -
widthFix:宽度固定,高度自适应(适合竖向滚动)。 -
heightFix:高度固定,宽度自适应(适合横向滚动)。
2. 固定宽高比容器
/* 方法1:aspect-ratio */
.container {
width: 100%;
aspect-ratio: 16/9;
}
/* 方法2:padding 百分比 */
.container {
width: 100%;
padding-top: 56.25%;
position: relative;
}
.container image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
3. 动态计算图片尺寸
<image
src="image.jpg"
mode="widthFix"
style="width: 100%;"
bindload="onImageLoad"
/>
Page({
data: { imageHeight: 0 },
onImageLoad(e) {
const { width, height } = e.detail;
const ratio = height / width;
// 假设容器宽度为 750rpx,动态计算高度
this.setData({ imageHeight: 750 * ratio });
}
});
三、对比总结
| 场景 | HTML(Web)方法 | 小程序方法 |
|---|---|---|
| 图片填充方式 | object-fit: cover/contain |
mode="aspectFill/aspectFit" |
| 背景图片 | background-size: cover |
用 view + image 模拟 |
| 固定宽高比容器 |
aspect-ratio 或 padding
|
同上,语法一致 |
| 动态尺寸计算 | JavaScript 监听 onload
|
bindload 事件 + mode="widthFix"
|
四、通用注意事项
-
避免同时设置
width和height
除非明确需要拉伸图片,否则优先使用自适应尺寸。 -
优先保持比例
使用aspect-ratio、mode或padding技巧控制宽高比。 -
性能优化
- 大图需压缩或使用 CDN。
- 小程序中推荐使用
lazy-load属性延迟加载非首屏图片。
-
设备适配
小程序中使用rpx单位适配不同屏幕,Web 端使用vw/vh或媒体查询。
通过合理选择上述方案,可轻松解决图片变形问题! 🚀
七、其他
1、PC 端优化
- 性能:路由懒加载
- 交互 :防抖/节流(如搜索框)
- 渲染 :减少 DOM 层级、避免频繁重排/重绘。
- 网络:CDN 加速静态资源、开启资源压缩Gzip。
2、http和https有区别
主要的区别在于安全性和数据传输方式上,HTTPS比HTTP更加安全,适合用于保护网站用户的隐私和安全,如银行网站、电子商务网站等。
- 安全性:HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输的数据可以被任何抓包工具截取并查看。而HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,更为安全。
- 数据传输方式:HTTP协议的端口号是80,HTTPS协议的端口号是443。
- 网址导航栏显示:使用HTTP协议的网站导航栏显示的是"http://“,而使用HTTPS协议的网站导航栏显示的是"https://”。
- 证书:HTTPS需要到CA申请证书,一般免费证书较少,因而需要一定费用。
- 网络速度:HTTP协议比HTTPS协议快,因为HTTPS协议需要进行加密和解密的过程。
- SEO优化:搜索引擎更倾向于把HTTPS网站排在更前面的位置,因为HTTPS更安全。
3、HTTP 请求方式
- GET:用于获取资源,通过URL传递参数,请求的结果会被缓存,可以被书签保存,不适合传输敏感信息。
- POST:用于提交数据,将数据放在请求体中发送给服务器,请求的结果不会被缓存。
- PUT:用于更新资源,将数据放在请求体中发送给服务器,通常用于更新整个资源。
- DELETE:用于删除资源,将数据放在请求体中发送给服务器,用于删除指定的资源。
- PATCH:用于部分更新资源,将数据放在请求体中发送给服务器,通常用于更新资源的部分属性。
4、Get / Post 的区别
区别:
- get 幂等,post 不是。(多次访问效果一样为幂等)
- get 能触发浏览器缓存,post 没有。
- get 能由浏览器自动发起(如 img-src,资源加载),post 不行。
- post 相对安全,一定程度上规避 CSRF 风险。
相同:
5. 都不安全,都是基于 http,明文传输。
6. 参数并没有大小限制,是URL大小有限制,因为要保护服务器。 (chrom 2M,IE 2048)
5、RESTful 规范
使用语义化的URL来表示资源的层级关系和操作,如/users表示用户资源,/users/{id}表示具体的用户。
- 资源:将系统中的实体抽象为资源,每个资源都有一个唯一的标识符(URI)。
- HTTP方法:使用HTTP请求方式来操作资源,如GET–查询(从服务器获取资源)、POST—新增(从服务器中新建一个资源);、PUT—更新(在服务器中更新资源)、DELETE—删除(从服务器删除资源),、PATCH—部分更新(从服务器端更新部分资源)等。
- 状态码:使用HTTP状态码来表示请求的结果,如200表示成功,404表示资源不存在等。
- 无状态:每个请求都是独立的,服务器不保存客户端的状态信息,客户端需要在请求中携带所有必要的信息。
6、Cookie 为了解决什么问题
定义:Cookie是一种存储在用户浏览器中的小文件,用于存储网站的一些信息。通过Cookie,服务器可以识别用户并保持会话状态,实现会话保持。用户再次访问网站时,浏览器会将Cookie发送给服务器,以便服务器可以识别用户并提供个性化的服务,存储上限为 4KB。
解决问题:Cookie诞生的主要目的是为了解决HTTP协议的无状态性问题。HTTP协议是一种无状态的协议,即服务器无法识别不同的用户或跟踪用户的状态。这导致了一些问题,比如无法保持用户的登录状态、无法跟踪用户的购物车内容等。
7、Cookie 和 Session 的区别
Cookie(HTTP Cookie)和 Session(会话)都是用于在 Web 应用程序中维护状态和用户身份的两种不同机制:
存储位置:
- Cookie:Cookie是存储在客户端的数据。每次请求自动发送Cookie到服务器,以便服务器可以识别用户。
- Session:Session数据通常存储在服务器上,而不是在客户端。服务器为每个用户创建一个唯一的会话,然后在服务器上存储会话数据。
持久性:
- Cookie:Cookie可以具有持久性,可以设置过期时间。如果没有设置过期时间,Cookie将成为会话Cookie,存在于用户关闭浏览器前的会话期间。
- Session:会话数据通常存在于用户活动的会话期间,一旦会话结束(用户退出登录或关闭浏览器),会话数据通常会被删除。
安全性:
- Cookie:Cookie数据存储在客户端,可能会被用户篡改或窃取。因此,敏感信息通常不应存储在Cookie中,或者应该进行加密。
- Session:Session数据存储在服务器上,客户端不可见,因此通常更安全,特别适合存储敏感信息。
服务器负担:
- Cookie:服务器不需要维护Cookie的状态,因为它们存储在客户端。每次请求中都包含Cookie,服务器只需要验证Cookie的有效性。
- Session:服务器需要维护会话数据,这可能会增加服务器的负担,尤其是在大型应用程序中。
跨多个页面:
- Cookie:Cookie可以被跨多个页面和不同子域共享,这使得它们适用于用户跟踪和跨多个页面的数据传递。
- Session:会话数据通常只在单个会话期间可用,而不容易在不同会话之间共享。
无需登录状态:
- Cookie:Cookie可以在用户未登录的情况下使用,例如用于购物车或用户首选项。
- Session:会话通常与用户的身份验证和登录状态相关,需要用户登录后才能创建和访问会话。
8、TCP(传输控制协议)和 UDP(用户数据报协议)的区别
两种常用的传输层协议,用于在网络中传输数据。
- TCP:一种面向连接的协议,提供可靠的数据传输。它通过三次握手建立连接,保证数据的完整性和顺序性。TCP使用流控制、拥塞控制和错误检测等机制来确保数据的可靠传输。它适用于需要可靠传输的应用,如文件传输、电子邮件和网页浏览等。
- UDP:一种无连接的协议,提供不可靠的数据传输。它不需要建立连接,直接将数据包发送给目标地址。UDP没有流控制和拥塞控制机制,也不保证数据的完整性和顺序性。UDP适用于实时性要求较高的应用,如音频、视频和实时游戏等。
总结来说,TCP提供可靠的、面向连接的数据传输,适用于对数据完整性和顺序性要求较高的应用;而UDP提供不可靠的、无连接的数据传输,适用于实时性要求较高的应用。选择使用TCP还是UDP取决于应用的需求和特点。
9、TCP 三次握手?为什么是三次两次不行吗?
- 第一次握手(SYN):发送方首先向接收方发送一个SYN(同步)标志的TCP包,该包包含一个随机生成的初始序列号(ISN)。这表示发送方希望建立一个连接,并且指定了一个用于数据传输的起始序号。
- 第二次握手(SYN + ACK):接收方接收到发送方的SYN包后,它会回应一个带有SYN和ACK(确认)标志的TCP包。这个响应包不仅确认了接收到的SYN,还包含了接收方的初始序列号。这两个序列号表示了双方用于传输数据的初始顺序。
- 第三次握手(ACK):最后,发送方接收到接收方的响应后,它会发送一个带有ACK标志的TCP包,表示对接收方的响应已经收到。至此,连接建立完成,双方可以开始进行数据传输。
TCP采用三次握手的核心原因是:
- 两次握手无法确保双方通信能力与序列号同步,服务器可能因未收到最终确认而维持无效连接(浪费资源),且无法阻止历史重复报文干扰;
- 四次握手冗余,因三次已能双向确认收发能力与初始序列号(SYN、SYN-ACK传递双方ISN,ACK确认双方ISN有效),四次则增加无意义延迟。
三次握手以最小成本实现可靠连接建立,避免资源泄漏与数据错乱。
10、什么是跨域?如何解决?
在 Web 应用程序中,一个网页的代码向不同源(即不同的域名、协议或端口)发起 HTTP 请求。浏览器的同源策略限制了跨域请求,以保护用户的安全性和隐私。同源策略要求网页只能与同一源的资源进行交互,而不允许与不同源的资源直接交互。
解决方法:
- Nginx 充当代理服务器,分发请求到目标服务器。
- 服务器端配置CORS策略
- Iframe 通讯,通过在主页面嵌入一个隐藏的 iframe,将目标页面加载到 iframe 中,并通过在主页面和 iframe 页面之间使用 postMessage() 方法进行消息传递,从而实现跨域的数据交换。
11、网页扫码登录如何实现
定时器(短轮询/长轮询):
-
短轮询:
实现原理:客户端通过 setInterval 定时(如每秒1次)请求服务端检查二维码状态。
缺点:高频请求导致服务器压力大,实时性差(依赖轮询间隔)。
适用场景:对实时性要求不高的简单场景(如低频操作)。 -
长轮询:
实现原理:客户端发起请求后,服务端保持连接直至二维码状态变更或超时(如30秒),响应后客户端立即重启轮询。
优点:减少无效请求,延迟可控(优于短轮询)。
案例:微信网页版扫码登录采用长轮询监听二维码状态变更。
WebSocket(全双工通信):
- 实现原理:
- PC 端生成二维码后,与服务器建立 WebSocket 连接。
- 手机扫码并确认登录时,服务端通过 WebSocket 主动推送状态变更至 PC 端。
- 优势:
- 高实时性:服务端可主动推送,无需客户端轮询。
- 双向通信:支持复杂交互(如登录确认弹窗的实时反馈)。
- 缺点:
- 需维护持久连接,服务端资源消耗较高。
- 需处理连接中断、重连等异常情况。
- 案例:部分企业 OA 系统通过 WebSocket 实现扫码登录的实时状态同步。
SSE(Server-Sent Events):
- 实现原理:
- 客户端通过 EventSource 与服务器建立单向长连接,服务端在二维码状态变更时推送事件。
支持自动重连和超时机制(如二维码过期触发刷新)。
- 优势:
- 轻量化:基于 HTTP 协议,无需额外协议支持。
- 低延迟:服务端可主动推送,实时性接近 WebSocket。
- 优局限性:
- 仅支持单向通信(服务端→客户端),无法处理客户端主动请求。
部分旧版本浏览器兼容性较差。
12、axios封装
全局配置:
- 设置基础 URL(baseURL)和超时时间(60秒)
- 自动携带 token(通过请求拦截器注入 headers.token)
统一错误处理:
- 响应拦截器处理网络错误、401 Token 过期跳转登录页、404 接口错误等
- 所有错误触发 message.error 提示并隐藏全局 loading(通过 Redux)
多种请求方法封装:
- 支持不同内容类型:JSON、FormData、x-www-form-urlencoded、text/plain
- 提供 get/post/下载文件/上传文件/验证码获取 等方法
- 验证码请求特殊处理:解析二进制图片为 base64 并返回 UUID
集成 React 生态:
- 通过 useMemo 缓存实例,结合 React Router 的 navigate 和 Redux 的 dispatch
响应数据简化:
- 所有方法自动返回 response.data 过滤外层结构
13、如何实现token过期无感刷新
需要后端配合,当发现token过期,后端返回新的token,前端在axios拦截器中获取到token过期错误码后刷新token缓存,刷新成功后重新发起原请求
14、HTTP缓存机制问题
HTTP 缓存是一种在客户端(如浏览器)或中间缓存代理服务器上保存资源副本的机制。通过设置 Cache-Control 响应头,强制浏览器或代理直接使用本地缓存副本,当用户首次请求某个资源时,服务器会将该资源以及相关的缓存控制信息一并返回给客户端。客户端接收到资源后,会根据缓存控制信息将资源存储在本地缓存中 。
当用户再次请求相同的资源时,客户端首先会检查本地缓存中是否存在该资源的副本。如果存在,并且缓存控制信息表明该副本仍然有效,客户端就会直接从本地缓存中获取资源,而无需再次向服务器发送请求。这大大减少了网络请求的次数和数据传输量,提高了资源的加载速度。
例如CDN的静态文件缓存功能就是通过配置 HTTP 响应头控制缓存周期,结合文件名哈希(如 style-abc123.css)实现内容更新后自动失效旧缓存
15、CDN 的核心功能
CDN(Content Delivery ***work,内容分发网络)是一种分布式服务器网络,通过以下机制加速内容分发:
- 就近访问:将资源缓存到全球多个边缘节点,用户从最近的节点获取内容(降低延迟)。
- 负载均衡:智能分配请求到最优节点,避免单点过载。
- 安全防护:防御 DDoS 攻击、隐藏源站 IP 等。
- 缓存功能:CDN 节点会缓存静态资源(如图片、CSS、JS),但这是 CDN 的附加能力,而非全部。
16、WebWork是什么
Web Worker 是浏览器提供的 JavaScript 多线程解决方案,JavaScript 默认在主线程(UI 线程)运行,Web Worker 允许在独立的后台线程执行代码,避免复杂计算阻塞页面交互,可以执行耗时的任务而不会阻塞用户界面的响应。使用 Web Worker 可以将一些计算密集型或耗时的任务从主线程中分离出来,以提高网页的性能和响应速度。主线程可以继续处理用户交互和界面更新,而 Web Worker 在后台进行计算或处理其他任务。
-
示例
HTML<button onclick="startWork()">开始计算</button> <div id="result">等待结果...</div> <div id="error" style="color:red"></div> <script> // 创建 Worker const worker = new Worker('worker.js'); // 向 Worker 发送任务 function startWork() { worker.postMessage({ type: 'fibona***i', num: 40 }); } // 监听 Worker 返回结果 worker.onmessage = function(e) { const result = e.data; document.getElementById('result').textContent = `结果:${result}`; }; // 监听 Worker 错误 worker.onerror = function(e) { document.getElementById('error').textContent = `错误:${e.message}`; worker.terminate(); // 终止异常 Worker }; </script>Web Worker(worker.js)
self.onmessage = function(e) { const { type, num } = e.data; let result; switch (type) { case 'fibona***i': result = fibona***i(num); break; // 可扩展其他计算类型 } self.postMessage(result); }; function fibona***i(n) { if (n <= 1) return n; let a = 0, b = 1; for (let i = 2; i <= n; i++) { [a, b] = [b, a + b]; } return b; }
17、内存缓存
JS 内存缓存(Memory Cache)是浏览器在内存(RAM)中临时存储 JavaScript 代码、资源文件(如图片、样式表等)或其他数据的机制,用于快速访问高频使用的资源,避免重复请求服务器或磁盘读取,从而提升页面性能。闭包(Closure)是内存缓存的典型应用场景,其数据也存储在堆中。
18、Service Worker 缓存
一种通过 JavaScript 脚本实现的代理机制,可拦截请求并返回自定义缓存内容,常用于 PWA(渐进式 Web 应用)。支持离线访问和动态更新策略6。
19、HTTP/2 Push Cache
仅适用于 HTTP/2 协议,允许服务器主动推送资源到客户端缓存。生命周期短暂,仅在当前会话有效,常用于优化首次加载速度。
20、浏览器存储类缓存
- LocalStorage
持久化存储,关闭浏览器后数据仍保留,容量约 5MB,适用于长期保存非敏感数据。 - SessionStorage
会话级存储,页面关闭后数据自动清除,容量同 LocalStorage。 - Cookie
用于会话跟踪,随 HTTP 请求自动发送到服务器。容量约 4KB,支持设置有效期。 - IndexedDB
支持结构化大数据存储(如 JSON 对象),适合复杂应用场景。
21、HTTP/2是什么?如何开启?
HTTP/2 是 HTTP 协议的第二个主要版本,由 IETF 于 2015 年发布,旨在提升网络传输效率和网页加载性能。
- 二进制分帧协议:采用二进制格式替代 HTTP/1.1 的文本协议,降低解析复杂度并提高传输效率。
- 多路复用(Multiplexing):单 TCP 连接可并行处理多个请求和响应,解决 HTTP/1.1 的队头阻塞问题。
- 头部压缩(HPACK):通过算法压缩冗余头部数据(如重复 Cookie),减少传输量。
- 流优先级控制:允许客户端指定资源加载优先级,优化关键资源的处理顺序。
如何开启 HTTP/2
- 基础条件
- 必须启用 HTTPS:HTTP/2 需基于 TLS 1.2+ 协议运行,需为域名配置有效的 SSL/TLS 证书
- 开启方式
-
服务器端配置
Nginx:
修改配置文件,在监听端口添加 http2 标识:listen 443 ssl http2; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; 支持版本要求:Nginx ≥1.9.5。 -
Apache:
加载 mod_http2 模块后,在配置中添加 Protocols h2 http/1.1。 -
云服务/CDN 配置
- 阿里云 DCDN:
登录控制台 → 域名管理 → HTTPS 配置 → 开启 HTTP/2 开关。 - 百度云 CDN:
默认已支持 HTTP/2,用户配置 HTTPS 证书后自动生效。 - 其他平台:
如火山引擎、腾讯云等,需在控制台的 HTTPS 高级选项中勾选 启用 HTTP/2.0。
- 阿里云 DCDN:
-
- 验证是否生效
- 浏览器开发者工具:在 Chrome 的 ***work 面板查看协议列(Protocol),显示 h2 表示成功。
- 命令行工具:执行 curl -I --http2 https://yourdomain.***,响应头包含 HTTP/2 标识即为启用。
22、 柯里化是什么?
柯里化(Currying)是函数式编程中的一种技术,它将一个接受多个参数的函数转换为一系列只接受单个参数的函数,并逐次返回新函数,直到所有参数收集完毕,最终返回结果。
这种技术由逻辑学家 Haskell Curry 提出,因此得名。
- 核心思想
- 分解参数:将多参数函数转换为链式调用的单参数函数。
- 延迟执行:分步传递参数,灵活控制函数执行时机。
- 函数复用:通过固定部分参数生成新的专用函数。
-
代码示例
普通函数 vs 柯里化函数:
// 普通加法函数(接受2个参数) function add(a, b) { return a + b; } add(2, 3); // 5 // 柯里化后的加法函数(分步传递参数) function curriedAdd(a) { return function(b) { // 返回一个新函数,等待第二个参数 return a + b; }; } const add2 = curriedAdd(2); // 固定第一个参数为2 add2(3); // 5 add2(5); // 7(复用固定参数2) -
柯里化的实现原理
利用闭包(Closure)保存已传递的参数,逐步收集所有参数后执行计算。
通用柯里化函数:// 将普通函数转换为柯里化函数 function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { // 参数数量足够时执行原函数 return fn.apply(this, args); } else { // 参数不足时返回新函数继续收集参数 return function(...args2) { return curried.apply(this, args.concat(args2)); }; } }; } // 示例:柯里化一个3参数函数 function sum(a, b, c) { return a + b + c; } const curriedSum = curry(sum); curriedSum(1)(2)(3); // 6 curriedSum(1, 2)(3); // 6(支持混合调用) -
柯里化的应用场景
(1) 参数复用:// 创建通用的“问候语”生成函数 function greet(greeting, name) { return `${greeting}, ${name}!`; } const curriedGreet = curry(greet); const sayHello = curriedGreet("Hello"); // 固定问候语为 "Hello" sayHello("Alice"); // "Hello, Alice!" sayHello("Bob"); // "Hello, Bob!"(2) 动态生成函数:
// 根据日志级别生成不同的日志函数 const log = curry((level, message) => { console.log(`[${level}] ${message}`); }); const debugLog = log("DEBUG"); const errorLog = log("ERROR"); debugLog("***work request sent"); // [DEBUG] ***work request sent errorLog("Database connection failed"); // [ERROR] Database connection failed(3) 函数组合
// 组合多个柯里化函数 const filter = curry((predicate, arr) => arr.filter(predicate)); const map = curry((fn, arr) => arr.map(fn)); const getEvenNumbers = filter(n => n % 2 === 0); const doubleNumbers = map(n => n * 2); const processData = (arr) => doubleNumbers(getEvenNumbers(arr)); processData([1, 2, 3, 4]); // [4, 8] -
柯里化与部分应用(Partial Application)的区别
| 特性 | 柯里化(Currying) | 部分应用(Partial Application) |
|---|---|---|
| 参数传递 | 必须按顺序逐个传递参数 | 可以一次性固定任意多个参数 |
| 返回结果 | 始终返回新函数,直到参数收集完毕 | 直接返回结果或部分固定参数的新函数 |
| 灵活性 | 适合需要严格分步的场景 | 适合快速固定部分参数的场景 |
- 注意事项
- 性能:频繁生成闭包可能增加内存开销,需避免过度使用。
- 可读性可读性:链式调用过多可能降低代码可读性。
- 参数顺序:柯里化依赖参数顺序,设计函数时需将易变的参数放在后面。
总结:
柯里化通过分解参数和闭包机制,提供了灵活的函数复用和组合能力,尤其适合函数式编程场景。但需权衡其带来的抽象性和性能成本,合理用于参数复用、延迟执行等需求。
23、webpack如何优化项目?
-
路由懒加载结合Webpack代码分割技术,将路由组件拆分为独立代码块,降低主包体积,避免生成单一巨型JS文件
{ path: '/old', meta: { title: '****' }, ***ponent: () => import(/* webpackChunkName: "about" */ '../views/Login_old.vue') } -
第三方组件按需加载
- 我使用的是element plus所以用一下方式进行按需加载
-
安装unplugin-auto-import和unplugin-vue-***ponents
npm install -D unplugin-vue-***ponents unplugin-auto-import -
配置vue.config.js
const { defineConfig } = require("@vue/cli-service"); const AutoImport = require("unplugin-auto-import/webpack"); // 导入webpack版的unplugin-auto-import const ***ponents = require("unplugin-vue-***ponents/webpack"); // 导入webpack版的unplugin-vue-***ponents const { ElementPlusResolver } = require("unplugin-vue-***ponents/resolvers"); // 导入elementplus组件解析器 module.exports = defineConfig({ transpileDependencies: true, configureWebpack: { // 这个节点用于配置webpack plugins: [ // 这个节点要放在configureWebpack下,否则会报错 ***ponents.default({ resolvers: [ElementPlusResolver()], // 指定unplugin-vue-***ponents的组件解析器为elementplus解析器 }), AutoImport.default({ resolvers: [ElementPlusResolver()], // 指定unplugin-auto-import的组件解析器为elementplus解析器 }), ], }, }); -
组件中直接使用,无需import和***ponent registration
<template> <el-button type="primary">hi</el-button> <el-row class="mb-4"> <el-button disabled>Default</el-button> <el-button type="primary" disabled>Primary</el-button> <el-button type="su***ess" disabled>Su***ess</el-button> <el-button type="info" disabled>Info</el-button> <el-button type="warning" disabled>Warning</el-button> <el-button type="danger" disabled>Danger</el-button> </el-row> </template>
-
- 如果使用的是elementui,用下面方法进行按需引入
-
安装npm install babel-plugin-***ponent -D
npm install babel-plugin-***ponent -D -
babel.config.js
module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ], plugins: [ [ '***ponent', { libraryName: 'element-ui', styleLibraryName: 'theme-chalk' } ] ] } -
按需引入elment-ui组件
import Vue from 'vue'; import { Button, Select } from 'element-ui'; Vue.use(Button) Vue.use(Select)
-
- 我使用的是element plus所以用一下方式进行按需加载
-
常用工具库使用CDN加速
-
开启gzip压缩,可以有效的减少代码体积
const webpack = require("webpack") const ***pressionWebpackPlugin = require("***pression-webpack-plugin") const productionGzipExtensions = ["js", "css"] module.exports = { configureWebpack: (config) => { const plugins = [ // 移除 moment.js 语言包(关键优化) new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ }), // Gzip 压缩配置 new ***pressionWebpackPlugin({ filename: '[path][base].gz', // 保持原文件名 algorithm: 'gzip', test: new RegExp(`\\.(${productionGzipExtensions.join('|')})$`), threshold: 10240, minRatio: 0.8, deleteOriginalAssets: false // 保留源文件(必须!) }) ] // 更安全的插件合并方式 if (config.plugins) { config.plugins = config.plugins.concat(plugins) } else { config.plugins = plugins } } } -
打包不生成map文件,.map文件生产环境用不到,还会增加打包后的体积也有可能有敏感信息
module.exports = { productionSourceMap: false, } -
代码分割
config.optimization = { splitChunks: { cacheGroups: { vendor: { chunks: 'all', test: /node_modules/, name: 'vendor', minChunks: 1, maxInitialRequests: 5, minSize: 0, priority: 100 }, ***mon: { chunks: 'all', test: /[\\/]src[\\/]js[\\/]/, name: '***mon', minChunks: 2, maxInitialRequests: 5, minSize: 0, priority: 60 }, styles: { name: 'styles', test: /\.(sa|sc|c)ss$/, chunks: 'all', enforce: true } } } }; -
代码压缩,删除无用代码
config.optimization.minimizer = [ new TerserPlugin({ terserOptions: { ecma: undefined, warnings: false, parse: {}, ***press: { drop_console: true, drop_debugger: true } } }) ]; -
关闭prefetch,默认开启Prefetch时,会提前加载所有路由对应的JS文件(即使未访问该路由),关闭后可避免非首屏资源的预加载请求,显著减少首屏HTTP请求数量
module.exports = { chainWebpack: config => { config.plugins.delete('prefetch') } }
24、JSbridge 是什么
JSBridge 是一种实现 JavaScript 与原生应用(如 Android 的 Java/Kotlin、iOS 的 Objective-C/Swift)双向通信的技术,核心作用是打破 Web 和 Native 的壁垒,扩展 Web 能力以调用设备原生功能
25、浏览器从请求地址到渲染页面都发生了什么?
浏览器从解析URL、DNS查询、建立TCP连接、发送HTTP请求,到接收响应后解析HTML/CSS构建DOM和CSSOM树,最终通过布局与绘制完成页面渲染。
26、页面首次加载出现白屏如何解决?
- 网络请求问题
原因:资源(HTML/CSS/JS)加载缓慢或失败,导致页面无法渲染。
解决方案:
- 压缩代码:使用 Webpack/Terser 压缩 JS,CSSNano 压缩 CSS
- 图片优化:转换为 WebP 格式,或用
<picture>标签适配不同格式 - CDN加速:将静态资源托管到 CDN 边缘节点
- 预加载关键资源:
<link rel="preload" href="critical.css" as="style"> <link rel="preload" href="main.js" as="script">
- JavaScript 执行阻塞
原因:主线程被同步任务阻塞或JS报错中断。
解决方案:
- 异步加载脚本:
<!-- 并行下载,下载完立即执行 --> <script async src="analytics.js"></script> <!-- 并行下载,HTML解析完执行 --> <script defer src="app.js"></script> - 错误捕获:
// 全局错误监听 window.addEventListener('error', (e) => { if (e.target.tagName === 'SCRIPT') { // 处理脚本加载失败 showFallbackUI(); } });
- 渲染阻塞
原因:CSS/JS文件阻塞DOM构建。
解决方案:
- 内联关键CSS:
<style> /* 首屏必要样式 */ .header { height: 60px; } </style> - 异步非关键CSS:
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
- 路由与代码分割问题(SPA)
原因:动态路由加载失败。
解决方案:
- React错误边界:
import { ErrorBoundary } from 'react-error-boundary'; <ErrorBoundary Fallback***ponent={ErrorFallback}> <Route***ponent /> </ErrorBoundary> - Webpack动态导入:
const Home = () => import(/* webpackChunkName: "home" */ './Home.vue');
- 缓存策略不当
解决方案:
# Nginx 配置示例
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
- 调整页面文件加载顺序
27、从文本话术中识别与特定职业相关的关键词,常用方法包括词典匹配、分词规则和算法/模型提取。
在对话或话术中识别职业等核心词语,常用方式有:
词典匹配:预设职业词库(如“学生”“老师”),在文本中直接匹配,简单高效。
分词 + 规则:用分词工具提取名词,再与职业词库比对,适合较复杂文本。
算法/模型:利用 TF-IDF、TextRank 或大模型提取关键词,可识别同义词和隐含语义。
👉 实际应用中,若只关注固定职业关键词,推荐 词典匹配;若需更灵活智能的识别,可结合 模型。
28、REST API 方法与 CRUD 对应及返回 Code 规范
REST API 是以 资源为中心 的接口设计风格,HTTP 方法对应 CRUD 操作如下:
| HTTP 方法 | CRUD 操作 | 含义 | 示例 |
|---|---|---|---|
| GET | Read | 获取资源 |
GET /users(获取用户列表)GET /users/123(获取 ID=123 的用户) |
| POST | Create | 新增资源 |
POST /users(创建新用户) |
| PUT | Update | 全量更新资源 |
PUT /users/123(替换用户信息) |
| PATCH | Update | 部分更新资源 |
PATCH /users/123(修改部分字段) |
| DELETE | Delete | 删除资源 |
DELETE /users/123(删除用户) |
返回 Code 规范(JSON 格式):
{
"code": 200, // 状态码
"message": "成功", // 提示信息
"data": {...} // 业务数据
}
| code | 含义 | 前端处理 |
|---|---|---|
| 200 | 成功 | 正常渲染数据 |
| 400 | 请求参数错误 | 提示用户检查输入 |
| 401 | 未登录/登录过期 | 清理 token → 跳转登录 |
| 403 | 无权限 | 提示“无权限访问” |
| 404 | 资源不存在 | 提示“未找到” |
| 500 | 服务器错误 | 提示“系统异常” |
| 1000+ | 业务级错误 | 具体业务提示 |
设计规范:
URL 使用 名词 表示资源,避免动词
请求体(body)用于 POST/PUT/PATCH,GET/DELETE 用 URL/query 参数
接口保持 无状态(Stateless)
前端统一拦截 code,按状态处理业务和提示
29、Element UI Tree组件数据量大卡顿解决方案
-
虚拟滚动:只渲染可视区域内的节点
-
懒加载:按需加载节点数据
-
数据扁平化:使用扁平数据结构优化处理
30、没有原码的项目如何修改或者增加功能
- 使用浏览器插件(如Tampermonkey)编写用户脚本,在页面加载后通过DOM操作修改列表文字或样式。
- 搭建反向代理(如Nginx),拦截后端返回的HTML/JSON数据,全局替换文本内容后再发给前端。
- 反编译
31、websocket底层如何知道断线和应用层心跳机制
-
如何知道断线
-
TCP 层面的断线:
-
如果客户端和服务器之间的 TCP 连接完全丢失(比如网络断开,或者服务器崩溃),WebSocket 会自动检测到并触发 onclose 事件,因为底层的 TCP 协议会发现无法继续传输数据。
-
这种情况会通过 WebSocket 的 close 事件触发,且你可以在 onclose 事件中处理重连或其他逻辑。
-
-
-
应用层心跳机制
应用层心跳机制主要是为了处理僵尸连接(连接看着正常,实际已断)、静默数据包丢失、 服务器无响应但连接保持这些问题的,因为这些问题TCP是检测不到的,所以需要我们自己手写一个心跳检测。
这个例子仅供参考没有实际意义// 1. 依赖 TCP 检测处理硬断开 socket.addEventListener("close", () => { console.warn("TCP 检测到硬断开,立即重连"); // 自动处理网络断开、服务重启等明显问题 }); // 2. 应用层心跳检测软断开和偶发问题 let isAlive = true; const startHeartbeat = () => { setInterval(() => { if (socket.readyState === WebSocket.OPEN) { isAlive = false; // 发送应用层心跳 socket.send(JSON.stringify({ type: 'HEARTBEAT', timestamp: Date.now(), id: docId })); // 检测偶发问题 setTimeout(() => { if (!isAlive) { console.warn("心跳超时,可能是偶发网络问题,主动重连"); socket.close(); // 手动触发重连 } }, 5000); // 5秒超时 } }, 30000); // 30秒一次心跳 };
32、html5和css3的规范是什么有哪些新特性
“HTML5和CSS3是现代Web开发的基石。HTML5通过语义化标签、多媒体支持和强大的本地存储API,使网页内容更丰富、结构更清晰,并具备了开发复杂Web应用的能力。CSS3则通过选择器、阴影、圆角等效果美化了界面,更重要的是通过Flexbox、Grid等革命性布局方案以及Transition、Animation动画,极大地提升了布局效率和交互体验。它们共同推动了响应式设计和移动优先的开发理念,让Web能提供媲美原生应用的体验。”
33、vux如何改动内容其他地方同步更新
-
异步或复杂业务逻辑:组件通过 dispatch 触发一个 action,action 中执行异步操作或复杂逻辑,然后通过 ***mit 提交一个 mutation,mutation 最终同步修改状态。
-
同步简单修改:组件直接通过 ***mit 提交一个 mutation,mutation 同步修改状态。
34、请详细说明 Vuex 中 this. s t o r e . d i s p a t c h ( ) 和 t h i s . store.dispatch() 和 this. store.dispatch()和this.store.***mit() 方法的区别,包括它们各自的用途、执行流程和适用场景。
- this.$store.***mit()
-
对应操作:提交 mutation
-
主要用途:同步、直接地修改 Vuex 状态
-
执行流程:直接调用对应的 mutation 函数修改 state
-
特点:
-
必须是同步操作
-
直接修改状态
-
在 DevTools 中可追踪每一个状态变化
-
适用场景:简单的状态变更,如计数器增减、开关切换等
-
// 定义
mutations: {
setUser(state, user) {
state.user = user;
}
}
// 使用
this.$store.***mit('setUser', userData);
- this.$store.dispatch()
-
对应操作:触发 action
-
主要用途:处理业务逻辑,特别是异步操作
-
执行流程:执行 action 中的业务逻辑,最终通过 ***mit 提交 mutation 来修改状态
-
特点:
-
可以包含异步操作
-
不能直接修改状态,必须通过 ***mit
-
适合处理复杂业务逻辑
-
适用场景:API 调用、多个状态变更的组合、条件判断等复杂逻辑
-
// 定义
actions: {
async login({ ***mit }, credentials) {
const user = await api.login(credentials);
***mit('setUser', user);
***mit('setLoggedIn', true);
}
}
// 使用
this.$store.dispatch('login', { username, password });
35、Vuex 中的 get 和 set(其实没有get和set而是Getters和Mutations)
- Vuex 中的 “Get” - Getters
-
作用:用于从 state 中派生计算状态,相当于计算属性的 getter
-
特点:
-
基于 state 进行计算,具有缓存机制
-
响应式:依赖的 state 变化时自动更新
-
只读,不能直接修改
-
示例:
getters: {
do***odos: state => state.todos.filter(todo => todo.done)
}
- Vuex 中的 “Set” - Mutations(重点!)
-
Vuex 没有直接的 setter,修改状态必须通过 mutation
-
Mutation 的作用:是修改 state 的唯一途径,相当于受控的 setter
-
设计原理:
-
保证状态变化的可追踪性
-
所有修改都同步进行,便于调试
-
通过 ***mit 提交,不能直接调用
-
- 完整的数据流理解
-
读取:通过 getters 或直接访问 state → 相当于 get
-
修改:通过 ***mit mutation → 相当于受控的 set
-
响应式机制:基于 Vue 的响应式系统(Object.defineProperty/Proxy)
36、请详细说明 JavaScript 中数组的 map、reduce 和 filter 方法的区别、用途和适用场景。
"这三个都是数组的高阶函数,但用途不同:
-
map 用于一对一的数据转换,比如把对象数组映射为ID数组
-
filter 用于条件筛选,返回满足条件的子集
-
reduce 最强大,用于多对一的聚合计算,比如求和、分组等
适用场景总结
-
数据转换:API响应数据 → 前端显示格式 → 用 map
-
搜索筛选:商品列表根据条件过滤 → 用 filter
-
统计分析:计算总和、平均值、分组 → 用 reduce
组合使用:复杂数据处理流程 → 链式调用
实际开发中经常链式使用,比如先filter筛选数据,再map转换格式,最后reduce统计结果。它们都不改变原数组,符合函数式编程的不可变理念。"
37、前端html如何和c#还有python对接实现物联网
"物联网系统通常采用分层架构。前端HTML通过WebSocket和REST API与后端通信:C#适合构建稳定的业务逻辑层,处理用户管理和设备控制;Python在设备通信和数据处理方面有优势,可以负责MQTT消息代理和实时数据推送。
具体实现上,前端用Chart.js展示实时数据图表,通过WebSocket接收Python后端推送的设备数据,同时通过HTTP API调用C#后端的控制接口。两种后端语言可以通过消息队列或共享数据库进行数据同步。"
38、前端与后端通信方式
明确否定:“不只有WebSocket和API,实际上有多种通信方案”
分类阐述:按实时性、协议类型等分类介绍
对比说明:指出各种方案的优缺点和适用场景
实战举例:结合具体业务场景说明如何选择
趋势展望:提到新兴技术如WebTransport、gRPC-Web等
39、JS数组如何去重复
- Set 方法(ES6+,最常用)
const array = [1, 2, 2, 3, 4, 4, 5];
// 最简单的方式
const uniqueArray = [...new Set(array)];
// 或使用 Array.from
const uniqueArray2 = Array.from(new Set(array));
console.log(uniqueArray); // [1, 2, 3, 4, 5]
优点:代码简洁,性能优秀
缺点:不能处理对象数组(对象引用不同就算不同)
- filter + indexOf(兼容性好)
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = array.filter((item, index) => {
return array.indexOf(item) === index;
});
console.log(uniqueArray); // [1, 2, 3, 4, 5]
原理:只保留第一次出现的元素
缺点:时间复杂度 O(n²),大数据量性能差
- reduce 方法(函数式编程)
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = array.reduce((a***, current) => {
return a***.includes(current) ? a*** : [...a***, current];
}, []);
// 更高效的版本(使用对象缓存)
const uniqueArray2 = array.reduce((a***, current) => {
if (!a***.seen.has(current)) {
a***.result.push(current);
a***.seen.add(current);
}
return a***;
}, { result: [], seen: new Set() }).result;
console.log(uniqueArray); // [1, 2, 3, 4, 5]
- for 循环 + 对象缓存(性能最优)
function uniqueArray(arr) {
const result = [];
const seen = {};
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
// 使用类型+值作为键,避免不同数据类型冲突
const key = typeof item + JSON.stringify(item);
if (!seen[key]) {
seen[key] = true;
result.push(item);
}
}
return result;
}
const array = [1, '1', 2, 2, 3, 4, 4, 5];
console.log(uniqueArray(array)); // [1, '1', 2, 3, 4, 5]
特殊场景处理
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' }, // 重复
{ id: 3, name: 'Charlie' }
];
// 方法1:使用 Map(推荐)
const uniqueUsers = Array.from(
new Map(users.map(user => [user.id, user])).values()
);
// 方法2:使用 reduce
const uniqueUsers2 = users.reduce((a***, current) => {
if (!a***.find(user => user.id === current.id)) {
a***.push(current);
}
return a***;
}, []);
// 方法3:使用 Set + JSON.stringify(完全比较)
const uniqueUsers3 = Array.from(
new Set(users.map(user => JSON.stringify(user)))
).map(str => JSON.parse(str));
console.log(uniqueUsers);
// [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}, {id: 3, name: 'Charlie'}]
复杂数据类型去重
// 处理 NaN、null、undefined 等特殊值
const ***plexArray = [NaN, NaN, null, null, undefined, undefined, 0, -0, 1, '1'];
const unique***plex = ***plexArray.filter((item, index, arr) => {
// 特殊处理 NaN(NaN !== NaN)
if (Number.isNaN(item)) {
return arr.findIndex(Number.isNaN) === index;
}
return arr.indexOf(item) === index;
});
console.log(unique***plex); // [NaN, null, undefined, 0, 1, '1']
大数据量优化版本
function uniqueLargeArray(arr) {
if (arr.length < 1000) {
return [...new Set(arr)]; // 小数组直接用 Set
}
// 大数组使用更高效的方法
const result = [];
const seen = new Set();
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if (!seen.has(item)) {
seen.add(item);
result.push(item);
}
}
return result;
}
“JavaScript 数组去重有多种方法,我最常用的是 Set 方法,因为它代码简洁性能好。如果需要兼容老浏览器,可以用 filter + indexOf。对于对象数组,可以使用 Map 按特定属性去重。”
根据不同场景选择
| 场景 | 推荐方法 | 代码示例 |
|---|---|---|
| 现代浏览器, | 基本类型 | Set |
| 兼容老浏览器 | filter + indexOf | arr.filter((v,i) => arr.indexOf(v) === i) |
| 对象数组(按属性) | Map | Array.from(new Map(arr.map(v => [key, v])).values()) |
| 性能要求极高 | for循环 + 对象缓存 | 上面示例中的 uniqueLargeArray |
| 函数式编程 | reduce | arr.reduce((a,c) => a.includes© ? a : […a,c], []) |
加分回答点
-
提到时间复杂度:Set 是 O(n),filter+indexOf 是 O(n²)
-
特殊值处理:如何正确处理 NaN、null、undefined
-
内存考虑:大数据量时的优化策略
-
扩展性:封装成可复用的工具函数
40、 “中大型项目如何规划,能让各部门间不相互冲突?”
“我会通过架构拆分、团队自治和契约化管理来实现这个目标。具体分三步:
-
架构层面:服务化拆分
将单体应用按业务域(如用户、订单、库存)拆分为独立的微服务。每个服务独立开发、部署和扩容,拥有自己的数据库,从技术上实现隔离。 -
组织层面:赋予团队端到端所有权
围绕这些微服务组建全功能团队(如“订单团队”)。团队对自己负责的服务拥有从开发到运维的完整决策权和职责,实现权责统一。 -
协作层面:通过API契约和自动化流程锁定边界
团队之间通过明确定义的API接口进行协作。所有接口在开发前先行约定,并利用自动化流水线实现各服务的独立测试与部署,确保协作稳定高效。
总结:
这套“服务化架构 + 自治团队 + 契约化协作”的体系,能从根源上减少部门间的依赖和冲突。”
这个回答结构清晰、要点明确,既体现了你的技术和管理思路,又非常精炼,适合面试场景。
41、vue3路由守卫和vue2区别
Vue 3 的路由守卫与 Vue 2 在核心概念上基本一致,但有一些重要区别:
主要区别
-
创建方式不同
Vue 2:const router = new VueRouter({ ... })Vue 3:
import { createRouter } from 'vue-router' const router = createRouter({ ... }) -
组件内守卫的 ***position API 用法
-
Vue 2 (Options API):
export default { beforeRouteEnter(to, from, next) { // 不能访问 this next(vm => { console.log(vm) // 通过回调访问实例 }) }, beforeRouteUpdate(to, from, next) { // 可以访问 this this.getData() next() } }Vue 3 (***position API):
import { onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router' export default { setup() { onBeforeRouteUpdate((to, from, next) => { // 在 setup 中处理路由更新 next() }) onBeforeRouteLeave((to, from, next) => { // 离开守卫 next() }) } }
-
beforeRouteEnter 的特殊处理
Vue 3 中 beforeRouteEnter 仍需要在 Options API 中使用,***position API 没有直接对应的函数。 -
TypeScript 支持
Vue 3 路由守卫有更好的 TypeScript 类型推断:import { NavigationGuard } from 'vue-router' const authGuard: NavigationGuard = (to, from, next) => { // 完整的类型提示 if (!isAuthenticated) next('/login') else next() }
守卫类型(两者都有)
-
全局前置守卫 router.beforeEach
-
全局解析守卫 router.beforeResolve
-
全局后置钩子 router.afterEach
-
路由独享守卫 beforeEnter
-
组件内守卫 beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
总结
Vue 3 路由守卫主要变化在于:
创建路由实例的方式不同
支持 ***position API 形式的组件内守卫
更好的 TypeScript 支持
核心功能和行为基本保持一致
实际使用中,如果你熟悉 Vue 2 的路由守卫,迁移到 Vue 3 会非常顺畅。
42、vu3如何异步更新数据不会主塞渲染层
Vue 3 中实现异步更新数据不阻塞渲染主要有以下几种方式:
-
使用 nextTick
import { ref, nextTick } from 'vue' const data = ref([]) async function fetchData() { const response = await fetch('/api/data') const result = await response.json() // 使用 nextTick 确保在 DOM 更新后执行 data.value = result await nextTick() // 此时 DOM 已更新,可以执行相关操作 } -
使用 Suspense(实验性)
<template> <Suspense> <template #default> <Async***ponent /> </template> <template #fallback> <div>Loading...</div> </template> </Suspense> </template> <script setup> const Async***ponent = defineAsync***ponent(() => import('./***ponents/Async***ponent.vue') ) </script> -
使用响应式更新 + 虚拟列表
import { ref, onMounted } from 'vue' const largeData = ref([]) const visibleData = ref([]) // 分批加载数据,避免一次性阻塞 async function loadDataInBatches() { const allData = await fetchLargeData() // 分批更新,给浏览器渲染机会 for (let i = 0; i < allData.length; i += 100) { const batch = allData.slice(i, i + 100) largeData.value.push(...batch) // 使用 setTimeout 让出主线程 await new Promise(resolve => setTimeout(resolve, 0)) } } -
使用 Web Workers
// main.js const worker = new Worker('./data-worker.js') worker.onmessage = (e) => { data.value = e.data } // data-worker.js self.onmessage = async function(e) { const response = await fetch('/api/large-data') const data = await response.json() self.postMessage(data) } -
使用 requestIdleCallback
import { ref } from 'vue' const data = ref([]) function loadDataIdle() { requestIdleCallback(async () => { const result = await fetchData() data.value = result }) } -
优化大量数据渲染
<template> <div> <!-- 使用虚拟滚动或分页 --> <div v-for="item in visibleItems" :key="item.id" > {{ item.name }} </div> </div> </template> <script setup> import { ref, ***puted, onMounted } from 'vue' const allData = ref([]) const currentPage = ref(1) const pageSize = 100 const visibleItems = ***puted(() => { const start = (currentPage.value - 1) * pageSize return allData.value.slice(start, start + pageSize) }) async function loadData() { allData.value = await fetchLargeData() } </script> -
使用防抖和节流
import { ref, watch, debounce } from 'vue' const searchQuery = ref('') const searchResults = ref([]) // 防抖搜索,避免频繁请求阻塞 const debouncedSearch = debounce(async (query) => { if (query) { searchResults.value = await searchAPI(query) } }, 300) watch(searchQuery, debouncedSearch)
总结
-
Vue 3 中避免异步更新阻塞渲染的关键策略:
-
使用 nextTick 管理更新时机
-
分批处理大数据,避免一次性更新
-
利用 Web Workers 处理复杂计算
-
使用虚拟列表/分页 减少 DOM 操作
-
合理使用浏览器空闲时间(requestIdleCallback)
-
防抖节流控制更新频率
这些方法可以确保 Vue 应用的流畅性和响应性。
43、 JS的基本类型、引用类型和特殊类型
基本类型:undefined、null、boolean、number、string、bigint、symbol
引用类型:object、array
特殊类型:function、date、regexp
44、 0.1 + 0.2 ≠ 0.3
根本原因:二进制浮点数表示
JavaScript 使用 IEEE 754 标准的64位双精度浮点数,有些十进制小数在二进制中是无限循环的:
// 0.1 在二进制中是无限循环的
0.1(十进制) = 0.0001100110011001100110011001100110011001100110011...(二进制)
// 0.2 在二进制中也是无限循环的
0.2(十进制) = 0.0011001100110011001100110011001100110011001100110011...(二进制)
由于存储空间有限,计算机会进行舍入误差,导致精度丢失。
// 实际存储的值(近似值)
let a = 0.1; // 实际存储: 0.1000000000000000055511151231257827021181583404541015625
let b = 0.2; // 实际存储: 0.200000000000000011102230246251565404236316680908203125
// 相加结果
let sum = a + b; // 实际: 0.3000000000000000444089209850062616169452667236328125
解决方案
-
使用容差比较
function numbersEqual(a, b, tolerance = 1e-10) { return Math.abs(a - b) < tolerance; } console.log(numbersEqual(0.1 + 0.2, 0.3)); // true -
转换为整数计算
// 先转整数,计算后再转回小数 let result = (0.1 * 10 + 0.2 * 10) / 10; console.log(result); // 0.3 console.log(result === 0.3); // true -
使用 toFixed()(注意返回的是字符串)
let result = (0.1 + 0.2).toFixed(1); console.log(result); // "0.3" console.log(Number(result) === 0.3); // true
45、如何将number转换成double
JavaScript中,你不需要将number转换为double,因为所有数字已经是双精度浮点数。如果你需要处理精度或格式化,可以使用toFixed()、toPrecision()等方法。
46、上机题:请安描述完成页面
<h1>公司抽奖活动</h1>
<h2>1. 公司有三位员工,点击【抽奖】按钮,抽取一位中奖员工</h2>
<h2>2. 中奖员工显示到中奖列表中,内容格式为三等奖【Alice】</h2>
<h2>3. 中奖后的员工不能再参加抽奖</h2>
<h2>4. 所有员工都中奖后,【抽奖】按钮不可点击</h2>
<h2>5. 中奖列表显示顺序从一等奖到三等奖的顺序显示</h2>
代码
<!DOCTYPE html>
<html lang="zh-***">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React 抽奖应用</title>
<!-- 引入 React 和 ReactDOM -->
<script src="https://unpkg.***/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.***/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<!-- 引入 Babel 编译器 -->
<script src="https://unpkg.***/@babel/standalone/babel.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin-top: 50px;
background-color: #f5f5f5;
}
.choujiang {
width: 600px;
margin: 0 auto;
border: 1px solid #***c;
padding: 20px;
background: white;
border-radius: 20px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
h1 {
color: #e74c3c;
margin-bottom: 20px;
}
h2 {
text-align: left;
font-size: 16px;
margin: 10px 0;
font-weight: normal;
}
button {
padding: 12px 25px;
font-size: 18px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
margin: 20px 0;
}
button:hover {
background-color: #c0392b;
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.winner-list {
margin-top: 30px;
text-align: left;
}
.winner-item {
font-size: 18px;
margin: 10px 0;
padding: 10px;
border-radius: 5px;
background-color: #f9f9f9;
display: flex;
justify-content: space-between;
}
.prize-level {
font-weight: bold;
color: #e74c3c;
}
.remaining {
margin-top: 15px;
color: #7f8c8d;
}
.highlight {
animation: highlight 1s ease-in-out;
}
@keyframes highlight {
0% { background-color: #ffeb3b; }
100% { background-color: #f9f9f9; }
}
.app-container {
width: 600px;
margin: 0 auto;
padding: 20px;
background: white;
border-radius: 20px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.status-info {
background-color: #e8f4fd;
padding: 10px;
border-radius: 5px;
margin: 15px 0;
border-left: 4px solid #3498db;
}
</style>
</head>
<body>
<div class="choujiang">
<h1>公司抽奖活动</h1>
<h2>1. 公司有三位员工,点击【抽奖】按钮,抽取一位中奖员工</h2>
<h2>2. 中奖员工显示到中奖列表中,内容格式为三等奖【Alice】</h2>
<h2>3. 中奖后的员工不能再参加抽奖</h2>
<h2>4. 所有员工都中奖后,【抽奖】按钮不可点击</h2>
<h2>5. 中奖列表显示顺序从一等奖到三等奖的顺序显示</h2>
</div>
<div id="root"></div>
<script type="text/babel">
function App() {
// 初始员工列表
const [participants, setParticipants] = React.useState([
{ id: '001', name: 'Alice' },
{ id: '002', name: 'Bob' },
{ id: '003', name: 'Charlie' }
]);
// 中奖列表,按奖项等级存储
const [winners, setWinners] = React.useState({
firstPrize: null,
secondPrize: null,
thirdPrize: null
});
// 新中奖者高亮效果
const [highlightPrize, setHighlightPrize] = React.useState(null);
// 抽奖函数
const draw = () => {
if (participants.length === 0) return;
// 随机选择一个员工
const randomIndex = Math.floor(Math.random() * participants.length);
const winner = participants[randomIndex];
// 从参与者中移除中奖者
const newParticipants = [...participants];
newParticipants.splice(randomIndex, 1);
setParticipants(newParticipants);
// 确定奖项等级(根据剩余可抽奖人数)
let prizeLevel;
if (newParticipants.length === 2) {
prizeLevel = 'thirdPrize';
} else if (newParticipants.length === 1) {
prizeLevel = 'secondPrize';
} else {
prizeLevel = 'firstPrize';
}
// 更新中奖列表
setWinners(prevWinners => ({
...prevWinners,
[prizeLevel]: winner
}));
// 设置高亮效果
setHighlightPrize(prizeLevel);
setTimeout(() => setHighlightPrize(null), 1000);
};
// 奖项配置
const prizeConfig = [
{ key: 'firstPrize', name: '一等奖', color: '#e74c3c' },
{ key: 'secondPrize', name: '二等奖', color: '#f39c12' },
{ key: 'thirdPrize', name: '三等奖', color: '#3498db' }
];
return (
<div className="app-container">
<h1 style={{color: '#e74c3c'}}>抽奖活动</h1>
<div className="status-info">
剩余可抽奖员工: {participants.length} 人
</div>
<div>
<button
onClick={draw}
disabled={participants.length === 0}
>
抽奖
</button>
</div>
<div className="winner-list">
<h2>中奖名单</h2>
{prizeConfig.map(prize => (
<div
key={prize.key}
className={`winner-item ${highlightPrize === prize.key ? 'highlight' : ''}`}
>
<span className="prize-level" style={{color: prize.color}}>
{prize.name}
</span>
<span>
{winners[prize.key]
? `【${winners[prize.key].name}】`
: '暂未抽取'
}
</span>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
</html>
46、浏览器跨域安全性
-
核心问题(没有跨域会怎样?)
“如果没有跨域限制,用户登录了银行网站 bank.*** 后,再去访问一个恶意网站 evil.***,那么 evil.*** 的脚本可以直接向 bank.*** 发起请求(比如转账请求),浏览器会自动带上用户的Cookie。银行服务器会认为这是用户的合法操作,从而导致严重的安全事故。” -
解决方案(跨域做了什么?)
“跨域策略(主要是浏览器的同源策略)就是这个问题的解决方案。它规定:不同源的脚本默认无法读取对方站点的资源内容和响应。这就建立了一个安全边界,把恶意网站隔离在了外面。” -
关键补充(它的定位是什么?)
“需要强调的是,跨域安全是浏览器端的一道基础防线,但它不是万能的。它主要防的是 CSRF(跨站请求伪造) 这类‘冒充’攻击。而现实中更多的威胁,比如 XSS(跨站脚本攻击),是由于网站自身的安全漏洞,让恶意脚本在‘同源’内部执行,这时跨域策略就无能为力了。所以,它是一个至关重要的基础安全模型,但需要和服务器端的CSRF Token、输入校验等安全措施共同构成一个完整的防御体系。”
终极精简版:
“跨域的安全性主要体现在通过同源策略实现了不同网站间的身份凭证隔离,核心是防止恶意网站滥用用户在正规网站的登录状态进行非法操作。”
47、websocket 前端需要注意什么
"前端使用 WebSocket 主要注意四点:
连接生命周期管理 - 及时创建和销毁,实现自动重连
健全的错误处理 - 监听 error 和 close 事件,添加心跳检测
数据安全处理 - 验证消息格式,防止 XSS
性能优化 - 控制消息频率,避免内存泄漏"
48、
"文档流就是元素在页面中’自然排队’的规则:
-
先来后到 - 按HTML书写顺序
-
身份决定站位 - 块级占整行,行内挤一起
-
CSS可以改变规则 - 比如让元素’漂浮’或’绝对定位’
-
脱离文档流就像插队 - 不按正常顺序排了"
"理解文档流是CSS布局的基础。在实际开发中,我最关注的是:
-
合理使用display属性控制元素排列
-
处理float和position导致的布局异常
-
通过BFC(块级格式化上下文)解决外边距重叠和浮动相关问题
-
在现代布局中,虽然Flexbox和Grid提供了新的布局方式,但文档流的概念依然是理解CSS的核心"
49、盒模型 是什么呢
"盒模型是CSS布局的基础概念,每个元素都被视为一个矩形盒子,由内容(content)、内边距(padding)、边框(border)、外边距(margin)四部分组成。
关键要理解两种盒模型:
-
标准盒模型(content-box):width/height只包含内容,加上padding和border才是总大小
-
IE盒模型(border-box):width/height直接定义元素的最终大小,更符合直觉
“实际开发中要注意什么”:
-
首先会全局设置 box-sizing: border-box,这样布局计算更直观,避免元素被意外撑大
-
特别注意垂直方向上的 margin 重叠问题,通常会采用统一使用单边 margin 或创建 BFC 的方式来避免
-
合理运用负 margin 来解决一些布局难题,比如消除元素间的默认间隙、实现等高布局等
-
使用 padding 和透明边框 来预留空间,避免动态添加样式时布局抖动
-
善用开发者工具的盒模型查看器 来快速定位布局问题
50、Viewport是什么?
Viewport(视口) 就是用户当前在屏幕上能够看到的网页区域。
你可以把它想象成你家里的窗户。你透过窗户能看到外面的风景,但风景本身(整个网页)可能比窗户大得多。Viewport 就是你观看网页内容的那个“窗口”。
-
是什么?
“Viewport就是浏览器里显示网页的那块区域,可以理解成看网页的‘窗口’。” -
为什么重要?
“主要是为了适配移动端。没有它,手机浏览器会把桌面网页缩得很小,导致用户体验很差。” -
怎么用?(核心答案)
“在HTML的里加上这个标签:
<meta name="viewport" content="width=device-width, initial-scale=1.0">
这行代码告诉浏览器:布局宽度就用设备的宽度,并且别缩放。”
- 额外了解(加分点)
“还有相关的CSS单位,比如vw(视口宽度的1%)、vh(视口高度的1%),用来做响应式布局很方便。”
一句话总结:
“Viewport是响应式设计的基石,设置了它,网页才能在移动端正确显示。”
51、JavaScript 事件循环机制:同步任务、微任务、宏任务
基本概念
-
任务类型
-
同步任务:立即执行的主线程代码
-
微任务:Promise 回调、await 后面的代码、queueMicrotask 等
-
宏任务:setTimeout、setInterval、I/O 操作、UI 渲染等
-
-
执行顺序规则
-
执行所有同步任务
-
执行所有微任务
-
如有必要,执行渲染
-
执行下一个宏任务
-
重复 1-4
-
-
示例分析
-
示例 1:基础 async/await
async function example() { console.log('A'); await someAsyncTask(); // 假设需要2秒 console.log('B'); } console.log('1'); example(); console.log('2');
-
-
执行流程:
- 同步任务阶段
-
console.log(‘1’) → 输出 “1”
-
调用 example() 函数
- console.log(‘A’) → 输出 “A”
-
遇到 await someAsyncTask()
-
执行 someAsyncTask()(启动异步操作)
-
将 example() 函数剩余代码(console.log(‘B’))包装成回调,放入微任务队列
-
-
console.log(‘2’) → 输出 “2”
-
此时输出:1 → A → 2
- 微任务执行阶段
-
当前同步代码全部执行完毕
-
检查微任务队列,等待 someAsyncTask() 完成(2秒后)
-
执行 console.log(‘B’) → 输出 “B”
最终输出:1 → A → 2 → B
示例 2:立即解决的 Promise
console.log('1');
async function test() {
console.log('2');
await Promise.resolve(); // 立即解决的Promise
console.log('3');
}
test();
console.log('4');
执行流程:
1.同步任务阶段
-
console.log(‘1’) → 输出 “1”
-
调用 test() 函数
-
console.log(‘2’) → 输出 “2”
-
await Promise.resolve()
-
Promise 立即解决
-
将 console.log(‘3’) 放入微任务队列
-
-
-
console.log(‘4’) → 输出 “4”
-
此时输出:1 → 2 → 4
2.微任务执行阶段
- 执行 console.log(‘3’) → 输出 “3”
最终输出:1 → 2 → 4 → 3
示例 3:混合微任务和宏任务
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
执行流程:
1.同步任务阶段
-
console.log(‘1’) → 输出 “1”
-
setTimeout 回调放入宏任务队列
-
Promise.resolve().then 回调放入微任务队列
-
console.log(‘4’) → 输出 “4”
此时输出:1 → 4
2.微任务执行阶段
- 执行 console.log(‘3’) → 输出 “3”
3.宏任务执行阶段
- 执行 setTimeout 回调 → 输出 “2”
最终输出:1 → 4 → 3 → 2
关键理解点
async/await 的执行机制
-
await 会暂停当前 async 函数的执行
-
将 await 后面的代码包装成微任务
-
不会阻塞外部同步代码的执行
-
async 函数总是返回一个 Promise
为什么需要这种机制?
-
保持 JavaScript 的单线程、非阻塞特性
-
让异步代码可以顺序编写,提高可读性
-
允许在等待异步操作时,主线程可以处理其他任务
总结
-
JavaScript 的事件循环机制确保了代码的高效执行:
-
同步任务立即执行
-
微任务在当前同步任务完成后立即执行
-
宏任务在微任务队列清空后执行
理解这个机制对于编写高效的异步代码和调试复杂的执行顺序问题至关重要。
52、vue-router路由守卫
- 全局守卫
- 作用于所有路由的守卫。
-
beforeEach、beforeResolve、afterEach
// 全局前置守卫 router.beforeEach((to, from, next) => { // 在路由跳转前执行 console.log('从', from.path, '跳转到', to.path) // 检查是否需要登录 if (to.meta.requiresAuth && !isLoggedIn()) { next('/login') // 重定向到登录页 } else { next() // 继续导航 } }) // 全局解析守卫 router.beforeResolve((to, from, next) => { // 在导航被确认前,组件内守卫和异步路由组件被解析后调用 next() }) // 全局后置钩子 router.afterEach((to, from) => { // 路由跳转完成后执行,没有 next 参数 // 适合用于页面统计、修改页面标题等 document.title = to.meta.title || '默认标题' })
-
- 路由独享守卫
- 只作用于特定路由的守卫。
-
beforeEnter
const router = new VueRouter({ routes: [ { path: '/admin', ***ponent: Admin, beforeEnter: (to, from, next) => { // 仅对该路由生效 if (!isAdmin()) { next('/unauthorized') } else { next() } } } ] })
-
- 组件内守卫
- 在组件内部定义的守卫。
-
beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
export default { name: 'UserProfile', // 在渲染该组件的对应路由被验证前调用 beforeRouteEnter(to, from, next) { // 不能访问组件实例 `this` // 因为当守卫执行时,组件实例还没被创建 next(vm => { // 通过 `vm` 访问组件实例 vm.fetchUserData(to.params.id) }) }, // 在当前路由改变,但是该组件被复用时调用 beforeRouteUpdate(to, from, next) { // 可以访问组件实例 `this` this.userId = to.params.id this.fetchUserData() next() }, // 在导航离开该组件的对应路由时调用 beforeRouteLeave(to, from, next) { // 可以访问组件实例 `this` if (this.hasUnsavedChanges) { const answer = window.confirm('您有未保存的更改,确定要离开吗?') if (answer) { next() } else { next(false) // 取消导航 } } else { next() } } }
-
52、JWT是什么?
JWT 是一个经过数字签名的、紧凑的、自包含的凭证,用于在双方之间安全地传输信息。
-
它的核心工作流程是:
-
认证:用户登录,服务器验证凭据后生成 JWT 并返回给客户端。
-
携带:客户端在后续请求中(通常在 HTTP Header 的 Authorization 字段中)携带此 JWT。
-
验证:服务器验证 JWT 的签名,如果有效,则信任其中的用户信息并处理请求。
-
-
总结
JWT就是后端生成后返回给前端,前端接收到后保管JWT请求接口是将JWT信息添加到请求头的Authorization 里发送给后端,后端在去验证用户是否能调用该接口或其他功能。
53、react高阶组件(hoc)是什么?
高阶组件是一个函数,它接收一个组件,返回一个增强的新组件,用于逻辑复用。
-
核心特点
-
本质:函数(不是组件)
-
输入:被包装的组件
-
输出:增强后的新组件
-
目的:组件逻辑复用和横切关注点
-
-
基本结构
const Enhanced***ponent = hoc(Wrapped***ponent); -
常见应用场景
-
认证授权 - 检查用户权限
-
数据获取 - 统一数据加载逻辑
-
状态管理 - 共享状态逻辑
-
性能优化 - 条件渲染、缓存
-
样式主题 - 注入主题配置
-
-
简单示例
// 认证 HOC
const withAuth = (***ponent) => {
return (props) => {
const isLogin = localStorage.getItem('token');
return isLogin ? <***ponent {...props} /> : <div>请先登录</div>;
};
};
// 使用
const PrivatePage = withAuth(UserPage);
-
与 Hooks 对比
-
HOC:组件层面的复用,可能产生嵌套
-
Hooks:逻辑层面的复用,更灵活直观
-
现状:Hooks 更流行,但理解 HOC 对维护老代码很重要
-
-
面试回答要点
- “高阶组件是 React 的代码复用模式,通过函数包装组件来添加额外功能。虽然现在 Hooks 更常用,但理解 HOC 对于处理遗留代码和某些复杂场景仍有价值。”
我自己的理解hoc的用法,就是把多个组件中重复的逻辑抽出来,提成一个公共函数减少代码量,只不过我们平常传递的是参数hoc传的是组件,原理是一样的。
63、vue的Provider 模式 和 react的Context API
- Vue 中的 AppProvider 组件内部使用的是 provide/inject API,实现了与 React Context 相同的跨组件数据传递功能。
64、vue的mixins是什么
- mixins就是我们可以定义一个公共的js里面存有data、methon和vue的生命周期,在需要使用这个功能js的组件中使用
mixins: []引入,我们就可以在模版中直接调用js里的方法或变量。
// toggleMixin.js
export const toggleMixin = {
data() {
return {
isShowing: false
};
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
},
mounted() {
console.log('来自混入的 mounted 钩子!');
}
};
<template>
<div>
<button @click="toggleShow">切换显示</button>
<p v-if="isShowing">你好,我是混入的例子!</p>
</div>
</template>
<script>
import { toggleMixin } from './toggleMixin.js'; // 引入混入
export default {
mixins: [toggleMixin], // 注册混入
mounted() {
console.log('来自组件自身的 mounted 钩子!');
}
// 组件自身的其他选项...
};
</script>
重要注意事项
选项合并规则:
-
数据对象:同名字段合并时,组件数据优先。
-
生命周期钩子:同名的钩子函数会被合并成一个数组,混入对象的钩子先执行。
-
方法、组件、指令:同名的键值冲突时,组件优先。
全局混入:由于会影响所有 Vue 实例,可能导致难以追踪的 bug,因此不推荐大规模使用。通常仅用于编写插件或处理自定义选项等特定场景。
65、flexbox是什么?grid是什么?grid和它的区别
-
Flexbox:一维布局专家
-
Flexbox 擅长控制单行或单列的布局
-
控制的是"一条线"上的布局 - 要么是行(水平),要么是列(垂直)
-
即使有多行(通过 flex-wrap: wrap),它也是按顺序排列,很难精确控制每行每列的对齐
-
好比: 排队 - 你可以控制队伍是横排还是竖排,以及队伍中每个人的间距和对齐
-
-
Grid:二维布局大师
-
Grid 擅长处理多行多列的复杂布局
-
同时控制行和列 - 像一个真正的网格系统
-
可以精确指定每个元素占据哪些行、哪些列
-
好比: 棋盘或Excel表格 - 你可以精确控制每个格子的大小和位置
-
| 特性 | Flexbox | CSS Grid |
|---|---|---|
| 维度 | 一维布局(行或列) | 二维布局(行和列同时) |
| 布局方式 | 基于内容流动 | 基于容器结构 |
| 适用场景 | 组件内部布局、 | 导航菜单 |
| 内容优先 | 是 - 内容决定布局 | 否 - 布局结构决定内容放置 |
| 对齐控制 | 单轴对齐 | 双轴同时控制 |
-
使用建议
-
使用 Flexbox 的情况:
-
导航菜单和工具栏
-
卡片组件内部布局
-
垂直居中内容
-
一维列表或图像库
-
-
使用 CSS Grid 的情况:
-
整体网页布局
-
复杂仪表板
-
杂志式排版
-
需要精确行列控制的情况
-
最佳实践:两者结合使用!用 Grid 创建整体布局框架,用 Flexbox 排列每个区域内的内容。
66、词法环境
一句话概括
-
词法环境就是JavaScript中存储变量和函数的地方,决定了代码在哪儿能访问什么变量。
-
核心要点
-
什么是词法环境?
每个函数执行时都会创建一个词法环境包含:当前函数的变量 + 对外部环境的引用
变量查找就是沿着这条引用链向上找
-
简单比喻
// 就像俄罗斯套娃 全局环境 ← 外部函数环境 ← 内部函数环境 -
面试回答模板
“词法环境是JavaScript管理变量的机制,包含当前作用域的变量和指向父级作用域的引用。它是在代码编写时就确定的,解释了为什么内部函数能访问外部函数的变量。” -
关键特点
-
静态作用域:在代码写定时就确定,不是运行时
-
形成闭包:内部函数保持对外部环境的引用
-
变量查找:先在当前环境找,找不到就沿链向上
- 实用例子
function outer() {
let x = 10; // 在outer的词法环境中
function inner() {
console.log(x); // 通过词法环境链找到x
}
return inner; }
记住:词法环境 = 当前变量 + 父级引用
67、vue 的生命周期和浏览器的事件循环
Vue 的异步更新队列确实是响应式系统的一部分,而不是生命周期与事件循环关系的核心。对于文档来说,这部分确实不需要详细展开。
让我们重新梳理一下生命周期和事件循环更直接的关系:
正确的核心联系:生命周期的执行时机受事件循环驱动
-
生命周期钩子本身的执行是同步的
- 当组件创建、挂载、更新、销毁时,Vue 会在对应时间点同步调用相应的生命周期钩子函数
-
但生命周期中经常包含异步操作
mounted() {
console.log('mounted 同步执行'); // 同步
// 异步操作 - 受事件循环控制
setTimeout(() => {
console.log('setTimeout 回调'); // 宏任务
}, 0);
this.$nextTick(() => {
console.log('nextTick 回调'); // 微任务
});
}
-
关键理解:$nextTick 是桥梁
-
$nextTick 利用事件循环的微任务机制
-
在生命周期中,如果你想在 DOM 更新后执行操作,必须用 $nextTick
-
这体现了生命周期与事件循环的紧密配合
-
文档中应该强调的重点:
-
生命周期钩子是同步执行的
-
在钩子中进行 DOM 操作时要注意时机问题
-
使用 $nextTick 来确保在 DOM 更新后执行代码
-
理解这一点有助于避免常见的时机错误
68、this指向问题
function Foo(){
console.info(this); // 这里 this 指向 window
this.name = 'zhangsan'
}
console.info( Foo() ); // 输出 undefined
直接调用 Foo(),没有使用 new
在非严格模式下,this 指向全局对象(浏览器中是 window)
this.name = ‘zhangsan’ 相当于 window.name = ‘zhangsan’
函数没有返回值,所以 Foo() 返回 undefined
function Foo(){
console.info(this); // 这里 this 指向新创建的 Foo 实例
this.name = 'zhangsan'
}
console.info( new Foo() ) // 输出 Foo { name: 'zhangsan' }
更准确的描述是:this 指向 Foo {} 对象(即 Foo 的实例)
控制台输出会是:Foo { name: ‘zhangsan’ }
69、说一下call、apply、bind区别
-
call 方法
语法:func.call(thisArg, arg1, arg2, ...)特点:
立即执行函数 参数逐个传递 改变函数的 this 指向示例:
function introduce(greeting, punctuation) { console.log(`${greeting}, 我是${this.name}${punctuation}`); } const person = { name: '张三' }; // 使用 call introduce.call(person, '你好', '!'); // 输出:你好, 我是张三! -
apply 方法
语法:func.apply(thisArg, [argsArray])特点:
立即执行函数 参数以数组形式传递 改变函数的 this 指向示例:
function introduce(greeting, punctuation) { console.log(`${greeting}, 我是${this.name}${punctuation}`); } const person = { name: '李四' }; // 使用 apply introduce.apply(person, ['你好', '!']); // 输出:你好, 我是李四! -
bind 方法
语法:func.bind(thisArg, arg1, arg2, ...)特点:
不立即执行,返回一个新函数 可以预设参数(柯里化) 改变函数的 this 指向示例:
function introduce(greeting, punctuation) { console.log(`${greeting}, 我是${this.name}${punctuation}`); } const person = { name: '王五' }; // 使用 bind - 不立即执行 const boundFunc = introduce.bind(person, '你好'); boundFunc('!'); // 输出:你好, 我是王五!详细对比示例
const user = { name: '小明', age: 18 }; function showInfo(city, country) { console.log(`${this.name},年龄${this.age},来自${city},${country}`); } console.log('=== call vs apply vs bind 对比 ==='); // 1. call 使用 showInfo.call(user, '北京', '中国'); // 输出:小明,年龄18,来自北京,中国 // 2. apply 使用 showInfo.apply(user, ['上海', '中国']); // 输出:小明,年龄18,来自上海,中国 // 3. bind 使用 const boundShowInfo = showInfo.bind(user, '广州'); boundShowInfo('中国'); // 可以继续传参 // 输出:小明,年龄18,来自广州,中国
实际应用场景对比
场景1:借用其他对象的方法
// call/apply 适合立即执行的场景
const obj1 = {
name: '对象1',
method: function() {
console.log(this.name);
}
};
const obj2 = {
name: '对象2'
};
// 立即借用方法
obj1.method.call(obj2); // 对象2
obj1.method.apply(obj2); // 对象2
// bind 创建新函数备用
const boundMethod = obj1.method.bind(obj2);
setTimeout(boundMethod, 1000); // 1秒后输出:对象2
场景2:参数传递方式
function sum(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
// call - 参数逐个传递
console.log(sum.call(null, 1, 2, 3)); // 6
// apply - 参数数组传递
console.log(sum.apply(null, numbers)); // 6
// bind - 预设参数
const sumWithFirstTwo = sum.bind(null, 1, 2);
console.log(sumWithFirstTwo(3)); // 6
场景3:事件处理函数
class Button {
constructor(text) {
this.text = text;
this.element = document.createElement('button');
this.element.textContent = text;
}
onClick() {
console.log(`点击了: ${this.text}`);
}
// 错误做法 - this 会丢失
addEventListenerWrong() {
this.element.addEventListener('click', this.onClick);
}
// 正确做法 - 使用 bind
addEventListenerCorrect() {
this.element.addEventListener('click', this.onClick.bind(this));
}
}
const btn = new Button('点击我');
btn.addEventListenerCorrect();
性能考虑
// 性能对比
function test() {
return this.value;
}
const obj = { value: 42 };
console.time('call');
for (let i = 0; i < 1000000; i++) {
test.call(obj);
}
console.timeEnd('call');
console.time('apply');
for (let i = 0; i < 1000000; i++) {
test.apply(obj);
}
console.timeEnd('apply');
console.time('bind');
const boundTest = test.bind(obj);
for (let i = 0; i < 1000000; i++) {
boundTest();
}
console.timeEnd('bind');
通常性能: bind > call > apply(但现代JS引擎优化得很好,差异不大)
总结对比表格
| 特性 | call | apply | bind |
|---|---|---|---|
| 执行时机 | 立即执行 | 立即执行 | 返回新函数,不立即执行 |
| 参数传递 | 逐个传递 | 数组传递 | 逐个传递,可预设参数 |
| 返回值 | 函数的返回值 | 函数的返回值 | 绑定后的新函数 |
| 使用场景 | 明确知道参数个数 | 参数不确定或已是数组 | 需要延迟执行或预设参数 |
| 性能 | 较快 | 较慢(需处理数组) | 一次绑定,多次调用快 |
手写实现理解原理
// 手写 call
Function.prototype.myCall = function(context, ...args) {
context = context || window;
const fn = Symbol('fn');
context[fn] = this;
const result = context[fn](...args);
delete context[fn];
return result;
};
// 手写 apply
Function.prototype.myApply = function(context, args) {
context = context || window;
const fn = Symbol('fn');
context[fn] = this;
const result = context[fn](...args);
delete context[fn];
return result;
};
// 手写 bind
Function.prototype.myBind = function(context, ...args) {
const self = this;
return function(...newArgs) {
return self.apply(context, [...args, ...newArgs]);
};
};
记忆口诀:
-
call:参数一个一个来
-
apply:参数放在数组里
-
bind:不调用,返回新函数等你调
理解这三者的区别对于掌握 JavaScript 的函数执行上下文和面向对象编程至关重要!
73、同步代码、宏任务、微任务
-
同步代码(Synchronous) - 详细分类
1.1 变量和声明// 变量声明 var a = 1; let b = 2; const c = 3; // 函数声明 function normalFunction() { console.log('普通函数'); } // 类声明 class MyClass { constructor() { console.log('类构造函数'); } } // import 语句 import { module } from './module.js';1.2 表达式和运算
// 算术运算 1 + 2 * 3; Math.pow(2, 3); // 逻辑运算 true && false; !!variable; // 字符串操作 'hello'.toUpperCase(); `Template ${string}`;1.3 控制流语句
// 条件语句 if (condition) {} switch (value) { case 1: break; } // 循环语句 for (let i = 0; i < 10; i++) {} while (condition) {} do {} while (condition); // 跳转语句 break; continue; return value; throw new Error();1.4 对象和数组操作
// 对象操作 const obj = { key: 'value' }; Object.keys(obj); JSON.parse('{}'); // 数组操作 [1, 2, 3].map(x => x * 2); Array.from([1, 2, 3]);1.5 Promise 相关(同步部分)
// Promise 构造函数同步执行 new Promise((resolve, reject) => { console.log('这个会立即执行'); // 同步 // resolve() 和 reject() 也是同步的! }); // Promise 静态方法的部分同步行为 Promise.resolve('value'); // 同步创建已解决的 Promise Promise.reject('error'); // 同步创建已拒绝的 Promise // Promise.all/race 的参数计算是同步的 Promise.all([promise1, promise2]); // 参数数组同步计算 -
微任务(Microtasks) - 详细分类
2.1 Promise 相关// .then() 方法 promise.then( value => console.log('成功回调'), // 微任务 error => console.log('失败回调') // 微任务 ); // .catch() 方法 promise.catch(error => console.log('捕获错误')); // 微任务 // .finally() 方法 promise.finally(() => console.log('最终执行')); // 微任务 // Promise 链式调用 Promise.resolve() .then(() => console.log('第一个 then')) // 微任务 .then(() => console.log('第二个 then')); // 微任务2.2 queueMicrotask API
// 明确的微任务 API queueMicrotask(() => { console.log('这是一个微任务'); }); // 与 Promise 的等价写法 queueMicrotask(() => {}); // 等价于 Promise.resolve().then(() => {});2.3 MutationObserver(浏览器环境)
// DOM 变化的微任务回调 const observer = new MutationObserver((mutations) => { console.log('DOM 发生变化'); // 微任务 }); observer.observe(targetElement, { childList: true, subtree: true });2.4 process.nextTick(Node.js)
// Node.js 中的微任务(优先级最高) process.nextTick(() => { console.log('nextTick 回调'); // 在 Promise 之前执行 });2.5 async/await 相关
async function asyncFunc() { console.log('同步代码'); await somePromise; // 等待的表达式同步执行 console.log('await 之后'); // 微任务 return 'result'; // 微任务 } // 等价于: function asyncFunc() { console.log('同步代码'); return somePromise.then(() => { console.log('await 之后'); // 微任务 return 'result'; // 微任务 }); } -
宏任务(Macrotasks) - 详细分类
3.1 定时器相关// setTimeout setTimeout(() => { console.log('setTimeout 回调'); // 宏任务 }, 0); // setInterval setInterval(() => { console.log('setInterval 回调'); // 宏任务 }, 1000); // setImmediate(Node.js) setImmediate(() => { console.log('setImmediate 回调'); // 宏任务 });3.2 I/O 操作
// 文件 I/O(Node.js) fs.readFile('file.txt', (err, data) => { console.log('文件读取完成'); // 宏任务 }); // 网络 I/O fetch('/api') .then(response => response.json()) // 微任务 .then(data => console.log(data)); // 微任务 // 注意:fetch 的 then 是微任务,但网络请求本身是宏任务 // 数据库操作 db.query('SELECT * FROM table', (err, results) => { console.log('查询结果'); // 宏任务 });3.3 UI 和渲染相关
// 请求动画帧 requestAnimationFrame(() => { console.log('动画帧回调'); // 宏任务 }); // 布局回调 requestIdleCallback((deadline) => { console.log('空闲时回调'); // 宏任务 });3.4 事件监听器
// DOM 事件 button.addEventListener('click', (event) => { console.log('点击事件'); // 宏任务 }); // 自定义事件 window.addEventListener('custom-event', (event) => { console.log('自定义事件'); // 宏任务 }); // 消息事件 window.addEventListener('message', (event) => { console.log('消息事件'); // 宏任务 });3.5 Web APIs
// Geolocation API navigator.geolocation.getCurrentPosition((position) => { console.log('位置信息'); // 宏任务 }); // Notification API Notification.requestPermission().then((permission) => { console.log('权限结果'); // 微任务 }); // Service Worker navigator.serviceWorker.register('sw.js').then((registration) => { console.log('注册完成'); // 微任务 }); -
复杂执行顺序示例
console.log('1. 同步开始'); setTimeout(() => { console.log('2. 宏任务 - setTimeout'); Promise.resolve().then(() => { console.log('3. 微任务 - 在 setTimeout 中'); }); }, 0); Promise.resolve().then(() => { console.log('4. 微任务 - 第一个 Promise'); return '结果'; }).then((result) => { console.log('5. 微任务 - 第二个 Promise', result); }); queueMicrotask(() => { console.log('6. 微任务 - queueMicrotask'); }); console.log('7. 同步结束'); // 输出顺序: // 1. 同步开始 // 7. 同步结束 // 4. 微任务 - 第一个 Promise // 5. 微任务 - 第二个 Promise // 6. 微任务 - queueMicrotask // 2. 宏任务 - setTimeout // 3. 微任务 - 在 setTimeout 中 -
特殊情况和边界案例
5.1 微任务中产生新的微任务Promise.resolve().then(() => { console.log('微任务 1'); Promise.resolve().then(() => { console.log('微任务 2'); // 会立即执行,不会等到下一轮 }); });5.2 宏任务中产生微任务
setTimeout(() => { console.log('宏任务开始'); Promise.resolve().then(() => { console.log('宏任务中的微任务'); // 在下一个宏任务之前执行 }); console.log('宏任务结束'); }, 0);5.3 Node.js 与浏览器的差异
// Node.js 中 setImmediate(() => console.log('setImmediate')); // 宏任务 setTimeout(() => console.log('setTimeout'), 0); // 宏任务 process.nextTick(() => console.log('nextTick')); // 微任务(最高优先级) // 浏览器中 setTimeout(() => console.log('setTimeout'), 0); // 宏任务 queueMicrotask(() => console.log('queueMicrotask')); // 微任务 -
执行优先级总结
完整执行顺序:-
同步代码
-
process.nextTick(Node.js,最高优先级微任务)
-
微任务队列清空(Promise.then, queueMicrotask, MutationObserver)
-
宏任务队列(取一个执行)
-
回到步骤 2,循环执行
-
这个详细的整理应该能帮助你完全理解 JavaScript 的执行机制!
74、vue2中使用冻结数据可以避免响应式从而提升性能,那在vue3中用该怎么做
-
在Vue 2中,使用 Object.freeze() 可以防止数据被响应式处理,从而提升性能,特别是对于大型静态数据列表。
<template> <div> <h2>用户列表</h2> <div v-for="user in users" :key="user.id"> {{ user.name }} - {{ user.email }} </div> </div> </template> <script> export default { data() { return { // 使用 Object.freeze 冻结静态数据,避免响应式开销 users: Object.freeze([ { id: 1, name: '张三', email: 'zhangsan@email.***' }, { id: 2, name: '李四', email: 'lisi@email.***' }, { id: 3, name: '王五', email: 'wangwu@email.***' } ]) } } } </script> -
在vue3中定义普通数据就行不使用ref和reactive,vue3也不推荐使用object.freeze
import { markRaw, reactive } from 'vue' // 创建一个永远不会是响应式的原始对象 const staticData = markRaw({ veryLargeNestedObject: { ... }, someThirdPartyInstance: new ThirdPartyLibrary() }) // 即使在响应式对象中使用,它也不会变成响应式 const state = reactive({ reactiveProperty: '我是响应式的', staticData: staticData // 这个不会变成响应式 })
75、css3有哪些新增内容
选择器增强
-
属性选择器:[attr=value]、[attr^=value]、[attr$=value]、[attr*=value]
-
结构伪类::nth-child()、:nth-of-type()、:last-child、:only-child
-
状态伪类::enabled、:disabled、:checked、:required
-
否定伪类::not(selector)
-
目标伪类::target
盒模型
-
box-sizing:content-box | border-box
-
resize:元素尺寸调整
-
box-shadow:盒子阴影
-
outline-offset:轮廓偏移
背景和边框
-
多背景:background: url1, url2, …
-
background-size:背景图片尺寸
-
background-origin:背景定位区域
-
background-clip:背景绘制区域
-
border-radius:圆角边框
-
border-image:边框图片
文本效果
-
text-shadow:文字阴影
-
word-wrap:break-word(长单词换行)
-
word-break:break-all(更严格的换行)
-
@font-face:自定义字体
2D/3D 变换
-
transform:
-
2D:translate()、rotate()、scale()、skew()
-
3D:translate3d()、rotate3d()、scale3d()
-
-
transform-origin:变换原点
-
perspective:透视视图
-
backface-visibility:背面可见性
过渡和动画
-
transition:property duration timing-function delay
-
@keyframes:关键帧动画
-
animation:
- name、duration、timing-function、delay、iteration-count、direction
布局
-
Flexbox:弹性盒子布局
-
Grid:网格布局(CSS3后期)
-
多列布局:column-count、column-gap、column-rule
其他重要特性
-
渐变:linear-gradient()、radial-gradient()
-
滤镜:filter: blur() grayscale() opacity()
-
媒体查询:@media 响应式设计
-
计算:calc()
-
变量:CSS Custom Properties (–main-color: #fff)
76、flex有哪些属性
容器属性(父元素)
-
display
.container { display: flex; /* 块级弹性容器 */ display: inline-flex; /* 行内弹性容器 */ } -
flex-direction - 主轴方向
.container { flex-direction: row; /* 默认:水平,起点在左端 */ flex-direction: row-reverse; /* 水平,起点在右端 */ flex-direction: column; /* 垂直,起点在上沿 */ flex-direction: column-reverse; /* 垂直,起点在下沿 */ } -
flex-wrap - 换行方式
.container { flex-wrap: nowrap; /* 默认:不换行 */ flex-wrap: wrap; /* 换行,第一行在上方 */ flex-wrap: wrap-reverse; /* 换行,第一行在下方 */ } -
flex-flow - 简写
.container { flex-flow: <flex-direction> <flex-wrap>; /* 示例:*/ flex-flow: row wrap; } -
justify-content - 主轴对齐
.container { justify-content: flex-start; /* 默认:左对齐 */ justify-content: flex-end; /* 右对齐 */ justify-content: center; /* 居中 */ justify-content: space-between; /* 两端对齐,项目间间隔相等 */ justify-content: space-around; /* 每个项目两侧间隔相等 */ justify-content: space-evenly; /* 每个项目间隔相等 */ } -
align-items - 交叉轴对齐(单行)
.container { align-items: stretch; /* 默认:拉伸填满容器高度 */ align-items: flex-start; /* 交叉轴起点对齐 */ align-items: flex-end; /* 交叉轴终点对齐 */ align-items: center; /* 交叉轴中点对齐 */ align-items: baseline; /* 项目的第一行文字基线对齐 */ } -
align-content - 多行对齐
.container { align-content: stretch; /* 默认:轴线占满整个交叉轴 */ align-content: flex-start; /* 交叉轴起点对齐 */ align-content: flex-end; /* 交叉轴终点对齐 */ align-content: center; /* 交叉轴中点对齐 */ align-content: space-between; /* 交叉轴两端对齐 */ align-content: space-around; /* 每根轴线两侧间隔相等 */ }
项目属性(子元素)
-
order - 排序
.item { order: <integer>; /* 默认0,数值越小排列越靠前 */ } -
flex-grow - 放大比例
.item { flex-grow: <number>; /* 默认0,不放大 */ } -
flex-shrink - 缩小比例
.item { flex-shrink: <number>; /* 默认1,空间不足时缩小 */ } -
flex-basis - 项目基准大小
.item { flex-basis: <length> | auto; /* 默认auto,项目本来大小 */ } -
flex - 简写
.item { flex: none | [ <flex-grow> <flex-shrink>? || <flex-basis> ]; /* 常用值:*/ flex: 0 1 auto; /* 默认 */ flex: auto; /* 1 1 auto */ flex: none; /* 0 0 auto */ flex: 1; /* 1 1 0% */ } -
align-self - 单独对齐
.item { align-self: auto; /* 默认:继承align-items */ align-self: flex-start; align-self: flex-end; align-self: center; align-self: baseline; align-self: stretch; }
实用记忆口诀
容器属性:direction wrap flow justify align-items align-content
项目属性:order grow shrink basis flex align-self
77、vue2用this$ref获取组件信息,vue3在组件上定义ref就行,那么在for循环里怎么获取组件?
Vue 2 中获取循环组件引用
在 Vue 2 中,主要通过 this.$refs 来访问。
-
同名 Ref 自动转为数组:在 v-for 循环中,如果多个元素或组件设置了相同的 ref 名称,Vue 2 会将这些引用自动收集为一个数组。
<template> <div> <child-***ponent v-for="item in list" :key="item.id" ref="child***ponents" /> </div> </template> <script> export default { mounted() { // 通过 this.$refs.child***ponents 访问所有子组件实例数组 console.log(this.$refs.child***ponents); // 这是一个数组 } }; </script> -
使用动态 Ref 名称:你也可以通过绑定动态的 ref 名称来为每个循环项创建单独的引用,这些引用会以你定义的名字存储在 $refs 对象中。
<template> <div> <div v-for="(item, index) in list" :key="index"> <child-***ponent :ref="'child' + index" /> </div> </div> </template> <script> export default { mounted() { // 通过动态生成的键名访问 console.log(this.$refs.child0); // 第一个子组件 } }; </script> -
使用 Ref 回调函数(函数式 Ref):这是一种更灵活的方式,允许你自定义引用收集的逻辑。
<template> <div> <div v-for="(item, index) in list" :key="index"> <child-***ponent :ref="el => setItemRef(el, index)" /> </div> </div> </template> <script> export default { data() { return { itemRefs: [] // 用于存储组件引用 }; }, methods: { setItemRef(el, index) { if (el) { this.itemRefs[index] = el; // 将引用存储到指定位置 } } }, mounted() { console.log(this.itemRefs); // 访问存储的引用数组 } }; </script>
⚡ Vue 3 中获取循环组件引用
Vue 3 提供了 ***position API 和 Options API 两种方式。
-
mposition API 中使用 ref 回调函数:这是 Vue 3 ***position API 中推荐使用的方法。
<template> <div> <child-***ponent v-for="(item, index) in list" :key="item.id" :ref="(el) => setItemRef(el, index)" /> </div> </template> <script> import { ref, onMounted } from 'vue'; export default { setup() { const list = ref([...]); // 你的列表数据 const itemRefs = ref([]); // 创建一个响应式数组来存储引用 const setItemRef = (el, index) => { if (el) { itemRefs.value[index] = el; // 将引用存储到数组的指定位置 } }; onMounted(() => { console.log(itemRefs.value); // 在挂载后访问引用数组 }); return { list, setItemRef, itemRefs }; } }; </script>注意:当组件卸载时,el 参数为 null,你可能需要清理对应的引用。
-
ue 3.5+ 使用 useTemplateRef:Vue 3.5 引入了 useTemplateRef 函数,可以更简洁地处理循环 ref,其值是一个数组,在元素被挂载后包含整个列表的所有元素。
<script setup> import { useTemplateRef, onMounted } from 'vue' const list = ref([...]) const itemRefs = useTemplateRef('items') // 使用 useTemplateRef onMounted(() => { console.log(itemRefs.value) // 访问所有循环项的引用数组 }) </script> <template> <ul> <li v-for="item in list" :key="item.id" ref="items"> {{ item }} </li> </ul> </template>在 3.5 之前的版本,你需要声明一个与模板引用 attribute 同名的 ref,其值也需要是一个数组。
-
vue 3 的 Options API:如果你在 Vue 3 中仍然使用 Options API,其用法与 Vue 2 类似,主要通过 this.$refs 访问。在 v-for 中同名 ref 的行为也类似,会是一个数组。
<template> <div> <child-***ponent v-for="item in list" :key="item.id" ref="child***ponents" /> </div> </template> <script> export default { mounted() { console.log(this.$refs.child***ponents); // 组件实例数组 } }; </script>
💡 重要注意事项
-
确保访问时机:无论 Vue 2 还是 Vue 3,ref 都需要在 DOM 渲染完成后才会填充。因此,务必在 mounted (Vue 2 / Options API) 或 onMounted (***position API) 生命周期钩子中,或者使用 this.$nextTick (Vue 2 / Options API) / nextTick (***position API) 确保 DOM 已更新后再访问 ref。
-
处理可能的 null 值:在使用 ref 回调函数时,当元素或组件被卸载时,回调函数会以 null 作为参数被调用。你需要处理好引用失效的情况,避免内存泄漏或访问错误。
-
组件使用
<!-- 子组件 Child.vue -->
<script setup>
import { ref } from 'vue'
const privateData = ref('私有数据')
const publicData = ref('公共数据')
defineExpose({
publicData
})
</script>
78、2’ + ‘2’ - ‘2’ 的结果是多少?
- ‘2’ + ‘2’
- ‘+’ 运算符在遇到字符串时执行字符串拼接,因此 ‘2’ + ‘2’ 得到字符串 “22”。
- “22” - ‘2’
- '-'运算符会将操作数转换为数字,因此 “22” 转换为 22,‘2’ 转换为 2。
最终计算 22 - 2,结果为 20。
79、vue模版的本质
vue2和vue3的模版本质就是一个语法糖它表达的是一个渲染过程,最终得到的是一个界面的结构。
这是一个模版
<template>
<div id="app">
<h1 v-if="showTitle">{{ title }}</h1>
<button @click="toggleTitle">Toggle</button>
</div>
</template>
经过编译后,可能会生成类似这样的渲染函数(简化理解,实际更复杂):
function render() {
return h('div', { id: 'app' }, [
this.showTitle ? h('h1', this.title) : null,
h('button', { onClick: this.toggleTitle }, 'Toggle')
]);
}
在真正运行的时候是没有模板的,真正运行的是js代码,vue会通过render函数把模版传化成js,在vue2和vue3中由于写法不同选项式和组合式api所以render的实现也不一样。
vue2
export default {
data() {
return {
title: 'Hello Vue 2!'
}
},
render(h) {
return h('h1', this.title) // 通过this访问数据
}
}
vue3
import { h } from 'vue' // 需要显式导入h函数
export default {
setup(props, context) {
const title = 'Hello Vue 3!'
// 返回一个渲染函数
return () => h('h1', title)
}
}
看上面的代码可以发现Vue 也是声明式的语法,这种写法的好处就是更方便开发因为是就是html我们上手会很快,如果让我们直接写js创建dom会很麻烦,声明式能更高效地描述 UI。
我们写的 .vue 文件中的 标签里的内容,并不是最终的 HTML。它在构建阶段(例如使用 Vue CLI 或 Vite)会被 Vue 的编译器 处理。
编译过程大致如下:
模板 (Template) -> 编译 (***pile) -> 渲染函数 (Render Function) -> 虚拟 DOM (Virtual DOM) -> 挂载/打补丁 (Mount/Patch) -> 真实 DOM (Real DOM)
-
编译:Vue 的编译器会解析你的模板,分析其中的指令(如 v-if, v-for)、插值({{ }})、事件绑定(@click)等。
-
生成渲染函数:编译器将分析结果转换成一个或多个 JavaScript 函数,这些函数就是渲染函数。渲染函数的返回值是 虚拟 DOM 节点。
举个例子,这样一个模板:
<template>
<div id="app">
<h1 v-if="showTitle">{{ title }}</h1>
<button @click="toggleTitle">Toggle</button>
</div>
</template>
经过编译后,可能会生成类似这样的渲染函数(简化理解,实际更复杂):
function render() {
return h('div', { id: 'app' }, [
this.showTitle ? h('h1', this.title) : null,
h('button', { onClick: this.toggleTitle }, 'Toggle')
]);
}
这里的 h 函数(是 createElement 的通用简写)就是用于创建虚拟节点的。
所以,Vue 模板最终干活的是 JavaScript 的渲染函数。
如何生成的虚拟dom呢
执行过程:
-
调用 render() 函数
-
执行 h() 函数(创建虚拟节点的函数)
-
返回虚拟DOM对象(类似下面的结构):
// 这就是虚拟DOM - 一个普通的JS对象 { tag: 'div', props: { id: 'app' }, children: [ { tag: 'h1', props: {}, children: ['Hello World'] // 假设 this.title 的值 }, { tag: 'button', props: { onClick: this.toggleTitle }, children: ['Toggle'] } ] }
如果我们是首次创建节点也是就失业初始化的时候
const container = document.getElementById('app');
const realDOM = createElement(vnode);
container.appendChild(realDOM);
会把render生成到的JS对象appendChild页面
如果是更新某个dom那么会进行一下操作
// 旧虚拟DOM(更新前)
const oldVNode = {
tag: 'ul',
props: { class: 'todo-list' },
children: [
{
tag: 'li',
key: 'item1',
props: { class: 'todo-item' },
children: ['Buy milk']
},
{
tag: 'li',
key: 'item2',
props: { class: 'todo-item' },
children: ['Walk dog']
},
{
tag: 'li',
key: 'item3',
props: { class: 'todo-item' },
children: ['Read book']
}
]
};
// 新虚拟DOM(更新后)
const newVNode = {
tag: 'ul',
props: { class: 'todo-list updated' }, // 类名更新了
children: [
{
tag: 'li',
key: 'item1',
props: { class: 'todo-item ***pleted' }, // 类名更新
children: ['Buy milk and eggs'] // 文本更新
},
// item2 被删除了
{
tag: 'li',
key: 'item3',
props: { class: 'todo-item' },
children: ['Read book']
}
]
};
经过Diff比较后,会生成这样的差异报告:
const patches = {
// 根节点的属性更新
props: {
class: { oldValue: 'todo-list', newValue: 'todo-list updated' }
},
// 子节点的变化
children: [
{
type: 'UPDATE',
index: 0, // 第一个li
patches: {
props: {
class: { oldValue: 'todo-item', newValue: 'todo-item ***pleted' }
},
children: [
{ type: 'UPDATE_TEXT', value: 'Buy milk and eggs' }
]
}
},
{
type: 'REMOVE',
index: 1 // 第二个li被删除
},
{
type: 'MOVE',
from: 2, // 原来的第三个li
to: 1 // 移动到第二个位置
}
]
};
Patch过程:将差异应用到真实DOM
有了差异报告后,开始执行具体的DOM操作:
function applyPatch(realDOM, patches) {
const ulElement = realDOM; // 真实的ul元素
// 1. 更新ul的属性
if (patches.props) {
ulElement.className = 'todo-list updated';
}
// 2. 处理子节点的变化
patches.children.forEach(change => {
switch (change.type) {
case 'UPDATE':
// 更新第一个li
const firstLi = ulElement.children[0];
firstLi.className = 'todo-item ***pleted';
firstLi.textContent = 'Buy milk and eggs';
break;
case 'REMOVE':
// 删除第二个li
const secondLi = ulElement.children[1];
ulElement.removeChild(secondLi);
break;
case 'MOVE':
// 移动第三个li到第二个位置
const thirdLi = ulElement.children[1]; // 注意:删除第二个后,第三个变成了第二个
// 实际上这里可能不需要移动,因为删除后自然就到了正确位置
break;
}
});
}
好了回归正题继续说模版
我们知道vue是通过编译器把html转换成js的那么是在什么时候转换的呢
编译时机
-
开发环境 (Development)
时机:在构建时编译# 当你运行开发服务器时 npm run dev # 或 vue-cli-service serve # 或 vite过程:
-
你保存.vue文件
-
构建工具(Webpack/Vite)检测到文件变化
-
vue-loader/@vitejs/plugin-vue 立即编译模板为渲染函数
-
热重载更新浏览器
-
-
生产环境 (Production)
时机:在构建时预编译# 当你构建生产版本时 npm run build # 或 vue-cli-service build过程:
-
所有.vue文件被一次性编译为渲染函数
-
编译后的JavaScript代码被打包到最终文件
-
浏览器只收到纯JS,无需编译模板
-
使用的编译器
-
主要编译器
-
@vue/***piler-dom (Vue 3)
-
Vue 3的官方模板编译器
-
将模板编译为渲染函数
-
-
vue-template-***piler (Vue 2)
- Vue 2的官方模板编译器
构建工具集成
// webpack.config.js (Vue 2)
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader', // 使用vue-loader处理.vue文件
options: {
***piler: require('vue-template-***piler') // Vue 2编译器
}
}
]
}
}
// vite.config.js (Vue 3)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' // 使用Vite的Vue插件
export default defineConfig({
plugins: [vue()] // 内部使用@vue/***piler-dom
})
具体编译过程示例
-
开发阶段实时编译
<!-- 你写的Single File ***ponent --> <template> <div class="hello"> <h1>{{ message }}</h1> <button @click="count++">Click {{ count }}</button> </div> </template> <script> export default { data() { return { message: 'Hello Vue!', count: 0 } } } </script> -
被编译为:
// vue-loader / @vitejs/plugin-vue 编译后的结果 import { openBlock, createElementBlock, createElementVNode, toDisplayString } from 'vue' export default { data() { return { message: 'Hello Vue!', count: 0 } }, render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", { class: "hello" }, [ _createElementVNode("h1", null, _toDisplayString(_ctx.message), 1 /* TEXT */), _createElementVNode("button", { onClick: _cache[0] || (_cache[0] = ($event) => (_ctx.count++)) }, _toDisplayString(_ctx.count), 1 /* TEXT */) ])) } }
不同使用场景的编译方式
-
场景1: Single File ***ponents (.vue文件)
<template> <div>Hello {{ name }}</div> </template>编译方式: vue-loader 或 @vitejs/plugin-vue 在构建时编译
-
场景2: 内联模板字符串
// 不推荐 - 需要在运行时编译 const app = Vue.createApp({ template: `<div>Hello {{ name }}</div>`, data() { return { name: 'Vue' } } })编译方式: 浏览器中运行时编译(需要完整版Vue)
-
场景3: 直接使用渲染函数
// 推荐 - 无需编译,最高性能 const app = Vue.createApp({ render() { return Vue.h('div', `Hello ${this.name}`) }, data() { return { name: 'Vue' } } })编译方式: 无需编译
版本差异
-
Vue 2
// 需要区分运行时版和完整版 // 完整版:包含编译器,可以在运行时编译模板 // 运行时版:只包含运行时,需要预编译 -
Vue 3
// Vue 3 更模块化,但同样推荐预编译 // 生产环境默认使用不包含编译器的版本
79、声明式和命令式的区别
声明式:我们写html结构的代码,编译器会把html转换成js然后渲染界面
命令式:我们自己写js代码然后自己把创建的节点挂在到界面中实现渲染
80、vite里esbuild和rollup是什么?
esbuild:影响开发环境
rollup:影响生产环境