一、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) 与 CompositionAPI(组合式 API)
Options API
-
在Vue3之前,我们主要使用的是选项式API(Options API)。这种API的设计方式是基于对象的,我们将一个Vue实例的各个部分拆分成不同的选项,如data、methods、computed、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'); } }
CompositionAPI
-
组合式API是 Vue.js 3.x 中引入的新特性,旨在解决选项式API 在复杂组件中难以维护的问题。组合式API允许将组件的逻辑按照功能相关性放在一起,而不是按照选项分散组织。组合式API 的特点包括:
逻辑复用:可以将逻辑抽取为可复用的函数,更方便地在不同组件之间共享逻辑。 代码组织:将相关逻辑放在一起,使得组件更加清晰和易于维护。 更好的类型推断:由于函数可以提供更多信息,TypeScript 在使用 Composition 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、computed中的,若想改动一个需求,就需要分别修改:data、methods、computed,不便于维护和复用。
Composition 可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。
5、vue3 setup
在 Vue3 中,setup 函数是一个新引入的概念,它代替了之前版本中的 data、computed、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-component @click="func" :numb="numb"></my-component>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import myComponent from '@/component/myComponent.vue';
//此时注册的变量或方法可以直接在template中使用而不需要导出
const numb = ref(0);
let func = ()=>{
numb.value++;
}
</script>
setup语法糖中新增的api
- defineProps:子组件接收父组件中传来的props
- defineEmits:子组件调用父组件中的方法
- defineExpose:子组件暴露属性,可以在父组件中拿到
defineProps
父组件代码
<template>
<my-component @click="func" :numb="numb"></my-component>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import myComponent from '@/components/myComponent.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-component @addNumb="func" :numb="numb"></my-component>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import myComponent from '@/components/myComponent.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-comp ref="myComponent"></my-comp>
<button @click="onClickButton">获取子组件中暴露的值</button>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import myComp from '@/components/myComponent.vue';
//注册ref,获取组件
const myComponent = ref();
function onClickButton(){
//在组件的value属性中获取暴露的值
console.log(myComponent.value.numb) //0
}
//注意:在生命周期中使用或事件中使用都可以获取到值,
//但在setup中立即使用为undefined
console.log(myComponent.value.numb) //undefined
const init = ()=>{
console.log(myComponent.value.numb) //undefined
}
init()
onMounted(()=>{
console.log(myComponent.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 '@/components/MyButton.vue'
import MyModal from '@/components/MyModal.vue'
const app = createApp(App)
// 全局注册组件
app.component('MyButton', MyButton)
app.component('MyModal', MyModal)
app.mount('#app')
- 特点:
全局可用,任何模板中直接使用 标签
适合基础组件,但可能导致打包体积冗余
局部注册(Local Registration)
在单个 .vue 文件中按需引入,适用于低频或专用组件。
1.使用 Options API(传统写法)
<!-- ParentComponent.vue -->
<template>
<ChildComponent />
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent } // 局部注册
}
</script>
2.使用 <script setup>
语法糖(推荐)
<!-- ParentComponent.vue -->
<template>
<ChildComponent />
</template>
<script setup>
// 直接导入即可使用,无需显式注册
import ChildComponent from './ChildComponent.vue'
</script>
- 特点:
组件仅在当前文件中可用
避免全局污染,更利于 Tree-shaking 优化
自动注册组件(Auto Registration)
1. 使用 Vite 的 Glob 导入(推荐)
动态扫描 components 目录下的所有 .vue 文件,批量全局注册。
// src/components/auto-register.ts
import { App } from 'vue'
export default {
install(app: App) {
// 匹配 components 目录下所有 .vue 文件
const modules = import.meta.glob('@/components/**/*.vue', { eager: true })
Object.entries(modules).forEach(([path, module]) => {
// 从文件路径提取组件名(如 MyButton.vue -> MyButton)
const name = path.split('/').pop()?.replace('.vue', '') || ''
app.component(name, (module as any).default)
})
}
}
// main.ts 中调用
import autoRegister from '@/components/auto-register'
app.use(autoRegister)
命名规则:
MyButton.vue
→ <my-button>
(推荐小写短横线命名)
强制命名规范:可在注册逻辑中添加 PascalCase 转换
2. 使用 unplugin-vue-components 插件(按需自动注册)
通过插件自动识别模板中的组件并动态导入(类似 Uniapp 的 Easycom)。
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
export default defineConfig({
plugins: [
Components({
// 指定扫描目录(默认 src/components)
dirs: ['src/components'],
// 生成类型声明文件(支持TS)
dts: 'src/components.d.ts'
})
]
})
- 特点:
无需手动导入,直接在模板中使用<MyComponent>
自动生成类型声明,完美支持 TypeScript
最佳实践选择
场景 | 推荐方案 |
---|---|
高频基础组件(如按钮、输入框) | 全局手动注册 或 unplugin 插件 |
低频专用组件 | 局部注册 + <script setup> |
UI 库组件(如 Element Plus) | unplugin 插件 + 按需导入 |
旧项目迁移 | Vite Glob 自动注册 |
8、Vue2 和 Vue3 的区别
- 响应式原理:Vue2 使用 Object.defineProperty,Vue3 改用 Proxy(支持数组和深层对象监听)。
- API 设计:Vue3 引入 Composition 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 和 computed 有什么区别?
- computed:
计算属性: computed是用于创建计算属性的方式,它依赖于Vue的响应式系统来进行数据追踪。当依赖的数据发生变化时,计算属性会自动重新计算,而且只在必要时才重新计算。
缓存: 计算属性具有缓存机制,只有在它依赖的数据发生变化时,计算属性才会重新计算。这意味着多次访问同一个计算属性会返回相同的结果,而不会重复计算。
无副作用: 计算属性应当是无副作用的,它们只是基于数据的计算,并不会修改数据本身。
用于模板中: 计算属性通常用于模板中,以便在模板中显示派生数据。
必须同步:只对同步代码中的依赖响应。
- watch:
监听数据: watch用于监视数据的变化,你可以监视一个或多个数据的变化,以执行自定义的响应操作。
副作用操作: watch中的回调函数可以执行副作用操作,例如发送网络请求、手动操作DOM,或执行其他需要的逻辑。
不缓存: watch中的回调函数会在依赖数据变化时立即被触发,不会像computed那样具有缓存机制。
用于监听数据变化: watch通常用于监听数据的变化,而不是用于在模板中显示数据。
支持异步:在检测数据变化后,可进行同步或异步操作。
我自己的理解watch 当数据变化后需要触发外部动作(如接口请求、DOM 操作)或处理异步任务时,而computed 当需要实时同步计算且结果需直接显示时(如购物车总价、表单验证)
14、谈谈 computed 的机制,缓存了什么?
Vue.js 中的 computed
属性确实具有缓存机制,这个缓存机制实际上是指对计算属性的值进行了缓存。当你在模板中多次访问同一个计算属性时,Vue.js只会计算一次这个属性的值,然后将结果缓存起来,以后再次访问时会直接返回缓存的结果,而不会重新计算。
假设你有一个计算属性 fullName
,它依赖于 firstName
和 lastName
两个响应式数据。当你在模板中使用 {{ fullName }}
来显示全名时,Vue.js会自动建立依赖关系,并在 firstName
或lastName
发生变化时,自动更新fullName
的值,然后将新的值渲染到页面上。
我的理解如果 computed 所依赖的响应式数据(如 data 中的属性或其他 computed 属性)没有发生变化,则无论多少次访问该 computed 属性,直接返回上一次的缓存值,不会重新计算,如果依赖发生变化了那么就重新计算。
15、为什么 computed 不支持异步
这个是 vue 设计层面决定的,computed 的定义是,“依赖值改变computed值就会改变”,所以这里必须是同步的,否则就可能 “依赖值改变但computed值未改变了”,一旦computed 支持异步,computed 就违背定义了,会变得不稳定。相反,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 表示深度监听
}
}
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 新特性 —— Composition API 与 React.js 中 Hooks 的异同点
相似点:
-
核心目标一致:解决「逻辑复用」难题
- Vue Composition 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 统一处理副作用(比如订阅、调接口)。
说白了:以前散落在生命周期里的“搞事情”代码(如 componentDidMount),现在都能集中管理,一目了然。
-
代码组织:把「相关的东西」放一起
- Vue:在 setup() 里,可以把“用户登录”、“表单验证”等逻辑各自打包成块。
- React:在函数组件里,用多个 Hooks 把“计数”、“动画”等逻辑拆分成独立单元。
说白了:以前按选项(data、methods)分类,现在按功能分类,改代码不用上下乱跳。
一句话总结:
Composition API 和 React Hooks 就像麦当劳和肯德基,虽然做法不同(响应式 vs 状态驱动),但核心目标都是让开发者吃上更香(复用逻辑)、更爽(代码清晰)的汉堡(写代码)!
不同点:
-
响应式原理不同
-
Vue Composition API:
基于“响应式系统”,数据变化自动触发更新。
(比如用 ref、reactive 定义数据,修改时视图自动跟着变,不用手动触发。) -
React Hooks:
基于“状态 + 副作用”,数据变化需要手动触发重新渲染。
(比如用 useState 定义数据,改完数据后,React 会自动重新执行组件函数来更新视图,但依赖闭包,容易遇到“过期值”问题。)
-
-
代码组织逻辑不同
-
Vue Composition API:
在 setup 函数里,可以把相关逻辑的变量、方法、计算属性等写在一起,像一个乐高积木块。
(比如把用户登录的逻辑集中在一个 useAuth 函数里,清晰隔离。) -
React Hooks:
逻辑分散在多个 useState、useEffect 中,需要靠开发者自己拆分组合。
(比如一个功能可能要用到多个 useEffect,代码容易分散在不同位置。)
-
-
对生命周期的依赖不同
-
Vue Composition API:
生命周期钩子(如 onMounted)可以直接写在 setup 里,但更多时候不需要关心生命周期,因为响应式系统自动跟踪依赖。
(比如一个数据变了,用到它的视图会自动更新,不用手动监听。) -
React Hooks:
重度依赖 useEffect 来模拟生命周期(如组件挂载、更新、卸载),需要手动管理依赖数组。
(比如忘记写依赖项,可能导致闭包问题,拿到旧值。)
-
-
条件限制不同
-
Vue Composition API:
没有条件限制,可以在任何地方写逻辑。
(比如在 if 语句里定义 ref,完全没问题。) -
React Hooks:
必须遵守“不能在条件、循环、嵌套函数中调用 Hooks”的规则。
(比如在 if 里写 useState,React 会直接报错。)
-
-
复用逻辑的方式不同
-
Vue Composition API:
通过组合函数(如 useXXX())返回响应式数据和方法,直接使用即可。
(复用逻辑像拼积木,拿来就能用。) -
React Hooks:
通过自定义 Hook(如 useXXX())返回状态和方法,但每次调用 Hook 会创建独立的状态。
(复用逻辑时,每个组件实例的状态是隔离的。)
-
一句话总结
Vue Composition API 像自动挡汽车,响应式系统帮你处理依赖和更新,代码可以自由组织;
React Hooks 像手动挡汽车,灵活但需要自己管理状态更新和副作用,还要遵守严格的规则。
选哪个?看你是喜欢省心(Vue)还是追求极致控制(React)!
26、Vue-Router 3.x hash模式 与 history模式 的区别
Hash 模式(默认):利用 #号使用 hash 来模拟一个完整的 URL,如:http://xxx.com/#/path/to/route。
History 模式:利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法来完成 URL 跳转而无须重新加载页面。服务端增加一个覆盖所有情况的候选页面,如果 URL 匹配不到任何资源,则返回这个页面。
27、什么是虚拟dom
- 虚拟dom就是在初始渲染时生成了一个用JS 对象 创建虚拟 DOM,当有数据改变的时候又会生成新的虚拟 DOM 树,新旧dom通过Diff 算法进行对比,找出差异(可以通过设置key来提高对比速度减少无意义对比),对比完成后将新的内容一次提交更新真实dom避免频繁操作dom造成回流和重绘,浪费性能,还有就是如果dom树对比发现新旧节点的标签类型或组件类型不同时就会变成直接销毁旧子树(解除旧节点的 事件监听 和 数据绑定、递归移除旧子树对应的 真实 DOM 节点、触发组件 生命周期钩子),新树替换旧树根据新虚拟 DOM 节点递归生成 真实 DOM 节点设置新节点的 属性(如 class、style)和 事件监听(如 addEventListener),由于出现销毁和重新创建所以会造成高额的开销,也会触发回流和重绘
- Vue3 的虚拟 DOM 通过 算法优化(双端 Diff、Patch Flag)、静态提升、事件侦听器缓存 等策略,显著降低了渲染开销和内存占用,同时结合 Fragments 支持 和 Tree Shaking 优化了开发体验。这些改进使得 Vue3 在复杂应用场景下(如动态列表、高频交互)的渲染性能远超 Vue2
- vue2和react的虚拟dom是一样的
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 Components
步骤 1:创建 React Web Component 封装器
使用 @lit/react 或自定义封装方法将 React 组件转换为 Web Component。
# 创建 React 项目(如果尚未创建)
npx create-react-app my-react-component --template typescript
cd my-react-component
npm install @webcomponents/webcomponentsjs @lit/react
// src/ReactCounter.tsx
import React, { useState } from 'react'
import { createComponent } 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 = createComponent({
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 Component
<!-- VueComponent.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 Component 加载完成
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 实现开箱即用的类型推断,同时依托 Composition API 实现更直观的响应式状态管理。其模块化架构天然规避命名空间冲突,体积仅 1KB(远小于 Vuex 的 10KB),完美适配 Vue 3 的轻量化与高效渲染特性,成为现代 Vue 应用开发的首选方案。
32、proxy能监听基础类型吗?
- 不能。Proxy 无法直接监听基本类型(如数字、字符串、布尔值),这是由 JavaScript 语言本身的特性决定的。
二、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),让开发者不用再手动处理每一步细节。
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 Component)和非受控组件(Uncontrolled Component)的区别?
- 受控组件:表单数据由 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、componentDidMount等方法,用于初始化组件、渲染到真实DOM和处理副作用。
- 更新阶段包括shouldComponentUpdate、render、componentDidUpdate等方法,用于控制组件的重新渲染和处理更新后的副作用。
- 卸载阶段包括componentWillUnmount方法,用于清理组件产生的副作用和资源。
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 { combineReducers, 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 = combineReducers({
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 zhCN from 'antd/locale/zh_CN';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
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-cn');
// 全局配置 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={zhCN}>
<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.Component { 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(() => computeExpensiveValue(a, b), [a, b]);
useMemo 接收两个参数一个函数和一个依赖项数组,当依赖项发生变化时,useMemo会重新执行该函数并返回新的计算结果,没有发生变化则返回上一次的计算结果,所以useMemo也有缓存机制,useMemo类似vue的computed。
-
-
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 Component),父组件可通过 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 renderComponent() {
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 Component({ 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 Component({ 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 Component() {
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() 和 componentDidCatch() 捕获子组件的错误(仅类组件)。
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 Components封装Vue组件
步骤1:将Vue组件转换为自定义元素
在Vue项目中,使用defineCustomElement将组件包装成自定义元素。
// MyVueComponent.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(MyVueComponent)
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/MyVueComponent.vue',
formats: ['es']
}
}
})
运行构建命令:
vite build
将生成的JS文件(如dist/MyVueComponent.js)复制到React项目的public目录。
步骤3:在React中引入自定义元素
在React入口文件(如index.js)中导入JS文件:
// index.js
import './public/MyVueComponent.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.component 方法进行全局注册。
- 页面组件:每个页面都有自己的组件,用于描述页面的结构和交互。页面组件只在当前页面有效,不能在其他页面中直接使用,但可以通过组件引用的方式进行复用。
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.com/data',
method: 'GET',
success: (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中如何使用组件?
可以在页面中引入组件,并在components属性中注册组件,然后在页面中使用。
<template>
<view>
<my-component></my-component>
</view>
</template>
<script>
import myComponent from '@/components/myComponent.vue';
export default {
components: {
myComponent
}
};
</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({
success: (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',
success: (res) => {
console.log(res);
},
fail: (err) => {
console.error(err);
}
});
13、uniApp中如何进行音频的播放和控制?
可以使用uni.createInnerAudioContext方法创建音频实例,通过调用实例的方法来实现音频的播放和控制。
// 创建音频实例
const audio = uni.createInnerAudioContext();
// 设置音频资源
audio.src = 'http://example.com/audio.mp3';
// 播放音频
audio.play();
// 暂停音频
audio.pause();
// 停止音频
audio.stop();
14、uniApp中如何进行图片的懒加载?
可以使用uni.lazyLoadImage组件实现图片的懒加载,将图片的src属性设置为需要加载的图片地址。
<template>
<view>
<uni-lazy-load-image src="http://example.com/image.jpg"></uni-lazy-load-image>
</view>
</template>
<script>
export default {
components: {
'uni-lazy-load-image': '@/components/uniLazyLoadImage.vue'
}
};
</script>
15、uniApp中如何获取设备信息?
可以使用uni.getSystemInfo方法获取设备信息,包括设备型号、操作系统版本等。
uni.getSystemInfo({
success: (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.com/image1.jpg', 'http://example.com/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中选择“发行-云打包”,输入包名(反向域名格式,如com.company.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.com/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、闭包
闭包是指引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的,也有一些非嵌套函数中的使用(块级作用域、回调函数)
用途:封装私有变量、防抖节流等。
风险:闭包会使得函数内部的变量在函数执行后仍然缓存于内存中,直到没有任何引用指向闭包。如果不注意管理闭包,可能会导致内存泄漏问题。
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 仍缓存于内存中。
效果:通过闭包隔离作用域,实现类似私有缓存的机制,避免全局变量污染
解决内存泄漏:
2、原型链
- 原型链的本质是对象间通过
__proto__
属性串联形成的链式关系。例如,若A继承B,则A的实例会通过A.prototype.__proto__
链接到B.prototype
,A实例 →A.prototype
→B.prototype
→Object.prototype
→null
的链条 prototype
:函数对象(构造函数)特有属性,每个函数对象都有一个prototype
属性,它是一个对象。通常用于定义共享的属性和方法,可以被构造函数创建的实例对象所继承。可以在构造函数的 prototype 上定义方法,以便多个实例对象共享这些方法, 从而节省内存。主要用于原型继承,它是构造函数和实例对象之间的链接,用于共享方法和属性。 __proto__
: 每个对象(包括函数对象和普通对象)都具有的属性,它指向对象的原型,也就是它的父对象。用于实现原型链,当你访问一个对象的属性时,如果对象本身没有这个属性,JavaScript 引擎会沿着原型链(通过__proto__
属性)向上查找, 直到找到属性或到达原型链的顶部(通常是Object.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
关联到原型链中。
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
是 JavaScript 中处理异步操作的对象,代表一个未完成但未来会完成的操作。它有三种状态:pending(等待)
、fulfilled(成功)
、rejected(失败
)。通过 .then()
处理成功结果,.catch()
捕获异常,.finally()
执行最终逻辑。Promise
解决了回调地狱问题,支持链式调用(then().then()...)
,使异步代码更清晰。配合 async/await
语法可进一步简化异步流程,是 ES6 后处理异步任务的核心方案。
20、promise 和 await/async 的关系
async/await是Promise的语法糖 ,它是 ES8)引入的特性,简化异步代码的编写和理解。async 函数返回一个Promise,允许在函数内使用 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 #ccc;
}
.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属性,也不能作为构造函数
六、CSS
1、CSS 中选择器的优先级,权重计算方式
内联样式(1000)、id(100)、class(10)、元素选择器、(1)伪元素(1)
2、响应式布局
- 使用媒体查询(Media Queries)
- 流式布局百分比宽度
- 弹性布局 Flex 布局
3、重绘和回流
重绘(Repaint) 和 回流(Reflow,又称重排) 是浏览器渲染引擎更新页面时的关键步骤,直接影响网页性能。
重绘:改变颜色阴影等,不会改变页面的大小,消耗资源小
回流:改变布局、宽高、字体大小会改变页面大小,会导致浏览器重新计算布局,消耗资源大
触发了回流一定会触发重绘
4、浏览器渲染流程
回流(Reflow) → 重绘(Repaint) → 合成(Composite)
回流会触发完整的渲染流水线:计算样式(Style) → 布局(Layout) → 绘制(Paint) → 合成(Composite)
重绘跳过布局阶段:计算样式(Style) → 绘制(Paint) → 合成(Composite)
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 Network,内容分发网络)是一种分布式服务器网络,通过以下机制加速内容分发:
- 就近访问:将资源缓存到全球多个边缘节点,用户从最近的节点获取内容(降低延迟)。
- 负载均衡:智能分配请求到最优节点,避免单点过载。
- 安全防护:防御 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: 'fibonacci', 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 'fibonacci': result = fibonacci(num); break; // 可扩展其他计算类型 } self.postMessage(result); }; function fibonacci(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 的 Network 面板查看协议列(Protocol),显示 h2 表示成功。
- 命令行工具:执行 curl -I --http2 https://yourdomain.com,响应头包含 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("Network request sent"); // [DEBUG] Network 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: '****' }, component: () => import(/* webpackChunkName: "about" */ '../views/Login_old.vue') }
-
第三方组件按需加载
- 我使用的是element plus所以用一下方式进行按需加载
-
安装unplugin-auto-import和unplugin-vue-components
npm install -D unplugin-vue-components unplugin-auto-import
-
配置vue.config.js
const { defineConfig } = require("@vue/cli-service"); const AutoImport = require("unplugin-auto-import/webpack"); // 导入webpack版的unplugin-auto-import const Components = require("unplugin-vue-components/webpack"); // 导入webpack版的unplugin-vue-components const { ElementPlusResolver } = require("unplugin-vue-components/resolvers"); // 导入elementplus组件解析器 module.exports = defineConfig({ transpileDependencies: true, configureWebpack: { // 这个节点用于配置webpack plugins: [ // 这个节点要放在configureWebpack下,否则会报错 Components.default({ resolvers: [ElementPlusResolver()], // 指定unplugin-vue-components的组件解析器为elementplus解析器 }), AutoImport.default({ resolvers: [ElementPlusResolver()], // 指定unplugin-auto-import的组件解析器为elementplus解析器 }), ], }, });
-
组件中直接使用,无需import和component 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="success" disabled>Success</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-component -D
npm install babel-plugin-component -D
-
babel.config.js
module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ], plugins: [ [ 'component', { 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 CompressionWebpackPlugin = require("compression-webpack-plugin") const productionGzipExtensions = ["js", "css"] module.exports = { configureWebpack: (config) => { const plugins = [ // 移除 moment.js 语言包(关键优化) new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ }), // Gzip 压缩配置 new CompressionWebpackPlugin({ 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 }, common: { chunks: 'all', test: /[\\/]src[\\/]js[\\/]/, name: 'common', 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: {}, compress: { 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、页面首次加载出现白屏如何解决?
1. 网络请求问题
原因:资源(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">
2. 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(); } });
3. 渲染阻塞
原因:CSS/JS文件阻塞DOM构建。
解决方案:
- 内联关键CSS:
<style> /* 首屏必要样式 */ .header { height: 60px; } </style>
- 异步非关键CSS:
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
4. 路由与代码分割问题(SPA)
原因:动态路由加载失败。
解决方案:
- React错误边界:
import { ErrorBoundary } from 'react-error-boundary'; <ErrorBoundary FallbackComponent={ErrorFallback}> <RouteComponent /> </ErrorBoundary>
- Webpack动态导入:
const Home = () => import(/* webpackChunkName: "home" */ './Home.vue');
5. 缓存策略不当
解决方案:
# Nginx 配置示例
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}