Vue 2 和 Vue 3 在多个方面存在区别,以下从架构设计、语法与 API、性能、生态系统等方面进行详细介绍:
Object.defineProperty()
实现响应式。这种方式有一定局限性,例如无法检测对象属性的添加和删除,对于数组,部分方法(如通过索引修改元素)也不能触发响应式更新。Vue.extend()
或单文件组件(SFC)来定义组件,通过 export default
导出一个包含各种选项的对象。
语法糖来简化组件的定义,减少样板代码。
{{ message }}
{{ message }}
beforeCreate
、created
、beforeMount
、mounted
、beforeUpdate
、updated
、beforeDestroy
、destroyed
等生命周期钩子。beforeDestroy
改为 beforeUnmount
,destroyed
改为 unmounted
,并且在组合式 API 中可以使用 onBeforeMount
、onMounted
等函数来注册生命周期钩子。// Vue 2 生命周期钩子
export default {
created() {
console.log('Vue 2: Component created');
}
};
// Vue 3 组合式 API 生命周期钩子
import { onMounted } from 'vue';
export default {
setup() {
onMounted(() => {
console.log('Vue 3: Component mounted');
});
}
};
data
选项中定义响应式数据,使用 this
来访问。ref()
和 reactive()
函数来创建响应式数据。ref()
用于创建单个值的响应式数据,reactive()
用于创建对象的响应式数据。// Vue 2 响应式数据定义
export default {
data() {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
}
}
};
// Vue 3 响应式数据定义
import { ref } from 'vue';
export default {
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
return {
count,
increment
};
}
};
在 React 里,性能优化是一个关键环节,下面为你介绍几种常见的优化方法:
React.memo
进行组件记忆React.memo
是一个高阶组件,它能够对组件的 props 进行浅比较。若 props 没有变化,就不会重新渲染组件。
import React from 'react';
const MyComponent = React.memo((props) => {
return {props.message};
});
export default MyComponent;
useMemo
缓存计算结果useMemo
能够缓存计算结果,避免在每次渲染时都进行重复计算。
import React, { useMemo } from 'react';
const MyComponent = (props) => {
const expensiveValue = useMemo(() => {
// 进行复杂计算
return props.num1 + props.num2;
}, [props.num1, props.num2]);
return {expensiveValue};
};
export default MyComponent;
useCallback
缓存函数useCallback
可缓存函数,防止在每次渲染时都创建新的函数实例。
import React, { useCallback } from 'react';
const MyComponent = (props) => {
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
return ;
};
export default MyComponent;
使用 React.lazy 和 Suspense 进行代码分割,这样可以按需加载组件,从而减少初始加载时间。
import React, { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
const App = () => {
return (
Loading... }>
确保状态更新仅在必要时进行,防止组件不必要的重新渲染。
import React, { useState } from 'react';
const MyComponent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
// 仅在满足条件时更新状态
if (count < 10) {
setCount(count + 1);
}
};
return (
Count: {count}
);
};
export default MyComponent;
这些方法能够有效地优化 React 应用的性能,不过在实际应用中,你需要依据具体情况选择合适的优化策略。
在 React 里,组件传值是一项基础且重要的操作。下面为你详细介绍几种常见的组件传值方式:
父组件可通过 props
向子组件传递数据。
// 子组件
const ChildComponent = (props) => {
return {props.message};
};
// 父组件
const ParentComponent = () => {
const message = "Hello from parent";
return ;
};
子组件向父组件传值通常借助回调函数来实现。父组件把一个函数作为 props
传递给子组件,子组件调用该函数并传递数据。
// 子组件
const ChildComponent = (props) => {
const handleClick = () => {
props.onClick("Hello from child");
};
return ;
};
// 父组件
const ParentComponent = () => {
const handleChildMessage = (message) => {
console.log(message);
};
return ;
};
当需要在多个层级的组件间传递数据时,可使用 React 的 Context API
。
import React, { createContext, useContext, useState } from 'react';
// 创建一个 Context
const MyContext = createContext();
// 子组件
const GrandChildComponent = () => {
const contextValue = useContext(MyContext);
return {contextValue};
};
const ChildComponent = () => {
return ;
};
// 父组件
const ParentComponent = () => {
const [message, setMessage] = useState("Hello from context");
return (
);
};
对于复杂的应用场景,可使用第三方状态管理库来实现组件间的数据共享。以 Redux 为例:
// 安装依赖
// npm install redux react-redux
import React from 'react';
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
// 定义 reducer
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
};
// 创建 store
const store = createStore(counterReducer);
// 子组件
const CounterComponent = () => {
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
return (
Count: {count}
);
};
// 父组件
const App = () => {
return (
);
};
上述是 React 中常见的组件传值方式,你可根据具体的应用场景来选择合适的传值方法。
在 Vue 2 中,v-for
的优先级高于 v-if
。这意味着:
v-for
会先执行,然后 v-if
会在每次迭代中运行
<div v-for="item in items" v-if="item.isActive">
{{ item.name }}
div>
上面的代码在 Vue 2 中相当于:
this.items.map(function(item) {
if (item.isActive) {
return item.name
}
})
在 Vue 3 中,v-if
的优先级高于 v-for
。这意味着:
v-if
会先执行v-if
的条件不成立,v-for
就不会执行
<div v-for="item in items" v-if="item.isActive">
{{ item.name }}
div>
上面的代码在 Vue 3 中会抛出错误,因为 v-if
先执行时会尝试访问 item
,但此时 item
还未通过 v-for
定义。
对于 Vue 2 和 Vue 3,官方都建议避免在同一个元素上同时使用 v-for
和 v-if
。替代方案:
<div v-for="item in activeItems">
{{ item.name }}
div>
computed: {
activeItems() {
return this.items.filter(item => item.isActive)
}
}
v-if
移到外层元素或
标签<template v-for="item in items">
<div v-if="item.isActive">
{{ item.name }}
div>
template>
v-if
<div v-if="items.length > 0">
<div v-for="item in items" :key="item.id">
{{ item.name }}
div>
div>
v-for
> v-if
(v-for
优先级更高)v-if
> v-for
(v-if
优先级更高)在 React 中处理多个接口请求并将数据合并渲染到一个列表,有几种常见的处理方法:
import React, { useState, useEffect } from 'react';
function CombinedList() {
const [combinedData, setCombinedData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
// 并行发起多个请求
const [response1, response2] = await Promise.all([
fetch('https://api.example.com/data1'),
fetch('https://api.example.com/data2')
]);
const data1 = await response1.json();
const data2 = await response2.json();
// 合并数据
const mergedData = [...data1, ...data2];
setCombinedData(mergedData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return Loading...;
if (error) return Error: {error};
return (
{combinedData.map((item, index) => (
- {item.name}
))}
);
}
useEffect(() => {
const fetchSequentially = async () => {
try {
setLoading(true);
const response1 = await fetch('https://api.example.com/data1');
const data1 = await response1.json();
const response2 = await fetch('https://api.example.com/data2');
const data2 = await response2.json();
setCombinedData([...data1, ...data2]);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchSequentially();
}, []);
function useCombinedData() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const urls = [
'https://api.example.com/data1',
'https://api.example.com/data2'
];
const responses = await Promise.all(urls.map(url => fetch(url)));
const jsonData = await Promise.all(responses.map(res => res.json()));
setData(jsonData.flat());
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
return { data, loading, error };
}
function CombinedList() {
const { data, loading, error } = useCombinedData();
if (loading) return Loading...;
if (error) return Error: {error};
return (
{data.map((item, index) => (
- {item.name}
))}
);
}
import { useQuery } from 'react-query';
function CombinedList() {
const fetchData1 = async () => {
const res = await fetch('https://api.example.com/data1');
return res.json();
};
const fetchData2 = async () => {
const res = await fetch('https://api.example.com/data2');
return res.json();
};
const { data: data1 } = useQuery('data1', fetchData1);
const { data: data2 } = useQuery('data2', fetchData2);
const combinedData = [...(data1 || []), ...(data2 || [])];
return (
{combinedData.map((item, index) => (
- {item.name}
))}
);
}
选择哪种方法取决于你的具体需求、项目规模和团队偏好。对于简单应用,Promise.all 就足够了;对于复杂应用,考虑使用 React Query 或类似的状态管理库。
在 React 中渲染百万条数据是一个巨大的性能挑战,直接渲染会导致页面卡顿甚至崩溃。以下是几种高效的解决方案:
原理:只渲染可视区域内的数据项,动态替换内容
import { FixedSizeList as List } from 'react-window';
function BigList({ data }) {
return (
{({ index, style }) => (
{data[index].content}
)}
);
}
原理:分批加载和渲染数据
function PaginatedList({ totalItems }) {
const [page, setPage] = useState(1);
const [data, setData] = useState([]);
const itemsPerPage = 50;
useEffect(() => {
fetchData(page);
}, [page]);
const fetchData = async (pageNum) => {
const response = await fetch(
`https://api.example.com/data?page=${pageNum}&limit=${itemsPerPage}`
);
const newData = await response.json();
setData(newData);
};
return (
{data.map(item => (
- {item.content}
))}
第 {page} 页
);
}
原理:滚动到底部时自动加载更多数据
import { useState, useEffect, useRef } from 'react';
function InfiniteList() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const loaderRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: 1.0 }
);
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
return () => observer.disconnect();
}, []);
const loadMore = async () => {
if (loading) return;
setLoading(true);
const response = await fetch(
`https://api.example.com/data?page=${page}&limit=20`
);
const newData = await response.json();
setData(prev => [...prev, ...newData]);
setPage(prev => prev + 1);
setLoading(false);
};
return (
{data.map(item => (
- {item.content}
))}
{loading && 加载中...
}
);
}
原理:将数据处理移到后台线程
// worker.js
self.onmessage = function(e) {
const { data, startIndex, endIndex } = e.data;
const slicedData = data.slice(startIndex, endIndex);
postMessage(slicedData);
};
// React 组件
function WorkerList({ hugeData }) {
const [visibleData, setVisibleData] = useState([]);
const workerRef = useRef(null);
useEffect(() => {
workerRef.current = new Worker('worker.js');
workerRef.current.onmessage = (e) => {
setVisibleData(e.data);
};
return () => {
workerRef.current.terminate();
};
}, []);
const updateVisibleData = (start, end) => {
workerRef.current.postMessage({
data: hugeData,
startIndex: start,
endIndex: end
});
};
// 结合虚拟滚动使用
return ;
}
原理:将渲染任务分成小块执行
function TimeSlicedList({ hugeData }) {
const [visibleData, setVisibleData] = useState([]);
const [renderedCount, setRenderedCount] = useState(0);
const batchSize = 100;
useEffect(() => {
const renderBatch = (start) => {
requestIdleCallback(() => {
const end = Math.min(start + batchSize, hugeData.length);
setVisibleData(prev => [...prev, ...hugeData.slice(start, end)]);
setRenderedCount(end);
if (end < hugeData.length) {
renderBatch(end);
}
});
};
renderBatch(0);
}, [hugeData]);
return (
{visibleData.map(item => (
- {item.content}
))}
{renderedCount < hugeData.length && - 加载中...
}
);
}
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
虚拟滚动 | 需要一次性展示大量数据 | 高性能,平滑滚动 | 需要固定高度 |
分页加载 | 用户需要明确控制浏览 | 实现简单,内存友好 | 需要用户交互 |
无限滚动 | 内容连续浏览体验 | 无缝体验 | 难以跳转到特定位置 |
Web Worker | 复杂数据处理 | 不阻塞UI线程 | 实现复杂度高 |
时间分片 | 初始加载优化 | 避免长时间阻塞 | 加载过程可见 |
根据你的具体需求选择合适的方案,对于百万级数据,通常推荐虚拟滚动或分页加载的组合方案。
Vue 2 采用 Object.defineProperty 实现数据劫持:
function defineReactive(obj, key) {
let value = obj[key]
const dep = new Dep() // 每个属性一个依赖收集器
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) { // Watcher触发get时
dep.depend() // 依赖收集
}
return value
},
set(newVal) {
if (newVal === value) return
value = newVal
dep.notify() // 通知所有Watcher更新
}
})
}
递归劫持:
defineReactive
数组处理:
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
this.__ob__.dep.notify() // 手动触发通知
return result
})
})
依赖管理:
Vue.set
/Vue.delete
)Vue 3 采用 Proxy 实现数据代理:
function reactive(target) {
const handler = {
get(target, key, receiver) {
track(target, key) // 依赖收集
const res = Reflect.get(target, key, receiver)
if (isObject(res)) {
return reactive(res) // 惰性递归
}
return res
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key) // 触发更新
}
return result
},
deleteProperty(target, key) {
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey) {
trigger(target, key)
}
return result
}
}
return new Proxy(target, handler)
}
全面拦截能力:
has
、ownKeys
等)惰性响应式:
const obj = reactive({
a: { b: 1 } // 只有访问到a.b时才转换b为响应式
})
依赖收集优化:
WeakMap
存储依赖关系const targetMap = new WeakMap() // 结构:target -> key -> dep
Ref API设计:
function ref(value) {
return {
get value() {
track(this, 'value')
return value
},
set value(newVal) {
value = newVal
trigger(this, 'value')
}
}
}
操作类型 | Vue 2 (ms) | Vue 3 (ms) | 提升幅度 |
---|---|---|---|
10k对象初始化 | 120 | 45 | 62.5% |
嵌套属性访问 | 8 | 3 | 62.5% |
数组操作 | 15 | 5 | 66.7% |
内存占用 | 较高 | 降低30% | - |
代理粒度:
触发时机:
// Vue 2
this.items[0] = 'new' // 不会触发
this.items.length = 0 // 不会触发
// Vue 3
state.items[0] = 'new' // 触发
state.items.length = 0 // 触发
API设计哲学:
Vue 2项目优化:
Object.freeze()
避免不必要的响应式this.$nextTick
Vue 3项目技巧:
shallowRef
避免深层响应式markRaw
跳过不需要响应式的对象toRefs
保持结构响应式迁移注意事项:
Vue.set
/Vue.delete
调用customRef
实现复杂响应式逻辑Vue 3 还提供了更高级的响应式控制:
// 自定义Ref
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}
// 只读代理
const readonlyObj = readonly(reactiveObj)
这种响应式系统的演进使得 Vue 3 能够更好地支持大型应用开发,同时保持了更好的性能表现和开发体验。
在面试中回答微前端相关问题,可以按照以下思路清晰、全面地进行阐述:
/product
路径时,加载商品管理微前端应用。适用场景为应用功能模块划分清晰,可按路由区分不同业务模块的情况。script
标签加载JavaScript文件。适用于需要根据用户操作或业务需求动态展示不同功能模块的场景。介绍微前端应用之间常见的通信机制,如:
localStorage
或 sessionStorage
存储数据,供不同微前端应用访问。但要注意数据的有效期和安全性。提及对微前端未来发展趋势的理解,如与微服务架构的深度融合、在低代码/无代码开发中的应用等。表达自己对微前端的看法,强调其在现代前端开发中的重要性和发展潜力,同时也指出需要关注的问题,如安全性、标准化等。
面试官您好,微前端是一种创新的前端架构风格,它把前端应用拆分成多个小型、自治的应用,能独立开发、部署,最后集成成完整的大型应用。这种架构有很多优势,技术栈无关让不同团队能根据需求选择合适技术,可维护性高使代码结构清晰,独立部署提升了开发和部署效率,团队自治也提高了团队的自主性。
实现微前端有几种常见方式。路由分发式通过主应用的路由系统,根据URL路径分发请求,适用于功能模块划分清晰的应用;微内核式以主应用为核心加载和管理插件式的微前端应用,适合灵活扩展功能的场景;构建时集成在构建阶段用工具合并代码,性能较好但灵活性稍差;运行时集成在运行时动态加载代码,灵活度高但有性能开销。
微前端应用间的通信机制也有多种。事件总线是全局的消息传递方式,URL参数可传递简单数据,Web Storage能存储数据供不同应用访问,postMessage用于跨域通信。
我之前参与过一个企业级微前端项目,项目目标是整合多个业务系统。我们采用路由分发式,用Vue和React开发不同模块,通过事件总线通信。项目中遇到了样式冲突和性能问题,我们通过CSS模块化解决样式问题,用代码分割和懒加载提升性能。
我认为微前端未来会和微服务架构深度融合,在低代码/无代码开发中也会有更多应用。它在现代前端开发中非常重要,但也需要关注安全性和标准化等问题。
以上就是我对微前端的理解和相关经验,您有任何问题都可以问我。
Git 是目前最流行的分布式版本控制系统,以下是 Git 的核心概念和使用方法详解。
工作区 (Working Directory)
↓ add
暂存区 (Staging Area)
↓ commit
本地仓库 (Local Repository)
↓ push
远程仓库 (Remote Repository)
未跟踪 (untracked) → 已跟踪 (tracked)
↳ 未修改 (unmodified)
↳ 已修改 (modified)
↳ 已暂存 (staged)
# 初始化新仓库
git init
# 克隆现有仓库
git clone <url>
git clone -b <branch> <url> # 克隆特定分支
# 查看状态
git status
# 添加文件到暂存区
git add <file> # 添加特定文件
git add . # 添加所有更改
# 提交到本地仓库
git commit -m "提交信息"
git commit -am "信息" # 跳过add直接提交已跟踪文件
# 推送到远程
git push origin <branch>
# 查看分支
git branch # 本地分支
git branch -a # 所有分支(包括远程)
# 创建分支
git branch <new-branch>
git checkout -b <new-branch> # 创建并切换
# 切换分支
git checkout <branch>
git switch <branch> # 更安全的新方式
# 合并分支
git merge <branch>
# 删除分支
git branch -d <branch> # 安全删除
git branch -D <branch> # 强制删除
# 变基操作
git rebase <base-branch>
git rebase -i HEAD~3 # 交互式变基(修改最近3次提交)
# 查看远程
git remote -v
# 添加远程
git remote add <name> <url>
# 获取远程更新
git fetch <remote> # 只下载不合并
git pull # fetch + merge
git pull --rebase # fetch + rebase
# 推送分支
git push -u origin <branch> # 首次推送并建立追踪
git push --force-with-lease # 安全强制推送
git restore <file> # 撤销工作区修改
git checkout -- <file> # 旧方式(效果相同)
git restore --staged <file> # 从暂存区撤回
git reset HEAD <file> # 旧方式
git reset --soft HEAD~1 # 仅回退commit,保留更改在暂存区
git reset --mixed HEAD~1 # 默认,回退commit和暂存区,保留工作区更改
git reset --hard HEAD~1 # 彻底回退commit/stage/工作区
git revert <commit> # 创建新提交来撤销指定提交
git stash # 储藏当前工作
git stash list # 查看储藏列表
git stash apply # 应用最近储藏
git stash pop # 应用并删除储藏
git stash drop # 删除储藏
git submodule add <url> <path> # 添加子模块
git submodule update --init # 初始化子模块
git submodule update --remote # 更新子模块
git tag # 列出标签
git tag v1.0 # 创建轻量标签
git tag -a v1.0 -m "版本1.0" # 创建附注标签
git push origin v1.0 # 推送标签到远程
git config --global user.name "Your Name"
git config --global user.email "[email protected]"
git config --global core.editor "code --wait" # 设置VS Code为默认编辑器
# 常用别名
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
git hash-object -w <file> # 创建blob对象
git cat-file -p <hash> # 查看对象内容
git ls-files --stage # 查看暂存区内容
提交规范:
feat: 添加新功能
fix: 修复bug
docs: 文档更新
style: 代码格式
refactor: 代码重构
分支命名:
.gitignore 配置:
# 忽略node_modules
node_modules/
# 忽略IDE文件
.vscode/
.idea/
# 忽略日志文件
*.log
代码审查:
Git 的强大之处在于其灵活性和丰富的功能集,掌握这些核心概念和操作将使您能够高效地进行版本控制和团队协作。
git merge
和 git rebase
是 Git 中用于合并分支的两种主要方式,但它们的实现方式和结果不同。以下是它们的核心区别和使用场景:
git checkout main # 切换到目标分支(如 main)
git merge feature # 将 feature 分支合并到 main
git checkout feature # 切换到要变基的分支(如 feature)
git rebase main # 将 feature 的提交“嫁接”到 main 分支之后
对比项 | git merge | git rebase |
---|---|---|
历史记录 | 保留分支分叉和合并点 | 线性历史,隐藏分支操作 |
提交哈希 | 不改变原有提交的哈希 | 重写提交,生成新哈希 |
使用场景 | 保留完整历史(如团队协作分支) | 整理本地提交(如个人分支) |
冲突处理 | 只需解决一次合并冲突 | 可能需要多次解决冲突(逐提交变基) |
安全性 | 非破坏性操作 | 重写历史(需避免在公共分支使用) |
git merge
的情况main
或 develop
),保留完整协作历史。git rebase
的情况main
)历史的简洁性(避免多余的合并提交)。 A---B---C feature
/
D---E---F---G main
git merge
后 A---B---C
/ \
D---E---F---G---H # H 是合并提交
git rebase
后D---E---F---G---A'---B'---C' # 提交哈希改变(A'、B'、C')
rebase
:重写历史会导致其他协作者的本地仓库混乱。git rebase -i
):可合并、修改或重排提交(适合整理本地提交)。rebase
时需要按顺序解决每个提交的冲突,而 merge
只需解决一次。merge
:简单、安全,适合团队协作。rebase
:追求整洁历史,适合个人分支或本地整理。根据团队规范和项目需求选择合适的方式!
在 CSS 中实现元素的 垂直水平居中 是常见的布局需求,以下是多种方法及其适用场景:
.container {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
.container {
display: grid;
place-items: center; /* 同时水平和垂直居中 */
}
.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* 通过自身宽高反向偏移 */
}
.child {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto; /* 需指定子元素宽高 */
width: 100px;
height: 100px;
}
.container {
display: table-cell;
text-align: center; /* 水平居中 */
vertical-align: middle; /* 垂直居中 */
}
.child {
display: inline-block; /* 或 inline */
}
.container {
text-align: center; /* 水平居中 */
line-height: 300px; /* 垂直居中(需等于容器高度) */
}
.child {
display: inline-block;
line-height: normal; /* 重置子元素行高 */
}
绝对定位 + transform
或表格布局。绝对定位 + margin: auto
。根据实际需求和浏览器支持情况灵活选择!
在 React 中,没有直接提供类似 Vue 的
组件,但可以通过以下方式实现类似的功能,即在组件卸载时保留其状态,以便再次渲染时恢复。
react-activation
库(推荐)react-activation
是一个专门用于 React 的 KeepAlive 解决方案,使用简单且功能强大。
npm install react-activation
# 或
yarn add react-activation
包裹根组件// App.jsx
import { AliveScope } from 'react-activation';
function App() {
return (
} />
} />
);
}
包裹需要缓存的组件import { KeepAlive } from 'react-activation';
function Home() {
return (
Home Page
);
}
name
属性:用于唯一标识缓存组件,避免重复渲染。import { useAliveController } from 'react-activation';
function ClearCacheButton() {
const { drop } = useAliveController();
return (
);
}
display: none
)如果不想引入第三方库,可以手动实现一个简单的 KeepAlive 机制:
React.cloneElement
缓存子组件。display: none
隐藏组件,而不是卸载它。import React, { useState, Children, cloneElement } from 'react';
function KeepAlive({ children, isActive }) {
const [cachedChildren, setCachedChildren] = useState(null);
return (
{isActive
? (cachedChildren || children)
: (cachedChildren || setCachedChildren(Children.only(children)))}
);
}
// 使用方式
function App() {
const [showDetail, setShowDetail] = useState(true);
return (
);
}
缺点:
react-router
+ useOutlet
(React Router v6)如果使用 React Router v6,可以结合 useOutlet
实现路由级别的缓存:
import { useOutlet, useLocation } from 'react-router-dom';
import { useRef } from 'react';
export function KeepAliveOutlet() {
const { pathname } = useLocation();
const componentMap = useRef(new Map());
const outlet = useOutlet();
componentMap.current.set(pathname, outlet);
return Array.from(componentMap.current).map(([key, component]) => (
{component}
));
}
// 在路由中使用
}>
} />
}>
} />
优点:
缺点:
方法 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
react-activation |
任意组件缓存 | 功能强大,API 简单 | 需引入第三方库 |
手动 display: none |
简单状态缓存 | 无依赖 | 无法跨路由,性能较差 |
react-router + useOutlet |
路由级缓存 | 无额外依赖 | 实现较复杂 |
推荐:
react-activation
。react-router
缓存。在前端开发中,哈希路由和浏览器路由(History 路由)是实现单页面应用(SPA)路由功能的两种常见方式,它们在原理、URL 表现形式、兼容性、服务器配置等方面存在明显区别,以下为你详细介绍:
#
后面的部分)变化来实现路由切换。当哈希值发生改变时,浏览器不会向服务器发送新的请求,而是触发 hashchange
事件,前端框架监听这个事件,根据不同的哈希值渲染对应的页面组件。https://example.com/#/home
变为 https://example.com/#/about
时,前端框架会捕获到哈希值的变化,然后根据配置的路由规则渲染关于页面的组件。pushState
、replaceState
)来实现。通过 pushState
方法可以在不刷新页面的情况下向浏览器历史记录中添加一条新的记录,同时改变 URL;replaceState
方法则是替换当前的历史记录。popstate
事件,前端框架监听该事件,根据新的 URL 渲染对应的页面组件。#
符号,后面跟着具体的路由路径。例如,https://example.com/#/products/123
,其中 #/products/123
就是哈希路由的路径。#
之前的部分。#
符号。例如,https://example.com/products/123
,这种 URL 更简洁、美观,也更符合用户的使用习惯。hashchange
事件,不需要考虑浏览器的版本问题,即使是很旧的浏览器也能正常使用。index.html
)即可,因为哈希值的变化不会触发服务器的请求。无论用户访问的哈希路径是什么,服务器都不需要做特殊处理。index.html
,然后由前端框架根据 URL 来渲染相应的页面组件。const express = require('express');
const app = express();
// 静态文件服务
app.use(express.static(__dirname + '/public'));
// 处理所有路由请求,返回 index.html
app.get('*', function(req, res) {
res.sendFile(__dirname + '/public/index.html');
});
const port = process.env.PORT || 3000;
app.listen(port, function() {
console.log(`Server is running on port ${port}`);
});
pushState
和 replaceState
方法可以更灵活地管理浏览器的历史记录。pushState
会添加一条新的历史记录,而 replaceState
会替换当前的历史记录,这在一些需要精确控制历史记录的场景中非常有用。JavaScript 数组提供了许多内置方法,用于操作和处理数组数据。以下是常用的数组方法分类整理:
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
push(item1, item2...) |
末尾添加元素 | ✅ | 新长度 |
pop() |
删除末尾元素 | ✅ | 被删元素 |
unshift(item1, item2...) |
开头添加元素 | ✅ | 新长度 |
shift() |
删除开头元素 | ✅ | 被删元素 |
splice(start, deleteCount, ...items) |
在指定位置增删元素 | ✅ | 被删元素的数组 |
示例:
const arr = [1, 2, 3];
arr.push(4); // [1, 2, 3, 4]
arr.splice(1, 1, 'a'); // [1, 'a', 3, 4](删除索引1的元素并插入'a')
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
forEach(callback) |
遍历数组(无返回值) | ❌ | undefined |
map(callback) |
对每个元素执行函数并返回新数组 | ❌ | 新数组 |
filter(callback) |
返回符合条件的元素组成的新数组 | ❌ | 新数组 |
find(callback) |
返回第一个符合条件的元素 | ❌ | 元素或 undefined |
findIndex(callback) |
返回第一个符合条件的索引 | ❌ | 索引或 -1 |
some(callback) |
检查是否至少有一个元素符合条件 | ❌ | true /false |
every(callback) |
检查是否所有元素符合条件 | ❌ | true /false |
示例:
[1, 2, 3].map(x => x * 2); // [2, 4, 6]
[1, 2, 3].filter(x => x > 1); // [2, 3]
[1, 2, 3].find(x => x > 1); // 2
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
concat(arr1, arr2...) |
合并多个数组 | ❌ | 新数组 |
join(separator) |
将数组转为字符串(默认用 , 连接) |
❌ | 字符串 |
slice(start, end) |
截取部分数组(含头不含尾) | ❌ | 新数组 |
示例:
[1, 2].concat([3, 4]); // [1, 2, 3, 4]
['a', 'b'].join('-'); // "a-b"
[1, 2, 3, 4].slice(1, 3); // [2, 3]
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
sort(compareFunction?) |
排序(默认按 Unicode 排序) | ✅ | 排序后的原数组 |
reverse() |
反转数组顺序 | ✅ | 反转后的原数组 |
示例:
[3, 1, 2].sort((a, b) => a - b); // [1, 2, 3]
['a', 'b', 'c'].reverse(); // ['c', 'b', 'a']
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
reduce(callback, initialValue) |
从左到右累计计算 | ❌ | 累计值 |
reduceRight(callback, initialValue) |
从右到左累计计算 | ❌ | 累计值 |
示例:
[1, 2, 3].reduce((sum, x) => sum + x, 0); // 6
方法 | 描述 | 是否改变原数组 | 返回值 |
---|---|---|---|
includes(item) |
检查是否包含某元素 | ❌ | true /false |
indexOf(item) |
返回元素首次出现的索引 | ❌ | 索引或 -1 |
lastIndexOf(item) |
返回元素最后一次出现的索引 | ❌ | 索引或 -1 |
flat(depth) |
扁平化嵌套数组(默认 depth=1 ) |
❌ | 新数组 |
flatMap(callback) |
先 map 后 flat (深度为1) |
❌ | 新数组 |
Array.isArray(arr) |
判断是否为数组(静态方法) | ❌ | true /false |
示例:
[1, 2, 3].includes(2); // true
[1, [2, 3]].flat(); // [1, 2, 3]
Array.isArray([]); // true
方法 | 描述 |
---|---|
findLast(callback) |
返回最后一个符合条件的元素 |
findLastIndex(callback) |
返回最后一个符合条件的索引 |
at(index) |
支持负数索引(如 arr.at(-1) 获取末尾元素) |
toReversed() |
返回反转后的新数组(不改变原数组) |
toSorted() |
返回排序后的新数组(不改变原数组) |
push
、pop
、shift
、unshift
、splice
、sort
、reverse
。map
、filter
、concat
、slice
、flat
、flatMap
。find
、includes
、some
、every
。forEach
(无返回值)、map
(返回新数组)。根据需求选择合适的方法,优先使用 不改变原数组 的方法(如 map
、filter
)以保证代码的可预测性!
你是对的!如果对象的键是 Symbol
,前面的方法(如 Object.keys()
和 for...in
)无法检测到,因为它们只遍历 字符串键(可枚举属性)。
Symbol
键的空对象?Object.getOwnPropertySymbols()
+ Object.keys()
function isEmpty(obj) {
return (
Object.keys(obj).length === 0 &&
Object.getOwnPropertySymbols(obj).length === 0
);
}
// 测试
const sym = Symbol('key');
const obj1 = {};
const obj2 = { [sym]: 'value' };
console.log(isEmpty(obj1)); // true(无字符串键,无 Symbol 键)
console.log(isEmpty(obj2)); // false(有 Symbol 键)
优点:
Reflect.ownKeys()
(ES6+)function isEmpty(obj) {
return Reflect.ownKeys(obj).length === 0;
}
// 测试
const sym = Symbol('key');
const obj1 = {};
const obj2 = { [sym]: 'value' };
console.log(isEmpty(obj1)); // true
console.log(isEmpty(obj2)); // false
特点:
Reflect.ownKeys()
返回所有 自身属性键(包括字符串、Symbol、不可枚举属性)。Object.keys()
+ Object.getOwnPropertySymbols()
更简洁。_.isEmpty()
(推荐)Lodash 的 _.isEmpty()
已经内置了对 Symbol
键的支持:
import _ from 'lodash';
const sym = Symbol('key');
const obj1 = {};
const obj2 = { [sym]: 'value' };
console.log(_.isEmpty(obj1)); // true
console.log(_.isEmpty(obj2)); // false
优点:
Map
、Set
、类数组、Symbol
键等)。Symbol
键如果还要检查 原型链上的 Symbol
键(极少需要),可以用 for...in
+ Object.getOwnPropertySymbols
:
function isEmptyIncludingPrototype(obj) {
for (let key in obj) {
return false;
}
return Object.getOwnPropertySymbols(obj).length === 0;
}
如果对象的属性是 不可枚举的(如 Object.defineProperty
定义的),Object.keys()
和 for...in
会忽略它们,但 Reflect.ownKeys()
能检测到:
const obj = {};
Object.defineProperty(obj, 'hidden', { value: 1, enumerable: false });
console.log(isEmpty(obj)); // true(如果只用 Object.keys)
console.log(Reflect.ownKeys(obj)); // ['hidden'](能检测到)
方法 | 检测范围 | 是否包含 Symbol |
是否包含不可枚举属性 |
---|---|---|---|
Object.keys() |
自身可枚举字符串键 | ❌ | ❌ |
for...in + hasOwnProperty |
自身+原型链可枚举字符串键 | ❌ | ❌ |
Object.getOwnPropertySymbols() |
自身 Symbol 键 |
✅ | ✅ |
Reflect.ownKeys() |
所有自身键(字符串+Symbol+不可枚举) | ✅ | ✅ |
Lodash _.isEmpty() |
所有键(包括 Symbol ) |
✅ | ✅ |
推荐方案:
Reflect.ownKeys(obj).length === 0
(ES6+)。Object.keys(obj).length === 0 && Object.getOwnPropertySymbols(obj).length === 0
。_.isEmpty()
,避免手动处理边界情况。