阅读视图
Vue Router 组件内路由钩子全解析
一、什么是组件内路由钩子?
在 Vue Router 中,组件内路由钩子(也称为导航守卫)是在路由变化时自动调用的特殊函数,它们允许我们在特定时机执行自定义逻辑,比如:
- • 权限验证(是否登录)
- • 数据预加载
- • 页面离开确认
- • 滚动行为控制
- • 动画过渡处理
// 一个简单的示例
export default {
name: 'UserProfile',
beforeRouteEnter(to, from, next) {
console.log('组件还未创建,但即将进入...')
next()
}
}
二、三大核心钩子函数详解
Vue Router 提供了三个主要的组件内路由钩子,它们组成了一个完整的导航生命周期:
1. beforeRouteEnter - 进入前的守卫
调用时机:在组件实例被创建之前调用,此时组件还未初始化。
特点:
- • 不能访问
this(因为组件实例还未创建) - • 可以通过回调函数访问组件实例
export default {
beforeRouteEnter(to, from, next) {
// ❌ 这里不能使用 this
console.log('from', from.path) // 可以访问来源路由
// ✅ 通过 next 的回调访问组件实例
next(vm => {
console.log('组件实例:', vm)
vm.loadData(to.params.id)
})
},
methods: {
loadData(id) {
// 加载数据逻辑
}
}
}
适用场景:
- • 基于路由参数的权限验证
- • 预加载必要数据
- • 重定向到其他页面
2. beforeRouteUpdate - 路由更新守卫
调用时机:在当前路由改变,但组件被复用时调用。
常见情况:
- • 从
/user/1导航到/user/2 - • 查询参数改变:
/search?q=vue→/search?q=react
export default {
data() {
return {
user: null
}
},
beforeRouteUpdate(to, from, next) {
// ✅ 可以访问 this
console.log('路由参数变化:', from.params.id, '→', to.params.id)
// 重新加载数据
this.fetchUserData(to.params.id)
// 必须调用 next()
next()
},
methods: {
async fetchUserData(id) {
const response = await fetch(`/api/users/${id}`)
this.user = await response.json()
}
}
}
实用技巧:使用这个钩子可以避免重复渲染,提升性能。
3. beforeRouteLeave - 离开前的守卫
调用时机:在离开当前路由时调用。
重要特性:
- • 可以阻止导航
- • 常用于保存草稿或确认离开
export default {
data() {
return {
hasUnsavedChanges: false,
formData: {
title: '',
content: ''
}
}
},
beforeRouteLeave(to, from, next) {
if (this.hasUnsavedChanges) {
const answer = window.confirm(
'您有未保存的更改,确定要离开吗?'
)
if (answer) {
next() // 允许离开
} else {
next(false) // 取消导航
}
} else {
next() // 直接离开
}
},
methods: {
onInput() {
this.hasUnsavedChanges = true
},
save() {
// 保存逻辑
this.hasUnsavedChanges = false
}
}
}
三、完整导航流程图
让我们通过一个完整的流程图来理解这些钩子的执行顺序:
是
否
是
next
next false
beforeRouteEnter 特殊处理
无法访问 this通过 next 回调访问实例开始导航组件是否复用?调用 beforeRouteUpdate调用 beforeRouteEnter组件内部处理确认导航 next创建组件实例执行 beforeRouteEnter 的回调渲染组件用户停留页面用户触发新导航?调用 beforeRouteLeave允许离开?执行新导航停留在当前页面
四、实际项目中的应用案例
案例1:用户权限验证系统
// UserProfile.vue
export default {
beforeRouteEnter(to, from, next) {
// 检查用户是否登录
const isAuthenticated = checkAuth()
if (!isAuthenticated) {
// 未登录,重定向到登录页
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else if (!hasPermission(to.params.id)) {
// 没有权限,重定向到403页面
next('/403')
} else {
// 允许访问
next()
}
},
beforeRouteLeave(to, from, next) {
// 如果是管理员,记录操作日志
if (this.user.role === 'admin') {
logAdminAccess(from.fullPath, to.fullPath)
}
next()
}
}
案例2:电商商品详情页优化
// ProductDetail.vue
export default {
data() {
return {
product: null,
relatedProducts: []
}
},
beforeRouteEnter(to, from, next) {
// 预加载商品基础信息
preloadProduct(to.params.id)
.then(product => {
next(vm => {
vm.product = product
// 同时开始加载相关商品
vm.loadRelatedProducts(product.category)
})
})
.catch(() => {
next('/404') // 商品不存在
})
},
beforeRouteUpdate(to, from, next) {
// 商品ID变化时,平滑过渡
this.showLoading = true
this.fetchProductData(to.params.id)
.then(() => {
this.showLoading = false
next()
})
.catch(() => {
next(false) // 保持当前商品
})
},
methods: {
async fetchProductData(id) {
const [product, related] = await Promise.all([
api.getProduct(id),
api.getRelatedProducts(id)
])
this.product = product
this.relatedProducts = related
},
loadRelatedProducts(category) {
// 异步加载相关商品
}
}
}
五、高级技巧与最佳实践
1. 组合式API中的使用
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
export default {
setup() {
const unsavedChanges = ref(false)
// 使用组合式API守卫
onBeforeRouteLeave((to, from) => {
if (unsavedChanges.value) {
return confirm('确定要离开吗?')
}
})
onBeforeRouteUpdate(async (to, from) => {
// 处理路由参数更新
await loadData(to.params.id)
})
return { unsavedChanges }
}
}
2. 异步操作的优雅处理
export default {
beforeRouteEnter(to, from, next) {
// 使用async/await
const enterGuard = async () => {
try {
const isValid = await validateToken(to.query.token)
if (isValid) {
next()
} else {
next('/invalid-token')
}
} catch (error) {
next('/error')
}
}
enterGuard()
}
}
3. 避免常见的坑
坑1:忘记调用 next()
// ❌ 错误示例 - 会导致导航挂起
beforeRouteEnter(to, from, next) {
if (checkAuth()) {
// 忘记调用 next()
}
}
// ✅ 正确示例
beforeRouteEnter(to, from, next) {
if (checkAuth()) {
next()
} else {
next('/login')
}
}
坑2:beforeRouteEnter 中直接修改数据
// ❌ 错误示例
beforeRouteEnter(to, from, next) {
next(vm => {
// 避免直接修改响应式数据
vm.someData = 'value' // 可能导致响应式问题
})
}
// ✅ 正确示例
beforeRouteEnter(to, from, next) {
next(vm => {
vm.$nextTick(() => {
vm.someData = 'value' // 在下一个tick中修改
})
})
}
六、与其他导航守卫的配合
组件内守卫还可以与全局守卫、路由独享守卫配合使用:
// 全局前置守卫
router.beforeEach((to, from, next) => {
console.log('全局守卫 → 组件守卫')
next()
})
// 路由配置中的独享守卫
const routes = [
{
path: '/user/:id',
component: UserProfile,
beforeEnter: (to, from, next) => {
console.log('路由独享守卫 → 组件守卫')
next()
}
}
]
执行顺序:
-
- 导航被触发
-
- 调用全局
beforeEach
- 调用全局
-
- 调用路由配置中的
beforeEnter
- 调用路由配置中的
-
- 调用组件内的
beforeRouteEnter
- 调用组件内的
-
- 导航被确认
-
- 调用全局的
afterEach
- 调用全局的
七、性能优化建议
1. 懒加载守卫逻辑
export default {
beforeRouteEnter(to, from, next) {
// 按需加载验证模块
import('@/utils/auth').then(module => {
if (module.checkPermission(to.meta.requiredRole)) {
next()
} else {
next('/forbidden')
}
})
}
}
2. 缓存验证结果
let authCache = null
export default {
beforeRouteEnter(to, from, next) {
if (authCache === null) {
// 首次验证
checkAuth().then(result => {
authCache = result
handleNavigation(result, next)
})
} else {
// 使用缓存结果
handleNavigation(authCache, next)
}
}
}
总结
Vue Router 的组件内路由钩子为我们提供了强大的导航控制能力。通过合理使用这三个钩子函数,我们可以:
- 1. beforeRouteEnter:在组件创建前进行权限验证和数据预加载
- 2. beforeRouteUpdate:优化动态参数页面的用户体验
- 3. beforeRouteLeave:防止用户意外丢失未保存的数据
记住这些钩子的调用时机和限制,结合实际的业务需求,你就能构建出更加健壮、用户友好的单页应用。
零硬件交互:如何用纯前端把摄像头变成 4000 个粒子的魔法棒?
关键词:Vue 3 / Three.js / MediaPipe / AI / WebGL / 创意编程 / 前端工程化
引言:当哈利波特的魔法棒变成了一行 URL
想象这样一个场景:
你不需要购买昂贵的 Apple Vision Pro,也不需要戴上笨重的 VR 头盔,甚至不需要安装任何 App。 你只需要打开一个网页,允许摄像头权限,对着屏幕伸出手。
握拳,屏幕上 4000 个原本漂浮的粒子瞬间向中心坍缩,聚集成一个紧密的能量球; 张手,这些粒子仿佛受到冲击,瞬间向四周炸裂,如同烟花般绚烂; 挥动,粒子流随着你的指尖起舞,从地球变成爱心,从爱心变成土星。
这就是我最近做的一个开源小项目 —— Hand Controlled 3D Particles。
在过去,这种级别的体感交互往往意味着:专业的深度摄像头(Kinect)、高性能的本地显卡、以及复杂的 C++/Unity 开发环境。 但现在,得益于 Web AI 和 WebGL 的进化,我们用纯前端技术就能复刻这种魔法。
一、效果:指尖上的粒子宇宙
在这个项目里,你的手就是控制一切的遥控器。
1. 实时手势识别
基于 Google MediaPipe Hands 模型,浏览器能以惊人的速度(60fps+)捕捉你手部的 21 个关键点。 这不是简单的"动量检测",而是真正的"骨骼识别":
- 拇指与食指捏合/靠近 → 触发引力场
- 手掌完全张开 → 触发斥力场
- 左右手势切换 → 切换 3D 模型形态
2. 视觉形态演变
粒子不仅仅是散乱的点,它们按照数学公式排列成 5 种形态:
- Earth(地球):经典的 Fibonacci 球面分布
- Heart(爱心):浪漫的心形数学曲线
- Saturn(土星):带光环的行星系统
- Tree(圣诞树):圆锥螺旋分布
- Fireworks(烟花):完全随机的爆炸效果
这一切,都运行在一个普通的 Chrome 浏览器里。
二、技术解构:三驾马车如何协同?
整个项目使用 Vue 3 + TypeScript 构建,核心逻辑其实非常简单,主要依赖三个技术的有机结合。
1. The Eye: MediaPipe Hands
这是 Google 开源的轻量级机器学习模型。它的特点是:快,极快。 它不需要将视频流上传到云端处理,而是直接在浏览器端(WASM)利用 GPU 加速推理。
核心逻辑只需要关注两个关键点:
- Landmark 4 (拇指尖)
- Landmark 8 (食指尖)
// 计算拇指和食指的欧几里得距离
const distance = Math.sqrt(
(thumbTip.x - indexTip.x) ** 2 +
(thumbTip.y - indexTip.y) ** 2
)
// 设定阈值进行状态判断
if (distance < 0.08) {
// 触发"握拳/收缩"状态
emit('contract')
} else if (distance > 0.16) {
// 触发"张开/爆炸"状态
emit('explode')
}
简单的高中数学,就能把连续的模拟信号转化为离散的交互指令。
2. The Canvas: Three.js 粒子系统
4000 个粒子在 Three.js 中并不是 4000 个独立的 Mesh(那样浏览器会卡死),而是一个 THREE.Points 系统。
所有的粒子共享一个 Geometry,通过 BufferAttribute 来管理每个粒子的位置。
不同形态的切换,本质上是目标位置(Target Position)的计算。比如心形曲线:
// 心形参数方程
const x = 16 * Math.pow(Math.sin(t), 3)
const y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)
const z = t * 2 // 增加一点厚度
3. The Motion: 简单的物理插值
为了让动画看起来自然,粒子不是"瞬移"到新位置的,而是每一帧都向目标位置"滑行"一点点。
// 每一帧的渲染循环中
for (let i = 0; i < particleCount; i++) {
// 当前位置 += (目标位置 - 当前位置) * 阻尼系数
positions[i] += (targetPositions[i] - positions[i]) * 0.08
}
这个简单的公式(Lerp,线性插值)赋予了粒子重量感和惯性。
三、Web AI 的启示:算力下沉与隐私红利
做完这个项目,我有几个关于 Web 技术演进的强烈感受。
1. 算力正在向端侧回流
在云计算主导的十年里,我们习惯了把一切丢给服务器。但 MediaPipe 这类 Web AI 技术的成熟,标志着端侧算力的觉醒。 现在的手机和笔记本显卡足够强大,可以在浏览器里跑百亿参数以下的小模型。 这意味着:零延迟的交互体验。你的手一动,粒子立马跟着动,不需要等待网络请求往返。
2. 隐私是最大的护城河
用户越来越介意"上传摄像头视频"。 纯前端实现的 AI 有一个天然优势:所有计算都在本地发生。 视频流只在内存里流转,从未离开过用户的设备。这种"隐私安全感"是云端 AI API 无法比拟的。
3. 创意的门槛在指数级降低
五年前,做这个效果需要懂 OpenCV、懂 C++、懂 Shader 编程。 现在,你只需要会写 JavaScript,调几个 npm 包。 技术在变得越来越平民化(Democratization),这让开发者能把精力从"怎么实现"转移到"做什么好玩的东西"上。
四、未来:还可以怎么玩?
这个 Demo 只是一个起点,它展示了 Web 交互的一种新范式——自然用户界面(NUI)。
基于这个架子,我们完全可以扩展出更多玩法:
- WebAR 营销:在电商页面,用手势"隔空"旋转商品模型;
- 无接触展示:在博物馆或展厅的大屏上,观众挥手就能翻页、缩放展品;
- 音乐可视化:让粒子不仅仅跟随手势,还随着背景音乐的频谱跳动;
- 多人远程互动:结合 WebRTC,让异地的两个人通过手势共同控制同一个 3D 场景。
五、最后
项目已开源,欢迎 Fork 玩耍,或者直接点个 Star ⭐️。
GitHub: github.com/wangmiaozer…
Flutter PopScope:iOS左滑返回失效分析与方案探讨
本文发布于公众号:移动开发那些事:Flutter PopScope:iOS左滑返回失效分析与方案探讨
1 背景
在Flutter 3.10版本中,WillPopScope被弃用并替换为更现代化、功能更强大的PopScope组件。PopScope主要用于拦截用户尝试退出当前路由的各种操作(如Android的返回键、AppBar的返回箭头、以及iOS的屏幕边缘左滑手势)。
PopScope的核心属性有两个:
-
canPop:一个布尔值,设置为false时,会阻止路由退出。 -
onPopInvokedWithResult:当尝试退出路由时触发的回调,并返回是否成功退出(即didPop)。
在开发过程中,笔者发现了一个显著的平台差异:
-
Android/其它平台: 当我设置
canPop = false并尝试使用返回键或返回箭头时,onPopInvokedWithResult会被调用。 -
iOS: 当我设置
canPop = false并尝试使用屏幕边缘左滑手势 (Edge Swipe Gesture) 时,onPopInvokedWithResult不会被调用,并且左滑手势会被系统完全忽略,导致应用看起来“卡住”了,没有提供任何用户反馈。
这无疑是一个严重的问题,它导致我们在iOS上无法通过PopScope来捕获左滑手势并执行自定义逻辑(如弹出“是否保存”对话框)。
一个典型的使用示例为:
PopScope(
canPop: false,
onPopInvokedWithResult: (value, _) {
// 在iOS平台这里永远不会被回调
if (!value) {
// do custom action here
}
return;
},
child: Container())
2 失效原因分析
2.1 PopScope失效原因
PopScope组件是通过 Platform Navigator 与原生平台的返回事件机制进行通信的, 主要负责监听和阻止 Navigator.pop() 调用或系统触发的 返回操作(Android 返回键,或者 iOS 的左滑返回)。
但iOS 左滑有特殊性: iOS 的左滑手势(Interactive Pop Gesture)是独立于 Flutter Navigator 运行的原生手势。当这个手势被启动时,如果它发现 PopScope 设置了 canPop: false,它会简单地取消手势并停止,而不会向 Flutter 的 Navigator 发送一个明确的弹出(Pop)请求。
因此,onPopInvokedWithResult 回调自然不会被触发,因为它只在 Flutter 接收到 Navigator.pop() 调用或系统返回键事件时触发。
-
Android: Android的返回键事件(
Back Button Event)是一个明确的、可拦截的系统事件,它会直接传递给Flutter引擎,引擎继而触发PopScope的回调。 -
iOS (左滑手势): iOS的左滑返回手势(
Interactive Pop Gesture)是由UINavigationController管理的。
在默认情况下,当一个Flutter View(FlutterViewController)被推入到原生导航栈中时:
- 如果设置了
canPop = false,Flutter引擎会通知原生层“当前路由不可退出”。 - 在iOS上,这个通知只阻止了系统尝试自动执行 Pop 操作,但没有将“用户进行了左滑手势”这一手势输入事件转化为一个通用的“Pop 尝试”事件并传给
PopScope。相反,系统只是简单地禁用了这个手势的默认行为,并未向上层报告任何事件。
简而言之,Android传递的是一个“意图”(返回键被按下),而iOS传递的是一个“手势”,且当手势被禁用时,这个“意图”就从未产生。
2.2 源码分析
通过分析Flutter源码,我们发现问题的核心在于 ModalRoute 的 popGestureEnabled 属性判断逻辑。当 PopScope.canPop 设置为false 时,路由的popDisposition 会被设置为 RoutePopDisposition.doNotPop ,这会导致 popGestureEnabled 变为 false ,从而禁用iOS的侧滑手势识别。
具体来说,在 CupertinoBackGestureDetector 中,有一个 enabledCallback 回调用于控制手势检测器的启用状态。当 enabledCallback 返回 false 时,手势检测器不会记录触摸起点,后续的一系列手势监听方法都不会被触发。
3 如何解决?
要解决这个问题,我们不能等待Flutter引擎去捕获一个它根本没收到的事件。我们必须在原生iOS层工作,主动拦截这个左滑手势,并在拦截成功后,手动地将其作为一个事件发送给Flutter。
我们的目标是:当用户在屏幕边缘左滑时,即使canPop为false,也要触发onPopInvokedWithResult。这里采用了在原生层面拦截左滑的手势,并传递给Flutter层的方案。
这里在原生的核心代码为:
/// 如果需要,设置左滑返回手势拦截(在这个plugin初始化时调用)
///
/// 该方法会检查 rootViewController 的类型:
/// - 如果是 UINavigationController,直接使用
/// - 如果是 FlutterViewController,会创建或使用现有的 NavigationController
private func setupInteractivePopGestureIfNeeded() {
guard let window = UIApplication.shared.windows.first,
let rootViewController = window.rootViewController else {
return
}
if let navController = rootViewController as? UINavigationController {
// 直接使用现有的 NavigationController
self.navigationController = navController
} else if let flutterVC = rootViewController as? FlutterViewController {
// 如果 FlutterViewController 已经有 navigationController,直接使用
if let existingNavController = flutterVC.navigationController {
self.navigationController = existingNavController
} else {
// 否则创建新的 NavigationController 并封装 FlutterViewController
// 先将 window.rootViewController 设置为 nil,避免视图层次冲突
window.rootViewController = nil
let newNavController = UINavigationController(rootViewController: flutterVC)
self.navigationController = newNavController
// 再设置新的 NavigationController 为 rootViewController
window.rootViewController = newNavController
}
}
setupInteractivePopGesture()
}
/// 设置左滑返回手势的拦截
///
/// 保存原始的手势识别器代理,然后将自己设置为新的代理,以便拦截手势事件
private func setupInteractivePopGesture() {
// 保存原始的代理
self.originalDelegate = self.navigationController?.interactivePopGestureRecognizer?.delegate
// 设置自己为代理
self.navigationController?.interactivePopGestureRecognizer?.delegate = self
}
/// 控制手势识别器是否应该开始识别手势
///
/// 当检测到左滑返回手势时,会通知 Flutter 层处理,并返回 false 阻止系统默认行为
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
// 检测到系统左滑手势,发送事件给 Flutter 关键点:当发现是对应事件时,发送给Flutter层
if gestureRecognizer == self.navigationController?.interactivePopGestureRecognizer {
channel?.invokeMethod("onSystemBackGesture", arguments: nil)
// 返回 false 阻止系统默认的返回行为,由 Flutter 层处理
return false
}
// 其他手势交由原来的代理处理
if let originalDelegate = self.originalDelegate {
return originalDelegate.gestureRecognizerShouldBegin?(gestureRecognizer) ?? true
}
return true
}
而在Flutter层,则监听对应的方法回调就可以:
/// 处理来自原生端的方法调用
Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'onSystemBackGesture':
// 当接收到系统返回手势时
debugPrint('PopscopeIos: onSystemBackGesture pop');
// 1. 如果设置了自动处理导航,尝试调用 maybePop()
if (_autoHandleNavigation) {
final navigator = _navigatorKey?.currentState;
if (navigator != null) {
// 关键点2: 这里其实就是调用导航的api: Navigator.maybePop(context);
await navigator.maybePop();
} else {
debugPrint('PopscopeIos: NavigatorState is null, cannot pop');
}
}
// 2. 调用用户自定义回调(无论是否自动处理)
_onSystemBackGesture?.call();
break;
default:
throw MissingPluginException('未实现的方法: ${call.method}');
}
}
当调用了Navigator.maybePop(context)的方法后,不管你PopScope里的canPop的值设置为什么,对应的回调方法onPopInvokedWithResult都会被调用了;
这个库对应的代码放在github:github.com/WoodJim/pop…
4 总结
通过在原生iOS层面上介入,我们成功地将iOS的左滑手势事件手动转化为一个Flutter可识别的 Pop 事件,从而弥补了原生和框架之间的差异。
现在,无论是Android的返回键还是iOS的左滑手势,当PopScope的canPop设置为false时,onPopInvokedWithResult回调都能可靠地被触发,确保了跨平台一致的用户体验,让开发者能够真正无忧地处理路由拦截逻辑。
但这个方案有个致命的缺点:只能保证有回调,无法做到跟手的左滑效果,如果想做到很丝滑的跟手的处理,还是需要去研究CupertinoPageTransitionsBuilder 相关的代码来看如何实现,但当项目使用GetX来做路由时,好像不管怎样设置这个CupertinoPageTransitionsBuilder都无法生效。
如果大家有其他更好的方案的话,欢迎一起探讨。
CSS中常使用的函数
除了 clamp(),CSS 还有很多功能强大的函数。以下是分类介绍:
一、尺寸与计算相关函数
1. min() - 取最小值
css
.element {
width: min(50%, 500px); /* 取50%和500px中的较小值 */
padding: min(2vw, 20px);
}
2. max() - 取最大值
css
.element {
width: max(300px, 50%); /* 至少300px */
font-size: max(1rem, 12px);
}
3. calc() - 数学计算
css
/* 基本计算 */
.element {
width: calc(100% - 2rem);
height: calc(50vh + 100px);
}
/* 复杂计算 */
.grid-item {
width: calc((100% - 3 * 20px) / 4); /* 4列网格 */
}
/* 嵌套计算 */
.element {
font-size: calc(var(--base-size) * 1.2);
}
4. fit-content() - 内容适应
css
.container {
width: fit-content(500px); /* 不超过500px的内容宽度 */
margin: 0 auto;
}
/* 表格列自适应 */
td {
width: fit-content;
}
二、clamp() 函数详解
语法
clamp(min, preferred, max)
- min:最小值(下限)
- preferred:首选值(通常使用相对单位)
- max:最大值(上限)
工作原理
/* 实际值会在这个范围内:min ≤ preferred ≤ max */
width: clamp(200px, 50%, 800px);
/* 意思是:宽度最小200px,首选50%视口宽度,最大800px */
实际应用示例
1. 响应式字体大小
/* 字体在16px-24px之间,首选3vw(视口宽度的3%) */
.font-responsive {
font-size: clamp(16px, 3vw, 24px);
}
/* 标题响应式 */
h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
}
2. 响应式容器宽度
.container {
width: clamp(300px, 80%, 1200px);
margin: 0 auto;
}
/* 图片自适应 */
img {
width: clamp(150px, 50%, 600px);
height: auto;
}
三、渐变与图像函数
1. linear-gradient() - 线性渐变
css
/* 基本渐变 */
.gradient-1 {
background: linear-gradient(45deg, red, blue);
}
/* 多色渐变 */
.gradient-2 {
background: linear-gradient(
to right,
red 0%,
orange 25%,
yellow 50%,
green 75%,
blue 100%
);
}
/* 透明渐变 */
.gradient-3 {
background: linear-gradient(
to bottom,
transparent,
rgba(0,0,0,0.5)
);
}
2. radial-gradient() - 径向渐变
css
/* 圆形渐变 */
.radial-1 {
background: radial-gradient(circle, red, yellow, green);
}
/* 椭圆渐变 */
.radial-2 {
background: radial-gradient(
ellipse at center,
red 0%,
blue 100%
);
}
/* 重复径向渐变 */
.radial-3 {
background: repeating-radial-gradient(
circle,
red,
red 10px,
blue 10px,
blue 20px
);
}
3. conic-gradient() - 锥形渐变
css
/* 色轮 */
.conic-1 {
background: conic-gradient(
red, yellow, lime, aqua, blue, magenta, red
);
}
/* 饼图效果 */
.pie-chart {
background: conic-gradient(
red 0% 33%,
yellow 33% 66%,
blue 66% 100%
);
}
4. image-set() - 响应式图像
css
/* 根据分辨率加载不同图片 */
.responsive-bg {
background-image: image-set(
"image-low.jpg" 1x,
"image-high.jpg" 2x,
"image-ultra.jpg" 3x
);
}
/* 格式支持 */
.optimized-bg {
background-image: image-set(
"image.avif" type("image/avif"),
"image.webp" type("image/webp"),
"image.jpg" type("image/jpeg")
);
}
四、变换与动画函数
1. translate() / translateX() / translateY()
css
.element {
transform: translate(50px, 100px);
transform: translateX(100px);
transform: translateY(50%);
}
/* 3D变换 */
.element-3d {
transform: translate3d(100px, 50px, 20px);
}
2. scale() / scaleX() / scaleY()
css
/* 缩放 */
.element {
transform: scale(1.5); /* 整体放大1.5倍 */
transform: scale(1.2, 0.8); /* 宽放大,高缩小 */
transform: scaleX(2); /* 水平放大2倍 */
}
3. rotate()
css
/* 旋转 */
.element {
transform: rotate(45deg); /* 45度旋转 */
transform: rotate(1turn); /* 360度旋转 */
transform: rotate3d(1, 1, 1, 45deg); /* 3D旋转 */
}
4. matrix() / matrix3d()
css
/* 矩阵变换(组合所有变换) */
.complex-transform {
transform: matrix(1, 0, 0, 1, 100, 50);
/* 等同于:translate(100px, 50px) */
}
5. cubic-bezier() - 贝塞尔曲线
css
/* 自定义缓动函数 */
.animation {
animation: move 2s cubic-bezier(0.68, -0.55, 0.27, 1.55);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 预设曲线 */
.ease-in-out { transition-timing-function: ease-in-out; }
.custom-ease { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
6. steps() - 步进动画
css
/* 逐帧动画 */
.sprite-animation {
animation: walk 1s steps(8) infinite;
}
/* 打字机效果 */
.typewriter {
animation: typing 3s steps(40) forwards;
}
五、滤镜效果函数
1. blur() - 模糊
css
.blur-effect {
filter: blur(5px);
backdrop-filter: blur(10px); /* 背景模糊 */
}
2. brightness() - 亮度
css
.image {
filter: brightness(150%); /* 变亮 */
filter: brightness(50%); /* 变暗 */
}
3. contrast() - 对比度
css
.photo {
filter: contrast(200%); /* 增加对比度 */
}
4. drop-shadow() - 阴影
css
/* 比 box-shadow 更符合元素形状 */
.icon {
filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.5));
}
/* 多重阴影 */
.text {
filter:
drop-shadow(1px 1px 0 white)
drop-shadow(-1px -1px 0 white);
}
5. 组合滤镜
css
.instagram-filter {
filter:
brightness(1.2)
contrast(1.1)
saturate(1.3)
sepia(0.1);
}
六、布局与网格函数
1. minmax() - 网格尺寸范围
css
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
/* 复杂的网格布局 */
.layout {
grid-template-columns:
minmax(200px, 300px)
minmax(auto, 1fr)
minmax(150px, 200px);
}
2. repeat() - 重复模式
css
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 重复3次 */
grid-template-rows: repeat(auto-fill, minmax(100px, auto));
}
/* 命名网格线 */
.complex-grid {
grid-template-columns:
[sidebar-start] 250px [sidebar-end content-start]
repeat(2, [col] 1fr)
[content-end];
}
3. fit-content() - 网格尺寸
css
.grid-item {
grid-column: 1 / fit-content(500px);
}
七、其他实用函数
1. var() - CSS 变量
css
:root {
--primary-color: #3498db;
--spacing: 1rem;
--font-family: 'Roboto', sans-serif;
}
.element {
color: var(--primary-color);
padding: var(--spacing);
font-family: var(--font-family);
/* 默认值 */
margin: var(--custom-margin, 10px);
}
2. attr() - 属性值
css
/* 显示 data-* 属性值 */
.tooltip::after {
content: attr(data-tooltip);
}
/* 配合计数器 */
.item::before {
content: attr(data-index);
}
/* 动态样式 */
.progress {
width: attr(data-progress percent);
}
3. counter() / counters() - 计数器
css
/* 自动编号 */
ol {
counter-reset: section;
}
li::before {
counter-increment: section;
content: counter(section) ". ";
}
/* 嵌套计数器 */
ol ol {
counter-reset: subsection;
}
li li::before {
counter-increment: subsection;
content: counter(section) "." counter(subsection) " ";
}
4. url() - 资源路径
css
/* 图片 */
.background {
background-image: url("image.jpg");
background-image: url("data:image/svg+xml,..."); /* 内联SVG */
}
/* 字体 */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
}
/* 光标 */
.custom-cursor {
cursor: url('cursor.png'), auto;
}
5. env() - 环境变量
css
/* 安全区域(适配刘海屏) */
.safe-area {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
/* 视口单位 */
.full-height {
height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
}
八、函数组合使用示例
复杂响应式设计
css
:root {
--min-width: 320px;
--max-width: 1200px;
--fluid-scale: min(max(var(--min-width), 100vw), var(--max-width));
}
.container {
/* 组合多个函数 */
width: clamp(
var(--min-width),
calc(100vw - 2 * var(--spacing)),
var(--max-width)
);
padding: var(--spacing);
/* 响应式字体 */
font-size: clamp(
1rem,
calc(0.5rem + 1vw),
1.5rem
);
/* 响应式渐变背景 */
background: linear-gradient(
to bottom right,
hsl(calc(220 + var(--hue-adjust)) 70% 50% / 0.9),
hsl(calc(280 + var(--hue-adjust)) 60% 40% / 0.8)
);
/* 动态阴影 */
box-shadow:
0 calc(2px + 0.1vw) calc(4px + 0.2vw)
color-mix(in srgb, currentColor 20%, transparent);
}
现代按钮组件
css
.button {
/* 尺寸响应式 */
padding: clamp(0.5rem, 2vw, 1rem) clamp(1rem, 4vw, 2rem);
/* 颜色动态 */
background: color-mix(
in srgb,
var(--primary-color)
calc(var(--hover, 0) * 20%),
black
);
/* 交互效果 */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* 悬停状态 */
&:hover {
--hover: 1;
transform: translateY(calc(-1 * var(--hover, 0) * 2px));
filter: brightness(1.1) drop-shadow(0 4px 8px rgba(0,0,0,0.2));
}
}
十、浏览器支持与回退方案
渐进增强策略
css
/* 1. 基础样式 */
.element {
width: 90%;
max-width: 800px;
min-width: 300px;
}
/* 2. 使用 @supports 检测支持 */
@supports (width: clamp(300px, 90%, 800px)) {
.element {
width: clamp(300px, 90%, 800px);
}
}
/* 3. 使用 CSS 变量提供回退 */
:root {
--fluid-width: 90%;
}
@supports (width: clamp(300px, var(--fluid-width), 800px)) {
.element {
--fluid-width: min(max(300px, 90%), 800px);
width: var(--fluid-width);
}
}
总结对比表
| 函数类别 | 核心函数 | 主要用途 | 兼容性 |
|---|---|---|---|
| 尺寸计算 | clamp(), min(), max(), calc() | 响应式尺寸 | 优秀 |
| 渐变背景 | linear-gradient(), radial-gradient() | 背景效果 | 优秀 |
| 变换动画 | translate(), rotate(), cubic-bezier() | 动画效果 | 优秀 |
| 滤镜效果 | blur(), drop-shadow() | 视觉效果 | 良好 |
| 网格布局 | minmax(), repeat() | 布局系统 | 优秀 |
| 变量函数 | var(), attr(), counter() | 动态内容 | 优秀 |
现代 CSS 函数大大增强了样式表达能力,减少了 JavaScript 的依赖,是构建现代响应式 Web 应用的重要工具。
js防抖技术:从原理到实践,如何解决高频事件导致的性能难题
在开发应用时,各位程序开发者们是否遇到一个性能问题,就是当用户网络卡顿时,点击一个按钮短时间内没反应后疯狂点击该按钮,频繁向后端发送请求,或者疯狂输入搜索框导致页面卡顿,频繁调整窗口大小导致重排重绘暴增等常见的性能问题。
一、 防抖的定义
此时聪明的你是否能想到一个解决方法,那就是,我们何不如设置一个定时器,当该事件被触发时,我们并不立即执行这个函数,而是等待一小段时间再触发,且当在等待的时间内没有被触发,便执行一次
想象你在坐电梯的时候,当你进入时,电梯不会立即关闭,而是会等待后面的人全部进入后再关闭(把每一次点击想象成按一次电梯按钮,每一次按按钮都会重置等待时间)
此时你就明白了解决频用户频繁发起请求导致的性能问题的解决方法:防抖
防抖 :在事件被频繁执行时,函数并不立即执行,而是在最后一次触发后等待指定的延迟时间,若延迟期间无新的触发,方才执行一次
二、防抖的实现逻辑
我们先假设在html界面中,我们添加了一个button按钮,且将其id命名为btn,此时我们想要为其添加一个点击事件,使其每点击一次便输出一次hello,我们需要写下如下js代码
1.实现具有打印功能的点击事件
const btn = document.getElementById('btn')
function ptn(){
console.log('hello')
}
btn.addEventListenner('click',ptn)
但是你会发现,将你带入疯狂点击该按钮的用户,你就会发现,此时前端会疯狂打印hello,若不能解决这个问题则不能解决相似的前端疯狂向后端发送请求的问题。
2.实现延迟打印(?不成功)
那你此时想到了有一个函数setTimeout(),诶,这个函数好像可以起到一种延迟的效果,或许可以添加请求的间隔?让我们看看
const btn = document.getElementById('btn')
function ptn(){
console.log('hello')
}
function debounce(fn,wait){
return function(){
setTimeout(function(){
fn()
},wait)
}
}
btn.addEventListenner('click',debounce(ptn,1000))
此时你慢慢地点几次,你或许会发现,诶,我好像实现了这个功能,每过一秒才会打印一次hello,但是但是一旦你连续快速地的点击,你就会惊奇的发现。
啊?为什么在我停下来的时候还会继续打印hello,而且好像打印的间隔不可能有一秒
失败原因
没错,因为单纯使用setTimeout而未使用闭包来保持状态(若想了解闭包,请参考往期文章《js中作用域及闭包的概念与基础应用》导致每次点击都创建了一个打印事件,事件函数频繁触发,间隔时间无法保证,这种被称为防抖失效或防抖函数未能实现
3.使用闭包解决未能更新定时器的
我们知道,闭包可以起到一个保存外部函数的变量,延迟销毁的作用
使用闭包处理后的代码
const btn = document.getElementById('btn')
function debounce(ptn,wait){
var time
return function(){
clearTimeout(time)
time = setTimeout(function(){
ptn()
},wait)
}
}
function ptn(){
console.log('hello')
}
btn.addEventListener('click',debounce(ptn,1000))
经过检验后,你发现,你成功通过闭包实现了‘‘hello’’的延迟打印效果,同样的,也可以应用在解决用户大量提交等操作导致的大量计算或布局操作,节省了服务器的算力
扩展
一、添加防抖函数导致this本应的指向改变
上述代码虽然是完成了防抖的基本功能:‘频繁触发,只执行最后一次’,但是你或许不知道的是,这份代码仍有缺陷,试试仔细观察一下这份代码,添加防抖再运行后ptn内的this指向似乎发生了改变
没错,在btn.addEventListener('click',debounce(ptn,1000))的运行时,函数体被btn调用时,其内部的ptn函数在被调用时,是独立调用的,而我们知道,当一个函数被独立调用时,其this是指向window全局的。
但是但是,在我们添加这个防抖函数前,我们是这样实现这个点击事件的btn.addEventListenner('click',ptn),此时ptn被addEventListenner触发,出现隐式绑定,this应该是指向btn的。
而在我们添加防抖函数后,却导致ptn函数的this指向了window全局对象,那我们此时就做了一件很糟糕的事情,显然我们需要将它的this指回给btn。
方法一
通过提前保存函数内this指向的btn,使用call函数控制其this指回btn
const btn = document.getElementById('btn')
function debounce(ptn,wait){
var time
return function(){
const _this = this
clearTimeout(time)
time = setTimeout(function(){
ptn.call(_this)
},wait)
}
}
function ptn(){
console.log('hello')
}
btn.addEventListener('click',debounce(ptn,1000))
方法二
通过利用箭头函数没有this对象,其内部的this是指向btn这一特点,使用call函数控制其this指回btn
const btn = document.getElementById('btn')
function debounce(ptn,wait){
var time
return function(){
clearTimeout(time)
time = setTimeout(() => {
ptn.call(this)
},wait)
}
}
function ptn(){
console.log('hello')
}
btn.addEventListener('click',debounce(ptn,1000))
二、遗漏addEventListener提供的event事件参数
当我们使用addEventListener触发一个函数时,会默认向其内部传入一个事件函数event对象以及其它对象,用于记录事件发生的详情
而我们遗漏了向防抖函数内部接受这个event对象以及其它的对象,以及将其提供给原本被调用的ptn函数,此时我们需要为其接受这个event对象以及其它的对象,以及将其提供给原本被调用的ptn函数。
通过...arg接受及结构向其内部传递
const btn = document.getElementById('btn')
function debounce(ptn,wait){
var time
return function(...arg){
clearTimeout(time)
time = setTimeout(() => {
ptn.call(this,...arg)
},wait)
}
}
function ptn(){
console.log('hello')
}
btn.addEventListener('click',debounce(ptn,1000))
到此,js防抖技术介绍为止,点赞+关注,后续继续提供实战演示题型
从爬楼梯到算斐波那契,我终于弄懂了递归和动态规划这俩 "磨人精"
最近在代码界摸爬滚打,总被两个词按在地上摩擦 —— 递归和动态规划。这俩货就像数学题里的 "小明",天天换着花样折磨人,今天就来好好扒一扒它们的底裤。
递归:自己调自己的 "套娃大师"
递归这东西,说白了就是函数自己喊自己的名字。就像小时候问妈妈 "我从哪来的",妈妈说 "你是妈妈生的",再问 "妈妈从哪来的",妈妈说 "外婆生的"... 一直问到祖宗十八代,这就是递归的精髓 —— 找规律 + 找出口。
比如算个阶乘,用递归(时间复杂度过高)写出来是这样:
function mul(n){
if(n === 1){ // 出口:问到祖宗了
return 1
}
return n * mul(n-1)
}
这代码简洁得像诗,但算个斐波那契数列就露馅了。那个 1,1,2,3,5,8... 的数列,递归写法看着简单:
递归的 "中年危机":重复计算让 CPU 原地冒烟
function fb(n){
if(n === 1 || n === 2){
return 1
}
return fb(n-1) + fb(n-2)
}
这代码算个 n=10 还行,要是算 n=40,能让你喝杯咖啡回来还没出结果。就像你查快递单号,每次都要从快递员刚取件的时候查起,哪怕昨天刚查过。
给递归装个 "备忘录":记忆化搜索救场
比如爬楼梯问题:一次能爬 1 或 2 阶,到第 n 阶有几种走法?
后来我灵机一动,给递归加了个小本本(数组 f):
const f = []
var climbStairs = function (n) {
if (n === 1 || n === 2) {
return n
}
if (f[n] === undefined) { // 查小本本,没记过才计算
f[n] = climbStairs(n - 1) + climbStairs(n - 2)
}
return f[n]
};
这招叫 "记忆化搜索"(提效),相当于把算过的结果记在通讯录里,下次直接拨号不用重新查号。
后来才发现,这代码就像给老年机装了智能手机的通讯录 —— 思路对但效率不够。全局数组 f 在多组测试用例下会残留历史数据,而且递归调用本身就有函数栈的开销,n 太大时还是扛不住。
彻底换个活法:动态规划的 "自底向上" 哲学
- 站在已知的角度,通过已知来定位未知
最后改用
纯动态规划找到动态方程)写法,直接逆袭:
var climbStairs = function (n){
const f = []
// 先搞定已知的1楼和2楼
f[1] = 1
f[2] = 2
// 从3楼开始往上爬,每步都踩在前人的肩膀上
for(let i = 3;i<=n;i++){
f[i] = f[i-1] + f[i-2]
}
return f[n]
}
这思路就像盖楼,从 1 层开始一层层往上盖,每一层的建材都直接用前两层的,根本不用回头看。没有递归的函数调用开销,也没有重复计算,效率直接拉满。
总结:三种写法的生存现状
| 写法 | 特点 | 适合场景 |
|---|---|---|
| 纯递归 | 代码简洁如诗 | 理解思路用,n≤30 |
| 记忆化搜索 | 加了缓存的递归 | 教学演示,n≤1000 |
| 动态规划 | 自底向上迭代 | 实际开发,n多大都不怕 |
总结:什么时候该套娃,什么时候该记笔记?
-
递归适合简单问题或调试时用,写起来爽,但容易重复劳动
-
动态规划适合复杂问题,虽然前期要多写几行,但跑起来飞快
-
记住:所有动态规划问题,
先建个空数组当小本本准没错
现在终于明白,递归是浪漫的诗人,只顾优雅不管效率; 动态规划是务实的会计,每一笔账都记得清清楚楚。 下次再遇到这俩货,我可不会再被它们忽悠了!
柯里化
函数柯里化的含义:将多个参数的函数 转化成 一个一个传入参数的函数。
目的:函数
参数复用或延迟执行它使用闭包记住之前传递的参数。
✅ 使用柯里化(参数复用)
我们将函数改造一下,让它先接收“规则”,返回一个专门检查这个规则的函数。
// 柯里化:第一层接收规则,第二层接收内容
function curriedCheck(reg) {
// 闭包记住了 reg
return function(txt) {
return reg.test(txt);
}
}
// 1. 参数复用:我们先生成一个“专门检查手机号”的函数// 这里我们将 reg 参数固定(复用)在了 checkPhone 函数内部
const checkPhone = curriedCheck(/^1\d{10}$/);
// 2. 以后使用,只需要传内容,不需要再传正则了
checkPhone('13800000001'); // true
checkPhone('13800000002'); // true
checkPhone('13800000003'); // true// 甚至可以再生成一个“专门检查邮箱”的函数const checkEmail = curriedCheck(/@/);
checkEmail('abc@qq.com');
结论: 在这里,正则表达式这个参数被复用了。checkPhone 就像是一个被填入了一半参数的模具,你只需要填入剩下的一半即可。
延迟执行
onClick在react渲染的时候就会 直接求值执行
react在渲染时,onclick会执行{}中的函数。
如果 onclick={handlerDelete(id)} 那么在渲染的时候直接就执行了这个函数,还没有点击就删除了。
所以使用匿名函数 or 柯里化
匿名函数 onclick={()=> handlerDelete(id)}
柯里化:
深入理解 JavaScript 中的 “this”:从自由变量到绑定规则
🧠 深入理解 JavaScript 中的 this:从自由变量到绑定规则
“
this是 JavaScript 最容易被误解的概念之一 —— 它不是由函数定义决定的,而是由调用方式决定的。”
在日常开发中,我们经常遇到这样的困惑:
- 为什么同一个函数,有时
this指向对象,有时却指向window? - 为什么在事件回调里
this是 DOM 元素,而赋值后调用就变成全局对象? - 严格模式下
this为什么会变成undefined?
本文将结合你可能写过的代码,系统梳理 this 的设计逻辑、绑定规则与常见陷阱,并告诉你:如何真正掌控 this。
🔍 一、this 不是“自由变量”——它和作用域无关!
很多初学者会混淆 变量查找 和 this 绑定,认为它们是一回事。其实:
| 特性 | 自由变量(如 myName) |
this |
|---|---|---|
| 查找时机 | 编译阶段(词法作用域) | 执行阶段(动态绑定) |
| 查找依据 | 函数定义位置(Lexical Scope) | 函数调用方式 |
| 是否受作用域链影响 | ✅ 是 | ❌ 否 |
来看一段典型代码:
'use strict'; // 严格模式
var myName = '极客邦'; // 挂载到 window
var bar = {
myName: 'time.geekbang.com',
printName: function () {
console.log(myName); // ✅ 自由变量 → '极客邦'(全局)
console.log(this.myName); // ❓ this 取决于怎么调用!
}
};
function foo() {
let myName = '极客时间';
return bar.printName;
}
var _printName = foo();
_printName(); // this → undefined(严格模式)
bar.printName(); // this → bar
-
myName是自由变量,按词法作用域查找 → 总是取全局的'极客邦' -
this.myName的值完全取决于函数如何被调用
💡 关键结论:
this和作用域链毫无关系!它是运行时的“调用上下文”决定的。
🎯 二、this 的五种绑定规则(优先级从高到低)
1️⃣ 显式绑定(Explicit Binding)
使用 call / apply / bind 强制指定 this
function foo() {
this.myName = '极客时间';
}
let bar = { name: '极客邦' };
foo.call(bar); // this → bar
console.log(bar); // { name: '极客邦', myName: '极客时间' }
✅ 最高优先级,直接覆盖其他规则。
2️⃣ 隐式绑定(Implicit Binding)
作为对象的方法调用 → this 指向该对象
var myObj = {
name: '极客时间',
showThis: function() {
console.log(this); // → myObj
}
};
myObj.showThis(); // 隐式绑定
⚠️ 陷阱:隐式丢失(Implicit Loss)
var foo = myObj.showThis; // 函数引用被赋值
foo(); // 普通函数调用 → this = window(非严格)或 undefined(严格)
这就是为什么
setTimeout(myObj.method, 1000)会丢失this!
3️⃣ 构造函数绑定(new Binding)
使用 new 调用函数 → this 指向新创建的实例
function CreateObj() {
this.name = '极客时间';
}
var obj = new CreateObj(); // this → obj
内部机制相当于:
var temObj = {};
temObj.__proto__ = CreateObj.prototype;
CreateObj.call(temObj);
return temObj;
4️⃣ 普通函数调用(Default Binding)
既不是方法,也没用 new 或 call → 默认绑定
-
非严格模式:
this → window(浏览器)或global(Node) -
严格模式:
this → undefined
function foo() {
console.log(this); // 非严格 → window;严格 → undefined
}
foo();
🚫 这是 JS 的一个“历史包袱”:作者 Brendan Eich 当年为了快速实现,让普通函数的
this默认指向全局对象,导致大量意外污染。
5️⃣ 箭头函数(Arrow Function)
没有自己的 this!继承外层作用域的 this
class Button {
constructor() {
this.text = '点击';
document.getElementById('btn').addEventListener('click', () => {
console.log(this.text); // ✅ 正确指向 Button 实例
});
}
}
✅ 箭头函数是解决“回调中
this丢失”的利器,但不能用于需要动态this的场景(如构造函数、对象方法)。
🌐 三、特殊场景:DOM 事件中的 this
<a href="#" id="link">点击我</a>
<script>
document.getElementById('link').addEventListener('click', function() {
console.log(this); // → <a id="link"> 元素
});
</script>
这是 addEventListener 的规范行为:
回调函数中的
this自动绑定为注册事件的 DOM 元素。
但注意:
- 如果用箭头函数 →
this不再是元素! - 如果把函数赋值给变量再调用 → 隐式丢失!
⚠️ 四、为什么 this 的设计被认为是“不好”的?
-
违反直觉:函数定义时看不出
this指向谁。 - 容易出错:隐式丢失、全局污染频发。
-
依赖调用方式:同一函数,不同调用,
this不同。
正因如此,ES6 引入了
class和箭头函数,弱化对this的依赖。
✅ 五、最佳实践建议
| 场景 | 推荐做法 |
|---|---|
| 对象方法 | 用普通函数,避免赋值导致隐式丢失 |
| 回调函数(如事件、定时器) | 用箭头函数,或 .bind(this)
|
| 构造函数 | 用 class 替代传统函数 |
| 需要强制指定上下文 | 用 call / apply / bind
|
| 避免全局污染 | 使用严格模式 + let/const
|
🔚 结语
this 并不神秘,它只是 JavaScript 动态绑定机制的一部分。理解它的核心在于:
“谁调用了这个函数,
this就是谁。”
掌握五种绑定规则,避开隐式丢失陷阱,你就能在任何场景下准确预测 this 的指向。
最后记住:现代 JavaScript 已经提供了更安全的替代方案(如 class、箭头函数),不必死磕 this —— 但你必须懂它。
📚 延伸阅读
- 《你不知道的JavaScript(上卷)》—— “this & 对象原型”章节
- MDN: this
- ECMAScript 规范:Function Calls
欢迎在评论区分享你踩过的 this 坑! 👇
如果觉得有帮助,别忘了点赞 + 关注~ ❤️
标签:#JavaScript #this #前端 #作用域 #掘金
Python避坑指南:基础玩家的3个"开挂"技巧
刚学会Python基础,写代码还在靠 for+append 凑数?别慌!这几个进阶偏基础的知识点,既能让代码变优雅,又不搞复杂概念,新手也能秒上手~
1. 推导式:一行搞定列表/字典(告别冗余循环)
还在这样写循环添加元素?
# 传统写法
nums = [1,2,3,4,5]
even_squares = []
for num in nums:
if num % 2 == 0:
even_squares.append(num**2)
print(even_squares) # 输出: [4, 16]
试试列表推导式,一行搞定,逻辑更清晰:
# 列表推导式
nums = [1,2,3,4,5]
even_squares = [num**2 for num in nums if num % 2 == 0]
print(even_squares) # 输出: [4, 16]
字典推导式也超实用,快速构建键值对:
fruits = ["apple", "banana", "cherry"]
fruit_len = {fruit: len(fruit) for fruit in fruits}
print(fruit_len) # 输出: {'apple':5, 'banana':6, 'cherry':6}
2. 解包操作:变量交换/多返回值的优雅姿势
交换变量不用临时变量,解包直接拿捏:
a, b = 10, 20
a, b = b, a # 一行交换,无需temp
print(a, b) # 输出: 20 10
函数多返回值接收更简洁,还能忽略无用值:
def get_user_info():
return "张三", 25, "北京"
name, age, _ = get_user_info() # _ 忽略不需要的字段
print(f"姓名:{name},年龄:{age}") # 输出: 姓名:张三,年龄:25
3. F-string:格式化输出的"天花板"
告别繁琐的 % 和 format ,F-string直观又强大:
score = 92.345
name = "李四"
# 直接嵌入变量,支持格式控制
print(f"{name}的成绩:{score:.1f}分") # 输出: 李四的成绩:92.3分
print(f"及格率:{score/100
如何使用 vxe-gantt table 甘特图来实现多个维度视图展示,支持切换年视图、月视图、周视图等
Python基础:被低估的"偷懒"技巧,新手必学!
刚入门Python,写代码总在"重复造轮子"?这几个进阶基础知识点,不用啃复杂文档,练熟直接少走弯路,代码简洁又高效~
1. 集合(set):去重+判断的"极速工具"
还在靠列表遍历去重?集合自带去重属性,效率翻倍:
# 列表去重(繁琐且慢)
nums = [1, 2, 2, 3, 3, 3]
unique_nums = []
for num in nums:
if num not in unique_nums:
unique_nums.append(num)
print(unique_nums) # 输出: [1,2,3]
# 集合去重(一行搞定)
unique_nums = list(set(nums))
print(unique_nums) # 输出: [1,2,3](顺序不保证,需排序可加sorted())
判断元素是否存在,集合比列表快100倍(大数据量更明显):
fruit_set = {"apple", "banana", "cherry"}
print("apple" in fruit_set) # 输出: True(O(1)时间复杂度)
2. enumerate:循环时"顺便"拿索引
遍历列表想同时要索引和元素?别用 range(len()) 了:
fruits = ["apple", "banana", "cherry"]
# 传统写法(麻烦且不优雅)
for i in range(len(fruits)):
print(f"索引{i}:{fruits[i]}")
# enumerate写法(直接获取索引+元素)
for idx, fruit in enumerate(fruits, start=1): # start指定索引起始值
print(f"第{idx}个水果:{fruit}") # 输出: 第1个水果:apple...
3. zip:多列表"配对"的神器
想同时遍历多个列表?zip直接打包,不用手动索引对齐:
names = ["张三", "李四", "王五"]
scores = [85, 92, 78]
subjects = ["数学", "语文", "英语"]
# 传统写法(容易出错)
for i in range(len(names)):
print(f"{names[i]} {subjects[i]}:{scores[i]}分")
# zip打包(简洁且安全)
for name, subject, score in zip(names, subjects, scores):
print(f"{name} {subject}:{score}分") # 输出: 张三 数学:85分...
打包后转字典更方便:
python
student_scores = dict(zip(names, scores))
print(student_scores) # 输出: {'张三':85, '李四':92, '王五':78}
这三个技巧都是日常开发高频用到的,看似简单却能解决很多冗余场景~ 赶紧复制代码实测,练熟直接提升代码整洁度!
数据驱动与CSS预定义样式:实现灵活多变的Banner布局
背景
在开发一个活动Banner组件时,每个Banner需要展示不同数量的图片,且每张图片的位置、大小都需要精确控制,以创造出独特的视觉效果。
传统的做法可能是:
- 为每个Banner写独立的组件
- 使用内联样式动态计算位置
但这些方案要么代码冗余,要么维护困难。我采用了一种更优雅的方案:数据驱动 + CSS预定义样式。
核心思路
我们的方案包含两个关键部分:
- 数据配置层:通过配置数据定义每个Banner的图片数组和唯一标识
- 样式预定义层:在CSS中为每个可能的图片位置预定义样式类
通过动态生成类名,将数据和样式完美连接。
实现细节
1. 数据配置
首先,我们定义一个数据数组,每个Banner项包含:
-
id: 唯一标识符,用于生成CSS类名 -
img: 图片数组,支持不同数量的图片 - 其他业务数据(数量、提示文字、链接等)
const list = [
{
id: "four",
num: 3,
hint: t("exclusiveEventDesc4"),
img: [img1, img2], // 2张图片
link: "https://forms.gle/XshRsPmkrKnaQUdA8",
targetTime: '2025-11-15T13:00:00Z'
},
{
id: "two",
num: 9,
hint: t("exclusiveEventDesc2"),
img: [img1, img2, img3, img4], // 4张图片
link: "https://forms.gle/UJ1oPxGgDzRqJ1y6A",
targetTime: '2025-11-13T13:00:00Z'
},
{
id: "one",
num: 50,
hint: t("exclusiveEventDesc1"),
img: [img1, img2, img3, img4], // 4张图片
link: "https://tinyurl.com/2025CCCCBREAKFAST",
targetTime: '2025-11-13T13:00:00Z'
},
];
2. 动态类名生成
在渲染时,我们通过模板字符串动态生成类名:
<div className="kol-img">
{item.img?.map((citem, cindex) => (
< img
src={citem}
key={cindex}
className={`${item.id}-img${cindex + 1}`} // 关键:动态生成类名
/>
))}
</div>
类名生成规则:
-
item.id = "one",cindex = 0→ 类名:one-img1 -
item.id = "two",cindex = 1→ 类名:two-img2 -
item.id = "four",cindex = 0→ 类名:four-img1
每个Banner的每张图片都对应唯一的类名。
3. CSS预定义样式
在CSS文件中,我们为每个可能的类名组合预定义样式:
.kol-img {
position: relative;
img {
filter: grayscale(100%);
border-radius: 100px;
}
}
// Banner "one" 的4张图片布局
.one-img1 {
width: 126px;
position: absolute;
top: 130px;
left: 20px;
@media @SmallScreen {
width: 44px;
top: 10px;
}
}
.one-img2 {
width: 88px;
position: absolute;
left: 130px;
top: 85px;
@media @SmallScreen {
width: 72px;
left: 56px;
top: 28px;
}
}
.one-img3 {
width: 72px;
position: absolute;
left: 39px;
top: 49px;
@media @SmallScreen {
width: 76px;
top: 96px;
left: 0;
}
}
.one-img4 {
width: 72px;
position: absolute;
left: 146px;
top: 231px;
@media @SmallScreen {
width: 44px;
left: 70px;
top: 170px;
}
}
// Banner "two" 的4张图片布局
.two-img1 {
width: 125px;
position: absolute;
top: 56px;
left: 100px;
@media @SmallScreen {
width: 75px;
top: 80px;
left: 60px;
}
}
.two-img2 {
width: 90px;
position: absolute;
left: 13px;
top: 160px;
@media @SmallScreen {
width: 43px;
top: 44px;
left: 28px;
}
}
// ... 更多样式定义
4. 完整组件代码
export default function BannerKOL() {
const [t] = useTranslation();
const isMobile = useIsMobile();
const list = [
// ... 数据配置
];
return (
isMobile ? (
<div className="bannerkol-wrap">
{list.map((item, index) => (
<div key={index}>
<div className="kol-line"></div>
<div className="kol-box df">
<div className="kol-img">
{item.img?.map((citem, cindex) => (
< img
src={citem}
key={cindex}
className={`${item.id}-img${cindex + 1}`}
/>
))}
</div>
<div className="kol-info">
{/* 其他内容 */}
</div>
</div>
</div>
))}
</div>
) : (
<Marquee className="bannerkol-marquee" speed={60} autoFill gradient={false} pauseOnHover={true}>
<div className="bannerkol-wrap">
{list.map((item, index) => (
<div key={index}>
<div className="kol-line"></div>
<div className={`kol-box df ${item.img?.length ? '' : 'no-img'}`}>
<div className="kol-img">
{item.img?.map((citem, cindex) => (
< img
src={citem}
key={cindex}
className={`${item.id}-img${cindex + 1}`}
/>
))}
</div>
<div className="kol-info">
{/* 其他内容 */}
</div>
</div>
</div>
))}
</div>
</Marquee>
)
);
}
总结
通过数据驱动 + CSS预定义样式的组合,我们实现了一个既灵活又易维护的Banner组件。这种方案的核心在于:数据驱动,易于扩展 ,数据和视图解耦, 添加新的Banner只需在list数组中添加配置,而且无需修改组件逻辑代码,配置与展示逻辑完全分离,产品提新的需求只需要改动数据,易于迭代
🧳 我的 React Trip 之旅(5):我的 AI 聊天机器人,今天又把用户气笑了
从 DeepSeek 到 Kimi,我写了个“模型切换器”,结果发现 AI 也会胡说八道
🤖 开头:用户问“附近有什么好玩的?”,AI 回“建议去火星”
集成 LLM 时,我以为只要调个 API 就行。结果第一次测试,用户问旅游建议,AI 回:“推荐您乘坐 SpaceX 前往火星,门票仅需 2 亿美元。”
我差点把电脑砸了。
后来才明白:不同模型,性格不同。DeepSeek 严谨,Kimi 活泼,Doubao 像个段子手。
于是,我决定——不绑死一个模型,做个“AI 模型切换器” !
🔌 核心思想:抽象出一个 chat(model, messages) 函数
不管外面风多大,我的组件只认一个接口:
// llm/chat.js
export async function chat(model, messages) {
const apiKey = getApiKey(model); // 从 .env 读取
const url = getModelEndpoint(model);
const response = await fetch(url, {
method: 'POST',
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ messages, stream: true })
});
if (!response.ok) throw new Error('LLM request failed');
return response; // 支持流式读取
}
✅ 组件调用:
const res = await chat('kimi', messages);
✅ 换模型?只需改第一个参数,业务代码一行不动!
💬 流式输出:让用户看到 AI “打字”的过程
普通请求是“等半天,啪一下全出来”。但聊天要有对话感!
我用 ReadableStream 逐字解析:
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = new TextDecoder().decode(value);
// 解析 chunk 中的 content,追加到 messages
setMessages(prev => [...prev.slice(0, -1), { role: 'assistant', content: newContent }]);
}
用户看到文字一个一个蹦出来,感觉 AI 在认真思考,而不是复制粘贴。
🧠 上下文管理:别让 AI 忘记刚才聊了啥
用户问:“推荐海岛”,AI 回:“马尔代夫”。
用户接着问:“那里签证难吗?”,AI 却回:“您说的是哪个地方?”
——因为它没收到上下文!
我在 Zustand 里维护完整对话历史,并用 LRU 缓存 控制长度(避免 token 超限):
// stores/useChatStore.js
const MAX_MESSAGES = 10;
const addMessage = (msg) => {
const newMsgs = [...get().messages, msg].slice(-MAX_MESSAGES);
set({ messages: newMsgs });
};
😅 结尾:AI 还是会犯傻,但至少我能快速换一个
现在,如果 Kimi 又开始胡说八道,我只需在代码里改一行:
// 从 'kimi' 换成 'deepseek-r1'
const res = await chat('deepseek-r1', messages);
用户毫无感知,但 AI 突然变得一本正经。
前端智能,不是让 AI 完美,而是让切换成本趋近于零。
下一站,我要解决一个看似简单却坑哭无数人的问题——如何全局弹出一个 Toast?UI 库不够用,我只好自己造了个“会飞的通知” 。
在flutter中dio应该如何封装和使用
JavaScript类型侦探:四大神器让你一眼看穿变量真身
每日一题-统计极差最大为 K 的分割方式数🟡
给你一个整数数组 nums 和一个整数 k。你的任务是将 nums 分割成一个或多个 非空 的连续子段,使得每个子段的 最大值 与 最小值 之间的差值 不超过 k。
返回在此条件下将 nums 分割的总方法数。
由于答案可能非常大,返回结果需要对 109 + 7 取余数。
示例 1:
输入: nums = [9,4,1,3,7], k = 4
输出: 6
解释:
共有 6 种有效的分割方式,使得每个子段中的最大值与最小值之差不超过 k = 4:
[[9], [4], [1], [3], [7]][[9], [4], [1], [3, 7]][[9], [4], [1, 3], [7]][[9], [4, 1], [3], [7]][[9], [4, 1], [3, 7]][[9], [4, 1, 3], [7]]
示例 2:
输入: nums = [3,3,4], k = 0
输出: 2
解释:
共有 2 种有效的分割方式,满足给定条件:
[[3], [3], [4]][[3, 3], [4]]
提示:
2 <= nums.length <= 5 * 1041 <= nums[i] <= 1090 <= k <= 109