普通视图

发现新文章,点击刷新页面。
今天 — 2025年8月24日技术

uniapp换肤最佳实践

2025年8月24日 14:44

前言

换肤是对应用的主题进行切换。需要在不修改代码和结构的前提下,通过动态切换一套样式规则,来改变应用的整体视觉外观,从而为用户提供不同的视觉体验。

而在uniapp中实现换肤,就需要考虑不同平台的实现,本文将介绍一种比较通用的最佳解决方案。

主要亮点有:

  1. 主题色后台进行不同平台配置,通过接口获取
  2. 无需使用类名切换,直接使用变量
  3. 可以利用主题色,衍生出颜色色阶,无需额外定义

在线预览:

image.png

接下来话不多说,开始实践部分。

1. 整体实现方式

整体流程梳理:

  1. 后台定义主题色等颜色
  2. 接口请求主题色,转换成CSS变量。注意: 主题色需要转换成rgb,用于衍生出其他颜色色阶, 如--mainColor:#FF4757, --mainRgbColor:255,71,87
  3. 页面根元素绑定,style="--mainColor:#FF4757"
  4. 页面使用,color: var(--mainColor);background: rgba(--mainRgbColor, 0.5)

image.png

说明:通用标签的背景色是根据主题色衍生出来的,这里就需要转换成rgb的形式进行使用。

2. 案例说明

接下来我们使用uniapp+vue3+pinia进行案例演示,包括编译后的app、h5、小程序进行实践。

我们整体代码实现思路是:通过pinia进行整体的设置和存储,接口获取时,我们需要额外计算出主题色的rgb

2.1 pinia设置和存储

我们使用pinia进行统一管理我们的换肤颜色。

import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useThemeStore = defineStore(
  'theme',
  () => {
    const themeInfo = ref(`--bg:#F8F8F8;--mainColor:#FF4757;--mainRgbColor:255,71,87;--subColor:#FFECED;--priceColor:#FF3838;`);

    const setTheme = (val: string) => {
      themeInfo.value = val;
    };

    const resetTheme = () => {
      themeInfo.value = '';
    };

    return {
      themeInfo,
      setTheme,
      resetTheme,
    };
  },
  {
    persist: true
  }
);

2.2 接口请求

我们需要请求自己平台的主题色,进行设置存储。需要将主题色进行转换成rgb形式,方便后续使用。

注意:如果在less中,我们可以使用fade进行rgba的操作,问题是fade函数不支持变量形式,仅仅只能用fade(#FFFFFF,0.2)。 如果使用变量,我们只能转换成rgb的字符,利用rgba实现

import { useThemeStore } from '@/store';

const hexToRgbStr = (hex: string): string => {
  try {
    // 扩展 3 位格式到 6 位
    const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
    hex = hex.replace(shorthandRegex, (m, r, g, b) => {
      return r + r + g + g + b + b;
    });
    // 解析 6 位 HEX 格式
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    if (!result) return ''
    return `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}`;

  } catch (error) {
    return ''
  }
}


const handleChange = () => {
  const mainColor = '#FF4757'
  const mainRgbColor = hexToRgbStr(mainColor)
  const colorStr = `--mainColor:${mainColor};--mainRgbColor:${mainRgbColor};--subColor:#FFECED;--priceColor:#FF3838;`
  useThemeStore().setTheme(colorStr)
}

2.3 页面使用

页面使用需要在页面的根元素绑定style,这样就可以在css中使用颜色变量了。

<div :style="useThemeStore().themeInfo"></div>
.main {
  color: var(--mainColor);
  background: rgba(var(--mainRgbColor),0.3);
}

2.4 一点开发优化

我们在使用css的时候,会一直使用var,我们希望用更简单的方式进行书写。例如:

.main {
  color: $color-theme;
  background: rgba($color-rgb-theme,0.3);
}

这种写法让我们的开发更加舒服,那么怎么去实现这样呢?也很简单,我们只需要定义一个通用的skin.scss,将这些变量定义进去,全局引入即可使用。

下面介绍两个引入方案:

2.4.1 使用vite全局引入

方案一:

  1. 定义skin.scss,定义变量
  2. 如果直接引入到页面会报错,我们直接在vite.config.ts中声明引入
  3. 页面直接使用

如果我们不想使用var()这种形式,需要在vite.config.ts中声明提前加载的css:

css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/skin.scss";` // 全局注入
      }
    }
  }

skin.scss:

$color-bg: var(--bg);
$color-theme: var(--mainColor);
$color-rgb-theme: var(--mainRgbColor);
$color-theme-d: var(--subColor);
$color-price: var(--priceColor);

这样就可以使用$形式,可以去除var这种写法。

.main {
  color: $color-theme;
}

2.4.2 使用uni.scss引入

方案二:

我们需要使用到uni.scss,它是一个特殊文件,在代码中无需 import 这个文件即可在scss代码中使用这里的样式变量。uni-app的编译器在webpack配置中特殊处理了这个uni.scss,使得每个scss文件都被注入这个uni.scss,达到全局可用的效果。

我们在此只需要将skin.scss定义的css变量移动到uni.scss中定义即可,也无需在vite中声明。

$color-bg: var(--bg);
$color-theme: var(--mainColor);
$color-rgb-theme: var(--mainRgbColor);
$color-theme-d: var(--subColor);
$color-price: var(--priceColor);

3. 效果展示

image.png

image.png

4. 总结

最后总结一下,主要就是利用CSS变量和RGB转换,实现换肤效果,并且在书写中,优化了一点开发体验。

如有错误,请指正O^O!

CSS 小技巧:如何将 img 转换成 background-image

作者 XboxYan
2025年8月24日 13:22

欢迎关注我的公众号:前端侦探

聊聊图片与背景图片

一、img vs background-image

大部分注重内容的图片(比如商品展示、文章配图)都推荐直接使用img标签,好处有很多,比如

  1. img支持天然懒加载,设置loading="lazy"
<img src='xxx.png' loading="lazy">
  1. img支持各种JS监听,比如加载成功、加载失败
<img src='xxx.png' onload="" onerror="">
  1. img支持自定义解码,同步还是异步
<img src='xxx.png' decoding="sync">
  1. img对SEO更加优化,对可访问性更好
<img src='xxx.png' alt="头像">

但是,除了上面这些,在视觉表现上,图片灵活性就不如背景图片了。

比如img不支持重复平铺、不支持图片叠加等等,也不像普通标签还可以使用伪元素。

这样就导致很多时候,比如要在img外面嵌套一层容器再额外处理,还是有些不便的,特别是在HTML不方便修改的情况下。

那么,有没有什么办法,可以将img转换成background-image呢? 也就是使用img标签,但是却可以使用背景图的诸多特性。

image-20250822194214247

二、img 的层级

img是一个可替换标签,意思就是这个标签的渲染内容是由外部决定的。

这样就导致img和一般标签有些不同,比如没有伪元素,而且资源的层级是高于一些装饰性属性的,例如背景、内阴影等

image-20250823103620523

比如,我们给一个img标签添加一个背景

<img src="xxx.png" style="background:red">

效果如下

image-20250823103843453

没有看到任何红色背景,说明图片的层级是高于背景的。

如果我们换一个带透明像素的图片,就能看到背景了

image-20250823104022461

所以,下面的问题就是,如何把img本身的图片给隐藏起来?

三、如何隐藏 img 资源?

当然这里说的隐藏并不是直接隐藏整个img标签,比如

img {
  opacity: 0 /*🙅🏻‍*/
}

这样的话,整个标签都看不见了,背景也看不见了,也就失去了转换的意义。

那还有什么办法可以隐藏呢?这里有两种方式

1. 通过content替换内容

在之前,可以从可替换标签入手,通过content属性可以改变可替换元素的内容,就像这样

img {
  content: url(xx.png)
}

我们给img标签一个透明的图片内容,就可以替换img原本的显示了,这里可以用1*1像素的透明gif

img {
  content: url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);
}

但是,这样设置以后,图片完全不可见了。这样因为,此时的img已经被渲染成1*1像素了

image-20250823111419050

所以在用这种方式时,必须手动指定宽高,

img {
  content: url(xxx.png);
  width: 300px;
  height: 300px;
}

为了方便观察,我们加上边框

image-20250823111006597

这样原始的img链接已经被隐藏了

2. 通过object-position偏移

上面的方式比较硬核,还可以设置一张错误的图片,让图片展示直接出错,这样还能使用伪元素。

不过,有个小缺陷是,必须要手动指定图片宽高,可能存在一定的限制。

下面介绍另一个方式,那就是通过object-positon收到改变图片的显示位置。比如

img {
  object-position: 100px;
}

效果如下

image-20250823112432629

我们只需要给个足够大的值,图片就完全移出标签外了

img {
  object-position: 100vw; /**足够大的偏移/
}

这样也能隐藏图片的展示,而且也不影响img原本尺寸

image-20250823111006597

注意,这里不能用object-position: 100%来实现,和背景位置比较像,100%表示居右了,并不是向右偏移自身的100%

四、如何通过背景图片显示

再回到img标签本身,有办法直接通过src属性直接显示为背景图片吗?

<img src="xxx.png">

在之前这篇文章:原子化的未来?了解一下全面进化的CSS attr函数 有提到attr的新特性。

但是,出于安全考虑,并不能直接显示背景图片

img{
  background: url(attr(src));
}

其实呢,还可以用image-set来直接渲染字符串格式的资源,写法如下

img {
  background: image-set(attr(src));
}

这个技巧是在张鑫旭的这篇文章中学到的:www.zhangxinxu.com/wordpress/2…

效果如下

image-20250823115359509

背景尺寸有些不对,我们调整一下

img {
  background: image-set(attr(src)) 0 0/100%;
}

这样就通过背景完美替换了图片

image.png

不过这个需要用过attr的新特性,要求兼容性 chrome 133+

更常见的做法是通过自定义属性来实现

<img src="xxx.png" style="--bg: url(xxx.png)">

然后直接使用这个变量

img {
  background: var(--bg) 0 0/100%;
}

五、转换成背景图片后的好处

费了一番功夫转换成了背景图片,有哪些好处呢?下面举几个例子

1. 图片内边框

通常我们使用border实现的边框都是外边框,无法直接覆盖在图片上,有时候需要一种半透明边框来强化图片的轮廓,比如下面的书封

image.png

这时,我们可以通过内阴影来实现这样的效果(内阴影的层级高于背景)

img {
  background: image-set(attr(src)) 0 0/100%;
  border-radius: 8px;
  box-shadow: inset 0 0 0 2px rgba(0,0,0,.1)
}

效果如下

image-20250823122050743

2. 图片高光或者水印

有时候书封还需要高光或者水印,这样会更有质感一些,就像这样

image-20250823123002130转存失败,建议直接上传图片文件

这时,我们可以直接给图片叠加一层高光素材就行了

img {
  background: url("https://imgservices-1252317822.image.myqcloud.com/coco/s03272025/e21047d7.v7q5ko.png") 0 0/100% 100%, image-set(attr(src)) 0 0/100%;
}

效果如下

image-20250823123221252

也可以叠加一层平铺的水印

img{
  --water: url("data:image/svg+xml,%3Csvg width='150' height='150' style='transform:rotate(-45deg)' xmlns='http://www.w3.org/2000/svg'%3E%3Ctext x='50%25' y='50%25' font-size='14' fill='%23a2a9b6' font-family='system-ui, sans-serif' text-anchor='middle' dominant-baseline='middle'%3E前端侦探%3C/text%3E%3C/svg%3E");
  background: var(--water) 50%/28%, image-set(attr(src)) 0 0/100%;
  box-shadow: inset 0 0 0 4px rebeccapurple;
}

效果如下

image-20250823123505939

3. 图片缩放效果

可以在不嵌套标签的情况下实现图片缩放效果

img{
  transition: .3s;
}
img:hover{
  background-size: 120%;
}

效果如下

Kapture 2025-08-23 at 14.01.09

以上所有 demo 可以查看:codepen.io/xboxyan/pen…

六、更多一种选择

其实上面的实现都可以通过嵌套一层标签来实现,这里其实针对的是那种 html 结构不方便修改的情况,比如是框架自己生成的,或者在一些富文本编辑器了,嵌套一层会有更多的麻烦。

这样一个自定义img标签小技巧,你学到了吗?最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发 ❤❤❤

掌控网页的灵魂!揭秘DOM事件中鼠标与滚轮的终极操控术

作者 coding随想
2025年8月24日 13:09

在Web开发的世界里,鼠标和滚轮事件是用户与页面交互的“神经末梢”。它们不仅是网页动态效果的基石,更是开发者实现炫酷功能的利器。无论是点击按钮的反馈、拖拽元素的流畅体验,还是通过滚轮实现的无限滚动、图片缩放,背后都离不开这些事件的精准控制。本文将带你深入挖掘DOM事件中鼠标和滚轮的核心机制,手把手教你从“小白”蜕变为“事件操控大师”。


一、鼠标事件:从点击到悬停的全生命周期

1. 定义与分类

鼠标事件是用户通过鼠标与页面元素互动时触发的行为集合。常见的鼠标事件包括:

  • click:单击元素(如按钮、链接)。
  • dblclick:双击元素(如文件夹展开)。
  • mousedown/mouseup:鼠标按键按下/释放(用于拖拽操作)。
  • mousemove:鼠标在元素内移动(如绘制图形)。
  • mouseenter/mouseleave:鼠标进入/离开元素边界(导航栏悬停菜单)。
  • mouseover/mouseout:鼠标进入/离开元素及其子元素(如商品卡片提示)。

2. 关键属性与方法

  • event.clientX/Y:获取鼠标相对于浏览器窗口的坐标。
  • event.button:标识按下的鼠标按键(0=左键,1=中键,2=右键)。
  • event.target:触发事件的具体元素(与this不同,后者指向绑定事件的元素)。
  • event.preventDefault():阻止默认行为(如右键菜单、链接跳转)。
  • event.stopPropagation():停止事件冒泡(防止事件传递到父元素)。

3. 使用技巧与场景

  • 拖拽功能:通过监听mousedownmousemovemouseup事件,实现元素的拖拽。

    const dragElement = document.getElementById('draggable');
    let isDragging = false;
    dragElement.addEventListener('mousedown', () => isDragging = true);
    document.addEventListener('mousemove', (e) => {
      if (isDragging) {
        dragElement.style.left = e.clientX + 'px';
        dragElement.style.top = e.clientY + 'px';
      }
    });
    document.addEventListener('mouseup', () => isDragging = false);
    
  • 悬停菜单:利用mouseentermouseleave实现导航栏的展开/收起动画。

    const menu = document.getElementById('nav-menu');
    menu.addEventListener('mouseenter', () => menu.classList.add('open'));
    menu.addEventListener('mouseleave', () => menu.classList.remove('open'));
    
  • 右键菜单:通过contextmenu事件拦截默认右键菜单,并自定义弹窗。

    document.addEventListener('contextmenu', (e) => {
      e.preventDefault();
      alert('这是你的专属右键菜单!');
    });
    

4. 注意事项

  • 事件冒泡与捕获mouseenter/mouseleave不冒泡,而mouseover/mouseout会冒泡。选择事件类型时需根据需求匹配。
  • 性能优化:频繁触发的mousemove事件需结合节流(throttle)技术避免性能问题。
  • 兼容性mouseenter/mouseleave在旧版IE中可能不被支持,需用mouseover/mouseout模拟。

二、滚轮事件:从“滚”到“飞”的交互艺术

1. 定义与分类

滚轮事件是用户通过鼠标滚轮或触摸板滚动页面时触发的事件,核心事件包括:

  • wheel:现代标准事件,兼容性优秀(推荐使用)。
  • mousewheel:旧版非标准事件(仅用于兼容IE8及以下)。
  • scroll:页面滚动事件(与滚轮行为相关但独立)。

2. 关键属性与方法

  • event.deltaY:垂直滚动的距离(正值=向下,负值=向上)。
  • event.deltaX:水平滚动的距离(如触控板左右滑动)。
  • event.deltaMode:滚动单位模式(0=像素,1=行,2=页)。
  • event.preventDefault():阻止默认滚动行为(如自定义滚动逻辑)。

3. 使用技巧与场景

  • 无限滚动加载:当用户滚动到底部时自动加载更多内容。

    window.addEventListener('wheel', (e) => {
      if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 50) {
        loadMoreContent();
      }
    });
    
  • 图片缩放:通过滚轮调整图片大小(需结合CSS transform: scale())。

    const img = document.getElementById('zoom-image');
    img.addEventListener('wheel', (e) => {
      e.preventDefault();
      const scale = img.style.transform ? parseFloat(img.style.transform.match(/scale\((\d+\.\d+)\)/)[1]) : 1;
      img.style.transform = `scale(${e.deltaY < 0 ? scale + 0.1 : scale - 0.1})`;
    });
    
  • 3D地图旋转:通过滚轮控制视角旋转(需结合WebGL或Three.js)。

    const camera = new THREE.PerspectiveCamera();
    window.addEventListener('wheel', (e) => {
      camera.rotation.x += e.deltaY * 0.01;
      camera.rotation.y += e.deltaX * 0.01;
      renderer.render(scene, camera);
    });
    

4. 注意事项

  • 不要混淆wheelscrollwheel事件只在用户主动滚动时触发,而scroll事件在滚动条变化时都会触发。
  • 兼容性陷阱:火狐浏览器曾使用DOMMouseScroll事件,需额外绑定。
  • 性能瓶颈:频繁的滚轮事件可能导致卡顿,建议使用防抖(debounce)或节流技术。

三、进阶技巧:鼠标与滚轮的“绝妙配合”

1. 滚轮+鼠标中键:快速缩放

鼠标中键(滚轮点击)常用于执行特殊操作,如地图缩放。

document.addEventListener('mousedown', (e) => {
  if (e.button === 1) { // 中键点击
    e.preventDefault();
    alert('中键点击触发,执行缩放操作!');
  }
});

2. 鼠标悬停+滚轮:动态交互

将鼠标悬停与滚轮滚动结合,实现更复杂的交互。例如,悬停时通过滚轮切换图片:

const images = ['img1.jpg', 'img2.jpg', 'img3.jpg'];
let index = 0;
document.getElementById('gallery').addEventListener('wheel', (e) => {
  e.preventDefault();
  index += e.deltaY > 0 ? 1 : -1;
  index = (index + images.length) % images.length;
  document.getElementById('image').src = images[index];
});

3. 移动端适配:滚轮事件的替代方案

在移动端,滚轮事件可能不可靠(如iOS触控板),需结合touchstart/touchmove事件模拟滚动行为。


四、结语:从“事件小白”到“交互大师”

鼠标和滚轮事件是Web交互的基石,但它们的潜力远不止于此。通过灵活组合事件类型、巧妙利用属性与方法,开发者可以创造出令人惊叹的用户体验。记住:事件是工具,创意是灵魂。下次当你看到一个炫酷的网页特效时,不妨想一想——它背后是否藏着鼠标和滚轮的“秘密”?


彩蛋:如果你对事件驱动开发感兴趣,不妨尝试用CustomEvent创建自定义事件,让你的代码像交响乐一样协调!

鸿蒙模块间资源引用

作者 风冷
2025年8月24日 12:03

CrossModuleResourceAccess项目

跨模块资源访问-程序包结构-应用框架 - 华为HarmonyOS开发者

根据官方文档和项目实践,以下是关于跨模块资源访问的总结:

1. 跨模块资源访问的核心目标

  • 资源共享:通过 HAR(Harmony Archive)和 HSP(Harmony Shared Package)模块,实现资源(如文本、图片、样式等)的复用,减少冗余定义。
  • 模块化开发:支持功能模块的独立开发和维护,提升开发效率和代码可维护性。

2. 资源访问方式

  • 直接引用
    • 使用 $r('app.type.name')$rawfile('name') 访问当前模块资源。
    • 使用 $r('[hsp].type.name')$rawfile('[hsp].name') 访问 HSP 模块资源。
  • 动态 API 访问
    • 通过 resourceManager 接口(如 getStringSyncgetMediaContentSync)动态获取资源。
    • 使用 createModuleContext 创建其他模块的上下文,获取其 resourceManager 对象。

3. 资源优先级规则

  • 优先级从高到低
    1. 当前模块(HAP/HSP):自身模块的资源优先级最高。
    2. 依赖的 HAR/HSP 模块
      • 如果多个依赖模块中存在同名资源,按照依赖顺序覆盖(依赖顺序靠前的优先级更高)。

4. 官方文档补充

  • 资源隔离与访问控制
    • 类似腾讯云 CAM(访问管理)的权限设计,HarmonyOS 通过模块化设计实现资源的逻辑隔离。
    • 开发者可以通过显式依赖和资源命名规范避免冲突。
  • 跨模块通信
    • 除了资源访问,还可以通过模块间接口调用实现功能共享。

5. 最佳实践

  • 命名规范:为资源文件添加模块前缀(如 hsp1_icon.png),避免命名冲突。
  • 依赖管理:在 oh-package.json5 中明确模块依赖顺序,确保资源优先级符合预期。
  • 动态加载:对于插件化场景,优先使用 resourceManager 动态加载资源。

6. 适用场景

  • 多模块共享通用资源(如主题、图标、多语言文本)。
  • 动态加载不同模块的资源(如插件化设计)。

如果需要进一步分析具体实现或优化建议,请告诉我!

鸿蒙Flex与Row/Column对比

作者 风冷
2025年8月24日 11:59

在鸿蒙(HarmonyOS)应用开发中,Flex布局与Row/Column布局是两种核心的容器组件,它们在功能、性能及适用场景上存在显著差异。以下从五个维度进行详细对比:


📊 1. 核心差异对比

特性 Flex布局 Row/Column布局
布局机制 动态弹性计算,支持二次布局(重新分配空间) 单次线性排列,无二次布局
方向控制 支持水平(Row)、垂直(Column)及反向排列 Row仅水平,Column仅垂直
换行能力 支持自动换行(FlexWrap.Wrap 不支持换行,子组件溢出时被截断或压缩
子组件控制 支持flexGrowflexShrinkflexBasis动态分配空间 仅支持layoutWeight按比例分配空间
性能表现 较低(二次布局增加计算开销) 较高(单次布局完成)

⚠️ 二次布局问题:当子组件总尺寸与容器不匹配时,Flex需通过拉伸/压缩重新计算布局,导致性能损耗。


🔧 2. Flex布局的核心特点与场景

  • 核心优势

    • 多方向布局:通过direction自由切换主轴方向(水平/垂直)。

    • 复杂对齐:组合justifyContent(主轴)和alignItems(交叉轴)实现精准对齐。

    • 动态空间分配

      • flexGrow:按比例分配剩余空间(如搜索框占满剩余宽度)。
      • flexShrink:空间不足时按比例压缩子组件(需配合minWidth避免过度压缩)。
  • 必用场景

    • 多行排列:标签组、商品网格布局(需设置wrap: FlexWrap.Wrap)。
    • 响应式适配:跨设备屏幕(如手机/车机动态调整列数)。

📐 3. Row/Column布局的核心特点与场景

  • 核心优势

    • 轻量高效:线性排列无弹性计算,渲染性能更高。

    • 简洁属性

      • space:控制子组件间距(如导航栏按钮间隔)。
      • layoutWeight:一次遍历完成空间分配(性能优于flexGrow)。
  • 推荐场景

    • 单向排列

      • Row:水平导航栏、头像+文字组合。
      • Column:垂直表单、卡片内容堆叠。
    • 固定尺寸布局:子组件尺寸明确时(如按钮宽度固定)。


4. 性能差异与优化建议

  • Flex性能瓶颈

    • 二次布局触发条件:子组件总尺寸 ≠ 容器尺寸、优先级冲突(如displayPriority分组计算)。
    • 后果:嵌套过深或动态数据下易引发界面卡顿。
  • 优化策略

    • 替代方案:简单布局优先用Row/Column,避免Flex嵌套超过3层。

    • 属性优化

      • 固定尺寸组件设置flexShrink(0)禁止压缩。
      • 等分布局用layoutWeight替代flexGrow(如Row中占比1:2)。
    • 预设尺寸:尽量让子组件总尺寸接近容器尺寸,减少拉伸需求。


🛠️ 5. 选择策略与工程实践

  • 何时选择Flex?

    ✅ 需换行(如标签云)、复杂弹性对齐(如交叉轴居中)、动态网格布局。

    ❌ 避免在简单列表、表单等场景使用,优先Row/Column。

  • 何时选择Row/Column?

    ✅ 单向排列(水平/垂直)、子组件尺寸固定或比例明确(如30%+70%)。

    ✅ 高频场景:导航栏(Row)、表单(Column)、图文混排(Row+垂直居中)。

  • 工程最佳实践

    • 多端适配:通过DeviceType动态调整参数(如车机增大点击区域)。
    • 调试工具:用DevEco Studio布局分析器监测二次布局次数。
    • 混合布局:Flex内嵌套Row/Column(如Flex容器中的商品项用Column)。

💎 总结

  • Flex:强大但“重”,适合复杂弹性多行响应式布局,需警惕二次布局问题。

  • Row/Column:轻量高效,是单向排列场景的首选,性能优势明显。

  • 决策关键

    简单布局看方向(水平用Row,垂直用Column),

    复杂需求看弹性(换行/动态分配用Flex)。

通过合理选择组件并优化属性配置,可显著提升鸿蒙应用的渲染效率与用户体验。

Canvas 复杂交互步骤:从事件监听 to 重新绘制全流程

2025年8月23日 23:58

Canvas 复杂事件交互处理指南

在 HTML5 的 Canvas 中,处理复杂的用户交互事件,如点击、拖拽等,与处理常规 HTML 元素有所不同。由于 Canvas 本身是一个像素绘制区域,不具备内置的事件处理机制,我们需要通过一些技巧来实现复杂的事件交互。本文将深入探讨 Canvas 复杂事件交互的处理方法。

一、基本原理 ✨

Canvas 作为一个绘图区域,其内部绘制的图形并没有独立的 DOM 元素,因此无法直接监听图形的事件。要实现 Canvas 内部的事件交互,主要依赖以下两个核心原理:

  1. 事件监听与坐标判断

    • 监听整个文档或包含 Canvas 的容器元素的事件(例如 mousemovemousedownmouseup 等鼠标事件,以及 touchstarttouchmovetouchend 等触摸事件)。
    • 根据事件发生的坐标位置,判断该坐标是否在 Canvas 内部,以及是否与 Canvas 内部的特定图形发生了交互。

Canvas 交互示意图

二、具体步骤 🛠️

1. 获取 Canvas 元素和绘图上下文

首先,我们需要获取 Canvas 元素及其 2D 绘图上下文,这是所有 Canvas 操作的基础。

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

2. 监听容器元素的事件

通常,我们会选择监听 document 对象或 Canvas 元素的父容器的事件。这里以监听 mousemove 事件为例:

document.addEventListener("mousemove", handleMouseMove);

3. 事件处理函数

在事件处理函数中,我们需要完成以下两个关键任务:

  • 计算鼠标或触摸点在 Canvas 内部的坐标:由于事件的 clientXclientY 属性是相对于浏览器视口的,我们需要将其转换为相对于 Canvas 元素的坐标。
  • 判断坐标是否在特定图形范围内:这是实现图形交互的核心。通过判断鼠标或触摸点是否落在某个图形的区域内,来确定是否发生了交互。
function handleMouseMove(event) {
  const rect = canvas.getBoundingClientRect();
  const mouseX = event.clientX - rect.left;
  const mouseY = event.clientY - rect.top;

  // 在这里判断鼠标是否在某个图形范围内,并执行相应的交互逻辑
  // 例如:if (isPointInCircle(mouseX, mouseY)) {
  //   // 执行与圆形交互的逻辑
  // }
}

4. 判断坐标是否在图形内的函数

针对不同形状的图形,我们需要编写相应的函数来判断一个点是否在其内部。以下是判断点是否在圆形内的示例:

function isPointInCircle(x, y, circleX, circleY, radius) {
  const dx = x - circleX;
  const dy = y - circleY;
  return dx * dx + dy * dy < radius * radius;
}

三、处理复杂交互的策略 🔄

1. 多个图形的交互

当 Canvas 中存在多个可交互图形时,我们可以维护一个图形对象的数组。在事件处理函数中,遍历这个数组,对每个图形进行坐标判断,从而实现与多个图形的交互。

const shapes = [
  { type: "circle", x: 100, y: 100, radius: 50 },
  { type: "rectangle", x: 200, y: 200, width: 100, height: 50 },
];

function handleMouseMove(event) {
  const rect = canvas.getBoundingClientRect();
  const mouseX = event.clientX - rect.left;
  const mouseY = event.clientY - rect.top;

  for (const shape of shapes) {
    if (shape.type === "circle" && isPointInCircle(mouseX, mouseY, shape.x, shape.y, shape.radius)) {
      // 圆形交互逻辑
    } else if (shape.type === "rectangle" && isPointInRectangle(mouseX, mouseY, shape.x, shape.y, shape.width, shape.height)) {
      // 矩形交互逻辑
    }
  }
}

// 假设存在 isPointInRectangle 函数用于判断点是否在矩形内
function isPointInRectangle(x, y, rectX, rectY, rectWidth, rectHeight) {
  return x >= rectX && x <= rectX + rectWidth && y >= rectY && y <= rectY + rectHeight;
}

2. 动态交互效果

为了提供更丰富的用户体验,我们可以根据交互状态(如鼠标悬停、点击等)动态改变图形的外观、位置等属性。这通常涉及到在事件处理函数中更新图形的样式,并重新绘制 Canvas。

以下示例展示了鼠标悬停在圆形上时改变其颜色的效果:

function handleMouseMove(event) {
  const rect = canvas.getBoundingClientRect();
  const mouseX = event.clientX - rect.left;
  const mouseY = event.clientY - rect.top;

  // 清除 Canvas 并重新绘制所有图形
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  for (const shape of shapes) {
    if (shape.type === "circle" && isPointInCircle(mouseX, mouseY, shape.x, shape.y, shape.radius)) {
      ctx.fillStyle = "red"; // 鼠标悬停时,圆形变为红色
    } else {
      ctx.fillStyle = "blue"; // 否则为蓝色
    }
    drawShape(shape);
  }
}

function drawShape(shape) {
  if (shape.type === "circle") {
    ctx.beginPath();
    ctx.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI);
    ctx.fill();
  } else if (shape.type === "rectangle") {
    ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
  }
}

总结 💡

通过以上方法,我们可以在 Canvas 中实现较为复杂的事件交互处理,为用户提供丰富的交互体验。核心在于将事件监听与坐标判断相结合,并根据业务需求灵活处理图形状态和绘制逻辑。

Vuex+TypeScript实现hook,以及类型增强Store

作者 安_An
2025年8月23日 23:43

image.png

最终实现效果

使用

import { useStates, useGetters, useMutations, useActions } from '@/hooks';

// 获取模块中的state
useStates('commonModule', ['adminUserInfo']);

// 获取root中的state
useStates(['adminToken']);

// root和模块中的都能获取,自动补充模块前缀
useGetters(['commonModule/test', 'test']);

// root和模块中的都能获取,自动补充模块前缀
useMutations(['SET_ADMIN_TOKEN', 'commonModule/SET_ADMIN_USERINFO']);

// root和模块中的都能获取,自动补充模块前缀
useActions(['fn', 'commonModule/GETADMINUSERINFO_ACTION']);

Q: 看到这里就有同学会问了,这么长的字符串,每次都要手动输入不是很麻烦吗 A: 在我们实现类型增强的时候,IDE就有类型提示了,我们只需要选择即可,如下图所示,非常安全!!!

目录组织

store目录组织 module里存放各各模块,如common root这里也认为是一个模块放在这里 types存放一些类型和工具类型 indexstore出口

hooks组织 实现4个hooks,useStates useMutations useGetters useActions

实现

root模块搭建

const.ts

export class Mutations_Const {
  static SET_ADMIN_TOKEN = 'SET_ADMIN_TOKEN' as const;
}

types.d.ts

import { ActionContext } from 'vuex';
import { Mutations_Const } from '@/store/modules/root/const';

type RootState = {
  version: string;
  adminToken: string;
};

type GettersInRoot = Record<string, (...args: any[]) => any>; // 测试数据

type MutationsInRoot = {
  [Mutations_Const.SET_ADMIN_TOKEN](state: RootState, token: string): void; // 测试数据
};

type ActionsInRoot = {
  fn(context: ActionContext<RootState, RootState>): void; // 测试数据
};

export { RootState, GettersInRoot, MutationsInRoot, ActionsInRoot };

index.ts

import type { InjectionKey } from 'vue';
import type { RootState } from '@/store/modules/root/types';
import { createStore, Store as VuexStore, useStore as useBaseStore } from 'vuex';
import { rootState, gettersInRoot, mutationsInRoot, actionsInRoot } from '@/store/modules/root';

// 创建一个具备类型安全的唯一注入 Key,用于在 Vue 组件中注入 Store 实例。一定需要
const key: InjectionKey<VuexStore<RootState>> = Symbol();

const store = createStore({

  // root模块
  state: () => rootState,

  mutations: mutationsInRoot,

  actions: actionsInRoot,

  getters: gettersInRoot,

});

function useStore() {
  return useBaseStore(key);
}

export { store, key, useStore };

在main.ts中使用即可

import { store, key } from '@/store';

import App from './App.vue';

const app = createApp(App);

app.use(store, key);

这样我们就创建好了一个不带类型提示的vuex仓库啦

common模块搭建

action-types.ts

两个都是用于保存常量的文件,可根据自己需要使用

export const GETPROFILE_ACTION = 'GETPROFILE_ACTION';

export const GETADMINUSERINFO_ACTION = 'GETADMINUSERINFO_ACTION';

mutation-types.ts

export const SET_PROFILE = 'SET_PROFILE';

export const SET_ADMIN_USERINFO = 'SET_ADMIN_USERINFO';

state.ts

state模块,定义一个类型和一个state对象,导出对象和类型, 具体类型朋友们可以忽略,直接自己定义几个字符串即可~


import type { Profile, AdminUserType } from '@/api/types';

interface State {
  profile: Profile.ProfileDetailData;
  adminUserInfo: AdminUserType.AdminUserInfo;
}

const state: State = {
  profile: {
    author: '',
    avatar: '',
    csdnHomepage: '',
    giteeHomepage: '',
    githubHomepage: '',
    introduction: '',
    logo: '',
    name: '',
    zhihuHomepage: '',
  },
  adminUserInfo: {
    username: '',
  },
};

export { state };
export type { State };

getters.ts

import type { GetterTree } from 'vuex';
// 刚刚写好的State类型,拿过来使用
import type { State } from './state';
// root模块的State
import type { RootState } from '@/store/modules/root/types';

// Getters 类型
type Getters = {
  test(): Record<string, Array<number>>; // 测试类型,随便定义
};

// 类型实现
const getters: GetterTree<State, RootState> & Getters = {
  test() {
    const a = { a: [1] };
    return a;
  },
};

// 依旧导出
export { getters };
export type { Getters };


mutations.ts

import type { MutationTree } from 'vuex';
// 刚刚写好的State类型
import type { State } from './state';
import { SET_PROFILE, SET_ADMIN_USERINFO } from './mutation-types';
import type { Profile, AdminUserType } from '@/api/types';

// 这里使用上述定义的常量进行赋值,不容易错
type Mutations = {
  [SET_PROFILE](state: State, payload: Profile.ProfileDetailData): void;
  [SET_ADMIN_USERINFO](state: State, payload: AdminUserType.AdminUserInfo): void;
};

const mutations: MutationTree<State> & Mutations = {
  [SET_PROFILE](state, payload) {
    state.profile = payload;
  },
  [SET_ADMIN_USERINFO](state, payload) {
    state.adminUserInfo = payload;
  },
};

// 依旧导出
export { mutations };
export type { Mutations };


actions.ts

import type { State } from './state';
import type { ActionContext as VuexActionContext, ActionTree } from 'vuex';
import type { RootState } from '@/store/modules/root/types';
import { GETPROFILE_ACTION, GETADMINUSERINFO_ACTION } from './actions-types';
import API from '@/api';

// VuexActionContext泛型,传入定义的State和RootState 类型,好让context有类型推导
type CommonActionContext = VuexActionContext<State, RootState>;

type Actions = {
  [GETPROFILE_ACTION](context: CommonActionContext): void;
  [GETADMINUSERINFO_ACTION](context: CommonActionContext): void;
};

const actions: ActionTree<State, RootState> & Actions = {
  async [GETPROFILE_ACTION](context) {
    const profile = await API.profileDetail.getProfileDetail();
    context.commit('SET_PROFILE', profile!.data);
  },

  async [GETADMINUSERINFO_ACTION](context) {
    const adminUserInfo = await API.AdminUser.getAdminUserInfo();
    context.commit('SET_ADMIN_USERINFO', adminUserInfo!.data);
  },
};

export { actions };
export type { Actions };

index.ts

import type { Module } from 'vuex';
import type { RootState } from '@/store/modules/root/types';
import type { State } from './state';

// 导入上述定义的文件
import { state } from './state';
import { getters } from './getters';
import { mutations } from './mutations';
import { actions } from './actions';

const commonModule: Module<State, RootState> = {
  namespaced: true, // 开启命名空间,独立

  state: () => state, // 单独定义state是为了让ts验证State类型

  getters,

  mutations,

  actions,
};

//导出模块对象
export default commonModule;

挂载到vuex中

const store = createStore({
  state: () => rootState,

  mutations: mutationsInRoot,

  actions: actionsInRoot,

  getters: gettersInRoot,

  // 这里注册模块
  modules: {
    commonModule,
  },
})

到这里,我们就搭建好了基本的的vuex了,接下来来增强其类型推导

types文件夹

root.d.ts

我们要增强其类型推导,就要实现一个增强后的仓库类型赋值给Store,如EnhancedStore 补充小知识,TS中,Omit类型工具可以对一个类型从该类型中去除指定的属性,extends可以继承某个类型 那么我们大概需要一个如下的伪代码

interface EnhancedStore extends Omit<Store,  'getters', 'commit', 'dispatch'> {
    getters:  xxx;
    commit: xxx;
    dispatch: xxx;
}

// 然后导出该类型

export { EnhancedStore }

types.d.ts

这个文件就用来写各种工具类型,来帮我们转换成想要的类型

// 首先处理state
// 因为state没有额外的处理,我们直接定义即可

// 访问需要store.state[namespace][key]
import { State as CommonState } from '@/store/modules/common/state';
type ModuleStates = {
  commonModule: CommonState;
  // otherModule: OtherState;
};


// 处理getters
// 开启了namespaced的module要访问,需要store.getters['modulename/key'],故我们要构造这样的结构
// 将模块化的Getters 映射为带模块名字的字面量:commonModule/name

// 泛型G是包含模块中getters的类型
type NamespacedGetters<ModuleName extends string, G> = {
  [K in keyof G as `${ModuleName}/${string & K}`]: ReturnType<G[K]>;
};

// common模块中的Getters类型
import { Getters as CommonGetters } from '@/store/modules/common/getters';
// Root模块中的Getters类型
import { GettersInRoot } from '@/store/modules/root/types';
//使用工具类型对common模块转换得到新类型,root模块直接store.getters[key], 故不用改造,最后合并类型
type RootGetters = NamespacedGetters<'commonModule', CommonGetters> & GettersInRoot;


// 处理mutations
// 开启了namespaced的module要访问,需要store.commit['modulename/key'],故我们要构造这样的结构
// actions同理,故类型工具可以合并在一起
type StoreTypes = 'Actions' | 'Mutations';

// 将模块化的Actions/Mutations映射为带模块名的字面量
type NameSpacedActionsOrMutations<
  ModuleName extends string,
  AllTypes extends (...args: any[]) => any,
> = {
  [K in keyof AllTypes as `${ModuleName}/${K}`]: AllTypes[K];
};

// 处理commit和dispatch的options
type GetTypesOptions<T extends StoreTypes> =
  T extends Extract<StoreTypes, 'Mutations'> ? CommitOptions : DispatchOptions;

// 处理commit和action中,payload携带参数的类型与提示
// 意思是对应的commit/action函数,如果第二个参数payload为undefined,payload为可选,补充options
// 如果是不为undefined,则把推断的P当作payload的参数类型
type GetParametersTypes<Type extends StoreTypes, Fn> =
  Parameters<Fn> extends [any, infer P]
    ? undefined extends P
      ? [payload?: undefined, options?: GetTypesOptions<Type>]
      : [payload: P, options?: GetTypesOptions<Type>]
    : [payload?: undefined, options?: GetTypesOptions<Type>];

// 使用起来!!!增强Store的commit和dispatch类型推导

// 泛型T为mutations或者actions对象,返回一个函数,第一个参数type为对应泛型T中的key,剩余参数由上述GetParametersTypes决定
type EnhanceStoreTypes<
  Type extends StoreTypes,
  T extends Record<string, (...args: any[]) => any>,
> = <K extends keyof T>(type: K, ...args: GetParametersTypes<Type, T[K]>) => ReturnType<T[K]>;



// 现在已经编写完类型工具了,可以直接来实现mutations和actions
import { Mutations as CommonMutations } from '@/store/modules/common/mutations';
import { MutationsInRoot } from '@/store/modules/root/types';

type RootMutations = NameSpacedActionsOrMutations<'commonModule', CommonMutations> &
  MutationsInRoot;
type RootCommit = EnhanceStoreTypes<'Mutations', RootMutations>;

import { Actions as CommonActions } from '@/store/modules/common/actions';
import { ActionsInRoot } from '@/store/modules/root/types';

type RootActions = NameSpacedActionsOrMutations<'commonModule', CommonActions> & ActionsInRoot;
type RootDispatch = EnhanceStoreTypes<'Actions', RootActions>;

// 导出工具
export {
  ModuleStates,
  RootGetters,
  RootCommit,
  RootMutations,
  RootDispatch,
  RootActions,
  StoreTypes,
  GetTypesOptions,
};

到这里,已经完成编写类型了,可以回到root.d.ts完成增强后的store类型了

root.d.ts--update

import { Store as VuexStore } from 'vuex';
import { RootGetters, RootCommit, RootDispatch } from '@/store/types/types-helper';

interface EnhancedStore extends Omit<VuexStore<RootState>, 'getters' | 'commit' | 'dispatch'> {
  getters: RootGetters;
  commit: RootCommit;
  dispatch: RootDispatch;
}

export { EnhancedStore };

index.ts--update

import type { InjectionKey } from 'vue';
import type { EnhancedStore } from './types/root';
import type { RootState } from '@/store/modules/root/types';
import { createStore, Store as VuexStore, useStore as useBaseStore } from 'vuex';
import commonModule from './modules/common';
import { rootState, gettersInRoot, mutationsInRoot, actionsInRoot } from '@/store/modules/root';

const key: InjectionKey<VuexStore<RootState>> = Symbol(); // 创建一个具备类型安全的唯一注入 Key,用于在 Vue 组件中注入 Store 实例。

const store = createStore({
  state: () => rootState,

  mutations: mutationsInRoot, 

  actions: actionsInRoot, 

  getters: gettersInRoot,

  modules: {
    commonModule,
  },
}) as EnhancedStore;

function useStore(): EnhancedStore {
  return useBaseStore(key);
}

export { store, key, useStore };


现在去使用store,就会具有对应的类型推导和提示了!!!!!!

hook实现

types.d.ts

import { ComputedRef } from 'vue';
import { StoreTypes, GetTypesOptions } from '@/store/types/types-helper';

// 处理mutations和actions函数的参数和函数的返回值类型
type GetFnParametersAndReturnType<Type extends StoreTypes, Fn> =
  Parameters<Fn> extends [any, infer P]
    ? undefined extends P
      ? (payload?: undefined, options?: GetTypesOptions<Type>) => ReturnType<Fn>
      : (payload: P, options?: GetTypesOptions<Type>) => ReturnType<Fn>
    : (payload?: undefined, options?: GetTypesOptions<Type>) => ReturnType<Fn>;

// 根据mutations和actions对象,映射函数
type FnMapper<Type extends StoreTypes, T extends Record<string, (...args: any[]) => any>> = {
  [K in keyof T]: GetFnParametersAndReturnType<Type, T[K]>;
};

// 因为state和getters需要包装成响应式的computedRef
type StateOrGettersMapper<T> = {
  [K in keyof T]: ComputedRef<T[K]>;
};

// 导出
export { GetFnParametersAndReturnType, FnMapper, StateOrGettersMapper };


useStates.ts

import type { ComputedRef } from 'vue';
import type { RootState } from '@/store/modules/root/types';
import type { ModuleStates } from '@/store/types/types-helper';
import type { StateOrGettersMapper } from '@/hooks/store-hooks/types-helper';
import { computed } from 'vue';
import { useStore } from '@/store';

//  重载定义

// 根模块数组
function useStates<K extends keyof RootState>(keys: K[]): Pick<StateOrGettersMapper<RootState>, K>;

// 根模块别名
function useStates<M extends Record<string, keyof RootState>>(
  keys: M
): { [K in keyof M]: ComputedRef<RootState[M[K]]> };

// 模块命名空间数组
function useStates<N extends keyof ModuleStates, K extends keyof ModuleStates[N]>(
  namespace: N,
  keys: K[]
): Pick<StateOrGettersMapper<ModuleStates[N]>, K>;

// 模块命名空间别名
function useStates<N extends keyof ModuleStates, M extends Record<string, keyof ModuleStates[N]>>(
  namespace: N,
  keys: M
): { [K in keyof M]: ComputedRef<ModuleStates[N][M[K]]> };

//  实现体
function useStates(
  namespaceOrKeys:
    | keyof RootState
    | (keyof RootState)[]
    | Record<string, keyof RootState>
    | keyof ModuleStates,
  keysOrMaybeNothing?: any
) {
  const store = useStore();
  const result: Record<string, ComputedRef<any>> = {};

  if (
    Array.isArray(namespaceOrKeys) ||
    (typeof namespaceOrKeys === 'object' && namespaceOrKeys !== null)
  ) {
    // 根模块
    const keys = namespaceOrKeys;
    if (Array.isArray(keys)) {
      keys.forEach((key) => {
        result[key as string] = computed(() => store.state[key]);
      });
    } else {
      Object.entries(keys).forEach(([alias, key]) => {
        result[alias] = computed(() => store.state[key]);
      });
    }
  } else if (typeof namespaceOrKeys === 'string') {
    const namespace = namespaceOrKeys as keyof ModuleStates;
    const keys = keysOrMaybeNothing;

    if (Array.isArray(keys)) {
      keys.forEach((key) => {
        result[key as string] = computed(
          () => store.state[namespace][key as keyof ModuleStates[typeof namespace]]
        );
      });
    } else {
      Object.entries(keys).forEach(([alias, key]) => {
        result[alias] = computed(
          () => store.state[namespace][key as keyof ModuleStates[typeof namespace]]
        );
      });
    }
  }

  return result;
}

export default useStates;


useGetters.ts

import type { ComputedRef } from 'vue';
import type { RootGetters } from '@/store/types/types-helper';
import type { StateOrGettersMapper } from '@/hooks/store-hooks/types-helper';
import { computed } from 'vue';
import { useStore } from '@/store';

//  重载定义

// 方式 1:传数组(返回 Pick 工具函数)
function useGetters<K extends keyof RootGetters>(
  keys: K[]
): Pick<StateOrGettersMapper<RootGetters>, K>;

// 方式 2:传别名映射(返回别名类型)
function useGetters<M extends Record<string, keyof RootGetters>>(
  keys: M
): { [K in keyof M]: ComputedRef<RootGetters[M[K]]> };

//  实现

function useGetters(keys: (keyof RootGetters)[] | Record<string, keyof RootGetters>) {
  const store = useStore();
  const result: Record<string, any> = {};

  if (Array.isArray(keys)) {
    keys.forEach((key) => {
      result[key as string] = computed(() => store.getters[key]);
    });
  } else {
    Object.entries(keys).forEach(([alias, key]) => {
      result[alias] = computed(() => store.getters[key]);
    });
  }

  return result;
}

export default useGetters;


useMutations.ts

import { useStore } from '@/store';
import type { CommitOptions } from 'vuex';
import type { RootMutations } from '@/store/types/types-helper';
import type { GetFnParametersAndReturnType, FnMapper } from '@/hooks/store-hooks/types-helper';

//  重载定义
// 方式 1:传数组(返回 Pick 工具函数)
function useMutations<K extends keyof RootMutations>(
  keys: K[]
): Pick<FnMapper<'Mutations', RootMutations>, K>;

// 方式 2:传别名映射(返回别名类型)
function useMutations<M extends Record<string, keyof RootMutations>>(
  keys: M
): {
  [K in keyof M]: GetFnParametersAndReturnType<'Mutations', RootMutations[M[K]]>;
};

// 实现

function useMutations(keys: (keyof RootMutations)[] | Record<string, keyof RootMutations>) {
  const store = useStore();
  const result: Record<string, any> = {};

  if (Array.isArray(keys)) {
    keys.forEach((key) => {
      result[key as string] = (payload?: any, options?: CommitOptions) =>
        store.commit(key as keyof RootMutations, payload, options);
    });
  } else {
    Object.entries(keys).forEach(([alias, key]) => {
      result[alias] = (payload?: any, options?: CommitOptions) =>
        store.commit(key, payload, options);
    });
  }

  return result;
}

/**
 * 含有payload的mutation使用时,不传会报错
 * 定义时未定义的则为可选,undefined
 *
 */
export default useMutations;


useActions.ts

import { useStore } from '@/store';
import type { DispatchOptions } from 'vuex';
import type { RootActions } from '@/store/types/types-helper';
import type { GetFnParametersAndReturnType, FnMapper } from '@/hooks/store-hooks/types-helper';

// 重载定义
// 1. 数组形式,返回 Pick
function useActions<K extends keyof RootActions>(
  keys: K[]
): Pick<FnMapper<'Actions', RootActions>, K>;

// 2. 别名映射形式,返回对应别名映射
function useActions<M extends Record<string, keyof RootActions>>(
  keys: M
): {
  [K in keyof M]: GetFnParametersAndReturnType<'Actions', RootActions[M[K]]>;
};

// 实现
function useActions(keys: (keyof RootActions)[] | Record<string, keyof RootActions>) {
  const store = useStore();
  const result: Record<string, any> = {};

  if (Array.isArray(keys)) {
    keys.forEach((key) => {
      result[key as string] = (payload?: any, options?: DispatchOptions) =>
        store.dispatch(key as keyof RootActions, payload, options);
    });
  } else {
    Object.entries(keys).forEach(([alias, key]) => {
      result[alias] = (payload?: any, options?: DispatchOptions) =>
        store.dispatch(key, payload, options);
    });
  }

  return result;
}
export default useActions;


index.ts

import useStates from '@/hooks/store-hooks/useStates';
import useGetters from '@/hooks/store-hooks/useGetters';
import useMutations from '@/hooks/store-hooks/useMutations';
import useActions from '@/hooks/store-hooks/useActions';

export { useStates, useGetters, useMutations, useActions };

这样子我们就封装好了,就可以和开头一样使用啦,有安全的类型提示啦!!!!

Web学习笔记(一):HTML篇

作者 陌离Morely
2025年8月23日 23:41

声明:本篇笔记部分摘自《Web前端技术 - 航空工业出版社》,遵循CC BY 4.0协议。 存在由AI生成的小部分内容,仅供参考,请仔细甄别可能存在的错误。 点击查看我的博客原文


一、HTML概述

HTML (HyperText Markup Language,超文本标记语言)是用于创建和设计网页的标准标记语言。它通过一系列 标签(Tags) 定义网页的结构和内容,浏览器会解析这些标签并渲染成用户看到的页面。

二、常用HTML标签

1.基本结构

<!DOCTYPE html>    <!-- 文档类型声明 -->
<html>     <!-- HTML部分 -->
<head>    <!-- 网页头部 -->
<meta charset="utf-8">    <!-- 元数据,声明字符集 -->
<title> </title>    <!-- 网页标题 -->
<link rel="shortcut icon" href="img/favicon.png">    <!-- 链接网页图标 -->
</head>
<body>     <!-- 网页主体部分 -->
<!-- 网页可见部分 -->
</body>
</html>

使用规范专用、结构清晰的标签,可以方便搜索引擎整理网页内容,有利于信息检索。

2.常用标签

① 文档标签

  • <!DOCTYPE>:文档声明,<!DOCTYPE html>表明此文档使用H5标准。
  • <html>:又称根标签,>表明这是一个H5文档。
  • <head>:标记文档头部,存储网页基本信息。
    • <meta>:元信息标签,用于设置描述和关键词,以便搜索引擎检索。
      • 字符集:<meta charset="utf-8"> 定义网页使用utf-8字符集。
      • 网页视口:<meta name="viewport"> 设置视口高度、缩放比等,常用于在响应式设计中使网页适配移动端。
    • <title>:标记网页标题,显示在浏览器标签上。
    • <link>:链接外部资源,规定了当前文档与某个外部资源的关系。
      • 链接图标:<link rel="shortcut icon" href="img/favicon.png">
      • 链接CSS样式:<link rel="stylesheet" type="text/css" href="index.css">
  • <body>:标记文档主体,用于设置展示给用户的内容。

② 结构标签

  • <header>:页眉标签,通常包含网站Logo、网页主导航和搜索框等。
  • <nav>:导航标签,标记页面导航的链接组,如主菜单、侧边栏导航或者页内导航等。
  • <article>:文章块标签,用于标记一块完整独立的内容,如文章、博客条目,用户评论。
  • <section>:区块标签,用于标记文档中的节,从而对内容进行分区,如章节、页眉页脚。
  • <aside>:附栏标签,用于标记引用内容、广告等与内容无关的部分。
  • <footer>:页脚标签,用于标记文档或节的页脚,如友链、版权等信息。
  • <div>:块级无语义容器,用于模块化布局。
  • <span>:行内无语义标签,常用标记于文章标题下的作者、时间、地点等附属信息。

★ 使用示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>简单网页示例</title>
</head>
<body>
    <!-- 页眉部分 -->
    <header>
        <h1>我的网站</h1>
        <nav>
            <ul>
                <li><a href="#">首页</a></li>
                <li><a href="#">关于我们</a></li>
                <li><a href="#">联系我们</a></li>
            </ul>
        </nav>
    </header>

    <!-- 主体内容 -->
    <main>
        <!-- 文章部分 -->
        <article>
            <h2>欢迎来到我的博客</h2>
            <section>
                <h3>第一章:HTML 简介</h3>
                <p>HTML 是用于构建网页的标准标记语言。</p>
            </section>
            <section>
                <h3>第二章:CSS 简介</h3>
                <p>CSS 用于控制网页的样式和布局。</p>
            </section>
            <footer>
                <p>发布时间:<span>2023年10月10日</span> | 作者:<span>张三</span></p>
            </footer>
        </article>

        <!-- 附栏部分 -->
        <aside>
            <h3>相关链接</h3>
            <ul>
                <li><a href="#">HTML 教程</a></li>
                <li><a href="#">CSS 教程</a></li>
            </ul>
        </aside>
    </main>

    <!-- 页脚部分 -->
    <footer>
        <p>&copy; 2023 我的网站. 版权所有.</p>
        <nav>
            <ul>
                <li><a href="#">隐私政策</a></li>
                <li><a href="#">使用条款</a></li>
            </ul>
        </nav>
    </footer>
</body>
</html>

③ 文本标签

  • <h1> ~ <h6>:1级标题 ~ 6级标题,默认使文字加粗,字号依次减小。
  • <p> (paragraph):段落标签,用于标记段落文本,默认使用系统的字体字号。
  • <strong>:强调标签,呈现粗体效果,语气较重。
    • <b>:只有加粗效果,无强调作用。
  • <em> (emphasis):强调标签,呈现斜体效果,语气较轻。
    • <i>:只有斜体效果,无强调作用。
  • <sup>:标记上标,如x2Register®x^{2}、Register^{®}
  • <sub>:标记下标,如x1CaCO3x_{1}、CaCO_{3}
  • <ins> (insert):表示插入的文本,默认添加下划线样式。
  • <del> (delete):表示删除的文本,默认添加删除线样式。
  • <abbr> (abbreviation):标记简称或缩写词,鼠标悬停时使用气泡显示全称。
    • 如:HTML
  • <br />:实现文本换行,不建议大量使用。
  • <hr />:标记水平线。
    • align属性:设置对齐方式,center 居中 | left 左对齐 | right 右对齐
    • size属性:设置粗细,以像素(px)为单位,默认2px
    • width属性:设置宽度,单位为px或%,默认100%
    • color属性:设置颜色,可用颜色名、#RGB十六进制、(r, g, b)设置。
  • <dfn>:用于标记专用术语,默认添加斜体效果。
  • <pre>:表示预定义格式文本,即保利原有的空格和换行。
  • <code>:用于标记代码或文件名,一般包裹在<pre>标签中以保留原有的格式。

④ 特殊字符转义

字符 含义 代码 解释
空格 &nbsp; Non-Breaking Space
< 小于号 &lt; less than
大于号 &gt; great than
& 逻辑与符号 &amp; ampersand
人民币符号 &yen; 类似拼音
© 版权符号 &copy; copyright
® 注册商标符号 &reg; register
° 度符号 &deg; degree
± 正负号 &plusmn; plus-minus
× 乘号 &times;
÷ 除号 &divide;

⑤ 多媒体

  • ★ 路径表示法

    • 图片在同级目录下:example.png
    • 图片在下级目录下:dic/example.png
    • 图片在上级目录下:../dic/example.png
  • 图片标签:<img src="路径" alt="提示文本" />

    • src支持链接 JPEG 、 GIF 和 PNG 三种格式的图片
  • 音频标签:<audio src="路径" controls="controls">提示文本</audio>

    • controls属性:显示音频控件
  • 视频标签:<video src="路径" controls="controls">提示文本</video>

    • controls属性:显示视频控件
  • 流标签:<figure></figure>

    • 表示页面中的一块独立的内容,表现为具有左右缩进的内容快。
    • <figcaption>:嵌套在<figure中标记流的标题,可以省略。

★ 使用示例

<figure>
<figcaption>流标题</figcaption>
<img src="img/p1.jpg" alt="示例图片" />
<p>流内容</p>
</figure>

⑥ 列表

  • 无序列表:
      • 各级列表项前,默认分别显示实心圆、空心圆、实心方块图标。
      • 也可通过type= "disc" "circle" "square" 强制指定序号样式。
    • 有序列表:<ol></ol>,具有以下属性:
      • reversed="reversed":降序排列(仅颠倒编号,各列表项内容不颠倒)。
      • start="1":指定序号的起始值。
      • type="1" "A" "a" "I" "i":指定序号的样式。
    • 自定义列表: <dl></dl>
      • 使用 <dt></dt> 标记列表标题
      • 使用 <dd></dd> 标记列表内容
    • 列表项:<li></li>,与<ol>相似,具有以下属性:
      • value="1":指定当前项的序号,并使之后的列表项重新编号。
      • type="1" "A" "a" "I" "i":指定序号的样式。

    ★ 使用示例

    <ul type="disc">
    <li>无序列表第一项</li>
    <li>无序列表第二项</li>
    <ol reversed="reversed">
    <li value="1" type="A">有序列表第一项</li>
    <li>有序列表第二项</li>
    </ol>
    </ul>
    <dl>   <!-- 自定义列表中可以有多个标题,列表项没有项目符号,也不强调次序 -->
    <dt> 自定义列表标题</dt>
    <dd>自定义列表第一项</dd>
    <dd>自定义列表第二项</dd>
    <dt>自定义列表标题</dt>
    <dd>自定义列表项目<dd>
    </dl>
    

    ⑦ 超链接

    • <a href="目标地址"> 载体 </a>
      • href属性:必须设置,若暂时未确定地址,用href="#"将链接置空。
      • target属性:self 当前窗口打开 | blank 新窗口打开
      • download属性:指定资源的文件名,并且强制浏览器执行下载操作(仅Chrome和FIreFox支持)。
    • 锚点链接:设置某个标签的id属性,将链接的href属性设置为href="#id名称",可以创建一个锚点。用户点击链接时会自动跳转到指定id所在的标签处。
    • 电子邮件链接:href = "mainto:电子邮件地址?subject=邮件主题"
    • 图像热点链接:在一张图片上根据坐标分别设置不同区域的超链接。步骤如下:
      • 在图片标签<img />下添加一个<map>标签,其name属性为图片的id,表示添加图像热点链接的作用区域
      • <map>标签中添加几个标签,使用下列属性设置热点链接:
        • shape:circle 圆形 | rect 矩形 | poly 多边形
        • coords:关键点的坐标,参数如下:
          • circle形状:coords = "圆心x, 圆心y,半径"
          • rect形状:coords = "左上顶点x, 左上顶点y, 右下顶点x, 右下顶点y"
          • poly形状:coords = "顶点1x, 顶点1y, 顶点2x, 顶点2y, …"

    ★ 使用示例

    <a href="img/p1.png" target="_blank" id=pic_dog>  <!-- 简单的超链接示例 -->
    <img src="img/p1.png" alt="小狗" />
    <p>点击预览</p>
    </a>
    
    <a href="#pic_dog">  <!-- 锚点示例:点击跳转到小狗图片 -->
    <p>查看图片</p>
    </a>
    
    <img src="img/main.png" alt="动物大全"  usermap="#map"/>  <!-- 图像热点链接 -->
    <map name="map">  <!-- 属性值应与usermap的值相同 -->
    <area shape="circle" coords="88, 77, 63" href="img/dogs.png" alt="小狗">
    <area shape="rect" coords="26, 190, 151, 357" href="img/cats.png" alt="小猫">
    </map>
    

    ⑧ 表格

    • 基本结构
      • <table>:标记表格。
      • <caption>:标记表格的标题。
      • <tr>:标记表格中的一行。
      • <th>:包含在<tr>中,标记表头内容,默认加粗居中。
      • <td>:包含在<tr>中,标记普通内容,默认不加粗左对齐。
    • 表格分组
      • 按行分组:
        • <thead>:标记表头部分(<th>标记的是表头的一格)。
        • <tbody>:标记表体部分。
        • <tfoot>:标记表尾部分。
      • 按列分组:
        • <col>:包含在<table>中,通过 span属性 设置每组的列数。
    • 常用属性
      • 整体边框
        • 设置<table>border属性,单位为px。
      • 单元格的内外边距
        • 内边距(内容 - 边框):设置<table>cellpadding属性,单位为px。
        • 外边距(边框 - 边框):设置<table>cellspacing属性,单位为px。
        • 图示: 20250514174921790.png
          • 这两个属性不常在HTML中使用(已过时),而是使用CSS中的border-spacing属性。
      • 表格内外边距(外遵框架frame,内守规矩rulles)
        • 表格内边框:设置<table>rules属性(已废弃) ,取值如下:
          • none:不显示内边框
          • all:显示所有边框
          • groups:只显示分组的边框
          • rows:显示行之间的边框
          • cols:显示列之间的边框
        • 表格外边框:设置<table>frame属性(已废弃) ,取值如下:
          • void:不显示外边框
          • box、boder:显示所有外边框
          • above:显示上边框
          • below:显示下边框
          • lhs:显示左外边框
          • rhs:显示有外边框
          • hsides:(horizon sides)显示上下边框
          • vsides:(vertical sides)显示左右边框
      • 单元格跨行、跨列
        • 跨行:设置<th><td>的rowspan属性,值为跨行数。
        • 跨列:设置<th><td>的colspan属性,值为跨列数。

    ★ 使用示例

    <table border="1" rules="all">      <!-- 以1px显示所有外边框 -->
    <caption>表格标题</caption>
    <col class="c1" span="1" />     <!-- 垂直分组:第一组占一列 -->                
    <col class="c2" span="3" />     <!-- 垂直分组:第二组占三列 -->
    <!-- 水平分组:表头部分 -->
    <thead>
    <tr><th>表头第一格</th><th>表头第二格</th><th>表头第三格</th><th>表头第四格</th></tr>
    </thead>
    <!-- 水平分组:表体部分 -->
    <tbody>
    <tr>
    <!--跨行内容-->
    <th rowspan="2">内容(1,1)<br/>(占两行)</th>
    <td>内容(1,2)</td><td>内容(1,3)</td><td>内容(1,4)</td>
    </tr>
    <tr>
    <td>内容(2,2)</td><td>内容(2,3)</td><td>内容(2,4)</td>
    </tr>
    <tr>
    <th>内容(3,1)</th>
    <td>内容(3,2)</td><td>内容(3,3)</td><td>内容(3,4)</td>
    </tr>
    </tbody>
    <!-- 水平分组:表尾部分 -->
    <tfoot>
    <tr>
    <th>表尾第一格</th>
    <!--跨列内容-->
    <th colspan="3">表尾第二格(占三列)</th></tr>
    </tfoot>
    </table>
    

    ★ 表格效果:

                                
    表格标题
    表头第一格 表头第二格 表头第三格 表头第四格
    内容(1,1)
    (占两行)
    内容(1,2) 内容(1,3) 内容(1,4)
    内容(2,2) 内容(2,3) 内容(2,4)
    内容(3,1) 内容(3,2) 内容(3,3) 内容(3,4)
    表尾第一格 表尾第二格(占三列)

    ⑨ 表单

    • 基本组成:表单域、表单控件、提交按钮、提示信息。
      • 表单域:网页中放置表单控件与提示信息的区域,用于采集用户输入信息并传输到服务器。
        • <form action="提交地址" method="提交方式"></form>(form标签不可互相嵌套。)
          • action属性:表示数据提交的地址,一般是一个URL,开发初期可使用#占位置空。
          • method属性:提交表单数据的方式,默认为get,一般使用post。
          • name属性:表单的名称。
          • autocomplete属性:自动记录并弹出历史记录。取值: on | off
          • novalidate属性:值为novalidate,若设置则不会对输入的内容进行检查。
          • enctype属性:设置数据发送到服务器时的编码类型,取值:
            • application/x-www-form-urlencoded:表示对所有字符编码再传输,会导致大文件传输效率降低。
            • mutipart/formdata:表示传输的数据为二进制类型。
            • text/plain:表示传输纯文本,不编码特殊字符,但是空格转换为加号“+”。
          • target属性:表示表单数据提交地址的打开方式,取值:self 当前窗口打开 | blank 新窗口打开
      • 提交按钮:用于用户确定信息填写完毕后将其传输至服务器。
      • 提示信息:提示用户输入信息的内容和类型。
      • 常用表单控件:提供表单功能,如文本框、按钮、单/复选框、搜索框等。
        • <input type="text" />:单行文本框,用于输入简短的文本,如账号密码。
        • <input type="password" />:密码文本框,会隐藏输入的内容,显示黑色圆点。
        • <input type="radio" />:单选框,用于单项选择,如性别、年级等。
        • <input type="checkbox" />:复选框,用于多项选择(也可以单选),如兴趣爱好爱好。
        • <input type="button" />:普通按钮,用于标记可单机的按钮,通过value属性可设置按钮内容。
          • 作用同<button>标签,后者可嵌入文本、图像等内容,同时拥有更丰富的样式。
        • <input type="submit" />:提交按钮,用于提交用户输入的数据,默认内容为“提交”。
        • <input type="reset" />:重置按钮,用于清空表单中的数据,默认内容为“重置”。
        • <input type="image" />:图像形式的提交按钮,使用图像代替普通提交按钮样。
        • <input type="file" />:文件域,包含一个“选择文件”的按钮和表示选中文件的文本,用户单机按钮可选择文件上传。
        • <input type="email" />:邮箱地址文本框,支持验证邮箱格式正确性,并提示错误信息。
        • <input type="url" />:地址文本框,支持验证URL格式正确性,并提示错误信息。
        • <input type="tel" />:电话号码文本框,通过pattern属性设置正则表达式限制输入格式。
        • <input type="search" />:搜索框,能够记录输入的字符,作为网站搜索的关键词。
        • <input type="number" />:数值文本框,只能输入数字,支持设置max,min,step,value属性限制输入内容的边界、间隔和默认值。
        • <input type="range" />:数值范围滑块,将数值文本框显示为滑动条控件。
        • <input type="date" />:日期时间文本框,可通过设置type来控制时间的精度:date(天) | week(周) | month(月) | time(分钟)
      • 其他表单控件:
        • <textarea clos="列数" rows="行数" palcehoder=“提示信息”>:文本区域(支持输入多行文本,类似于留言板)
        • <select size="选项个数" mutiple="mutiple"><option>选项一</option><option>选项二</option><option>选项三</option></select>:选择框(下拉列表)
          • 若为select设置mutiple属性,则选项会按多行显示,且支持按Ctrl多选
          • 若为option设置selected属性,默认选中此选项
          • 若选项较多,可使用<optgroup label="组名"></optgroup>包含多个<option>标签,进行选项分组
        • 数据列表:支持用户输入关键词匹配选项,同时也支持用户直接选择列表中的选项,格式如下:

    20250514175031977.png

    <input type="类型" list="列表名称">
    <datalist id="列表名称">
    <option label="说明内容1">选项1</option> <!-- 说明内容不会被填入输入框 --->
    <option label="说明内容2">选项2</option>
    ...
    </datalist>
    
    • 常用表单属性
    属性 属性值 说明
    name 自定义 表单控件的名称
    value 自定义 表单控件的默认值
    readonly readonly 表单控件不可编辑修改
    disabled disabled 禁用该表单控件(显示为灰色)
    checked checked 该项默认选中(单选钮或复选框)
    autocomplete on/off 自动完成功能
    autofocus autofocus 自动获取焦点
    form <form>的id属性值 指定控件所属表单
    placeholder 字符串 显示在输入型文本框中的输入提示
    required required 该表单控件不可为空
    pattern 字符串(正则表达式) 验证输入内容的模式
    • 提示信息:<label for="目标控件id">提示信息</label>
      • 用于单选/复选框选择钮后的文字说明,或按钮中的文字(如:○ 18岁以下)
      • 点击提示信息也能够激活对应的控件,有利于优化用户体验
    • 表单对象分组:<fieldset>
      • 格式:
        <fieldset>
            <legend>登录</legend>
            <p>账号:<input type="text" /></p>
            <p>密码:<input type="password" /></p>
        </fieldset>
    

    20250514175326762.png


    ★ 常用表单标签使用示例

    <form action="#" method="post">
                <p>会员信息表</p>
                <fieldset>
                    <legend>基本信息</legend>
                    <label for="name">昵称:</label>
                    <input id="name" type="text" /><br /><br />
                    <label for="idc">头像:</label>
                    <input id="idc" type="file" />
                </fieldset>
                <fieldset>
                    <legend>其他信息</legend>
                    <p>性别:
                        <label for="nan"></label>
                        <input id="nan" type="radio" name="rad" />
                        <label for="nv"></label>
                        <input id="nv" type="radio"  name="rad" />
                    </p>
                    <p>兴趣:
                        <label for="chang">唱歌</label>
                        <input id="chang" type="checkbox" name="chb" />
                        <label for="tiao">跳舞</label>
                        <input id="tiao" type="checkbox" name="chb" />
                        <label for="dong">运动</label>
                        <input id="dong" type="checkbox" name="chb" />
                    </p>
                    <p>
                        <label for="gq">个性签名:</label><br />
                        <textarea id="gq" cols="40" rows="5"></textarea>
                    </p>
                </fieldset><br />
                <input type="submit" />
                <input type="reset" />
            </form>
    

    ★显示效果

    20250514175343378.png

    JavaScript事件循环:一次浏览器线程的"约会"指南

    作者 日月晨曦
    2025年8月23日 23:25

    你是否曾好奇,为什么setTimeout有时候"不准时"?为什么Promise.then总能插队成功?今天,我们就来揭秘JavaScript中最核心的异步机制——事件循环(Event Loop),看看浏览器里的线程们是如何"约会"的。

    一、进程与线程:浏览器里的"打工人"和"部门"

    • 进程:相当于一个完整的"公司",比如打开一个Chrome标签页就是创建了一个新公司。它包含了所有资源和员工(线程),从成立到倒闭(关闭标签)的整个生命周期。
    • 线程:进程里的"打工人",比如负责和服务器聊天的HTTP线程、负责执行JS代码的引擎线程、负责画画的渲染线程

    有意思的是,JS引擎线程渲染线程是对"冤家"——它们不能同时工作!就像公司里的两个部门抢会议室,一个用着另一个就得等着。这就是为什么JS是"单线程"的,同一时间只能干一件事。

    二、异步:单线程的"时间管理大师"

    JS既然是单线程,那遇到耗时任务(比如请求数据)怎么办?总不能干等着吧?于是它学会了"时间管理":

    • 先把同步代码全部执行完(这是主线任务)
    • 遇到异步代码,就把它暂时放到"任务队列"里排队
    • 等主线任务干完了,再去"任务队列"里取异步代码执行

    这就像你在公司上班:先处理完手头的紧急工作(同步代码),再去看邮件(异步任务)。

    三、Event Loop:异步任务的"优先级排序"

    异步任务也分"高低贵贱",Event Loop就是负责给它们排优先级的"HR":

    1. 微任务:办公室里的"关系户"

    微任务是最优先处理的,相当于公司里的"关系户",包括:

    • Promise.then
    • process.nextTick(Node.js)
    • MutationObserver

    这些任务会在同步代码执行完后立即插队执行,而且会一直执行到队列为空。

    2. 宏任务:老老实实排队的"普通员工"

    宏任务就得老老实实排队,包括:

    • setTimeout/setInterval
    • AJAX请求
    • I/O操作
    • UI渲染

    3. 执行顺序:就像吃火锅

    Event Loop的执行顺序可以类比吃火锅:

    1. 先吃主食(同步代码)
    2. 再吃小料(微任务)
    3. 然后涮肉(渲染页面)
    4. 最后煮面条(执行宏任务,开启下一轮循环)

    四、代码实战:Event Loop的"狼人杀"

    来看个经典例子,猜猜输出顺序:

    console.log('script start');
    async function async1() {
      await async2()
      console.log('async1 end');
    }
    async function async2() {
      console.log('async2 end');
    }
    async1()
    setTimeout(() => {
      console.log('setTimeout');
    }, 0)
    new Promise((resolve) => {
      console.log('promise');
      resolve()
    }).then(() => {
      console.log('then1');
    }).then(() => {
      console.log('then2');
    });
    console.log('script end');
    

    正确答案

    script start
    promise
    script end
    async2 end
    async1 end
    then1
    then2
    setTimeout
    

    是不是很神奇?这里的关键是:await会先执行右边的代码,然后把后续代码扔入微任务队列。

    五、await:异步世界的"插队小能手"

    async/await是Promise的语法糖,但它有个特殊能力:

    1. 会把后续代码挤入微任务队列
    2. 浏览器会"提前"执行await后面的代码(相当于同步)

    就像你在排队买奶茶,突然有个人拿着await的VIP卡,先点单(执行右边代码),然后去旁边等着(后续代码入微任务),等前面的人都买完了,他再回来取奶茶。

    总结:Event Loop的"潜规则"

    1. 同步代码先执行(宏任务的一部分)
    2. 微任务队列清空后才会执行宏任务
    3. 每个宏任务执行完后,会检查微任务队列
    4. await后面的代码是微任务

    理解了Event Loop,你就能解释为什么有些代码的执行顺序总是超出预期,也能写出更高效的异步代码。下次面试遇到这类问题,记得用"公司部门"

    前端居中布局:从 "卡壳" 到 "精通" 的全方位指南

    作者 复苏季风
    2025年8月23日 23:11

    🚀 前端居中布局:从 "卡壳" 到 "精通" 的全方位指南

    你是否也曾在调试居中布局时,对着屏幕反复修改代码却毫无进展?是否在面试被问到 "如何实现元素居中" 时,只能说出一两种方法而错失加分机会?

    居中布局作为前端开发的基础中的基础,既是日常开发的高频需求,也是面试中的常客。但看似简单的 "居中" 二字,背后却藏着十几种实现方案,每种方案都有其适用场景和坑点。

    今天这篇文章,我们就用 "场景化 + 对比分析" 的方式,带你彻底吃透前端居中布局的所有核心方案,从水平居中到垂直居中,再到复合场景的水平垂直居中,让你面对任何居中需求都能游刃有余。

    🔍 先搞懂前提:这些概念决定你的方案选择

    在开始之前,我们需要明确几个关键概念,它们直接影响居中方案的选择:

    关键因素 核心影响 常见场景
    元素类型 行内元素 (inline)、块级元素 (block)、行内块元素 (inline-block) 的居中方案截然不同 文本 (行内)、div(块级)、图片 (行内块)
    尺寸确定性 元素是否有固定宽高,决定了能否使用基于尺寸计算的偏移方案 固定宽度卡片 (定宽)、动态文本容器 (不定宽)
    兼容性要求 需兼容 IE 还是仅支持现代浏览器,决定了能否使用 Flex/Grid 等现代方案 企业级旧系统 (需兼容 IE)、新开发 H5 页面 (现代浏览器)
    父容器限制 父容器是否有固定高度、是否使用特殊布局 (如定位、Flex),影响子元素居中策略 全屏弹窗 (父容器全屏)、滚动容器内居中

    一、水平居中:让元素在 X 轴上 "站对位置"

    水平居中是最常见的布局需求,实现方案根据元素类型和尺寸特性可分为三大类:

    1. 行内 / 行内块元素:最简单的文本级居中

    适用场景:文本、链接、按钮、图片等行内 (inline) 或行内块 (inline-block) 元素。

    核心方案:父元素设置text-align: center
    .parent {
      text-align: center; /* 核心属性 */
      background: #f0f0f0;
      padding: 20px;
    }
    
    .child {
      /* 行内元素无需额外设置 */
      /* 行内块元素需保持inline-block特性 */
      display: inline-block;
      width: 100px;
      height: 50px;
      background: #42b983;
    }
    

    效果演示

    +------------------------+
    |                        |
    |        [ child ]       |  ← 子元素水平居中
    |                        |
    +------------------------+
    

    优缺点分析

    优点 缺点
    1. 实现简单,一行代码搞定 2. 兼容所有浏览器 (包括 IE6) 3. 支持动态宽度元素 1. 会影响父元素内所有行内元素的对齐方式 2. 块级子元素需转为 inline-block 才生效

    实战技巧:如果只需单个元素居中,可给该元素套一层容器,避免影响其他元素:

    预览

    <div class="parent">
      <!-- 其他不受影响的内容 -->
      <div class="center-wrapper" style="text-align: center;">
        <div class="child" style="display: inline-block;">需要居中的元素</div>
      </div>
    </div>
    

    2. 块级元素:分 "定宽" 和 "不定宽" 两种情况

    块级元素默认占满父容器宽度,因此水平居中的核心是 "收缩宽度 + 左右留白"。

    (1)定宽块级元素:经典的margin: 0 auto

    适用场景:已知宽度的块级元素(如固定宽度的卡片、容器)。

    .parent {
      background: #f0f0f0;
      padding: 20px;
    }
    
    .child {
      width: 200px; /* 必须指定宽度 */
      height: 100px;
      margin: 0 auto; /* 左右margin自动平分剩余空间 */
      background: #42b983;
    }
    

    原理图解

    
    +------------------------+
    |                        |
    |  [------child------]   |
    |  ↑      定宽       ↓   |
    |  左margin自动      右margin自动
    |                        |
    +------------------------+
    

    常见误区

    • 忘记设置width:块级元素默认宽度为 100%,margin: 0 auto不会有任何效果
    • 对行内元素使用:margin: 0 auto仅对块级元素生效,需先设置display: block
    • 父元素有浮动:若父元素浮动,需清除浮动后居中才会生效
    (2)不定宽块级元素:现代布局方案

    适用场景:宽度随内容变化的块级元素(如动态文本容器、自适应卡片)。

    方案 A:Flex 布局(推荐)
    .parent {
      display: flex;
      justify-content: center; /* 主轴(水平)居中 */
      background: #f0f0f0;
      padding: 20px;
    }
    
    .child {
      /* 无需指定宽度 */
      height: 100px;
      background: #42b983;
    }
    
    方案 B:定位 + transform
    .parent {
      position: relative; /* 父元素相对定位 */
      background: #f0f0f0;
      padding: 20px;
      height: 100px; /* 需有明确高度或被内容撑开 */
    }
    
    .child {
      position: absolute;
      left: 50%; /* 相对于父元素左移50% */
      transform: translateX(-50%); /* 相对于自身左移50% */
      background: #42b983;
    }
    

    两种方案对比

    方案 优点 缺点 兼容性
    Flex 布局 1. 代码简洁 2. 支持多个子元素同时居中 3. 不影响元素定位特性 1. 父元素会变为 Flex 容器,影响子元素布局 2. 旧浏览器 (IE9 及以下) 不支持 IE10+
    定位 + transform 1. 不影响父元素布局 2. 子元素可叠加其他定位属性 1. 子元素脱离文档流,可能影响其他元素 2. transform 在 IE9 以下不支持 IE9+

    水平居中方案全景对比表

    元素类型 尺寸特性 推荐方案 核心代码 兼容性
    行内 / 行内块 任意 text-align: center 父元素设置 text-align: center IE6+
    块级元素 定宽 margin: 0 auto 子元素设置 width 和 margin: 0 auto IE6+
    块级元素 不定宽 Flex 布局 父元素 display: flex; justify-content: center IE10+
    块级元素 不定宽 定位 + transform 子元素 left: 50%; transform: translateX (-50%) IE9+

    二、垂直居中:比水平居中更 "棘手" 的布局难题

    垂直居中因受元素高度、父容器高度、文档流等多重因素影响,实现方案更为复杂。

    1. 行内元素:文本与图文组合的垂直居中

    (1)单行文本:line-height神来之笔

    适用场景:按钮文字、标题等单行文本元素。

    .parent {
      height: 100px; /* 父元素固定高度 */
      background: #f0f0f0;
    }
    
    .child {
      line-height: 100px; /* 行高 = 父元素高度 */
    }
    

    原理图解

    +------------------------+
    |                        |
    |       [ child ]        |  ← 文本垂直居中
    |                        |
    +------------------------+
     ↑
     行高 = 父元素高度,文本在一行内垂直居中
    

    注意事项

    • 文本不能换行,换行后会导致多行文本间距过大
    • 子元素若有上下内边距 (padding),需相应减小line-height的值
    (2)多行文本 / 行内块元素:表格布局的妙用

    适用场景:多行文本段落、图片 + 文字组合等行内块元素。

    .parent {
      display: table-cell; /* 模拟表格单元格 */
      vertical-align: middle; /* 垂直居中 */
      height: 200px; /* 必须指定高度 */
      width: 300px; /* 需指定宽度 */
      background: #f0f0f0;
    }
    
    .child {
      display: inline-block; /* 行内块元素 */
      background: #42b983;
      padding: 10px;
    }
    

    效果对比

    未居中 已居中
    +----------------+ ``` [child] <br> <br> <br>+----------------+``` +----------------+ ``` <br> [child] <br> <br>+----------------+```

    2. 块级元素:从 "定高" 到 "不定高" 的解决方案

    (1)定高块级元素:经典定位 + 负 margin

    适用场景:已知高度的块级元素(如固定尺寸的弹窗、卡片)。

    .parent {
      position: relative;
      height: 300px;
      background: #f0f0f0;
    }
    
    .child {
      position: absolute;
      top: 50%; /* 相对于父元素上移50% */
      height: 100px; /* 固定高度 */
      margin-top: -50px; /* 向上偏移自身高度的一半 */
      background: #42b983;
    }
    

    计算逻辑margin-top = -元素高度 ÷ 2,这是一种基于精确计算的偏移方案。

    (2)不定高块级元素:现代布局的优雅实现
    方案 A:Flex 布局(推荐)
    .parent {
      display: flex;
      align-items: center; /* 交叉轴(垂直)居中 */
      height: 300px;
      background: #f0f0f0;
    }
    
    .child {
      /* 无需指定高度 */
      background: #42b983;
      padding: 20px;
    }
    
    方案 B:Grid 布局(极简语法)
    .parent {
      display: grid;
      align-items: center; /* 垂直居中 */
      height: 300px;
      background: #f0f0f0;
    }
    
    .child {
      background: #42b983;
      padding: 20px;
    }
    

    Flex 与 Grid 垂直居中对比

    布局方式 语法特点 适用场景 兼容性
    Flex 布局 需设置display: flexalign-items: center 适合一维布局 (单行 / 单列) IE10+
    Grid 布局 需设置display: gridalign-items: center 适合二维布局 (多行多列) IE11+(部分支持)

    垂直居中方案全景对比表

    元素类型 尺寸特性 推荐方案 核心代码 兼容性
    行内元素(单行) 任意 line-height 子元素 line-height = 父元素高度 IE6+
    行内元素(多行) 任意 表格布局 父元素 display: table-cell; vertical-align: middle IE8+
    块级元素 定高 定位 + 负 margin 子元素 top: 50%; margin-top: - 高度 / 2 IE6+
    块级元素 不定高 Flex 布局 父元素 display: flex; align-items: center IE10+
    块级元素 不定高 Grid 布局 父元素 display: grid; align-items: center IE11+

    三、水平垂直居中:复合场景的综合解决方案

    当需要同时实现水平和垂直居中时,我们可以组合上述方案,形成更强大的居中策略。

    1. 现代浏览器首选:Flex 布局(万能方案)

    适用场景:几乎所有场景(定宽高 / 不定宽高、单行 / 多行),尤其是现代 Web 应用。

    .parent {
      display: flex;
      justify-content: center; /* 水平居中 */
      align-items: center; /* 垂直居中 */
      height: 400px;
      background: #f0f0f0;
    }
    
    .child {
      /* 无需指定宽高 */
      background: #42b983;
      padding: 20px 40px;
    }
    

    优势分析

    • 一行代码实现双轴居中,无需关心子元素尺寸
    • 支持动态内容,子元素宽高变化仍能保持居中
    • 可通过flex-direction轻松切换主轴方向
    • 支持多个子元素同时居中

    2. 兼容性方案:定位 + transform(IE9+)

    适用场景:需要兼容 IE9,但子元素尺寸不固定的场景。

    .parent {
      position: relative;
      height: 400px;
      background: #f0f0f0;
    }
    
    .child {
      position: absolute;
      left: 50%; /* 水平偏移 */
      top: 50%; /* 垂直偏移 */
      transform: translate(-50%, -50%); /* 双向校准 */
      background: #42b983;
      padding: 20px 40px;
    }
    

    原理图解

    +------------------------+
    |                        |
    |                        |
    |       [ child ]        |  ← 先左移父元素50%、上移父元素50%
    |                        |    再左移自身50%、上移自身50%
    |                        |
    +------------------------+
    

    3. 定宽高场景:绝对定位 + margin: auto

    适用场景:子元素宽高固定,且需要严格居中的场景(如弹窗)。

    .parent {
      position: relative;
      height: 400px;
      background: #f0f0f0;
    }
    
    .child {
      position: absolute;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      width: 200px; /* 定宽 */
      height: 150px; /* 定高 */
      margin: auto; /* 自动平分所有方向空间 */
      background: #42b983;
    }
    

    水平垂直居中方案对比表

    方案 核心原理 适用场景 兼容性 代码复杂度
    Flex 布局 利用 Flex 容器的双轴对齐属性 所有场景,尤其不定宽高 IE10+ ★☆☆☆☆
    定位 + transform 先偏移父元素 50%,再校准自身 50% 不定宽高,需兼容 IE9+ IE9+ ★★☆☆☆
    绝对定位 + margin: auto 利用绝对定位的空间分配特性 定宽高元素 IE7+ ★★☆☆☆
    Grid 布局 利用 Grid 容器的对齐属性 现代浏览器,复杂布局 IE11+ ★☆☆☆☆
    表格布局 模拟表格单元格的居中特性 需兼容 IE6 + 的旧系统 IE6+ ★★★☆☆

    📝 面试 & 实战指南:如何选择最优方案?

    面对具体需求时,推荐按以下优先级选择居中方案:

    1. 优先考虑现代布局方案

      • 若无需兼容 IE9 及以下,直接使用 Flex 布局(简单场景)或 Grid 布局(复杂场景)
      • 理由:代码简洁、适应性强、维护成本低
    2. 兼容性场景分级处理

      • IE9+:定位 + transform 方案
      • IE8 及以下:表格布局或定位 + 负 margin(定宽高)
    3. 特殊场景特殊处理

      • 单行文本:line-height(最简单)

      • 定宽块级元素:margin: 0 auto(经典方案)

      • 多个元素同时居中:Flex 布局(justify-content: center

    面试应答技巧
    当被问到 "如何实现元素居中" 时,不要直接说方法,而是先反问:

    • "这个元素是行内元素还是块级元素?"

    • "它的宽高是固定的吗?"

    • "需要兼容到哪个浏览器版本?"

    这种思考方式能体现你的工程思维,远比单纯背诵方法更能获得面试官认可。

    总结:从 "会用" 到 "用好" 的关键

    居中布局看似简单,却能反映前端开发者对布局模型的理解深度。从最基础的text-align到现代的 Flex/Grid,每种方案都有其设计理念和适用边界。

    记住:没有 "最好" 的居中方案,只有 "最合适" 的方案。掌握各种方案的优缺点和适用场景,根据实际需求灵活选择,才能真正做到 "居中自由"。

    最后,建议你在实际项目中多尝试不同方案,对比它们在各种场景下的表现 —— 实践,才是掌握布局的最佳途径。

    如果觉得这篇文章对你有帮助,欢迎点赞收藏,也欢迎在评论区分享你在居中布局中遇到的坑和解决方案!🚀

    学会让Trae老师教我们代码逻辑完成数独小游戏

    2025年8月24日 11:01

    数独:从零到解的思考与实现

    前几天,我突然对数独产生了兴趣,那种在一个 9×9 的格子里填数字的游戏,看着简单,其实充满了挑战。 我就想,为啥不自己试试用Trae写代码实现一个数独呢?既能练练手,又能顺便测试一下Trae的逻辑能力。

    数独是个啥玩意儿

    先简单说说数独吧。这是一个 9×9 的大棋盘,被分成 9 个小 3×3 的格子(叫小宫格)。

    游戏中,有的格子已经填了数字(1 到 9),有的格子是空的。

    游戏的规则就是,每行、每列以及每个小宫格里的数字都不能重复,你要把空着的格子填上数字,让整个棋盘符合这个规则。

    我把规则给到Trae image.png

    Trae分步完成

    image.png

    Trae 拆解数独

    要实现数独,我得先把它拆成几个小问题来解决。

    1. 画个棋盘

    这个最简单,就是造一个 9×9 的表格。

    想象一下,你面前有张纸,你用尺子把它画成 81 个方格,这就是棋盘的雏形。

    image.png

    2. 填些初始数字

    真正的数独不是一张白纸,它有一些初始数字。这些数字就像是“线索”,告诉你从哪里开始。

    image.png

    Trae的初始数字方法是这样完成的,保证每一次的顺序都不一样

    image.png

    3. 检查规则

    这是关键。每次你想在某个格子里填个数字,就得先检查三件事:

    • 这个数字在同一行里有没有了?有了就不能再填。
    • 在同一列里有没有?有了也不行。
    • 在它所属的小宫格里有没有?有还是不行。

    image.png 只有这三处都没有,你才能把数字稳稳填进去。

    4. 解题思路

    这个最烧脑。你不能瞎填,得有策略。最笨的方法是试错法,也就是先随便填个数字,然后一步步往下试。要是后面发现填错了,就得往回退,换个数字再试。听起来很笨,但这种方法管用,只是有时候会很慢。

    测试一下成果

    代码写好了,得试试啊。我把之前填好线索的棋盘扔进去,让程序跑起来。一开始我还担心会不会出错,没想到它真的开始一行一行解决问题。

    image.png

    慢慢地,棋盘上的空格子越来越少,数字一个个填进去,最后,一个完整的、符合规则的数独就出现在我眼前。

    那一刻,我觉得Trae的ai编程能力还是很牛的,太强了,让我很快的就理清思路,并实现一个数独游戏

    总结

    Trae 帮我实现数独就像是在拼图,你得先把规则弄清楚,然后一点一点把代码逻辑拼起来。过程中肯定会有卡壳的时候,但只要耐心一点,多试试,总能找到答案。

    我有个建议,如果你也想试试,不要一开始就想搞一个超级复杂的版本。

    从最简单的小九宫格开始,然后一点点加规则,这样会更有成就感。

    而且,你会发现,解决复杂问题的很多时候,其实就是把复杂的问题简单化,一步步写代码,一步步解决报错,这样你就会有成长,也会感到快乐~

    赛车竞速,看看Trae老师是怎么完成这款h5小游戏的

    2025年8月24日 10:48

    前言

    今天来还原童年记忆中的赛车戏,主要是让Trae用代码实现这个游戏的核心功能,全程不用自己写一行代码。

    这个游戏的核心功能

    先把这个核心逻辑发给Trae,看看他完成的是不是你想要的童年记忆

    1. 玩家可以通过上下左右、wasd来控制赛车的加速、减速、左右方向。
    2. 还有氮气加速等道具加成。
    3. 页面要精美,游戏要流畅。
    4. 碰撞到其他东西会死亡,结束游戏。
    5. 根据行驶的距离计算分数。

    我们先让Trae他生成的时候要精美的页面,先来看看开始页面,哇塞,好像很精美 的样子,那就点击开始游戏吧

    image.png 什么鬼呀?这是啥,玩都玩不了,是不是要求太复杂了,还是让他重新写吧

    image.png 看看能不能解决问题

    image.png

    这效果还是不错的,毕竟我们没有资源文件,只是简单的说一下逻辑,有这样的完成度已经不错了,看起来也有些微信小游戏的味道了

    image.png

    Trae代码解读

    通过设定赛车的初始位置和状态,来初始化游戏的布局,并通过事件监听来判断玩家对加速、转向等操作输入。

    track.addEventListener('change', (e) => {
        if (car.style.position === 'absolute') {
            startTime = new Date().getTime();
            car.style.position = 'relative';
            moveCar();
        }
    });
    

    车道切换

    image.png 随机生成道具函数

    image.png 通过逻辑判断实现玩家操作后的车辆移动效果,并计算通过赛道的时间,给出相应的评价。

    function moveCar() {
        let endTime = new Date().getTime();
        raceTime = endTime - startTime;
        raceTimeElement.textContent = raceTime + 'ms';
        // 给出评价
        if (raceTime < 10000) {
            evaluationElement.textContent = '优秀!赛车手级操作';
        } else if (raceTime < 20000) {
            evaluationElement.textContent = '良好!速度很快';
        } else if (raceTime < 30000) {
            evaluationElement.textContent = '一般!需要提升';
        } else {
            evaluationElement.textContent = '较慢!多加练习';
        }
        updateLeaderboard();
    }
    

    测试过程中避免玩家在赛车未准备好时就开始操作的代码逻辑

    最后是Trae自己对这款赛车竞速游戏的总结,主要是游戏功能和设计,还有考虑到游戏体验,非常的人性化,设计了赛道标识、驾驶舱视角等元素,给玩家一种身临其境的竞速体验。

    image.png

    image.png

    总结

    1、这个游戏的核心功能主要是玩家驾驶着赛车,然后不断的突破障碍去行驶,通过行驶的距离来计算分数,只有撞到汽车才会结束游戏,过程中可以通过获取道具进行加速,从而获取更多的分数。

    2、这个游戏在当年的按键手机上可谓是非常的火爆,再次在html上面玩,确实勾起了初中的时候的回忆,你是否也玩过这个赛车游戏,并且还跟别人一起比拼谁获得的分数多,走得远?

    Three.js后处理UnrealBloomPass的分析

    作者 刘皇叔code
    2025年8月23日 22:58

    前言

    (Bloom)泛光是后处理中最常用的技术之一,通过提升超过阈值部位的亮度,并模拟高亮部分向周围扩散光晕的效果,能够让整个场景更梦幻、更有真实感,极大的提升了视觉表现力。
    Bloom的原理并不复杂,就是将超过阈值的高亮像素提取出来,用高斯模糊过滤后,再和原图叠加,以达到增强高亮和向周围扩散的效果。
    所以我认为,了解Bloom的原理非常有必要,在Three.js的开发中,对于隧道、城市、地下、夜晚等一些场景,使用Bloom可以让很大程度的提升渲染效果。恰好Three.js中有两种Bloom后处理方式,BloomPass和UnrealBloomPass,两种方式的实现非常不同,UnrealBloomPass的渲染效果明显更好,这里我们用Three.js官方实例中的法拉利车做一个对比。 a16d734f221a8253cce54430ca290839.png UnrealBloomPass

    28904ffb-5867-4ba3-94db-883790671a4a.png BloomPass

    从上面例子看来,BloomPass的效果非常差,完全没有达到泛光的效果,而且画面非常模糊,这是因为Bloom直接将一个高斯模糊后的图像与原图混合,在这个场景中就导致画面非常模糊。UnrealBloomPass的效果不错,它具有以下特征:

    • 高亮的区域足够亮;
    • 光扩散的范围足够大;
    • 光晕的过度比较自然。

    所以接下来,我们来分析UnrealBloomPass的效果是如何达到的。

    bloom的原理及源码分析

    1.识别高亮区域

    这一步比较简单,就是根据一个设置的阈值将原图上亮度超过阈值的像素提取出来,低于这个阈值的像素直接去掉,渲染输出到一张纹理上。

    // 原图的纹理
    uniform sampler2D tDiffuse;
    uniform vec3 defaultColor;
    uniform float defaultOpacity;
    // 阈值
    uniform float luminosityThreshold;
    //平滑过渡的区域
    uniform float smoothWidth;
    
    varying vec2 vUv;
    
    void main() {
    
        vec4 texel = texture2D( tDiffuse, vUv );
    
        float v = luminance( texel.xyz );
    
        vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity );
    
        float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );
    
        gl_FragColor = mix( outputColor, texel, alpha );
    
    }
    

    2.高光扩散——下采样

    要形成高光的扩散效果,原理就是对高光区域进行模糊处理,模糊滤波本质上就是将一个像素和它周围的像素进行加权平均,比如这个像素本来很暗,但周围有高光像素,这样一平均,这个像素也就变亮了,这就形成了高光的扩散效果。
    想要高光扩散的范围比较大,就需要这个“周围”的范围比较大,也就是滤波的kernel尺寸比较大。但是扩大kernel尺寸肯定要增加很大的性能开销,想要快速查询更大范围的加权平均值,这就需要采用mipmap的思想。mipmap将形成一个金字塔结构,每一级mipmap图像的长宽都是上一级图像的1/2,像素值都是上一级图像2x2像素的加权平均值。查询高层级的mipmap图像得到的像素值近似相当于查询低层级图像的加权平均值。
    在纹理渲染时,通常需要设置gl.TEXTURE_MIN_FILTER,代表将大纹理渲染到小屏幕时,采用怎样的过滤方式,如果选择了使用mipmap,比如gl.LINEAR_MIPMAP_LINEAR,就需要执行gl.generateMipmap()来生成的mipmap,但是这里生成的mipmap只能在这个纹理最终渲染时由系统自动查询并插值计算最终渲染到屏幕的像素值,我们不能使用mipmap某个层级的图像。所以如果我们要使用mipmap中某个层级的图像,只能我们自己手动生成mipmap。
    自己生成mipmap的方式就是下采样,将原图像渲染到长宽都是原图1/2的纹理上,依次缩小,比如1024×1024到512×512、512×512到256×256、……,依此类推。这个渲染到纹理时要将gl.TEXTURE_MIN_FILTER设置成gl.GL_LINEAR,恰好Three.js中WebGLRenderTarget的默认设置就是gl.GL_LINEAR。所以我们就需要依此使用WebGLRenderTarget渲染到比之前缩小的纹理上。
    但是gl.GL_LINEAR采用的双线性插值,取周围4个纹素的加权平均值本质上是一个2×2的box滤波,为了更平滑,这里就采用更高级的滤波核,那就是在下采样时使用高斯模糊。
    下面看下源码:

    //nMips为5,一共五层,创建五组RenderTarget,每一层都比上一次缩小,长宽时上一层的1/2
    for ( let i = 0; i < this.nMips; i ++ ) {
    const renderTargetHorizontal = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
    renderTargetHorizontal.texture.name = 'UnrealBloomPass.h' + i;
    renderTargetHorizontal.texture.generateMipmaps = false;
    this.renderTargetsHorizontal.push( renderTargetHorizontal );
    const renderTargetVertical = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
    renderTargetVertical.texture.name = 'UnrealBloomPass.v' + i;
    renderTargetVertical.texture.generateMipmaps = false;
    this.renderTargetsVertical.push( renderTargetVertical );
    resx = Math.round( resx / 2 );
    resy = Math.round( resy / 2 );
    }
    
    //设置每一层的shader,shader写在ShaderMaterial中,kernelSizeArray为每一层高斯函数的标准差。
    this.separableBlurMaterials = [];
    const kernelSizeArray = [ 3, 5, 7, 9, 11 ];
    resx = Math.round( this.resolution.x / 2 );
    resy = Math.round( this.resolution.y / 2 );
    for ( let i = 0; i < this.nMips; i ++ ) {
    this.separableBlurMaterials.push( this.getSeperableBlurMaterial( kernelSizeArray[ i ] ) );
    this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );
    resx = Math.round( resx / 2 );
    resy = Math.round( resy / 2 );
    }
    
    //根据标准差,将高斯函数计算的权重提取计算好,根据权重采样,最后加权求均值。
    getSeperableBlurMaterial( kernelRadius ) {
    const coefficients = [];
    for ( let i = 0; i < kernelRadius; i ++ ) {
    coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( kernelRadius * kernelRadius ) ) / kernelRadius );
    }
    
    return new ShaderMaterial( {
    defines: {
    'KERNEL_RADIUS': kernelRadius
    },
    uniforms: {
    'colorTexture': { value: null },
    'invSize': { value: new Vector2( 0.5, 0.5 ) }, // inverse texture size
    'direction': { value: new Vector2( 0.5, 0.5 ) },
    'gaussianCoefficients': { value: coefficients } // precomputed Gaussian coefficients
    },
    vertexShader:
    `varying vec2 vUv;
    void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }`,
    fragmentShader:
    `#include <common>
    varying vec2 vUv;
    uniform sampler2D colorTexture;
    uniform vec2 invSize;
    uniform vec2 direction;
    uniform float gaussianCoefficients[KERNEL_RADIUS];
    
    void main() {
    float weightSum = gaussianCoefficients[0];
    vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum;
    for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
    float x = float(i);
    float w = gaussianCoefficients[i];
    vec2 uvOffset = direction * invSize * x;
    vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb;
    vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb;
    diffuseSum += (sample1 + sample2) * w;
    weightSum += 2.0 * w;
    }
    gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
    }`
    } );
    }
    
    //初始化inputRenderTarget为上一步提取的高亮纹理的renderTarget
    let inputRenderTarget = this.renderTargetBright;
    
    //依次下采样得到一组金字塔型的mipmap
    for ( let i = 0; i < this.nMips; i ++ ) {
    this.fsQuad.material = this.separableBlurMaterials[ i ];
    this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = inputRenderTarget.texture;
    this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionX;
    renderer.setRenderTarget( this.renderTargetsHorizontal[ i ] );
    renderer.clear();
    this.fsQuad.render( renderer );
    
    this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = this.renderTargetsHorizontal[ i ].texture;
    this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionY;
    renderer.setRenderTarget( this.renderTargetsVertical[ i ] );
    renderer.clear();
    this.fsQuad.render( renderer );
    
    inputRenderTarget = this.renderTargetsVertical[ i ];
    }
    

    3.mipmap叠加

    下来一步就是将之前的的5层mipmap全部叠加,再叠加到原图上,这样主要有两个目的:

    • 让中心足够亮;
    • 让扩散的光晕不仅最够大,而且过度均匀。 我们来看代码:
    this.compositeMaterial = this._getCompositeMaterial( this.nMips );
    this.compositeMaterial.uniforms[ 'blurTexture1' ].value = this.renderTargetsVertical[ 0 ].texture;
    this.compositeMaterial.uniforms[ 'blurTexture2' ].value = this.renderTargetsVertical[ 1 ].texture;
    this.compositeMaterial.uniforms[ 'blurTexture3' ].value = this.renderTargetsVertical[ 2 ].texture;
    this.compositeMaterial.uniforms[ 'blurTexture4' ].value = this.renderTargetsVertical[ 3 ].texture;
    this.compositeMaterial.uniforms[ 'blurTexture5' ].value = this.renderTargetsVertical[ 4 ].texture;
    this.compositeMaterial.uniforms[ 'bloomStrength' ].value = strength;
    this.compositeMaterial.uniforms[ 'bloomRadius' ].value = 0.1;
    
    const bloomFactors = [ 1.0, 0.8, 0.6, 0.4, 0.2 ];
    this.compositeMaterial.uniforms[ 'bloomFactors' ].value = bloomFactors;
    this.bloomTintColors = [ new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ) ];
    this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;
    
    _getCompositeMaterial( nMips ) {
    
        return new ShaderMaterial( {
    
            defines: {
                'NUM_MIPS': nMips
            },
    
            uniforms: {
                'blurTexture1': { value: null },
                'blurTexture2': { value: null },
                'blurTexture3': { value: null },
                'blurTexture4': { value: null },
                'blurTexture5': { value: null },
                'bloomStrength': { value: 1.0 },
                'bloomFactors': { value: null },
                'bloomTintColors': { value: null },
                'bloomRadius': { value: 0.0 }
            },
    
            vertexShader:
                `varying vec2 vUv;
                void main() {
                    vUv = uv;
                    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
                }`,
    
            fragmentShader:
                `varying vec2 vUv;
                uniform sampler2D blurTexture1;
                uniform sampler2D blurTexture2;
                uniform sampler2D blurTexture3;
                uniform sampler2D blurTexture4;
                uniform sampler2D blurTexture5;
                uniform float bloomStrength;
                uniform float bloomRadius;
                uniform float bloomFactors[NUM_MIPS];
                uniform vec3 bloomTintColors[NUM_MIPS];
    
                //mipmap叠加时每个层级图像的系数由lerpBloomFactor函数决定,由上文const bloomFactors = [ 1.0, 0.8, 0.6, 0.4, 0.2 ]可知,
                //层级越高,lerpBloomFactor的factor参数越小,mirrorFactor越大。所以可以看到bloomRadius越小,低层级占比越大,这样光晕的
                //范围就比较小,反之光晕的效果就比较大。所以bloomRadius就决定了光晕的大小。显然,bloomStrength决定了mipmap叠加以后光的
                //整体亮度。
                float lerpBloomFactor(const in float factor) {
                    float mirrorFactor = 1.2 - factor;
                    return mix(factor, mirrorFactor, bloomRadius);
                }
    
                void main() {
                    gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) +
                        lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) +
                        lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) +
                        lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) +
                        lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) );
                }`
        } );
    
    }
    

    这里需要注意的是,lerpBloomFactor这个函数。mipmap叠加时每个层级图像的系数由lerpBloomFactor函数决定,由bloomFactors可知,层级越高,lerpBloomFactor的factor参数越小,mirrorFactor越大。所以可以看到bloomRadius越小,低层级占比越大,这样光晕的范围就比较小,反之光晕的效果就比较大。所以bloomRadius就决定了光晕的大小。显然,bloomStrength决定了mipmap叠加以后光的整体亮度。

    4.和原图叠加

    this.copyUniforms = UniformsUtils.clone( CopyShader.uniforms );
    
    this.blendMaterial = new ShaderMaterial( {
            uniforms: this.copyUniforms,
            vertexShader: CopyShader.vertexShader,
            fragmentShader: CopyShader.fragmentShader,
            blending: AdditiveBlending,
            depthTest: false,
            depthWrite: false,
            transparent: true
    } );
    

    最后一步唯一需要注意的就是采用了blending参数使用了AdditiveBlending,将叠加mipmap后的图像和原图进行了1:1混合。

    最后使用UnrealBloomPass一定要设置renderer的toneMapping属性,推荐使用ReinhardToneMapping或ACESFilmicToneMapping。因为图像叠加后有些高亮区域的亮度远远大于1,如果不设置toneMapping会导致高光位置过曝,闪烁,画面一片白,暗部细节丢失等不自然的问题。

    vue3.5.18源码:computed 在发布订阅者模式中的双重角色

    作者 码山有路
    2025年8月23日 22:48

    computed在发布订阅者模式中的双重角色,是指它既是订阅者又是发布者。

    响应式就是当数据变化时,会自动执行某些操作。

    但是,computed的响应式又和refreactive有所不同,refreactive是发布订阅者模式中的发布者,而computed既是发布者,又是订阅者。

    本文就以一个例子开头,详细介绍computed的双重角色。

    // 以下整个所有的代码可以看做是渲染函数render
    <div id="app"></div>
    <script>
        const App = {
            template:`{{ computedCount }}<button @click="plus">plus</button>
            `,
            setup() {
                // 定义响应式数据,ref是RefImpl类的实例对象
                debugger;
                const count = Vue.ref(0);
                // 定义计算属性,computed中的核心类是ComputedRefImpl
                debugger
                const computedCount = Vue.computed(() => {
                    return count.value + 1;
                })
                // 定义修改数据的方法
                const plus = () => {
                    debugger;
                    count.value++;
                }
                // 暴露给模板
                return {
                    plus,
                    computedCount
                };
            }
        };
        // 创建应用并挂载
        const app = Vue.createApp(App);
        app.mount("#app");
    </script>
    

    以上例子中,定义响应式数据count,初次渲染count的值为0。定义的计算属性computedCount的计算值依赖count,所以computedCountcount的订阅者。

    全文渲染函数render在生成vnode时,会访问到computedCount,所以rendercomputedCount的订阅者。

    当点击修改数据count的值会触发computedCount的更新,进而触发render的更新。

    接下来首先介绍整个过程中的三个核心类RefImplComputedRefImplReactiveEffect

    一、生成类(时机:setup)

    接下来我们首先介绍这三个实例对象所对应的生成类。

    1、RefImpl

    const count = Vue.ref(1)执行,最终会返回RefImpl的实例。RefImpl类的核心逻辑如下:

    // ref函数
    function ref(value) {
      return createRef(value, false);
    }
    // createRef函数
    function createRef(rawValue, shallow) {
      if (isRef(rawValue)) {
        return rawValue;
      }
      return new RefImpl(rawValue, shallow);
    }
    // RefImpl类
    class RefImpl {
      constructor(value, isShallow2) {
        // 依赖管理
        this.dep = new Dep();
        this["__v_isRef"] = true;
        this["__v_isShallow"] = false;
        this._rawValue = isShallow2 ? value : toRaw(value);
        this._value = isShallow2 ? value : toReactive(value);
        this["__v_isShallow"] = isShallow2;
      }
      get value() {
        {
          // 在访问RefImpl实例化的对象的值的时候,会进行依赖收集
          this.dep.track({
            target: this,
            type: "get",
            key: "value",
          });
        }
        return this._value;
      }
      set value(newValue) {
        const oldValue = this._rawValue;
        const useDirectValue =
          this["__v_isShallow"] || isShallow(newValue) || isReadonly(newValue);
        newValue = useDirectValue ? newValue : toRaw(newValue);
        if (hasChanged(newValue, oldValue)) {
          this._rawValue = newValue;
          this._value = useDirectValue ? newValue : toReactive(newValue);
          {
            // 在修改RefImpl实例化的对象的值的时候,会执行派发更新
            this.dep.trigger({
              target: this,
              type: "set",
              key: "value",
              newValue,
              oldValue,
            });
          }
        }
      }
    }
    // Dep类,作为发布者和订阅者之间的关系管理类
    class Dep {
      constructor(computed) {
        this.computed = computed;
        this.version = 0;
        // 核心: dep和当前活跃的sub之间的关系
        this.activeLink = void 0;
        this.subs = void 0;
        this.map = void 0;
        this.key = void 0;
        // 订阅者个数
        this.sc = 0;
        this.__v_skip = true;
        {
          this.subsHead = void 0;
        }
      }
      track(debugInfo) {
        // 依赖收集,省略逻辑...
      }
      trigger(debugInfo) {
        // 派发更新,省略逻辑...
      }
      notify(debugInfo) {
        // 通知更新,省略逻辑...
      }
    }
    

    当执行到const count = Vue.ref(1)时,会执行响应式数据初始化函数,返回一个RefImpl对象,该对象包含了一个依赖管理器:this.dep = new Dep()。并且定义了取值和赋值两个方法:getset, 在首次渲染时,会访问到get函数,进而执行this.dep.track收集依赖。在数据变化时,会访问到set函数,执行this.dep.trigger派发更新。

    至此,count就具备了依赖收集派发更新的功能。

    2、ComputedRefImpl

    Vue.computed(() => { return count.value + 1; })开始,会先获得gettersetter函数,然后作为ComputedRefImpl类的参数来创建实例。核心逻辑如下:

    // computed函数
    const computed = (getterOrOptions, debugOptions) => {
      const c = computed$1(getterOrOptions, debugOptions, isInSSRComponentSetup);
      {
        const i = getCurrentInstance();
        if (i && i.appContext.config.warnRecursiveComputed) {
          c._warnRecursive = true;
        }
      }
      return c;
    };
    // computed$1函数
    function computed$1(getterOrOptions, debugOptions, isSSR = false) {
      let getter;
      let setter;
      if (isFunction(getterOrOptions)) {
        // 如果是函数,直接赋值为getter
        getter = getterOrOptions;
      } else {
        // 如果是对象,直接获取getterOrOptions中的get
        getter = getterOrOptions.get;
        setter = getterOrOptions.set;
      }
      // 实例化ComputedRefImpl类
      const cRef = new ComputedRefImpl(getter, setter, isSSR);
      if (debugOptions && !isSSR) {
        cRef.onTrack = debugOptions.onTrack;
        cRef.onTrigger = debugOptions.onTrigger;
      }
      return cRef;
    }
    // ComputedRefImpl类
    class ComputedRefImpl {
      constructor(fn, setter, isSSR) {
        // 这里的fn就是传入的参数getter
        this.fn = fn;
        this.setter = setter;
        this._value = void 0;
        // 以自身实例为参数,作为Dep的参数
        this.dep = new Dep(this);
        this.__v_isRef = true;
        this.deps = void 0;
        this.depsTail = void 0;
        this.flags = 16;
        this.globalVersion = globalVersion - 1;
        this.next = void 0;
        this.effect = this;
        this["__v_isReadonly"] = !setter;
        this.isSSR = isSSR;
      }
      // notify函数
      notify() {
        this.flags |= 16;
        if (!(this.flags & 8) && activeSub !== this) {
          batch(this, true);
          return true;
        }
      }
      // get函数
      get value() {
        // 收集订阅者(渲染函数),建立`computed`和`render`之间的关系
        const link = this.dep.track({
          target: this,
          type: "get",
          key: "value",
        });
        // 将自己作为订阅者,让`count`订阅,建立`computed`和`count`之间的关系
        refreshComputed(this);
        if (link) {
          link.version = this.dep.version;
        }
        return this._value;
      }
      // set函数
      set value(newValue) {
        if (this.setter) {
          this.setter(newValue);
        } else {
          warn$2("Write operation failed: computed value is readonly");
        }
      }
    }
    

    这里可以看出ComputedRefImpl类中也包含getset方法,当访问computed.value时,会执行get方法,该方法会执行this.dep.track收集依赖,建立computedrender之间的关系。

    同时,会执行refreshComputed函数,该函数的作用,就是将computedcount建立关系,即computed订阅count,当count发生变化时,会派发更新,通知computed重新计算。具体实现在构建三者关系时详细介绍,这里先有个印象。

    接下来介绍最后和渲染相关的ReactiveEffect类。

    3、ReactiveEffect

    ReactiveEffect的实例化对象就是一个订阅者。渲染过程的调度过程就是由其实例化对象中的run方法进行的,核心代码如下:

    // ReactiveEffect类
    class ReactiveEffect {
      constructor(fn) {
        // 这里的fn就是渲染函数componentUpdateFn
        this.fn = fn;
        // 记录依赖有哪些
        this.deps = void 0;
        this.depsTail = void 0;
        this.flags = 1 | 4;
        this.next = void 0;
    
        this.cleanup = void 0;
        this.scheduler = void 0;
        if (activeEffectScope && activeEffectScope.active) {
          activeEffectScope.effects.push(this);
        }
      }
      pause() {
        // 省略逻辑...
      }
      resume() {
        // 省略逻辑...
      }
      notify() {
        // 省略逻辑...
      }
      run() {
        // 当前活跃的订阅者就是this,即effect
        activeSub = this;
        try {
          return this.fn();
        } finally {
          // 省略逻辑...
        }
      }
      stop() {
        // 省略逻辑...
      }
      trigger() {
        // 省略逻辑...
      }
      runIfDirty() {
        // 省略逻辑...
      }
      get dirty() {
        // 省略逻辑...
      }
    }
    

    在上述对象中,this.fn=fn,实例化时指向的是实例化的执行函数componentUpdateFn,获取vnodepatch的逻辑都在其中。const update = instance.update = effect.run.bind(effect),执行update时,实际执行的是ReactiveEffectrun方法。此时,就将当前实例赋值给了activeSub

    介绍完RefImplComputedRefImplReactiveEffect类后,我们再介绍他们之间关系时如何建立的。

    二、构建关系(时机:首次渲染)

    在首次渲染生成vnode时,会访问到computedCount,进而执行到ComputedRefImplget函数。get函数中的核心作用就是建立computedrender之间的关系,以及computedcount之间的关系。

    // get函数
    get value() {
      // 建立`computed`和`render`之间的关系
      const link = this.dep.track({
        target: this,
        type: "get",
        key: "value",
      });
      // 建立`computed`和`count`之间的关系
      refreshComputed(this);
      if (link) {
        link.version = this.dep.version;
      }
      return this._value;
    }
    

    1、computedrender之间的关系

    // this.dep.track
    track(debugInfo) {
      if (!activeSub || !shouldTrack || activeSub === this.computed) {
        return;
      }
      let link = this.activeLink;
      if (link === void 0 || link.sub !== activeSub) {
        // 建立activeSub和dep的关系
        link = this.activeLink = new Link(activeSub, this);
        if (!activeSub.deps) {
          // 订阅者的依赖deps指向link
          activeSub.deps = activeSub.depsTail = link;
        } else {
          // 链表的形式可以管理多个订阅者
          link.prevDep = activeSub.depsTail;
          activeSub.depsTail.nextDep = link;
          activeSub.depsTail = link;
        }
        addSub(link);
      }
      return link;
    }
    // Link类的实例将包含sub和dep两个属性,分别指向依赖和订阅者
    class Link {
      constructor(sub, dep) {
        this.sub = sub;
        this.dep = dep;
        this.version = dep.version;
        // 链表的形式管理dep和sub
        this.nextDep = this.prevDep = this.nextSub = this.prevSub = this.prevActiveLink = void 0;
      }
    }
    // addSub函数
    function addSub(link) {
      link.dep.sc++;
      if (link.sub.flags & 4) {
        if (link.dep.subsHead === void 0) {
          // link.dep.subsHead作为链表头,起初也指向link
          link.dep.subsHead = link;
        }
        // 依赖的订阅者subs也指向link
        link.dep.subs = link;
      }
    }
    

    这里需要重点关注的是activeSub.deps = activeSub.depsTail = linklink.dep.subs = link,两者都指向了同一个link对象,该对象包含depsub属性,这样activeSubdep之间建立了关系。实现了你中有我, 我中有你的双向依赖关系。

    2、computedcount之间的关系

    function refreshComputed(computed) {
      // 省略其他逻辑
      const dep = computed.dep;
      const prevSub = activeSub;
      const prevShouldTrack = shouldTrack;
      // 这里将activeSub赋值为computed,以方便建立computed和count之间的关系
      activeSub = computed;
      shouldTrack = true;
      try {
        prepareDeps(computed);
        // 这里执行computed.fn,即count.value += 1
        const value = computed.fn(computed._value);
        if (dep.version === 0 || hasChanged(value, computed._value)) {
          computed.flags |= 128;
          computed._value = value;
          dep.version++;
        }
      } catch (err) {
        dep.version++;
        throw err;
      } finally {
        // 这里将activeSub赋值为prevSub,恢复activeSub的值
        activeSub = prevSub;
        shouldTrack = prevShouldTrack;
        cleanupDeps(computed);
        computed.flags &= -3;
      }
    }
    

    以上代码中通过activeSub = computed的方式,将computed作为订阅者,再通过const value = computed.fn(computed._value)的方式执行到代码count.value += 1。近而触发countget函数,即this.dep.track({ target: this, type: "get", key: "value", });。这里的逻辑和computed-render之间的关系的逻辑类似,请自行debugger断点调试。

    此时,rendercomputedcount之间就建立了依赖关系。接下来看,当count的值发生变化时,是如何触发render重新渲染的。

    三、触发更新(时机:数据修改)

    当执行count.value += 1操作时,会触发countset函数,进而执行到this.dep.trigger函数,派发更新的核心代码如下:

    1、依赖Dep触发逻辑

    // Dep的trigger函数
    trigger(debugInfo) {
      this.version++;
      globalVersion++;
      this.notify(debugInfo);
    }
    // Dep的notify函数
    notify(debugInfo) {
      startBatch();
      try {
        // 通过链表的形式,执行所有的订阅者
        for (let link = this.subs; link; link = link.prevSub) {
          // 执行订阅者link.sub的notify
          if (link.sub.notify()) {
            ;
            link.sub.dep.notify();
          }
        }
      } finally {
        endBatch();
      }
    }
    

    以上代码中,执行link.sub.notify(),即执行computednotify函数,核心逻辑如下:

    2、订阅者ComputedRefImpl触发逻辑

    // sub的notify函数
    notify() {
      this.flags |= 16;
      if (!(this.flags & 8) &&
      activeSub !== this) {
        batch(this, true);
        return true;
      }
    }
    

    需要注意的是,这里的notify执行完之后,执行了return true,即:

    if (link.sub.notify()) {
      link.sub.dep.notify();
    }
    

    中,link.sub.notify()返回了true,进而执行了link.sub.dep.notify()link.sub.notify()表示的是computednotify

    那么,computed作为发布者,link.sub.dep.notify()表示的就是ReactiveEffectnotify。即rendernotify

    当以上逻辑结束时,会执行到.finallyDependBatch函数,核心代码如下:

    3、订阅者ReactiveEffect视图渲染

    // dep的endBatch方法
    function endBatch() {
      if (--batchDepth > 0) {
        return;
      }
      // 省略计算属性相关的逻辑...
      while (batchedSub) {
        let e = batchedSub;
        batchedSub = void 0;
        while (e) {
          const next = e.next;
          e.next = void 0;
          e.flags &= -9;
          if (e.flags & 1) {
            try {
              // 执行e(batchedSub)的trigger()方法
              e.trigger();
            } catch (err) {
              if (!error) error = err;
            }
          }
          e = next;
        }
      }
      if (error) throw error;
    }
    // sub的trigger()方法
    trigger() {
      if (this.flags & 64) {
        pausedQueueEffects.add(this);
      } else if (this.scheduler) {
        // 即effect.scheduler = () => queueJob(job);
        this.scheduler();
      } else {
        this.runIfDirty();
      }
    }
    

    再看 queueJob 的核心逻辑

    function queueJob(job) {
      if (!(job.flags & 1)) {
        const jobId = getId(job);
        const lastJob = queue[queue.length - 1];
        if (!lastJob || (!(job.flags & 2) && jobId >= getId(lastJob))) {
          // 将当前任务插入到数组尾部
          queue.push(job);
        } else {
          // 根据jobId,将其移动到合适的位置
          queue.splice(findInsertionIndex(jobId), 0, job);
        }
        job.flags |= 1;
        queueFlush();
      }
    }
    // queueFlush
    function queueFlush() {
      if (!currentFlushPromise) {
        // flushJobs是异步任务,得等下个异步队列才执行
        currentFlushPromise = resolvedPromise.then(flushJobs);
      }
    }
    

    下一个异步队列,执行的任务:

    function flushJobs(seen) {
      {
        seen = seen || /* @__PURE__ */ new Map();
      }
      const check = (job) => checkRecursiveUpdates(seen, job);
      try {
        for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
          const job = queue[flushIndex];
          if (job && !(job.flags & 8)) {
            if (check(job)) {
              continue;
            }
            if (job.flags & 4) {
              job.flags &= ~1;
            }
            // 在错误处理函数中执行job
            callWithErrorHandling(job, job.i, job.i ? 15 : 14);
            if (!(job.flags & 4)) {
              job.flags &= ~1;
            }
          }
        }
      } finally {
        for (; flushIndex < queue.length; flushIndex++) {
          const job = queue[flushIndex];
          if (job) {
            job.flags &= -2;
          }
        }
        flushIndex = -1;
        queue.length = 0;
        flushPostFlushCbs(seen);
        currentFlushPromise = null;
        if (queue.length || pendingPostFlushCbs.length) {
          flushJobs(seen);
        }
      }
    }
    // 错误处理函数
    function callWithErrorHandling(fn, instance, type, args) {
        try {
          return args ? fn(...args) : fn();
        } catch (err) {
          handleError(err, instance, type);
        }
      }
    // 因为job = instance.job = effect.runIfDirty.bind(effect);,所以,fn就是runIfDirty函数
    runIfDirty() {
      if (isDirty(this)) {
        // 这里就是最终的渲染逻辑
        this.run();
      }
    }
    

    以上,就是从数据count变化,到订阅者执行渲染逻辑的全过程。

    总结:计算属性computed作为订阅者,在访问count时实现了订阅;作为发布者,在生成vnode时会访问到computed对应的数据,渲染函数render订阅了它。当count发生变化时,会触发computedset函数,进而触发render重新渲染。

    我要成为vue高手01:上下文

    作者 s3xysteak
    2025年8月23日 22:34

    今天我要跟你讨论一下上下文

    上下文是天然存在于代码中的概念,举一个经典例子:

    let one = 1
    {
      let one = 2
    }
    one === 1 // ?
    

    上面这段代码的结果是什么呢?显而易见是true,也就是说经过中间大括号内的一顿操作后,one的值并没有发生变化,这是因为大括号中声明的变量只在大括号的作用域内。

    作用域大家都知道,这是一个很基础的概念,而作用域是上下文的一种。所谓上下文,顾名思义就是一段代码附近的区域,或者说这段代码在哪儿。上下文是要有对象的,一定是“对于某段代码”的上下文。比如这里,对于大括号中声明的one的生命周期,其上下文就是大括号内。

    <script setup>
    usePointer()
    
    function test() {
      usePointer()
    }
    </script>
    
    <template>
      <button @click="test()">test</button>
    </template>
    

    假设usePointer是一个组合式函数(composables),上面这段代码有什么问题呢?这里我抄录一段vue官方文档的原文:

    组合式函数只能在 <script setup> 或 setup() 钩子中被调用。在这些上下文中,它们也只能被同步调用。在某些情况下,你也可以在像 onMounted() 这样的生命周期钩子中调用它们。

    这些限制很重要,因为这些是 Vue 用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,这样才能:

    1. 将生命周期钩子注册到该组件实例上
    2. 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。

    也就是说,在上面的代码中,第一次调用时正确的,第二次调用时错误的。这是因为第一次调用时在setup上下文中,第二次调用时上下文是不确定的,从逻辑上说,你无法推断用户在什么时候点下这个按钮,所以其上下文一定是不确定的:

    <script setup>
    usePointer() // ✔
    
    function test() {
      usePointer() // ✖
    }
    </script>
    

    当然这是大部分情况。如果一个组合式函数只是对基本的响应式系统进行无状态的算法操作,倒也可以在其他地方调用,只是这种情况很少见,因为我们在使用组合式函数时通常都是要依赖于组件的生命周期的。

    新的问题出现了,什么是生命周期?vue的组件有三种常见的生命周期,按顺序他们的钩子分别是:

    1. setup - 组件初始化时
    2. onMounted - html挂载后,意味着到此时你才能操作html相关的内容
    3. onUnmounted - 卸载时

    setup不必多说。我们是这样使用生命周期钩子的:

    onMounted(() => {
      // 随便做点啥
    })
    

    发现了吗?“随便做点啥”位于一个大括号内,这意味着其实我们使用生命周期钩子时,其上下文就是这个钩子内。在vue中,我们所说的上下文,大部分都和生命周期的上下文有关,接下来会详细阐述这一点

    而生命周期钩子本身也有上下文,那就是setup:

    <script setup>
    onMounted() // ✔
    
    function test() {
      onMounted() // ✖
    }
    </script>
    
    <template>
      <button @click="test()">test</button>
    </template>
    

    还是一样的例子,既然已知生命周期钩子需要在setup上下文内,那么答案显而易见,我就不赘述了。

    由于组件拥有生命周期,也就是说组件会经历一个“活了然后死了”的过程,那么同样的,从逻辑上说,你无法推断用户在什么时候点下这个按钮,那么第二次调用可能会发生mounted周期已经结束了的时候,那么必须要将其限制在setup内,也就是保证那个生命周期的阶段还没开始前,先说好到了那个时候要做的事。

    非常常用的watchEffect也是可以依赖于生命周期的,如果watchEffect在setup阶段被声明,那么在组件开始时侦测将会开始,在组件销毁时侦测同样也自动被销毁。所以最好我们只在setup中使用这个函数。

    而watchEffect本身也有上下文,这和computed类似,当你在其上下文内使用响应式变量,就会触发这个侦测器:

    const a = ref('one')
    const b = ref(1)
    watchEffect(() => {
      // 随便做点啥
      b.value = a.value === 'one' ? 1 : 2
    })
    

    和上面的例子类似,可以注意到“随便做点啥”位于一个大括号内,相信你已经敏锐的意识到了,我们知道在watchEffect内使用的响应式变量会触发watchEffect的更新,换而言之,就是在这个上下文内使用响应式变量会触发watchEffect更新,computed也一样。

    那么如果这样呢?

    const a = ref('one')
    const b = ref(1)
    watchEffect(async () => {
      await fetchSomething() // 一个异步函数
      b.value = a.value === 'one' ? 1 : 2
    })
    

    在这个例子中,computed传入了一个异步函数。你可能在日常经验,或者其他文章中学到,最好不要在watchEffect中使用异步函数了。显然,此时b失去了响应。原因也很简单,从逻辑上说,或许当这个异步任务结束时,已经经过了setup阶段,甚至说不定这个组件都被销毁了,那么他肯定不能被算作这个上下文了。虽然这段代码在watchEffect传入函数的作用域内,但是他却不在watchEffect的上下文中。

    那这样呢?

    const a = ref('one')
    const b = ref(1)
    watchEffect(async () => {
      b.value = a.value === 'one' ? 1 : 2
      await fetchSomething() // 一个异步函数
    })
    

    看起来只是把一行代码调换了顺序,但其实有天大的不同。这段代码是能正常运行的。可是这不是在异步函数中吗?仔细分析,你会发现截止到await之前的代码是同步的,换而言之,此前的代码都在外层的上下文内,而直到await,也就是等待异步函数开始,从那之后就脱离了上下文了。

    const one = ref(1)
    function test() {
      one.value = 2
    }
    watchEffect(() => {
      test()
    })
    

    这段代码会触发更新吗?一些新手会觉得,watchEffect并没有出现响应式变量,所以不会触发更新。但按照此前的理解来看,在执行到test()时,test()也是一个同步函数,换而言之这个watchEffect内部都是同步发生的,所以test正处于watchEffect上下文内,他可以正常更新。

    至此,我们能清晰的意识到一件事,那就是异步函数/用户操作通常会导致上下文的丢失。这也是为什么vue的组合式函数经常要在setup周期中以同步的方式运行。

    // utils.ts
    function useXXX() {
      watchEffect()
      onMounted()
      // ...
    }
    
    <script setup>
    useXXX()
    </script>
    

    这也正应了vue最核心的内容之一:响应式语法。看起来“用户操作通常会导致上下文的丢失”好像会导致上下文的概念很垃圾,毕竟不涉及用户操作与其叫程序不如叫美术。那么实际使用时如何防止异步函数/用户操作丢失上下文呢?很简单,既然上下文注定保不住了,那何不用响应式变量来保存状态呢?

    const a = ref('one')
    const b = ref(1)
    
    const ready = ref(false)
    
    watchEffect(async () => {
      b.value = a.value === 'one' ? 1 : 2
      
      ready.value = false
      await fetchSomething() // 一个异步函数
      ready.value = true
    })
    
    <script setup>
    const click= ref(false)
    const { /* ... */ } = usePointer()
    watchEffect(() => {
      // ...
    })
    
    function test() {
      click.value = true
    }
    </script>
    
    <template>
      <button @click="test()">test</button>
    </template>
    

    我们通过一个简单的布尔值来保存了异步任务/用户操作的状态,然后setup中的其他部分比如watchEffect就可以通过这个响应式变量来获悉异步任务状态/用户操作内容。其核心思想在于:你应该在如setup这种最初的时候就准备好一切,而后面只是借助于响应式自然而然的发生,如此就能避免上下文的丢失,也正应了响应式思想的核心

    类似的例子有很多,比如地图类的库通常都依赖于html的初始化,需要等到页面挂载后才能获得dom,才能初始化地图,而此时已经失去了setup周期,你就无法挂载组合式函数了,而解决办法就是在最初的时候用一个响应式变量存储你需要使用的内容,在组合式函数里应对好这个响应式变量空值时、初始化后等各种情况,然后在初始化只需要更新这个响应式变量,你所需要在初始化后执行的逻辑就自然而然发生。

    这篇文章是我要成为vue高手的第一篇文章,所讲的也是基础中的基础,毕竟如果对上下文都处于模糊的认知下,那有再多的骚操作也难以正确的使用了。总结一下,本文的核心也就是最后的一段话,其核心思想在于:你应该在如setup这种最初的时候就准备好一切,而后面只是借助于响应式自然而然的发生,如此就能避免上下文的丢失,也正应了响应式思想的核心。

    前端HTTP请求:Fetch api和Axios

    2025年8月23日 22:16

    当我们谈论起网络请求,总绕不开两个老朋友:Fetch APIAxios。一个出身名门,是浏览器的原生API;另一个则是久经沙场,功能强大的第三方库。二者都是两种常用的 HTTP 请求方式,各有其独特的优势和适用场景。

    第一回合:自我介绍

    首先,让我们来认识一下它们。

    Fetch API 是浏览器原生提供的网络请求接口,基于 Promise,它的目标是取代传统的 XMLHttpRequest。由于是原生能力,你不需要安装任何依赖,开箱即用,这在追求轻量化的项目中非常诱人。

    Axios 则是前端社区中的明星,一个基于 Promise 的 HTTP 客户端。它不依赖于任何框架,可以在浏览器和 Node.js 环境下使用。Axios 强大的功能和友好的 API 设计,让它成为了许多大型项目中的首选。

    第二回合:核心功能大比拼

    我们通过几个具体的场景来对比它们的核心功能。

    1. 语法:谁更简洁?

    让我们以一个简单的 GET 请求为例,看看两者的代码。

    使用 Fetch API 获取数据:

    fetch('https://api.example.com/users')
      .then(response => response.json())
      .then(data => console.log(data))
      .catch(error => console.error('出错了:', error));
    

    而使用 Axios,代码则简洁得多:

    axios.get('https://api.example.com/users')
      .then(response => console.log(response.data))
      .catch(error => console.error('出错了:', error));
    

    你会发现,Axios 自动帮我们完成了 JSON 数据的解析,我们不需要再调用 response.json() 这一步,直接通过 response.data 就能拿到数据。

    2. 错误处理:谁更靠谱?

    这是 Fetch API 最常被吐槽的一个“陷阱”。它只会对网络错误(比如断网)触发 catch,而对于HTTP 状态码错误(比如 404 Not Found 或 500 Internal Server Error),它会认为这是一个成功的请求,并进入 then 方法。你需要在 then 中手动判断 response.okresponse.status 来处理。

    fetch('https://api.example.com/not-found')
      .then(response => {
        // 即使是 404,也会进入这里
        if (!response.ok) {
          throw new Error(`HTTP 错误!状态码: ${response.status}`);
        }
        return response.json();
      })
      .catch(error => console.error('出错了:', error));
    

    Axios 则不会有这个烦恼。它会智能地判断响应状态码,只要状态码不在 2xx 的范围内,它就会自动将请求视为失败,并直接进入 catch 块。

    axios.get('https://api.example.com/not-found')
      .then(response => console.log(response.data))
      .catch(error => {
        // 404 错误会直接进入这里
        if (error.response) {
          console.error('HTTP 错误,状态码:', error.response.status);
        }
      });
    

    这种统一的错误处理机制,大大简化了我们的开发逻辑。

    3. 请求/响应拦截器:谁更强大?

    在实际项目中,我们经常需要在每次请求中带上 Token 来验证身份,或者在收到响应时统一处理错误码或展示加载动画。

    Fetch API 原生不支持拦截器,你需要自己封装一个高阶函数或者在每个请求前手动添加 headers,这会造成大量重复代码。

    const myFetch = (url, options = {}) => {
      options.headers = {
        ...options.headers,
        'Authorization': `Bearer ${localStorage.getItem('token')}`
      };
      return fetch(url, options);
    };
    
    myFetch('https://api.example.com/profile')
      .then(response => response.json());
    

    Axios 则内置了强大的拦截器(Interceptors) 功能。它允许你在请求发送前和响应返回后,对数据进行统一处理。

    // 请求拦截器
    axios.interceptors.request.use(config => {
      const token = localStorage.getItem('token');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    }, error => {
      return Promise.reject(error);
    });
    
    // 响应拦截器
    axios.interceptors.response.use(response => {
      // 可以统一处理成功响应
      return response;
    }, error => {
      // 可以统一处理错误,比如根据状态码跳转到登录页
      if (error.response.status === 401) {
        console.log('Token过期,请重新登录!');
      }
      return Promise.reject(error);
    });
    
    // 现在,所有通过 axios 发出的请求都会自动带上 Token
    axios.get('https://api.example.com/profile');
    

    这样的设计,让代码的可维护性可扩展性得到了极大提升。

    第三回合:生产环境中的抉择

    经过前面的对比,我们可以清晰地看到它们的优劣势。

    Fetch API 的优势在于:

    • 轻量化: 无需额外安装依赖,减小了最终打包的体积,这在对包大小有严格要求的场景下很有优势。
    • 原生支持: 现代浏览器都已支持,兼容性良好。

    Axios 的优势在于:

    • 功能全面: 除了上面提到的拦截器,它还支持请求取消、请求超时、上传文件进度条、以及更友好的并发请求等高级功能。
    • 统一体验: 在浏览器和 Node.js 中都提供了相同的 API,这对于全栈开发者来说非常方便。
    • 社区支持: 社区活跃,遇到问题很容易找到解决方案。

    那么,我们该如何选择?

    • 如果你在开发一个轻量级的个人项目、静态网站,或者对包体积有苛刻要求,并且你的请求逻辑相对简单,那么 Fetch API 绝对是你的首选。它的原生特性让你告别额外依赖,代码也足够简洁。
    • 但如果你在开发一个中大型项目,需要处理复杂的认证、统一的错误提示、全局的加载动画,或者需要经常取消请求,那么 Axios 几乎是唯一的选择。它的拦截器机制、统一的错误处理以及丰富的功能,可以让你用最少的代码实现最多的功能,极大地提高开发效率。

    小结:没有最好,只有最合适

    总的来说,Fetch API 是一个功能精简、性能优秀的浏览器原生API,而 Axios 是一个功能强大、易于使用的第三方库。

    官方资料: 使用 Fetch

    Axios中文文档 | Axios中文网

    Vue3+ElementPlus倒计时示例

    作者 程序员张3
    2025年8月23日 22:11
    • 按钮文字默认显示“开始倒计时”
    • 当点击按钮时,显示正在倒计时(倒计时数字)
    • 倒计时结束按钮显示“开始倒计时”

    倒计时逻辑 Hooks 函数

    hooks/useCountDown.js

    /**
     * hooks函数:函数是用于封装和复用组件逻辑的一种机制
     * 定义:Hooks 是一种在不使用类组件的情况下复用状态逻辑的方法
     * 目的:将组件中的逻辑抽取出来,形成可复用的函数
     * 特点:遵循 Composition API 的思想,使逻辑组织更加灵活
     * 命名规范:通常以 use 开头命名 Hook 函数,如 useCounter、useFetch 等
     */
    import {ref, watch} from "vue";
    
    // 封装并导出倒计时函数
    export function useCountDown(num = 60) {
      // 倒计时剩余秒数
      const count = ref(num);
      // 是否倒计时中
      const isDown = ref(false);
      // 定时器的 id 编号
      let timerId = null;
      // 开始倒计时函数
      const start = () => {
        if (isDown.value) return; //当前正在倒计时中,则返回
        isDown.value = true;      //设置 isDown 值,表示正在倒计时中
        timerId = setInterval(() => {
          count.value--;
        }, 1000);
      }
      // 使用 watch 监听 count 值,当 count 值变为 0 时,停止计时器
      watch(count, (newCount) => {
        if (newCount <= 0) {
          clearInterval(timerId); //清除计时器
          count.value = num;      //重置 count 值
          isDown.value = false;   //重置 isDown 值
        }
      })
    
      return {
        count,
        isDown,
        start
      }
    }
    

    页面按钮

    xxx/index.vue

    <template>
      <el-button type="primary" @click="countDownFn">
        <span v-if="isDown">正在倒计时({{ count }})</span>
        <span v-else>开始倒计时</span>
      </el-button>
    </template>
    
    <script setup>
    // 导入 hooks 函数
    import { useCountDown } from "@/hooks"
    // 调用 useCountDown 函数,得到 count计数, isDown是否开始, start 函数
    const { count, isDown, start } = useCountDown(10)
    
    // 倒计时
    const countDownFn = () => {
      start()
    }
    </script>
    
    <style lang="scss" scoped>
    </style>
    

    JavaScript原型链没那么难:一文彻底搞懂

    作者 南篱
    2025年8月23日 22:02

    1. 原型(Prototype)

    什么是原型?

    每个 JavaScript 对象都有一个隐藏的 [[Prototype]] 属性,它指向另一个对象,这个被指向的对象就是它的"原型"。

    如何访问原型?

    // 通过 __proto__ 属性(非标准,但浏览器都支持)
    const obj = {};
    console.log(obj.__proto__); // 指向 Object.prototype
    
    // 通过 Object.getPrototypeOf()(推荐的标准方式)
    console.log(Object.getPrototypeOf(obj)); // 同样指向 Object.prototype
    

    示例:对象的原型

    const person = {
        name: 'Alice',
        age: 25
    };
    
    console.log(person.__proto__ === Object.prototype); // true
    

    2. 构造函数与原型

    构造函数创建对象

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    
    // 在原型上添加方法
    Person.prototype.sayHello = function() {
        console.log(`Hello, I'm ${this.name}`);
    };
    
    const alice = new Person('Alice', 25);
    const bob = new Person('Bob', 30);
    
    alice.sayHello(); // "Hello, I'm Alice"
    bob.sayHello();   // "Hello, I'm Bob"
    

    为什么使用原型?

    // 如果不使用原型,每个实例都会有独立的方法副本
    function BadPerson(name) {
        this.name = name;
        this.sayHello = function() { // ❌ 每个实例都有独立的函数
            console.log(`Hello, I'm ${this.name}`);
        };
    }
    
    // 使用原型,所有实例共享同一个方法
    function GoodPerson(name) {
        this.name = name;
    }
    GoodPerson.prototype.sayHello = function() { // ✅ 所有实例共享
        console.log(`Hello, I'm ${this.name}`);
    };
    

    3. 原型链(Prototype Chain)

    什么是原型链?

    当访问一个对象的属性时,JavaScript 会:

    1. 先在对象自身查找
    2. 如果找不到,就去它的原型上找
    3. 如果还找不到,就去原型的原型上找
    4. 直到找到 null 为止

    这种一层层的查找关系就形成了"原型链"。

    原型链示例

    const grandparent = { grandpa: '老爷爷' };
    const parent = { papa: '爸爸' };
    const child = { child: '孩子' };
    
    // 设置原型链:child -> parent -> grandparent
    Object.setPrototypeOf(parent, grandparent);
    Object.setPrototypeOf(child, parent);
    
    console.log(child.child);    // "孩子" - 自身属性
    console.log(child.papa);     // "爸爸" - 父级原型属性
    console.log(child.grandpa);  // "老爷爷" - 祖父级原型属性
    console.log(child.xxx);      // undefined - 找不到
    

    4. 完整的原型链图示

    [你的对象] → Object.prototypenull
        ↑
    [数组对象] → Array.prototypeObject.prototypenull
        ↑  
    [函数对象] → Function.prototypeObject.prototypenull
    

    实际代码验证

    const arr = [1, 2, 3];
    
    // 数组的原型链
    console.log(arr.__proto__ === Array.prototype);        // true
    console.log(Array.prototype.__proto__ === Object.prototype); // true
    console.log(Object.prototype.__proto__);               // null
    
    // 函数的原型链  
    function test() {}
    console.log(test.__proto__ === Function.prototype);    // true
    console.log(Function.prototype.__proto__ === Object.prototype); // true
    

    5. 重要的内置原型

    Object.prototype

    // 所有对象的最终原型
    console.log(Object.prototype.toString()); // "[object Object]"
    console.log(Object.prototype.hasOwnProperty); // 检查自身属性的方法
    

    Array.prototype

    const arr = [1, 2, 3];
    console.log(Array.prototype.push === arr.push); // true
    console.log(Array.prototype.map === arr.map);   // true
    

    Function.prototype

    function test() {}
    console.log(Function.prototype.call === test.call);     // true
    console.log(Function.prototype.bind === test.bind);     // true
    

    6. 实际应用场景

    1. 继承的实现

    function Animal(name) {
        this.name = name;
    }
    Animal.prototype.eat = function() {
        console.log(`${this.name} is eating`);
    };
    
    function Dog(name, breed) {
        Animal.call(this, name); // 调用父类构造函数
        this.breed = breed;
    }
    
    // 设置原型链继承
    Dog.prototype = Object.create(Animal.prototype);
    Dog.prototype.constructor = Dog;
    
    Dog.prototype.bark = function() {
        console.log('Woof!');
    };
    
    const myDog = new Dog('Buddy', 'Golden');
    myDog.eat();  // "Buddy is eating" (继承的方法)
    myDog.bark(); // "Woof!" (自身的方法)
    

    2. 方法扩展(猴子补丁)

    // 为数组添加自定义方法
    Array.prototype.last = function() {
        return this[this.length - 1];
    };
    
    const arr = [1, 2, 3, 4];
    console.log(arr.last()); // 4
    

    3. 属性检查

    const obj = { a: 1 };
    
    console.log(obj.hasOwnProperty('a')); // true - 自身属性
    console.log(obj.hasOwnProperty('toString')); // false - 继承属性
    console.log('toString' in obj); // true - 包括继承属性
    

    7. 注意事项

    1. 性能考虑

    // 原型链太长会影响查找性能
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) { // 只遍历自身属性
            console.log(key);
        }
    }
    

    2. 现代替代方案

    // ES6 class 语法(本质还是基于原型)
    class Person {
        constructor(name) {
            this.name = name;
        }
        
        sayHello() { // 这个方法会在 Person.prototype 上
            console.log(`Hello, ${this.name}`);
        }
    }
    
    // 等同于
    function Person(name) {
        this.name = name;
    }
    Person.prototype.sayHello = function() {
        console.log(`Hello, ${this.name}`);
    };
    

    总结

    概念 说明 示例
    原型 每个对象都有一个指向原型的链接 obj.__proto__
    原型链 通过原型链接形成的查找链条 obj → proto1 → proto2 → null
    作用 实现继承和方法共享 所有数组共享 Array.prototype 的方法
    重点 属性查找会沿着原型链向上 obj.toString() 找到 Object.prototype.toString

    原型机制是 JavaScript 实现面向对象编程的基础,理解它对于掌握 JavaScript 至关重要!

    ❌
    ❌