普通视图

发现新文章,点击刷新页面。
今天 — 2025年5月19日首页

Canvas签名功能常见的几种问题

2025年5月18日 23:52

. 如何实现基础签名功能?


<!DOCTYPE html>
<canvas id="signature" width="500" height="300"></canvas>
<script>
  const canvas = document.getElementById('signature');
  const ctx = canvas.getContext('2d');
  let isDrawing = false;
  
  canvas.addEventListener('mousedown', startDrawing);
  canvas.addEventListener('mousemove', draw);
  canvas.addEventListener('mouseup', stopDrawing);
  canvas.addEventListener('mouseout', stopDrawing);

  function startDrawing(e) {
    isDrawing = true;
    ctx.beginPath();
    ctx.moveTo(e.offsetX, e.offsetY);
  }

  function draw(e) {
    if (!isDrawing) return;
    ctx.lineTo(e.offsetX, e.offsetY);
    ctx.stroke();
  }

  function stopDrawing() {
    isDrawing = false;
  }
</script>

1. 如何检测签名是否为空?

function isCanvasBlank(canvas) {
  // 获取画布像素数据
  const context = canvas.getContext('2d');
  const pixelBuffer = new Uint32Array(
    context.getImageData(0, 0, canvas.width, canvas.height).data.buffer
  );
  
  // 检查是否有非透明像素
  return !pixelBuffer.some(color => color !== 0);
}

2. 如何处理不同设备DPI问题?

function setupHighDPICanvas(canvas) {
  // 获取设备像素比
  const dpr = window.devicePixelRatio || 1;
  
  // 获取CSS显示尺寸
  const rect = canvas.getBoundingClientRect();
  
  // 设置实际尺寸为显示尺寸乘以像素比
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;
  
  // 缩放上下文以匹配CSS尺寸
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);
  
  // 设置CSS尺寸保持不变
  canvas.style.width = `${rect.width}px`;
  canvas.style.height = `${rect.height}px`;
}

3. 如何实现撤销/重做功能?

class SignatureHistory {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.history = [];
    this.currentStep = -1;
  }
  
  saveState() {
    // 截取当前画布状态
    const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
    
    // 如果当前不是最新状态,截断历史
    if (this.currentStep < this.history.length - 1) {
      this.history = this.history.slice(0, this.currentStep + 1);
    }
    
    this.history.push(imageData);
    this.currentStep++;
  }
  
  undo() {
    if (this.currentStep > 0) {
      this.currentStep--;
      this.ctx.putImageData(this.history[this.currentStep], 0, 0);
    }
  }
  
  redo() {
    if (this.currentStep < this.history.length - 1) {
      this.currentStep++;
      this.ctx.putImageData(this.history[this.currentStep], 0, 0);
    }
  }
}

4. 如何添加签名压力感应效果?

// 监听指针事件(支持压力感应设备)
canvas.addEventListener('pointerdown', startDrawing);
canvas.addEventListener('pointermove', drawWithPressure);
canvas.addEventListener('pointerup', stopDrawing);

function drawWithPressure(e) {
  if (!isDrawing) return;
  
  // 获取压力值(0-1),默认0.5用于鼠标
  const pressure = e.pressure || 0.5;
  
  // 根据压力调整线条宽度
  ctx.lineWidth = pressure * 10 + 2; // 2-12px范围
  
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(e.offsetX, e.offsetY);
}

5. 如何防止签名图片被篡改?

function generateSignatureHash(canvas) {
  // 获取画布数据
  const imageData = canvas.toDataURL('image/png');
  
  // 使用SHA-256生成哈希
  return crypto.subtle.digest('SHA-256', new TextEncoder().encode(imageData))
    .then(hash => {
      // 转换为十六进制字符串
      const hashArray = Array.from(new Uint8Array(hash));
      return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    });
}

6. 如何加密存储签名数据?

async function encryptSignatureData(canvas) {
  // 获取画布数据
  const imageData = canvas.toDataURL('image/png');
  
  // 生成加密密钥
  const key = await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt']
  );
  
  // 加密数据
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    new TextEncoder().encode(imageData)
  );
  
  return {
    key,
    iv,
    encryptedData: Array.from(new Uint8Array(encrypted))
  };
}

7. 如何实现多人协同签名?

class CollaborativeSignature {
  constructor(canvas, socket) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.socket = socket;
    
    // 本地绘制事件
    canvas.addEventListener('mousedown', this.handleLocalDrawStart.bind(this));
    canvas.addEventListener('mousemove', this.handleLocalDrawing.bind(this));
    canvas.addEventListener('mouseup', this.handleLocalDrawEnd.bind(this));
    
    // 远程绘制事件
    socket.on('remote-draw-start', this.handleRemoteDrawStart.bind(this));
    socket.on('remote-drawing', this.handleRemoteDrawing.bind(this));
    socket.on('remote-d
昨天 — 2025年5月18日首页

小红书一面:长达一个小时的拷打😭

作者 Danta
2025年5月18日 22:40

前言

兄弟也是好起来了,又又有大厂面试了。


面试过程:

一、自我介绍

这个自我介绍我之前在面试文章中提到过,大家可以翻翻查看。

二、实习经历

面试官看到我目前在一家公司实习,于是让我聊了聊我的业务内容。


三、项目方面

1. 你为什么选用 Tailwind CSS?能说说有什么好处吗?

  • 原子化设计:Tailwind CSS 是一种原子化 CSS 框架,将样式拆分为最小的功能单元,每个类只负责一个特定的样式属性。
  • 开发效率高:像写内联类一样快速编写样式,无需额外创建 CSS 文件。
  • 响应式友好:支持大量响应式类,例如 md:w-1/2lg:w-1/4lg:flex-row 等。
  • 样式隔离性强:在 Vue 单文件组件中使用 Tailwind 类,避免传统 CSS 中的样式冲突问题,比如 TabBar、ShowProducts 等组件各自维护自己的样式。
  • 技术广度体现:其实当时用这个是想展示一下对现代前端工具链的理解。
  • 缺点
    • 学习曲线较陡,需要记忆大量类名和约定;
    • 熟练后开发效率更高。
  • 拓展思考
    • 便于 AI 辅助开发:原子类不依赖嵌套或继承,减少 AI 理解上下文的压力;
    • 降低样式覆盖风险,减少了 CSS 的“层叠”问题和选择器冲突。

2. 你的项目用到了组件懒加载,讲讲好处?

  • 在路由懒加载中使用了组件懒加载,实现 按需加载,只有当用户导航到特定路由时,才会加载相应的组件。
  • 如果不使用懒加载,打包时会把所有页面打包成一个文件,首页一次性加载全部资源,导致加载速度慢,用户体验差。
  • 使用路由懒加载后,首页资源被拆分为多个 chunk 文件(如 app.js, home.js),CSS 同样被拆分。
  • 文章参考链接:前端性能优化
面试官追问:你知道为什么会这样吗?

我当时没回答上来,但后来查资料得知:

  • import() 的调用处被视为代码分割点,被请求模块及其子模块会被分离为独立的 chunk。
  • Webpack 等构建工具识别 import(),并将动态导入的模块单独打包,从而减小初始加载体积。
  • 总体积不变,但首屏加载资源减少,提升用户体验。

3. 聊聊你项目中的动态组件

  • 在实现一个礼物推荐助手时,我需要展示用户提问与 AI 回答。
  • 为此我封装了两个组件:一个是用户消息组件,一个是 AI 回复组件。
  • 每次对话内容存储在一个数组中,根据标志属性判断渲染哪个组件。
  • 最终通过 Vue 内置的 <component> 标签结合 :is 属性实现了动态组件切换。

4. 你实现 keep-alive 的目的,以及和 v-if / v-show 的区别?应用场景?

  • keep-alive 目的
    • 缓存组件状态(如表单输入、滚动位置);
    • 避免组件频繁销毁重建;
    • 减少 API 请求,提高性能和用户体验。
  • 缓存控制
    • 使用 include="cachedComponents" 属性,只缓存设置了 meta.cache = true 的组件。
v-ifv-show 的区别:
对比项 keep-alive v-if v-show
是否保持状态 ✅ 是 ❌ 否 ✅ 是
渲染机制 组件缓存 条件为 false 不渲染 切换 display 属性
性能 切换成本低,适合频繁切换 初始化开销小 切换快,初始渲染全量
适用场景 多 tab 切换、表单缓存 不常切换、复杂组件 高频切换简单元素

5. 自定义图片懒加载怎么实现的?

流程如下:

scrollTop + offsetTop
=> getBoundingClientRect()
=> IntersectionObserver

从手动计算逐步过渡到现代浏览器 API,性能越来越好。

又问:你了解 HTML 中原生的 lazy 吗?能否讲讲?
  • 原生 HTML 支持懒加载:<img loading="lazy"><iframe loading="lazy">
  • 优点:简单易用,无需 JS,现代浏览器原生支持;
  • 缺点:兼容性一般,IE 不支持,功能有限;
  • 定制性不强,更高级的需求建议使用 IntersectionObserver 自定义实现。

6.响应式布局这方面,你是怎么做的?

  1. 我的项目中,有一个商品展示的功能,使用的是wc-waterfall ,动态的绑定gap和cols两个属性,通过生命周期挂载,添加事件监听,根据屏幕的大小,调整相应的值,来实现响应式布局。
  2. 通过@media声明在不同尺寸下微调样式细节
  3. 商城项目,经常用得到商品的展示,所以我会将它封装成一个组件,方便复用

四、场景题

1.setimeout

这个是一个面试经常问到的题目,但是他问的很细

for(var i=0;i<4;i++){
    setTimeout(function(){
        console.log(i);
    },1000)
}
首先问你输出什么?

由于 var i 的声明具有函数作用域(在这里指全局作用域),所有的 setTimeout 回调函数实际上引用的是同一个 i 变量。当定时器触发时(即循环已经结束),i 的值已经是 4,因此所有回调打印的结果都是 4。在整个循环过程中只有一个 i,最后连着输出四个4.

又问大概在什么时候输出: 因为定时器不一定准,所以是大概的时间,可能就会回答在4秒后了,实际上,执行同步代码的循环后,定时器四个任务,相继执行,因为间隔时间很短,所以就很像四个定时器并发了一样,实际上还是一个又一个执行的。在大概一秒后。

如何输出 0 1 2 3呢?

var => let

  • 块级作用域:在每次 for 循环迭代中,都会创建一个新的 i 实例。这些 i 变量被限制在循环体的块级作用域内。
  • 延迟执行的回调函数:每个 setTimeout 回调函数捕获的是它对应的那个特定的 i 实例。因此,当定时器触发并执行回调函数时,它们能够访问到正确的 i 值,而不是所有回调都指向同一个 i
那我想要每隔大概一秒输出一个数字呢?
  1. 当时我想到了是:
for (let i = 0,time=1000; i < 4; i++,time+=1000) {
    setTimeout(function () {
        console.log(new Date())
        console.log(i);
    }, time);
}
2025-05-18T14:14:54.808Z
0
2025-05-18T14:14:55.806Z
1
2025-05-18T14:14:56.814Z
2
2025-05-18T14:14:57.800Z
3
  1. 还有就是使用闭包加立即执行函数了:
for (var i = 0; i < 4; i++) {
    (function (i) {
        setTimeout(function () {
            console.log(i);
        }, 1000*i);
    })(i);
}

  1. 进阶:

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

(async function () {
    for (var i = 0; i < 4; i++) {
        await delay(1000);
        console.log(i);
    }
})();

2. 数组求和

const nestedObj = [1, [2, [3, [4, 5]]], 6, 7], 求和。前几天刚好看到了数组和对象的扁平化,刚好就能用上了,不过面试官好像想让我用更简单的方法,我没想出来。

let sum=0
function flattenObject(obj ) {
    for (const item of obj) {
        if (Array.isArray(item)) {
            flattenObject(item);
        } else {
            sum=sum+item;
        }
    }
    return sum;
}
console.log(flattenObject(nestedObj));// [ 1, 2, 3, 4, 5, 6, 7]

学习点其他简单的方法:

  1. 递归
function sum(arr) {
    return arr.reduce((total, item) => {
        return total + (Array.isArray(item) ? sum(item) : item);
    }, 0);
}

const nestedObj = [1, [2, [3, [4, 5]]], 6, 7];
console.log(sum(nestedObj)); // 输出: 28

结语

面了几家大厂后,也有一些心得:

大厂面试一定是穷追猛打,问到你不会为止。所以有些难题回答不出来也没关系。

而面试其实就是一场表演,大家可以在项目中准备几个亮点,自己演练几遍,在面试时流畅表达出来,体现出自己的深度和思考能力,这才是关键!

面试之道——手写call、apply和bind

作者 哆啦美玲
2025年5月18日 18:30

嗨嗨嗨~这里是哆啦美玲分享的知识点,一起来学呀!

这次的文章基于我之前写的this的显式绑定的文章,有不懂的可以倒回去看看哦——搞懂this,如此简单 - 掘金

image.png

callapplybind 都是 JavaScript 中函数的调用方法,它们的作用是改变函数的上下文 (this) 和传递参数,但它们之间有一些不同点。

首先我们看下面这段代码:

let obj = {
    a: 1
}

function foo(x, y) {
    console.log(this.a, x + y);
    return 'hello'
}
console.log(foo(1, 2)); // undefined 3 hello

我们声明了一个对象obj和函数foo,在独立调用foo时,this指向的是全局,所以this.a会返回undefiend。那我们如何实现this指向obj呢?

一、call

1. call的特点

  • call 方法立即调用一个函数,并且可以指定 this 的值,同时传入参数。
  • 参数是按顺序传递的,多个参数使用逗号分隔。
const res = foo.call(obj, 3, 4) // call的this指向foo,foo的this指向obj
console.log(res); // 1 7 hello

2. 手写myCall(context, ...args)

在手写myCall之前我们需要分析:myCall写哪里? image.png 如图的代码结果,foo既是函数也是对象,但是使用对象obj调用call方法会报错:call is not a function,所以myCall方法写在构造函数Function的原型(Function.prototype)。

因为函数也是对象,所以foo.call()会导致call的this指向foo;call的执行会让obj调用foo,让foo的this指向obj;

foo在声明前需要多少参数call并不清楚,所以call可以使用...args的形式接收剩余参数;另外,我们写的foo是有返回值的,在call的调用后会返回foo的结果,所以我们手写call的时候也要写返回值。

我们再看,如果call传入的参数中第一位不是对象,会得到什么? image.png 从图上代码的输出结果可以看出来:call会把传入的参数除去第一位后按顺序交给foo,且第一位必须是对象才能改变this的指向。

根据分析,手写代码如下:

// 手写call
Function.prototype.myCall = function (context, ...args) { // foo.__proto__ == Function.prototype
    if (typeof (this) !== 'function') { // 判断this必须是函数
        throw new TypeError('Error')
    }
    context = context || window

    const key = Symbol('fn') // 唯一的key
    context[key] = this // this = foo
    const res = context[key](...args) // 隐式绑定 this = context
    delete context[key]
    return res
}
console.log(foo.myCall(obj, 2, 3)); // 1 5 hello

代码第6行 context = context || window 的意思是:如果 context 变量已经有值(即不是 null 或 undefined),那么就使用 context 的值;如果 context 没有值(即为 null 或 undefined),就使用 window 作为默认值。

代码第8-9行是给foo函数创建一个唯一的key值,确保不会修改掉函数内部原本的属性值。

最后为了不修改原对象,要记得把新增的属性删除!

二、apply

1. apply的特点

  • apply方法也立即调用一个函数,并指定 this 的值,同时传入参数。
  • 参数传递方式是将一个数组或类数组对象作为参数列表传入。
const res1 = foo.apply(obj, [3, 4]) // apply的this指向foo, foo的this指向obj
console.log(res1); // 1 7 hello

2. 手写myApply

apply与call的用法是一样的,唯一不同的就是接收的参数不一样,所以只需要修改一点点就可以了,代码如下:

// 手写apply
Function.prototype.myApply = function (context, args) { // foo.__proto__ == Function.prototype
    if (typeof (this) !== 'function') { // 判断this必须是函数
        throw new TypeError('Error')
    }
    context = context || window

    const key = Symbol('fn')
    context[key] = this // this = foo
    const res = context[key](...args) // 隐式绑定 this = context
    delete context[key]
    return res
}

console.log(foo.myApply(obj, [2, 4])); // 1 6 hello

三、bind

1. bind的特点

  • bind方法并不立即调用函数,而是返回一个新的函数,新的函数会绑定指定的 this 和参数。
  • 这个返回的新函数可以在之后的某个时刻被调用,且新函数也可以接收零散参数。
  • 当新函数被 new 调用时, 返回的是调用 bind 的那个函数的实例对象
const fn = foo.bind(obj, 4)
const res2 = fn(4)
console.log(res2); // 1 8 hello

const f = new fn(4)
console.log(f); // undefined 8 foo {}

从代码中可以看出:bind函数调用时,foo函数在接收参数时会先在bind传入的参数里面按顺序找,如果不够再去找bind返回的新函数f传入的参数找。

另外,在代码的5-6行我们会发现,在new fn()时,本来应该返回一个fn的实例对象fn{},但实际返回的却是foo的实例对象foo{},并且foo中的this指向了全局,所以在new的过程会导致this指向全局,fn()执行返回foo的实例对象。

2. 手写myBind

bind与前面两个方法很不一样,第一需要注意的点就是调用bind后会返回一个新的函数体。

接下来我们看看:如果bind传入的第一个不是参数,新函数会是什么? image.png

从图中的结果可知:foo.bind(123)返回的是一个foo的实例对象,所以this指向的是全局。

另外,在前面我们已经说了如果我们new foo.bind(obj)得到的新函数,也会得到一个foo的实例对象。所以bind返回的函数体不能是箭头函数,因为箭头函数里面没有this,不能被new。

这就需要我们区分new fn()fn():因为new会使函数的this直接指向得到的实例对象,且让实例对象的隐式原型等于构造函数的显式原型,所以我们采用判断this.__proto__ === F.prototype来判断是否被new。

代码如下:

Function.prototype.myBind = function (context, ...args) {
    if (typeof (this) !== 'function') { // 判断this必须是函数
        throw new TypeError('Error')
    }
    context = context || window
    const self = this // 存储this的值 foo

    return function F(...args2) { 
        if(this.__proto__ === F.prototype){ // 被 new 直接返回 foo 实例,所以这里不能是箭头函数
            return new self(...args, ...args2) // 返回foo 的实例对象
        }else{
            return self.apply(context, [...args, ...args2])  // foo指向obj,foo执行且返回值接收并返回
        }
    }
}

const fo = foo.myBind(obj, 1, 2)
console.log(fo(),'//////'); // 1 3 hello //////

const fun = new fo()
console.log( 'new fo() 得到的结果:', fun); // undefined 3  new fo() 得到的结果:foo {}

好啦,本次知识点分享完毕,家人们下次见!!!

喜欢这次的文章就麻烦点个赞赞啦~谢谢大家!

image.png

从浏览器进程层面理解事件循环

2025年5月18日 17:49

现代浏览器的进程架构

进程之间是独立存在的,因此为了防止浏览器 tab 连续崩坏的情况,如今已经演变成了多进程架构,每个进程都会有个独立的内存空间

一般就是下面这六个进程:

  1. 浏览器主进程(Browser Process)
  • 负责管理浏览器的界面显示(比如导航栏)、用户交互、子进程管理
  • 处理书签、历史记录、下载等功能
  • 协调其他进程
  1. 渲染进程(Renderer Process)
  • 负责网页内容的渲染
  • 每个标签页通常会有自己的渲染进程(沙箱隔离)
  • 包含多个线程:
    • 主线程:处理JavaScript执行、DOM解析、CSS计算等
    • 合成线程:处理图层合成
    • 工作线程:处理Web Workers
    • 光栅化线程:将图形转换为位图
  1. GPU进程(GPU Process)
  • 处理GPU任务,加速图形渲染
  • 负责将渲染进程的输出合成并显示到屏幕上
  1. 网络进程(Network Process)
  • 处理网络请求
  • 实现HTTP/HTTPS协议
  • 管理缓存
  1. 插件进程(Plugin Process)
  • 运行浏览器插件(如Flash)
  • 每个插件通常有自己的进程
  1. 实用程序进程(Utility Process)
  • 处理一些辅助功能,如音频服务、打印等

1.png 在 windows 下,我们可以通过快捷键 Ctrl + Shift + Esc 来打开 windows 的实时进程,其实浏览器也有对应的界面,我们可以通过 Shift + Esc 来打开浏览器的实时进程

2.png

比如我这里开了两个 tab,首先每个 tab 都会存在一个独立的进程,这个进程就是我们说的渲染进程,图中可以看到 第一个 Chrome 图标的那个就是浏览器的主进程,第二个为 GPU 进程,第三个为 网络进程,第四个为 实用程序进程,然后后面几个 service worker 其实就是后台的特殊进程

其实早在 2018年,Chrome 就已经更新了一个名为 站点隔离(Site Isolation)的机制,这意味着不再是简单地一个标签页一个进程,而是按照网站的源(协议 + 域名 + 端口)来分配进程,这么做对于很多用户来讲应该可以很大程度减少 Chrome 内存占用,像是开发过程中你可能 csdn 会有很多 tab 同时存在,不过Chrome 会够根据用户的硬件设备调整不同的进程架构,像是站点隔离在内存受限的设备上就不会生效

实用程序进程这里可以看到是个 storage service ,这就是他处理存储的功能,当我们看 B站 视频时,就会存在一个 audio service,就说明在发挥它的 音频 功能

六个进程中我们了解大概就差不多了,但是渲染进程我们需要单独聊聊,这对前端仔来讲还是非常重要的

因为我们的前端代码均是在这个进程程执行的

渲染进程

渲染进程负责将 HTML,CSS 和 JS 转换为用户可以看到的网页内容和交互

渲染进程核心的职责就是我们熟知的下面五个步骤:

  1. 解析HTML和CSS:将HTML转换为DOM树,将CSS转换为CSSOM树
  2. 执行JavaScript:运行页面中的JavaScript代码
  3. 布局计算:确定每个元素在屏幕上的确切位置和大小
  4. 绘制:将元素绘制到内存中的位图
  5. 合成:将不同的绘制层合成为最终显示的图像

若将渲染进程进行线程拆分,那么它主要是靠三个线程:主线程、合成线程和光栅线程

还有 工作线程(Web Workers),定时器线程,事件触发线程

3.png 我们的前端代码其实就是在 渲染进程中的 主线程 执行的,接下来我们引入事件循环来结合讲解

渲染主线程

渲染主线程负责渲染进程的大部分工作,所以它需要处理许多任务,执行 js 是它,绘制页面又是它,可能发生性能瓶颈,说 js 代码阻塞其实就是因为渲染主线程只有一个,无法同时处理两份属于自己的工作,渲染主线程的主要工作如下:

  • 执行 JS 代码
  • 处理 DOM 操作
  • 处理用户事件(如 Click)
  • 处理计时器回调
  • 处理网络请求回调
  • 执行微任务和宏任务

这个时候其实你可能会很好奇为何渲染主线程要做的东西这么多,也确实容易出现问题,那他为何不分配一些新的线程来做呢,比如专门给一个 js 线程来执行 js 代码,专门一个 dom 线程来处理 dom 操作

其实这个问题会有很多原因,其中我们最容易理解的就是因为 js 可以操作 dom,多个线程同时修改 dom 会导致难以预测的竞态条件;还有就是历史原因,js 最初就是个单线程语言,web 本身就非常向后兼容,改变这个线程模型就会破坏现有网站;再一个就是实际上浏览器已经采取了多线程优化方案,比如 WebWorker 新开了一个线程执行 js,但是这些 js 又不能直接访问 dom

事件循环(event-loop / message-loop)我们已经很熟悉了,但是想要真正理解透彻我们应该将 UI 渲染这一步骤结合进来

事件循环出来的目的也就是因为 js 执行变得复杂,此前单线程的原因并没有考虑这个问题,后来才逐步引入的机制,比如一个 for 循环很多次,执行的过程中某个定时器的回调也到了时间应该如何调度呢,如何调度其实就是事件循环,通俗理解这个东西就是让任务之间进行排队

事件循环在主线程上的工作流程如下:

  1. 执行当前的 JS 调用栈中的所有同步代码
  2. 检查微任务队列(microtask queue),执行所有微任务直到队列清空
  3. 执行一个宏任务(macrotask)
  4. 再次检查微任务队列,执行所有微任务
  5. 如果需要,执行UI渲染
  6. 返回步骤3,继续循环

我们再来复习下常见的宏微任务有哪些:

微任务

  • Promise回调(.then/.catch/.finally)
  • MutationObserver回调
  • queueMicrotask() API
  • 处理优先级较高,在每个宏任务之后立即执行

宏任务

  • Script 标签
  • setTimeout/setInterval回调
  • 用户交互事件(点击、键盘输入等)
  • 网络请求回调(XHR, fetch)
  • MessageChannel
  • requestAnimationFrame
  • I/O操作

也许有人会争 宏任务比 同步先执行,这么说也是对的,因为本身 Script 就是个宏任务

4.png 执行 js 代码过程中,不一定会是当前执行代码所引入的 回调 进入消息队列,也有可能来自浏览器进程监听的用户交互,比如 click 事件,浏览器进程虽然不会执行 js 代码,js 执行是在渲染进程的渲染主线程中,浏览器进程可以监听事件然后放入消息队列,js 再从消息队列拿回调来执行

所以我们现在可以明白浏览器如何处理这些事件的,我们可以临时往消息队列中塞任务,但是执行就不一定是立即执行,因为他需要先等当前的调用栈执行完毕后,再依次检查消息队列中前面的事件是否执行完毕再去执行这个临时加的任务

5.png

event-loop 就是为了解决 js 单线程问题,异步的目的就是为了不让页面卡死,因为渲染这个关键步骤也是在 event-loop 之中或者说由渲染主线程负责

消息队列的优先级

消息队列可以理解为任务队列,我们在面试时常常会说宏任务队列,微任务队列,但是大家也清楚,其实宏任务这个概念官方已经抛弃了,此前宏任务就是 macrotask,现在官方换成了 task ,就是一个 任务,微任务依旧为 microtask

我也不清楚为何要换个名字,其实 task 和 macrotask 没有本质区别,因此我们完全可以沿用这个宏任务概念

对于 js 而言,任务是没有优先级的,它只管从 消息队列 依次去获取任务执行,这个任务就是回调,其实每个任务都会被浏览器包装成一个对象或者说是一个块

但是消息队列是具有优先级的,task queue 其实就是宏任务队列,因为浏览器的复杂程度越来越高,宏任务队列其实会被划分成不同的队列,有可能定时器会专门有一个定时器的队列,然后事件回调会有一个事件回调的队列,ui 渲染会专门有一个 渲染队列,这些队列具有优先级,另外,每种任务其实会专门放到对应的任务队列中,但是其实也可以放到其他不同的任务队列,比如监听事件的回调可以放到定时器队列,但是这样做后,它就不能放到其余的队列中了

这是 w3c 给出的规定,同一个类型必须在同一个队列,也可以分属不同的队列

宏任务细分下去的任务队列优先级其实不需要我们关注,我们只需要清楚微任务队列的优先级永远是最高的

不过对于宏任务队列而言,一般用户交互的事件队列是最高级的,比如点击,键盘,鼠标,其次再是 ui 渲染,这个也很好理解,为用户体验着想,总不可能用户点击后这个任务还有延迟执行吧,后面的优先级详细内容请看下面图示

6.png

有个地方需要特别留意,微任务优先级最高,这就意味着 高于 了 ui 渲染这个队列

另外,这里也可以看到定时器哪怕在宏任务队列中都是优先级较低的存在,就算不管微任务队列这个最高优先级,他的执行都是靠后的,因此他的计时肯定是不准的,因为他的回调需要等待前面的任务执行完毕才能继续执行,另外在 w3c 中有个 规定,定时器嵌套(nesting)层级超过了 5 层,后面的定时器就会增加 4 ms 的误差,其实浏览器的定时器实现本身就是调用的操作系统,操作系统本身的计时也是存在误差的,这点无法避免,真正准时的永远是原子钟

最后

总结下本文主要内容

浏览器有很多进程,但是对于前端仔来讲主要关注渲染进程,这个进程其实主要发挥作用的又是渲染主线程,由于前端代码都是在这个进程运行的,因此可以说 js 是个单线程语言,渲染其实也是个 task queue 类型,他的优先级还是比较高的,因此在一轮 event-loop 结束后,就是 ui 渲染线程开始执行

JavaScript执行栈和执行上下文

2025年5月18日 16:16

在JavaScript中,执行栈和执行上下文是理解代码执行流程和作用域链的关键概念。它们决定了代码如何执行以及变量和函数如何被查找和访问。本文将详细介绍执行上下文的生命周期、执行栈的工作原理以及它们在实际编程中的应用。

一、执行上下文

(一)什么是执行上下文?

执行上下文(Execution Context)是JavaScript代码执行的环境。它是一个抽象的概念,用于描述代码在运行时的状态。每当JavaScript代码运行时,它都在某个执行上下文中运行。

(二)执行上下文的类型

JavaScript中有三种执行上下文类型:

  1. 全局执行上下文:这是默认的上下文,任何不在函数内部的代码都在全局上下文中运行。全局执行上下文在页面加载时创建,当页面关闭时销毁。
  2. 函数执行上下文:每当一个函数被调用时,都会为该函数创建一个新的执行上下文。函数执行上下文在函数调用时创建,函数执行完成后销毁。
  3. eval函数执行上下文eval函数内部的代码也有自己的执行上下文。不过,eval函数的使用并不推荐,因为它会带来安全问题和性能问题。

(三)执行上下文的生命周期

执行上下文的生命周期分为两个阶段:

  1. 创建阶段:当代码执行进入一个环境时,会创建一个执行上下文。在这个阶段,执行上下文会进行以下操作:

    • 创建变量对象(Variable Object,VO):包括函数的形参、arguments对象、函数声明和变量声明。
    • 确定this的指向。
    • 确定作用域链。
  2. 执行阶段:在执行阶段,代码开始执行,变量被赋值,函数被调用,其他代码按顺序执行。

二、执行栈

(一)什么是执行栈?

执行栈(Call Stack)是JavaScript运行时用来管理执行上下文的一种数据结构。它是一个后进先出(LIFO)的栈结构,用于跟踪函数调用的顺序。

(二)执行栈的工作原理

  1. 入栈:当代码执行进入一个新的环境时,对应的执行上下文会被推入执行栈中。
  2. 出栈:当函数执行完成时,对应的执行上下文会被从执行栈中弹出,控制权交由下一个执行上下文。

(三)执行栈的特点

  • 后进先出:最后进入执行栈的执行上下文最先被弹出。
  • 栈顶是当前执行的上下文:执行栈的栈顶总是当前正在执行的函数的执行上下文。

(四)执行栈的图解

以下是一个具体的代码示例及其对应的执行栈图解:

function foo() { 
    function bar() {        
        return 'I am bar';
    }
    return bar();
}
foo();

对应的执行栈图解如下:

执行栈图解

(五)执行栈的数量限制

虽然执行上下文的数量没有明确的限制,但如果超出栈分配的空间,会造成堆栈溢出。常见于递归调用,没有终止条件造成死循环的场景。

// 递归调用自身
function foo() {
    foo();
}
foo();
// 报错:Uncaught RangeError: Maximum call stack size exceeded

三、执行上下文的生命周期

(一)创建阶段

在创建阶段,执行上下文会进行以下操作:

  1. 创建变量对象(VO)

    • 确定函数的形参(并赋值)。
    • 初始化arguments对象(并赋值)。
    • 确定普通字面量形式的函数声明(并赋值)。
    • 变量声明,函数表达式声明(未赋值)。
  2. 确定this的指向this的值由调用者决定。

  3. 确定作用域:由词法环境决定,哪里声明定义,就在哪里确定。

(二)执行阶段

在执行阶段,执行上下文会进行以下操作:

  1. 变量对象赋值

    • 变量赋值。
    • 函数表达式赋值。
  2. 调用函数

  3. 顺序执行其他代码

四、变量对象

变量对象(Variable Object,VO)是执行上下文的一个重要组成部分,它是一个包含变量、函数声明和形参的对象。在创建阶段,变量对象会被初始化,包括以下内容:

  • arguments对象:包含函数调用时传入的参数。
  • 形参:函数的形参会被赋值。
  • 函数声明:函数声明会被提升并赋值。
  • 变量声明:变量声明会被提升,但未赋值。

(一)变量对象的示例

以下是一个具体的代码示例及其对应的变量对象:

const foo = function(i) {
    var a = "Hello";
    var b = function privateB() {};
    function c() {}
}
foo(10);

在创建阶段,变量对象如下:

fooExecutionContext = {
    variableObject: {
        arguments: {0: 10, length: 1}, // 确定Arguments对象
        i: 10, // 确定形参
        c: pointer to function c(), // 确定函数引用
        a: undefined, // 局部变量初始值为undefined
        b: undefined // 局部变量初始值为undefined
    },
    scopeChain: {},
    this: {}
}

在执行阶段,变量对象如下:

fooExecutionContext = {
    variableObject: {
        arguments: {0: 10, length: 1},
        i: 10,
        c: pointer to function c(),
        a: "Hello", // a变量被赋值为Hello
        b: pointer to function privateB() // b变量被赋值为privateB()函数
    },
    scopeChain: {},
    this: {}
}

五、总结

执行上下文和执行栈是JavaScript中非常重要的概念。理解它们的工作原理和生命周期,可以帮助你更好地理解代码的执行流程和作用域链。

变量声明需谨慎!!!💣这几种声明变量的方式(var、let、const)还有作用域,绝不能含糊!

2025年5月18日 15:56

引言

JavaScript作为一门动态脚本语言,其变量声明机制和作用域规则一直是我们需要深入理解的核心内容。从早期的var到ES6引入的letconst,JavaScript的变量管理方式经历了显著的变化。今天,我们从多个角度,全面解析varletconst的异同。

一、JS代码的执行机制

对于给定的JS代码文件,首先要做的是将其从硬盘读入内存,然后开始执行。JavaScript代码的执行依赖于引擎,如Chrome的V8引擎。V8负责将代码从硬盘读入内存后,进行解析、编译和优化。其核心流程分为两个阶段:

  • 编译阶段:引擎对代码进行词法分析、语法分析,并确定作用域规则。
  • 执行阶段:逐行执行代码,处理变量赋值、函数调用等操作。

二、作用域与作用域链

2.1 作用域的类型

作用域是变量和函数的可访问性规则,JavaScript中分为三类:

  1. 全局作用域:在函数或代码块外声明的变量,全局可访问。
  2. 函数作用域:在函数内部声明的变量,仅函数内可见。
  3. 块级作用域(ES6新增):由{}包裹的代码块(如iffor),使用letconst声明的变量仅块内有效。

2.2 作用域链的运作机制

当访问一个变量时,引擎会按照作用域链逐层查找:

当前作用域 → 父级作用域1 → 父级作用域2 → ... → 全局作用域
(ps:父作用域可嵌套)

这种链式结构确保了变量的层级隔离性。

function outer() {
    let a = 10;
    function inner() {
        console.log(a); // 通过作用域链找到outer的a
    }
    inner();
}
outer(); // 输出10

image.png

三、变量提升(Hoisting)

3.1 var的变量提升

在编译阶段,var声明的变量会被提升到作用域顶部,并初始化为undefined,而赋值操作保留在执行阶段。

示例

console.log(x); // undefined
var x = 5;
console.log(x); // 5

等效于:

var x; // 提升声明
console.log(x); // undefined
x = 5; // 赋值
console.log(x); // 5

3.2 let的变量提升

let声明的变量也会被提升到其所在的作用域顶部,但与var不同,let声明的变量在初始化之前会进入一个“暂时性死区”(Temporal Dead Zone, TDZ)。在这个区域内,变量是“提升”了,但尚未初始化,因此不能被访问,任何尝试访问这些变量的操作都会抛出[ReferenceError]

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;

3.3 函数声明提升

函数声明整体被提升,提升的是定义,而不是调用,因此可以在声明前调用:

showName() // 驼峰式命名
console.log(myName);

var myName = 'wym'
function showName() {
  let b = 2;
  console.log(myName)
  console.log('函数执行了')
}

运行结果:

image.png 解析:这段代码等效于

function showName() { 
  let b = 2; 
  console.log(myName) 
  console.log('函数执行了') 
}

showName()
console.log(myName)
var myName = 'wym'

注意:函数和变量之间,函数通常先于变量提升,即:优先提升函数,后提升变量。

四、var、let、const的全面对比

4.1 作用域差异

关键字 作用域 重复声明 变量提升 必须初始化
var 函数/全局 允许
let 块级 不允许 否(TDZ)
const 块级 不允许 否(TDZ)

4.2 使用场景分析

  • var:ES6之前的主要声明方式,因作用域和提升问题,现不推荐使用。
  • let:适用于需要重新赋值的块级变量(如循环计数器)。
  • const:声明常量或引用类型(对象、数组),确保变量指向不变。

五、建议

定义变量时如果后续不需要修改了,建议优先使用const,提高代码可读性和安全性;次选let,杜绝重复声明报错,作用域更安全,其独特的TDZ机制也能够有效防止在声明前访问变量;var能不用就不用,防止变量污染和意外覆盖。

《JavaScript语言精粹》读书笔记之第3章:对象Object

作者 小飞悟
2025年5月18日 15:30

小飞悟申明:小编的笔记只针对强者!!!

一、对象字面量 Object Literals

属性名可以是包括空字符串在内的任何字符串。在对象字面量中,如果属性名是一个合法的JavaScript标识符且不是保留字,则并不强制要求用引号括住属性名。所以用引号括住"first-name"是必需的,但是否括住first_name则是可选的。逗号用来分隔多个“名/值”对。

  1. 合法标识符 :
  • 如果属性名是合法的JavaScript标识符且不是保留字,可以不加引号。
- 例如:
const person = {
    firstName: 'John',
    lastName: 'Doe'
};


var flight = {
       airline: "Oceanic",
       number: 815,
       departure: {
          IATA: "SYD",
          time: "2004-09-22 14:55",
          city: "Sydney"
       },
  1. 非法标识符 :
  • 如果属性名包含特殊字符、空格或不是合法标识符,必须加引号。
- 例如
const person = {
    "first-name": 'John',
    "last name": 'Doe'
};

二、检索 Retrieval

要检索对象里包含的值,可以采用在[ ]后缀中括住一个字符串表达式的方式。如果字符串表达式是一个字符串字面量,而且它是一个合法的JavaScript标识符且不是保留字,那么也可以用.表示法代替。优先考虑使用.表示法,因为它更紧凑且可读性更好。

    stooge["first-name"]    // "Jerome"
    flight.departure.IATA   // "SYD"

运算符可以用来填充默认值:

    var middle = stooge["middle-name"] || "(none)";
    var status = flight.status || "unknown";

尝试从undefined的成员属性中取值将会导致TypeError异常。这时可以通过&&运算符来避免错误。

    flight.equipment                            // undefined
    flight.equipment.model                      // throw "TypeError"
    flight.equipment && flight.equipment.model  // undefined

三、引用 Reference

对象通过引用来传递。它们永远不会被复制:

理解对象引用

在JavaScript中,对象是通过引用来传递的,这意味着当将一个对象赋值给另一个变量时,两个变量实际上指向同一个对象。以下是对代码的详细解释:

  1. 对象引用 :
var x = stooge;
x.nickname = 'Curly';
var nick = stooge.nickname;
  • x 和 stooge 指向同一个对象,因此修改 x 的属性也会影响 stooge 。
  • nick 的值为 'Curly' ,因为 x 和 stooge 是同一个对象的引用。
  1. 多个对象引用 :
var a = {}, b = {}, c = {};
  • a 、 b 和 c 分别引用不同的空对象。
  1. 同一对象引用 :
a = b = c = {};
  • a 、 b 和 c 现在都引用同一个空对象。

四、原型 Prototype(简单介绍,后续会细讲)

每个对象都连接到一个原型对象,并且它可以从中继承属性。所有通过对象字面量创建的对象都连接到Object.prototype,它是JavaScript中的标配对象。 原型连接只有在检索值的时候才被用到。如果我们尝试去获取对象的某个属性值,但该对象没有此属性名,那么JavaScript会试着从原型对象中获取属性值。如果那个原型对象也没有该属性,那么再从它的原型中寻找,依此类推,直到该过程最后到达终点Object.prototype。如果想要的属性完全不存在于原型链中,那么结果就是undefined值。这个过程称为委托。

  • 这里仅是定义,后续会详细讲

刚开始有点难理解很正常,建议先看看blog.csdn.net/flyingpig20…

五、反射 Refelection

  • 用typeof操作符来确定属性类型很有帮助。
    typeof flight.number      // 'number'
    typeof flight.status      // 'string'
    typeof flight.arrival     // 'object'
    typeof flight.manifest    // 'undefined'
  • 在JavaScript中,原型链上的属性(如 toString 和 constructor )可能会产生值,但这些值通常是函数,可能并非我们需要的。以下是两种处理这些不需要的属性的方法:
    typeof flight.toString     // 'function'
    typeof flight.constructor  // 'function'
  1. 检查并丢弃函数值 :

    • 在程序中检查属性值是否为函数,如果是则丢弃。
    • 适用于需要动态获取对象信息且仅关注数据的场景。
  2. 使用 hasOwnProperty 方法 :

    • hasOwnProperty 方法用于检查对象是否拥有独有的属性(不检查原型链)。
    • 示例:
      flight.hasOwnProperty
      ('number');      // true
      flight.hasOwnProperty
      ('constructor'); // false
      
    • 适用于需要区分对象自身属性和继承属性的场景。 通过这两种方法,可以更精确地处理对象属性,避免不必要的函数值干扰。

六、枚举Enumeration

for in 语句的总结

for in 语句用于遍历对象的所有属性名,包括原型链中的属性。为了过滤掉不需要的属性,可以使用 hasOwnProperty 方法或 typeof 来排除函数。

示例 1:过滤函数属性

var name;
for (name in another_stooge) {
  if (typeof another_stooge[name] !== 'function') {
    document.writeln(name + ': ' + another_stooge[name]);
  }
}

示例 2:按特定顺序遍历属性

var i;
var properties = ['first-name', 'middle-name', 'last-name', 'profession'];
for (i = 0; i < properties.length; i += 1) {
  document.writeln(properties[i] + ': ' + another_stooge[properties[i]]);
}

总结:for in 语句遍历对象属性时,属性顺序不确定。如果需要特定顺序,可以使用数组存储属性名,并通过 for 循环遍历。

七、删除Delte

删除对象属性的总结

delete 运算符用于删除对象的属性,但它不会影响原型链中的属性。如果删除的对象属性存在于原型链中,删除后原型链中的属性会“透现”出来。

示例

const stooge = {
  nickname: 'Curly'
};
const another_stooge = Object.create(stooge);
another_stooge.nickname = 'Moe';
console.log(another_stooge.nickname); // 'Moe'

delete another_stooge.nickname;
console.log(another_stooge.nickname); // 'Curly'(来自原型链)

解释

  1. another_stooge对象继承了stooge对象的nickname属性。
  2. 通过delete删除了another_stooge自身的nickname属性后,原型链中的nickname属性(值为'Curly')会显示出来。

通过这个示例,可以更好地理解delete运算符的作用及其对原型链的影响。

关键点

  1. 删除属性delete 删除对象自身的属性。
  2. 原型链delete 不会影响原型链中的属性。
  3. 透现属性:如果删除的属性存在于原型链中,删除后原型链中的属性会显示出来。

八、减少全局变量污染 Global Abatement

减少全局变量污染

在JavaScript中,全局变量(var)会削弱程序的灵活性,应尽量避免使用。最小化全局变量污染的一种方法是创建一个唯一的全局变量作为应用的容器,将所有全局性资源纳入该名称空间下。

示例

var MYAPP = {};
MYAPP.stooge = {
  "first-name": "Joe",
  "last-name": "Howard"
};
MYAPP.flight = {
  airline: "Oceanic",
  number: 815,
  departure: {
    IATA: "SYD",
    time: "2004-09-22 14:55",
    city: "Sydney"
  },
  arrival: {
    IATA: "LAX",
    time: "2004-09-23 10:42",
    city: "Los Angeles"
  }
};


通过将全局资源集中在一个名称空间下,可以显著降低程序与其他应用程序、组件或类库之间的冲突风险,同时提高代码的可读性和维护性。但方法不止一种,ES6推出了let,const,在后面章节将会详细介绍,先看下区别:

let 、var 和const的区别

let和const 与var有什么区别

let和const 是es6的新语法,在函数预编译的时候会进行变量提升,这样在变量还没有赋值的时候就可以进行访问。

但是let和const不会,而且let和const遇到{}会形成块级作用域,并且let和const在声明之前是不能访问的,也不能访问

外部具有相同名字的变量因为会形成暂时性死区。这就是let、const和var的区别。

let和const的区别

它们两个的区别主要在let是声明变量,而const是声明常量的。

结语:

本文简单讲了下JavaScript中对象操作的核心概念,包括对象字面量、属性检索、原型链、反射、枚举、删除操作以及减少全局变量污染等关键点。现阶段建议读者继续深入了解下原型链和代理模式,后续小编还会奉上精彩好文!!!

作用域链和闭包(clousre)拆解(3)

2025年5月18日 14:00

一、作用域链

1.1 代码分析:

看看输出啥。

function cat() {
  console.log(myName);
}
function dog() {
  var myName = "王中王";
  function dogking() {
    console.log(myName);
  }
  dogking();
  cat();
}
var myName = "旺中旺";
dog();

1.2 调用栈分析

在这里插入图片描述

全局执行上下文和dog函数执行上下文中都包含myname 变量,dogKing和cat函数中的myname变量会取哪个呢? 其实两个函数输出结果并不相同。 dogKing函数输出为“王中王”,cat函数输出为“旺中旺”。 为什么呢?

1.3 作用域链

首先,变量的查找是根据作用域链的规则来的。

那么作用域链是什么呢?作用域链是js引擎用来解析变量的机制。查找变量时,js引擎会先在当前作用域查找,如果没找到,继续向外层查找,直至全局作用域。这个从内向外的查找链条就是作用域链。

那按照这个概念理解,dogKing函数的输出是按照作用域链查找的,cat函数则不是。因为dog和dogKing函数组成了一个闭包,闭包比较特殊,dogKing的外级作用域就是dog函数。

js调用栈中,每个执行上下文中都包含全局执行上下文的引用,我们把这个引用称为outer。 在这里插入图片描述 cat和dog函数查找变量时,首先在当前的执行上下文中查找,没有找到,会继续查询outer指向的全局执行上下文中进行查找。

那为什么cat的外部引用时全局执行上下文,而不是dog函数执行上下文呢?这是因为在执行过程中,作用域链是根据词法作用域决定的。

1.4 词法作用域

词法作用域是js中作用域的静态结构。在代码编写时确定,与代码执行无关。是由函数的嵌套结构确定,与函数调用无关。 因此在函数定义时,根据词法作用域,dog和cat函数的上级作用域都是全局作用域。

1.5 练习

块级作用域变量查找同理。

function cat() {
  let age1 = 20
  console.log(age);
}
function dog() {
  var myName = "王中王";
  function dogking() {
    console.log(myName);
  }
  let age = 18
  dogking();
  cat();
}
var myName = "旺中旺";
let age = 10
dog();

试着根据调用栈分析下cat函数中输出的值。

在这里插入图片描述 这是这段程序的调用栈。var声明的变量和函数声明在变量环境中,let和const声明在词法环境中。 在这里插入图片描述 变量查找时, ①查找当前作用域的词法环境,从栈顶到栈底 ②查找当前作用域的变量环境 ③查找全局作用域的词法环境,从栈顶到栈底,找到age=10 ④找到,结束。

二、闭包

2.1 上代码

function func() {
    var myName = "李三岁"
    let num1 = 1
    const num2 = 2
    var innerFunc = {
        getName:function(){
            console.log(num1)
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerFunc
}
var tem = func()
tem.setName("李逍遥")
console.log(tem.getName())

首先,我们看当执行到func函数结尾时的调用栈情况: 在这里插入图片描述

上述代码中,innerFunc对象中包含getName和setName两个方法,定义在func函数内部。根据词法作用域,即声明时,getName和setName方法可以顺着作用域链访问到func函数中的变量。因此可以引用myName和num1两个变量。 接着innerFunc返回给tem变量后,虽然func函数执行完毕,但是此时依然引用func函数中的两个变量,因此并不会被回收。此时的调用栈情况为:

在这里插入图片描述 func函数执行完成以后,其执行上下文从栈顶弹出,但myName和num1变量还没getName和setName方法使用,因此还保存在内存中。无论在哪里调用这两个方法,都可以访问这两个变量,其他任何方法都访问不到这两个变量。因此这两个方法和变量就组成了闭包。

2.2 定义

mdn中闭包的定义为:闭包是由捆绑起来的(封闭的)函数和函数周围状态(词法环境)的引用组合而成。 因此,闭包可以让内部函数访问其外部作用域,即使外部函数执行结束,内部函数引用的外部函数的变量依然保存在内存中,内部函数及其引用的变量组成闭包。其实宽泛理解,在js中所有的函数都是一个闭包。

2.3 变量查找

闭包中的函数执行后,变量查找时,js引擎会沿着:setName函数执行上下文>func闭包>全局执行上下文的顺序查找。 在这里插入图片描述 在浏览器中打断点后,我们看开发者工具的信息: 在这里插入图片描述 当给myName赋值时,Scope项体现了作用链为:Local>Closure>Block>Global。因为我在vue3的项目中运行,如果在单独的js文件中运行,Block中的变量会在Global中,没有Block这一环。Local就是当前setName方法的作用域。Closure(func)就是func函数的闭包。Global为全局作用域。

2.4 闭包回收

  • 如果引用闭包的变量是个局部变量,等该作用域销毁后,下次gc执行垃圾回收时,进行是否还在使用的判断和内存回收。
  • 如果引用闭包的变量是个全局变量,那么该闭包会一直存在知道页面关闭。若以后闭包不再使用,会造成内存泄漏。(总的内存大小不变,可用的内存大小变小了)这也是闭包的一大缺点。

2.5 闭包的用途

2.5.1 数据封装和私有化

上代码

function createPerson(age) {
  const privateAge = age;   // 私有变量
  
  return {
    getAge: function() {
      return privateAge;
    },
    setAge: function(newAge) {
      if (typeof newAge === 'number' && newAge > 0) {
        privateAge = newAge;
      }
    }
  };
}

const person = createPerson(30);
console.log(person.getAge());  // 输出: 30
person.setAge(35);
console.log(person.getAge());  // 输出: 35

上述代码中,person变量可以通过闭包访问privateAge变量,但外部代码不能访问。 同时也可以作为缓存,保存在内存中。

因此闭包可以用来创建私有变量和方法,防止外部直接访问和修改。

2.5.2 防抖和节流

防抖:

function shake(){
let timer = null

function func(){
if(timer != null) clearTimeout(timer)

timer = setTimeout(()=>{
// todo want to do
},200)
}
}

节流

function throttle(){
let timer = null

function func(){
if(timer != null) return

timer = setTimeout(()=>{
// todo want to do
timer = null
},200)
}
}

防抖和节流都是通过闭包使变量存在于内存中,借助变量实现想要的功能。

三、做个题

var obj = {
    myName:"time",
    printName: function () {
        console.log(myName)
    }    
}
function func() {
    let myName = "李三岁"
    return obj.printName
}
let myName = "刘大哥"
let _printName = func()
_printName()
obj.printName()

分析一下,当执行到func()未return时的调用栈: 在这里插入图片描述 执行完成后弹出栈顶: 在这里插入图片描述 此时_printName被赋值,执行时: 在这里插入图片描述 查找myName变量:_printName函数执行上下文词法环境>_printName函数执行上下文变量环境>全局词法环境,找到,输出“刘大哥”。 执行至obj.printName()时,情况相同,obj.printName函数执行上下文中的词法环境和变量环境中均为空,所以查找到全局执行上下文中。 因此printName函数的myName变量是属于全局作用域下的,此作用域链由词法作用域决定。

总结:此段程序中并没有生成闭包。obj不是一个函数,其中的myName和printName是他的两个属性,彼此并没有联系。若想产生联系,需要加上this关键字。否则printName会通过词法作用域链查找myName

文章参考:time.geekbang.org/column/intr… zhuanlan.zhihu.com/p/683323392 juejin.cn/post/737617…

走出变量提升的迷雾:10分钟彻底搞懂JavaScript执行机制与作用域

2025年5月18日 13:23

前言

JavaScript作为一门灵活的编程语言,其执行机制和变量声明规则有着诸多特性。理解这些特性对于编写高质量的JavaScript代码至关重要。本文将深入探讨JavaScript的执行机制、作用域、变量提升以及ES6引入的新特性,帮助开发者避免常见陷阱。

1. JavaScript代码的执行机制

JavaScript代码执行分为两个关键阶段:

1.1 编译阶段

当JavaScript引擎(如Chrome的V8)拿到代码后,首先进入编译阶段:

  • 代码从硬盘读入内存
  • 语法检测
  • 创建执行上下文环境
  • 处理变量声明和函数定义
// 编译阶段会创建类似这样的结构
currentVariable {
  showName: <function reference>,
  myName: undefined,
  // ...其他变量
}

1.2 执行阶段

编译完成后,代码按顺序执行,完成实际的赋值和函数调用操作。

2. 作用域:变量查找的规则

作用域决定了变量的可访问性和生命周期,JavaScript中包含三种主要作用域:

  • 全局作用域:在最外层定义的变量
  • 函数作用域:函数内部定义的变量
  • 块级作用域:ES6引入,在{}内使用letconst定义的变量

2.1 作用域链

当访问一个变量时,JavaScript会按照"冒泡查找"的规则:

  1. 先在当前作用域查找
  2. 找不到则向上层作用域查找
  3. 直到找到变量或到达全局作用域
  4. 全局作用域也没有则报错

这种查找路径构成了作用域链:当前作用域 → 父作用域 → ... → 全局作用域

2.2 词法作用域

JavaScript采用的是词法作用域(Lexical Scope),也称为静态作用域,这意味着函数的作用域在函数定义时就已确定,而非函数调用时:

let globalVar = 'global';

function outer() {
  let outerVar = 'outer';
  
  function inner() {
    console.log(outerVar); // 访问的是定义时的外部变量
    console.log(globalVar); // 同样可以访问更外层的全局变量
  }
  
  return inner;
}

const innerFn = outer();
innerFn(); // 输出: "outer" 和 "global"

词法作用域的特点:

  1. 静态确定:函数的作用域在编写代码时(词法分析阶段)就已确定
  2. 嵌套关系:内部函数可以访问外部函数中声明的变量
  3. 闭包基础:正是因为词法作用域,JavaScript才能实现强大的闭包功能

2.2.1 词法作用域与动态作用域的区别

为了理解词法作用域的特性,我们来看一个对比示例:

let value = 'global';

function foo() {
  console.log(value);
}

function bar() {
  let value = 'local';
  foo();
}

bar(); // 在词法作用域下输出: "global"
       // 如果是动态作用域则会输出: "local"

在上面的例子中,foo函数中的value引用的是全局变量,而非bar函数中的局部变量,这正是词法作用域的体现。

3. 变量提升(Hoisting)现象

3.1 var的变量提升

使用var声明的变量会在编译阶段被"提升"到当前作用域的顶部,但只提升声明,不提升赋值:

console.log(myName); // 输出:undefined
var myName = '曾小贤';

实际执行顺序相当于:

var myName; // 声明被提升
console.log(myName); // undefined
myName = '曾小贤'; // 赋值保留在原位置

3.2 函数声明的提升

函数声明会被完整提升到作用域顶部,包括函数体:

showName(); // "函数执行了"
function showName() {
    let b = 2;
    console.log('函数执行了');
}

这就是为什么在示例代码中,showName()可以在函数声明之前调用。

3.3 变量提升的问题

showName(); // 函数执行了
console.log(myName); // undefined
var myName = '曾小贤';
function showName() {
    let b = 2;
    console.log('函数执行了');
}

变量提升会导致代码执行结果与阅读顺序不一致,造成困惑,是JavaScript设计上的一个争议点。

4. let/const与暂时性死区(TDZ)

4.1 TDZ现象

ES6引入的letconst解决了变量提升的混乱,它们声明的变量不会被提升,相反会创建"暂时性死区":

console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 1;

从变量声明的块作用域开始,到该变量被赋值之前的区域,称为"暂时性死区"。在这个区域内,访问该变量会抛出错误。

4.2 块级作用域

{
  let blockVar = 'block scope';
  var functionVar = 'function scope';
}
console.log(functionVar); // "function scope"
console.log(blockVar); // ReferenceError: blockVar is not defined

letconst声明的变量严格遵循块级作用域,而var则遵循函数作用域。

5. var与let的关键区别

特性 var let
作用域 函数作用域 块级作用域
变量提升 会提升声明,初始值为undefined 不提升,有TDZ
重复声明 允许在同一作用域重复声明 禁止在同一块作用域重复声明
全局声明 成为window对象属性 不会成为window对象属性

6. 执行上下文示意图

从图中可以看到,执行上下文包含:

  • 全局上下文:最外层的执行环境
    • 变量环境:存储var声明的变量和函数声明
      a = undefined
      fn=function
      
    • 词法环境:存储let和const声明的变量

这种设计使得var声明的变量会出现提升现象,而letconst则不会。

7. 最佳实践建议

  1. 优先使用let和const:避免var的提升问题
  2. 默认使用const:如果变量不需要重新赋值,增加代码可靠性
  3. 理解TDZ:养成先声明后使用的习惯
  4. 合理划分作用域:减少作用域链查找,提高性能

总结

JavaScript的执行机制和变量声明规则看似复杂,实则有迹可循。理解编译和执行的双阶段过程、作用域链的查找规则、变量提升的工作原理以及ES6引入的新特性,不仅能帮助我们写出更可靠的代码,也能在面试中脱颖而出。

变量提升是JavaScript的一个历史包袱,但通过使用let和const,我们可以避开大多数陷阱。在现代JavaScript开发中,遵循"先声明后使用"的原则,合理利用块级作用域,才能充分发挥这门语言的优势。

正则表达式与文本处理的艺术

作者 BitCat
2025年5月18日 11:50

引言

在前端开发领域,文本处理是一项核心技能。正则表达式作为一种强大的模式匹配工具,能够帮助我们高效地处理各种复杂的文本操作任务。

正则表达式基础

什么是正则表达式?

正则表达式是一种用于匹配字符串中字符组合的模式。它由一系列字符和特殊符号组成,用于定义搜索模式。

// 基本示例:匹配所有数字
const numberPattern = /\d+/g;
const text = "我有23个苹果和45个橙子";
const numbers = text.match(numberPattern); // 结果: ["23", "45"]

基本语法元素

元素 描述 示例
. 匹配任意单个字符 /a.c/ 匹配 "abc", "axc" 等
[] 字符集,匹配方括号内的任意字符 /[abc]/ 匹配 "a", "b", 或 "c"
[^] 否定字符集,匹配任何不在方括号内的字符 /[^abc]/ 匹配除 "a", "b", "c" 之外的字符
\d 匹配任意数字,等价于 [0-9] /\d{3}/ 匹配三个连续数字
\w 匹配任意字母、数字或下划线,等价于 [A-Za-z0-9_] /\w+/ 匹配一个或多个字母数字字符
\s 匹配任意空白字符 /\s/ 匹配空格、制表符等

量词

量词决定了模式应该匹配多少次。

量词 描述 示例
* 匹配前一个元素零次或多次 /a*/ 匹配 "", "a", "aa", ...
+ 匹配前一个元素一次或多次 /a+/ 匹配 "a", "aa", ... 但不匹配 ""
? 匹配前一个元素零次或一次 /a?/ 匹配 "" 或 "a"
{n} 精确匹配前一个元素n次 /a{3}/ 匹配 "aaa"
{n,} 匹配前一个元素至少n次 /a{2,}/ 匹配 "aa", "aaa", ...
{n,m} 匹配前一个元素n至m次 /a{1,3}/ 匹配 "a", "aa", 或 "aaa"

锚点

锚点用于指定匹配的位置。

// 使用锚点匹配行首和行尾
const pattern = /^开始.*结束$/;
console.log(pattern.test("开始这是中间内容结束")); // true
console.log(pattern.test("这不是开始的内容结束")); // false

贪婪与惰性匹配

正则表达式的默认行为是贪婪匹配,它会尽可能多地匹配字符。相比之下,惰性匹配则尽可能少地匹配字符。

贪婪匹配

// 贪婪匹配示例
const htmlText = "<div>内容1</div><div>内容2</div>";
const greedyPattern = /<div>.*<\/div>/;
const greedyMatch = htmlText.match(greedyPattern);
console.log(greedyMatch[0]); // 结果: "<div>内容1</div><div>内容2</div>"

贪婪模式下,.* 会匹配尽可能多的字符,导致整个字符串都被匹配。

惰性匹配

// 惰性匹配示例
const htmlText = "<div>内容1</div><div>内容2</div>";
const lazyPattern = /<div>.*?<\/div>/g;
const lazyMatches = htmlText.match(lazyPattern);
console.log(lazyMatches); // 结果: ["<div>内容1</div>", "<div>内容2</div>"]

通过在量词后添加问号 ?,可以将贪婪匹配转为惰性匹配。惰性模式下,正则表达式引擎会尽可能少地匹配字符,在第一次找到完整匹配后就停止。

性能对比

// 贪婪匹配性能测试
const longText = "<div>".repeat(1000) + "</div>".repeat(1000);
console.time('greedy');
const greedyResult = /<div>.*<\/div>/.test(longText);
console.timeEnd('greedy'); // 可能需要很长时间甚至超时

// 惰性匹配性能测试
console.time('lazy');
const lazyResult = /<div>.*?<\/div>/.test(longText);
console.timeEnd('lazy'); // 通常比贪婪匹配快得多

在处理长文本时,惰性匹配通常比贪婪匹配有更好的性能,因为它避免了过度回溯。

捕获组

捕获组允许我们提取模式的特定部分,这在需要处理复杂文本时尤为有用。

基本捕获组

// 基本捕获组
const dateString = "今天是2023-05-15";
const datePattern = /(\d{4})-(\d{2})-(\d{2})/;
const match = dateString.match(datePattern);
console.log(match[0]); // "2023-05-15"(完整匹配)
console.log(match[1]); // "2023"(第一个捕获组)
console.log(match[2]); // "05"(第二个捕获组)
console.log(match[3]); // "15"(第三个捕获组)

命名捕获组

命名捕获组使代码更易理解,特别是在复杂模式中。

// 命名捕获组
const dateString = "今天是2023-05-15";
const datePattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = dateString.match(datePattern);
console.log(match.groups.year);  // "2023"
console.log(match.groups.month); // "05"
console.log(match.groups.day);   // "15"

非捕获组

当我们只需要分组但不需要捕获匹配内容时,可以使用非捕获组。

// 非捕获组
const text = "HTML和CSS都是前端必备技能";
const pattern = /(?:HTML|CSS)和(?:HTML|CSS)/;
console.log(pattern.test(text)); // true

反向引用

反向引用允许我们在模式中引用之前的捕获组。

// 反向引用
const htmlWithAttrs = '<div class="container">内容</div>';
const pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/;
const match = htmlWithAttrs.match(pattern);
console.log(match[1]); // "div"(标签名)
console.log(match[2]); // ' class="container"'(属性)
console.log(match[3]); // "内容"(内容)

性能优化技巧

避免过度使用贪婪模式

贪婪模式可能导致大量回溯,降低性能。在适当的情况下,使用惰性匹配可以显著提高效率。

// 不推荐(在大文本中可能很慢)
const slowPattern = /<div>.*<\/div>/;

// 推荐
const fastPattern = /<div>.*?<\/div>/;

优先使用更具体的模式

// 不推荐(太宽泛)
const emailCheck1 = /.*@.*/;

// 推荐(更具体)
const emailCheck2 = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;

避免嵌套量词

嵌套量词如 (a+)+ 可能导致指数级的性能下降,被称为"灾难性回溯"。

// 危险模式,可能导致回溯爆炸
const badPattern = /^(a+)*$/;
const input = "aaaaaaaaaaaaaaa!"; // 以感叹号结尾
console.time('test');
badPattern.test(input); // 可能导致浏览器挂起
console.timeEnd('test');

使用原子组优化

在支持原子组的环境中,可以使用原子组 (?>...) 来控制回溯。

// 在某些正则实现中支持原子组(JavaScript标准还不支持)
// const atomicGroup = /(?>a+)b/;

实际应用案例

表单验证

// 邮箱验证
function validateEmail(email) {
  const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return pattern.test(email);
}

// 密码复杂度验证(至少8位,包含大小写字母、数字和特殊字符)
function validatePassword(password) {
  const pattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/;
  return pattern.test(password);
}

// 手机号验证(中国大陆)
function validatePhone(phone) {
  const pattern = /^1[3-9]\d{9}$/;
  return pattern.test(phone);
}

高亮文本匹配

// 搜索关键词高亮
function highlightKeywords(text, keyword) {
  const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const pattern = new RegExp(`(${escapedKeyword})`, 'gi');
  return text.replace(pattern, '<span class="highlight">$1</span>');
}

// 使用示例
const searchResult = highlightKeywords(
  "JavaScript是一种用于网页交互的编程语言",
  "javascript"
);
console.log(searchResult); // "<span class="highlight">JavaScript</span>是一种用于网页交互的编程语言"

URL解析

// 提取URL参数
function getUrlParams(url) {
  const params = {};
  const pattern = /[?&]([^=&#]+)=([^&#]*)/g;
  let match;
  
  while ((match = pattern.exec(url)) !== null) {
    params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
  }
  
  return params;
}

// 使用示例
const url = "https://example.com/search?q=正则表达式&page=1&sort=desc";
const params = getUrlParams(url);
console.log(params); // {q: "正则表达式", page: "1", sort: "desc"}

代码格式化

// 格式化数字为千分位表示
function formatNumber(num) {
  return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

// 使用示例
console.log(formatNumber(1234567)); // "1,234,567"

边缘情况和限制

正则表达式的局限性

正则表达式不适合处理一些特定的文本结构,如HTML解析或嵌套结构。

// 错误的做法:使用正则表达式解析HTML
const htmlContent = '<div><p>文本1</p><p>文本2 <a href="#">链接</a></p></div>';
const badPattern = /<p>(.*?)<\/p>/g; // 不能正确处理嵌套标签

// 更好的做法:使用DOM解析
function extractParagraphText(html) {
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, 'text/html');
  const paragraphs = doc.querySelectorAll('p');
  return Array.from(paragraphs).map(p => p.textContent);
}

处理Unicode字符

JavaScript正则表达式对Unicode的支持有限,需要使用u标志。

// 没有u标志,无法正确处理Unicode
console.log(/^.$/.test('😊')); // false(表情符号被视为两个字符)

// 使用u标志正确处理Unicode
console.log(/^.$/u.test('😊')); // true

避免过度依赖正则表达式

有时候,使用字符串方法或专门的解析库可能是更好的选择。

// 对于简单的字符串操作,使用内置方法可能更清晰
// 不推荐
const csv = "a,b,c";
const values1 = csv.match(/([^,]+),([^,]+),([^,]+)/);

// 推荐
const values2 = csv.split(',');

对比分析

正则表达式 vs. 字符串方法

方法 优势 劣势
正则表达式 强大的模式匹配能力,简洁的代码 学习曲线陡峭,调试困难,性能问题
字符串方法 直观易懂,性能可预测 复杂模式匹配需要更多代码
// 提取域名 - 正则表达式方法
function getDomainRegex(url) {
  const match = url.match(/^https?:\/\/([^/]+)/);
  return match ? match[1] : null;
}

// 提取域名 - 字符串方法
function getDomainString(url) {
  if (!url.startsWith('http://') && !url.startsWith('https://')) {
    return null;
  }
  const withoutProtocol = url.replace(/^https?:\/\//, '');
  const firstSlash = withoutProtocol.indexOf('/');
  return firstSlash === -1 ? withoutProtocol : withoutProtocol.substring(0, firstSlash);
}

浏览器兼容性

大多数现代浏览器支持ES2018中引入的正则表达式功能(如命名捕获组),但在支持旧浏览器的项目中需要注意。

// 命名捕获组(在较旧的浏览器中不支持)
const datePattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

// 向后兼容的替代方案
const oldDatePattern = /(\d{4})-(\d{2})-(\d{2})/;
const match = "2023-05-15".match(oldDatePattern);
const [_, year, month, day] = match;

结论

正则表达式是前端开发中强大而必不可少的工具。通过深入理解贪婪与惰性匹配、捕获组、性能优化等核心概念,我们可以编写出高效、可读的正则表达式,解决各种文本处理问题。虽然学习曲线较陡,但掌握这一技能将极大提升我们的开发效率和代码质量。

正则表达式的精髓在于找到复杂性和可读性之间的平衡。一个好的正则表达式应当既能解决问题,又便于其他人理解和维护。

学习资源


如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇

终身学习,共同成长。

咱们下一期见

💻

vue 入门到实战 一

2025年5月18日 13:11

第1章 初始Vue.js

1.1 网站交互方式

Web网站有单页应用程序(SPA,Single-page Application)和多页应用程序(MPA,Multi-page Application)两种交互方式。

多页应用程序,顾名思义是由多个页面组成的站点。在多页应用程序中,每个网页在每次收到相应的请求时都会重新加载。多页应用程序很大,由于不同页面的数量和层数,有时甚至可以认为很麻烦,我们可以在大多数电子商务网站上找到MPA的示例。

多页应用程序以服务端为主导,前后端混合开发,例如:.php、.aspx、.jsp。技术堆栈包括HTML、CSS、JavaScript、jQuery,有时还包括AJAX。

图片

单页应用程序,就是只有一张Web页面的应用。单页应用程序是加载单个HTML页面并在用户与应用程序交互时,动态更新该页面的Web应用程序。浏览器一开始会加载必需的HTML、CSS和JavaScript,所有的操作都在这张页面上完成,都由JavaScript来控制。因此,对单页应用来说模块化的开发和设计显得相当重要。单页应用开发技术复杂,所以诞生了许多前端开发框架:Angular.js、React.js、Vue.js等。

选择单页应用程序开发时,软件工程师通常采用以下技术堆栈:HTML5、Angular.js、React.js、Vue.js、Ember.js、AJAX等。

图片

1.2 MVVM模式

MVVM是Model-View-ViewModel的缩写,它是一种基于前端开发的架构模式,其核心是提供对View和ViewModel的双向数据绑定,这使得ViewModel的状态改变可以自动传递给View,即所谓的数据双向绑定。

在MVVM架构下,View和Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model和ViewModel之间的交互是双向的,因此View数据的变化会同步到Model中,而Model数据的变化也会立即反应到View上。

图片

1.2.1、Model

模型(Model):对应data中的数据,一般JS对象

data: {a'',address: '',name: ''}

data中书写的Key:Value都会出现在Vue实例VM身上

图片

1.2.2、View

视图(View):对应模板(DOM)

<div id="root"><h2>{  { a }}</h2><h2>{{ address }}</h2><h2>{  { name }}</h2></div>

1.2.3、ViewModel

视图模型(ViewModel):对应Vue实例对象(VM)

3、ViewModel

视图模型(ViewModel):对应Vue实例对象(VM)<script type="text/javascript">// ViewModelnew Vue({.   // Viewel: '#root', // Modeldata: {  a: '',address: '', name: ''})</script>

1.3 Vue.js是什么

Vue(读音/vjuː/,类似于view)是一套构建用户界面的渐进式框架。与其它重量级框架不同的是,Vue.js采用自底向上增量开发的设计。

Vue.js本身只是一个JS库,它的目标是通过尽可能简单的API实现响应的数据绑定和组合的视图组件。Vue.js可以轻松构建SPA(Single Web Application)应用程序,通过指令扩展HTML,通过表达式将数据绑定到HTML,最大程度解放DOM操作。

1.4 安装Vue.js

将Vue.js添加到项目中有4种主要方法:本地独立版本方法、CDN方法、NPM方法以及命令行工具(CLI)方法。

本地独立版本方法

可通过地址“unpkg.com/vue@next”将最…

首先安装一个live server插件,在helllovue.html代码上点击右键出现一个名为Open with Live Server的选项,自动打开浏览器,默认端口号是5500。

图片图片

CDN方法

可通过CDN(Content Delivery Network,内容分发网络)引入最新版本的Vue.js库。

图片

NPM方法

在使用Vue.js构建大型应用时推荐使用NPM安装最新稳定版的Vue.js,因为NPM能很好地和webpack模块打包器配合使用。示例如下:

npm install vue@next

命令行工具(CLI)方法

Vue.js提供一个官方命令行工具(Vue CLI),为单页面应用快速搭建繁杂的脚手架。对于初学者不建议使用NPM和Vue CLI方法安装Vue.js。

具体步骤可以参考下面的链接;

aistudy.baidu.com/okam/pages/…

1.5 第一个Vue.js程序

可通过“code.visualstudio.com”地址下载VSCode,本书使用的安装文件是VSCodeUserSetup-x64-1.52.1.exe(双击即可安装)。

图片图片

const vueApp = Vue.createApp({        //数据        data() {            return {                title"Vue3.0 使用 Vue.createApp() 创建一个应用程序 ",                userInfo: {} //定义用户对象            }        },        //初始化的入口        created: function () {            //调用方法:获取用户信息            this.getUserInfo();        },        //方法        methods: {            //获取用户信息            getUserInfo: function () {                this.userInfo = {                    userId1,                    userName"vivi的博客",                    blogName"您好,欢迎访问 vivi的博客",                    blogUrl"https://blog.csdn.net/vivi"                }            }        }        //使用 mount() 方法,装载应用程序实例的根组件    }).mount('#app'); 

每个Vue.js应用都是通过用createApp函数创建一个新实例开始,具体语法如下:

  const app = Vue.createApp({ /* 选项 */ }) 通过上面那个图片,可以看出这个选项可以定义一个参数,也可以包裹一个data数据,所以选项是选择。

传递给createApp的选项用于配置根组件(渲染的起点)。Vue.js应用创建后,调用mount方法将Vue.js应用挂载到一个DOM元素(HTML元素或CSS选择器)中,例如,如果把一个Vue.js应用挂载到

上,应传递#app。示例代码如下:

  const HelloVueApp = {}//配置根组件

  const vueApp = Vue.createApp(HelloVueApp)//创建Vue实例

  const vm = vueApp.mount('#hello-vue')//将Vue实例挂载到#app

1.6 插值与表达式

Vue的插值表达式“{ { }}”的作用是读取Vue.js中data数据,显示在视图中,数据更新,视图也随之更新。“{ { }}”里只能放表达式(有返回值),不可以放语句,例如,{ { var a = 1 }}与{ { if (ok) { return message } }}都是无效的。

数据绑定最常见的形式就是使用“Mustache(小胡子)”语法(双花括号)的文本插值,它将绑定的数据实时显示出来。例如,{ { counter }},无论何时,绑定的Vue.js实例的counter属性值发生改变,插值处的内容都将更新。

“{ { }}”将数据解释为普通文本,而非HTML代码。当我们需要输出真正的HTML代码时,可使用v-html指令。

假如,Vue.js实例的data为:

data() {            return {                rawHtml'<hr>'            }    }

则“

无法显示HTML元素内容: { { rawHtml }}

”显示的结果是
;而“

可正常显示HTML元素内容:

”显示的结果是一条水平线。

对于所有的数据绑定,Vue.js都提供了完全的JavaScript表达式支持。示例如下:

{ { number + 1 }}

{ { isLogin? 'True' : 'False' }}

{ { message.split('').reverse().join('')}}

HTML Element 的 alt 属性详解 —— 渐进式 Web 可访问性实践

2025年5月18日 11:00

在 Web 开发领域, HTML 元素 中的 alt 属性 是一项关键技术,其作用涉及到浏览器渲染、搜索引擎优化以及无障碍访问。 alt 属性 通常应用于 img 标签 上,其主要功能是为图像提供替代文本,以便在图像无法正常加载或用户使用屏幕阅读器时,仍能获取图像所表达的信息。下文将通过严谨的推理与分步分析,结合真实世界的案例,深入探讨 alt 属性 的各个方面。

在理解 alt 属性 之前,有必要认识到图像在 Web 页面中占据的重要地位。 Web 页面的图像能够传递丰富的信息与情感,但同时也存在图像加载失败、网络延迟或设备兼容性问题等风险。考虑到这一点, alt 属性 的诞生即为解决这些问题而设计。它不仅使页面内容在图像缺失时仍然保持可读性,同时也是提升 Web 可访问性和用户体验的基础措施之一。

在解释 alt 属性 的工作原理时,可以将其视为图像的备用说明。设想某个电子商务网站展示产品图片,但在网络状况不佳或图片资源被阻止加载的情况下,用户会看到一段简洁的文字描述,而不是空白区域或破损图标。此时, alt 属性 就发挥了关键作用。借助 alt 属性,浏览器能够在无法渲染图像时,显示由开发者预先定义的文本,从而保证用户仍能理解该部分内容。这一机制不仅提升了用户体验,也为搜索引擎提供了识别页面内容的依据,间接助力页面排名和搜索优化。

通过对 alt 属性 工作机制的深入分析,可以发现浏览器内核在渲染页面时,会首先加载 HTML 代码并解析各个标签,遇到 img 标签 时便会检查其 alt 属性 是否存在。如果图像资源加载成功, alt 属性 中的文本通常不会直接呈现;但当图像加载失败或者用户启用了无图模式时,浏览器会显示 alt 属性 的文本内容。屏幕阅读器 在为视障用户朗读页面内容时,同样会调用 alt 属性 的文本,使得用户能够理解图像所要表达的信息。这样一来,无论用户处于何种环境, alt 属性 都能够保证页面信息传达的完整性。

考虑到实际应用情景,不少知名企业均将 alt 属性 作为无障碍设计的重要组成部分。例如,某国际知名电商平台在产品详情页中,不仅为产品图片添加了详细描述,而且在图片为装饰性用途时,会将 alt 属性 设置为空字符串(alt=``""``")以避免冗余信息被屏幕阅读器朗读。这种实践经过反复验证,显著提升了页面对各类用户的友好度。以实际案例来看,一家主打家居产品的企业,在设计网站时,专门聘请了无障碍设计专家,通过对每个 img 标签 设置符合语义要求的 alt 属性,最终实现了访问量和转化率的双提升,用户反馈中对页面易用性给予了高度评价。

对 alt 属性 的最佳实践之一是确保替代文本的描述应尽可能精确与简洁。开发者在撰写 alt 属性 文本时,应避免使用模糊或无意义的词语。换句话说, alt 属性 的文本内容需要能够客观、准确地传达图像信息。设想一家旅游网站展示风景照片,如果 alt 属性 文本仅仅写成 图片,便难以让用户感知图像所蕴含的具体内容;而如果能够写成 夕阳西下 海边悬崖,则能更好地传递情感与场景。因而在实际操作中,合理描述图像内容显得尤为重要。

在讨论 alt 属性 的应用时,还需要注意其与其他 HTML 属性 的关系。 Web 开发过程中常常会使用到 title 属性、 aria-label 属性等,这些属性各有侧重。 title 属性 提供了鼠标悬停提示信息,而 aria-label 属性 则用于无障碍辅助技术,两者与 alt 属性 有相互补充的作用。与此同时,开发者需要区分装饰性图像与功能性图像之间的差异。对于纯粹起装饰效果的图像, alt 属性 通常应设置为空字符串(例如: <img src=decorative.jpg alt=```` />),以免干扰屏幕阅读器的内容输出。反之,对于承载主要信息的图像, alt 属性 的文本需要包含足够的信息细节,使得不使用图像的用户也能理解页面的意图。

回顾 alt 属性 的历史背景,可以发现早期的 Web 标准对无障碍访问的要求较低,但随着无障碍立法和用户需求的不断提升, alt 属性 的重要性逐步凸显。开发者们逐渐认识到,任何忽视 alt 属性 设计的网页,往往难以满足现代 Web 用户的多样化需求。国际标准组织( W3C )在其 Web 可访问性指南( WCAG )中,对 alt 属性 的使用提出了详细建议。透过这些指导原则,开发者可以确保页面在视觉、听觉和认知等各个层面都具有良好的可访问性,从而实现真正的包容性设计。

对浏览器内核而言, alt 属性 的处理机制体现了软件设计中容错与降级加载的思想。具体而言,当图像资源无法呈现时,内核会自动调用 alt 属性 中的文本作为占位符,这种设计不仅提高了系统的健壮性,还确保了用户体验的一致性。开发者在编写 HTML 代码时,应充分考虑到这一点,从而构建出既美观又实用的 Web 页面。举例来说,在新闻网站上,编辑在发布文章时,经常会上传配图;若因网络故障或图片链接失效, alt 属性 中的描述文本便成为文章内容的重要组成部分,使得读者仍能领略文章所要表达的意境。

在真实项目中, alt 属性 的设置经常涉及跨部门协作。设计师、开发者与内容编辑需要密切配合,确保每个图像都能准确反映其语义。例如,某大型在线教育平台在设计课程介绍页面时,不仅在产品原型阶段就明确规定了图像 alt 属性 的填写要求,还在上线后定期通过无障碍测试工具检测页面是否存在缺失或错误的 alt 属性。该平台借助详细的规范和自动化测试手段,成功降低了因图像描述不当导致的用户困惑和无障碍障碍风险,进一步巩固了品牌在用户心中的专业形象。

在技术实现上, alt 属性 与浏览器渲染引擎 的协作体现了 Web 开发中“优雅降级”与“渐进增强”的设计理念。渲染引擎在解析 HTML 代码时,会优先确定页面结构,然后根据各标签的属性值进行资源加载与显示。当 img 标签 中存在 alt 属性 时,无论图像是否加载成功,页面最终都能呈现出对用户友好的文本信息。对开发者而言,理解这一点可以帮助其在出现异常情况时迅速定位问题,并对症下药。举例而言,某网站在服务器负载高峰期出现部分图片加载失败,通过检测 HTML 源码发现部分 img 标签 缺失 alt 属性,故而在后续版本中及时修正,最终使得用户体验得到明显改善。

在讨论 alt 属性 的同时,还应关注到其在搜索引擎优化( SEO )中的作用。搜索引擎在对网页进行爬取和索引时,会分析 alt 属性 中的文本,从而判断图像所代表的内容是否与页面主题相关。合理使用 alt 属性 能够提升网页在相关搜索结果中的排名,进而吸引更多目标用户访问。比如,一家美食博客在展示菜肴图片时,通过在 alt 属性 中描述菜名与主要原料,成功使页面在美食搜索关键词中获得较高排名,从而带来大量精准流量。

在项目实践过程中, alt 属性 的编写需要遵循统一标准与规范。部分开发团队会借助静态代码分析工具检测 HTML 文件中的 alt 属性 是否合理存在,确保没有遗漏或错误。团队内部通常会制定详细的文档说明,规定何时应设置 alt 属性、何时应将其置为空以及如何描述图像信息。某科技公司在制定前端开发规范时,将 alt 属性 的设置作为代码审核的重要一环,并通过代码提交钩子( hook )自动检查这一问题。经过这种流程管控,团队大大降低了因 alt 属性 忽略导致的用户体验问题,并为后续的无障碍测试提供了有力的数据支持。

针对不同类型的图像, alt 属性 的描述策略也有所区别。功能性图像,例如按钮或图标,应当通过 alt 属性 明确传达其操作意义;而纯装饰性图像则可以将 alt 属性 设置为空字符串,从而让辅助技术忽略这些无关信息。以一个社交平台为例,其在设计消息通知图标时,会在 img 标签 中设置 alt=``""提醒图标"",而对于背景装饰图片则使用 alt=``""`。这种区分处理既保证了视觉效果,又提升了页面在无障碍环境下的使用流畅度。

综上所述, alt 属性 在 HTML 元素 中具有不可替代的重要性,它不仅是图像加载失败时的备用文本,更是提升无障碍访问与搜索引擎优化的重要手段。通过对浏览器内核渲染流程的理解、对无障碍设计需求的关注以及对真实项目案例的借鉴,开发者可以更好地掌握 alt 属性 的使用技巧,从而打造出兼具美观与实用的 Web 页面。无论是面向广泛用户群体的商业网站,还是注重用户体验的个人博客, alt 属性 的合理设置都将成为成功 Web 开发的重要基石。通过不断实践与改进,开发团队能够在提升页面加载效率的同时,保障所有用户均能获得一致且优质的体验,最终实现包容性设计与高效沟通的目标。

计算机图形学三维坐标系统全面解析

作者 Mintopia
2025年5月18日 10:45

一、三维坐标系统基础认知

在计算机图形学的领域中,三维坐标系统是构建虚拟三维空间的重要基石,它借助三个相互垂直的坐标轴来精准定位空间里的每一个点。

(一)常见三维坐标系统分类

  1. 笛卡尔坐标系统
    • 该系统包含三个两两垂直的坐标轴,分别是 X 轴(代表水平方向)、Y 轴(代表垂直方向)和 Z 轴
    • 依据坐标轴方向的不同,又可细分为左手坐标系和右手坐标系。
      • 左手坐标系:伸出左手,让大拇指指向 X 轴正方向,食指指向 Y 轴正方向,那么中指所指方向就是 Z 轴正方向。
      • 右手坐标系:伸出右手,使大拇指指向 X 轴正方向,食指指向 Y 轴正方向,此时中指所指方向为 Z 轴正方向。
  1. 摄像机坐标系统
    • 此系统以虚拟摄像机的视角作为参考,其坐标轴定义如下:
      • X 轴:指向摄像机的右侧。
      • Y 轴:指向摄像机的上方。
      • Z 轴:指向摄像机的前方(也就是视线的方向)。
  1. 局部坐标系统与世界坐标系统
    • 局部坐标系统:每个物体都拥有自身独立的坐标系统,便于对物体进行局部变换操作。
    • 世界坐标系统:是整个场景所使用的全局坐标系统,用于确定所有物体在场景中的绝对位置。

二、三维坐标变换操作

在三维空间中,物体的位置、方向和大小等属性可以通过坐标变换来实现调整,主要的变换类型包括平移、旋转和缩放。

(一)平移变换

平移变换是指将物体沿着某个坐标轴方向进行移动。假设在三维空间中有一个点 ( (x, y, z) ),要将其沿着 X 轴平移 ( t_x ) 个单位,沿着 Y 轴平移 ( t_y ) 个单位,沿着 Z 轴平移 ( t_z ) 个单位,那么平移后的点坐标为 ( (x + t_x, y + t_y, z + t_z) )。

在 JavaScript 中,可以通过以下代码实现平移变换:

function translate(point, tx, ty, tz) {
    return {
        x: point.x + tx,
        y: point.y + ty,
        z: point.z + tz
    };
}
// 示例:将点(1, 2, 3)沿X轴平移2个单位,Y轴平移3个单位,Z轴平移4个单位
const point = { x: 1, y: 2, z: 3 };
const translatedPoint = translate(point, 2, 3, 4);
// 输出结果:{ x: 3, y: 5, z: 7 }

(二)旋转变换

旋转变换是围绕某个坐标轴对物体进行旋转操作,这里我们以绕 X 轴、Y 轴、Z 轴旋转为例进行说明。

  1. 绕 X 轴旋转

假设点 ( (x, y, z) ) 绕 X 轴旋转角度 ( \theta ),旋转后的点坐标计算方式如下:

新的 Y 坐标为 ( y \times \cos\theta - z \times \sin\theta )

新的 Z 坐标为 ( y \times \sin\theta + z \times \cos\theta )

X 坐标保持不变,即仍为 ( x )。

在 JavaScript 中实现绕 X 轴旋转的代码如下:

function rotateX(point, angle) {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    return {
        x: point.x,
        y: point.y * cos - point.z * sin,
        z: point.y * sin + point.z * cos
    };
}
// 示例:将点(0, 1, 0)绕X轴旋转90度(π/2弧度)
const point = { x: 0, y: 1, z: 0 };
const rotatedPoint = rotateX(point, Math.PI / 2);
// 输出结果:{ x: 0, y: 0, z: 1 }
  1. 绕 Y 轴旋转

当点 ( (x, y, z) ) 绕 Y 轴旋转角度 ( \theta ) 时,旋转后的点坐标为:

新的 X 坐标为 ( x \times \cos\theta + z \times \sin\theta )

新的 Z 坐标为 ( -x \times \sin\theta + z \times \cos\theta )

Y 坐标不变,为 ( y )。

JavaScript 实现代码如下:

function rotateY(point, angle) {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    return {
        x: point.x * cos + point.z * sin,
        y: point.y,
        z: -point.x * sin + point.z * cos
    };
}
// 示例:将点(1, 0, 0)绕Y轴旋转90度(π/2弧度)
const point = { x: 1, y: 0, z: 0 };
const rotatedPoint = rotateY(point, Math.PI / 2);
// 输出结果:{ x: 0, y: 0, z: 1 }
  1. 绕 Z 轴旋转

点 ( (x, y, z) ) 绕 Z 轴旋转角度 ( \theta ) 后,坐标变化为:

新的 X 坐标为 ( x \times \cos\theta - y \times \sin\theta )

新的 Y 坐标为 ( x \times \sin\theta + y \times \cos\theta )

Z 坐标不变,是 ( z )。

JavaScript 代码如下:

function rotateZ(point, angle) {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    return {
        x: point.x * cos - point.y * sin,
        y: point.x * sin + point.y * cos,
        z: point.z
    };
}
// 示例:将点(0, 1, 0)绕Z轴旋转90度(π/2弧度)
const point = { x: 0, y: 1, z: 0 };
const rotatedPoint = rotateZ(point, Math.PI / 2);
// 输出结果:{ x: -1, y: 0, z: 0 }

(三)缩放变换

缩放变换用于改变物体的大小。对于点 ( (x, y, z) ),分别沿着 X 轴、Y 轴、Z 轴进行缩放,缩放因子为 ( s_x )、( s_y )、( s_z ),缩放后的点坐标为 ( (x \times s_x, y \times s_y, z \times s_z) )。

JavaScript 实现代码如下:

function scale(point, sx, sy, sz) {
    return {
        x: point.x * sx,
        y: point.y * sy,
        z: point.z * sz
    };
}
// 示例:将点(2, 2, 2)在三个轴上都缩放2倍
const point = { x: 2, y: 2, z: 2 };
const scaledPoint = scale(point, 2, 2, 2);
// 输出结果:{ x: 4, y: 4, z: 4 }

三、三维坐标系统的实际应用场景

(一)三维建模领域

在三维建模软件中,如 Blender、Maya 等,设计师借助三维坐标系统来精确确定模型中每个顶点的位置,通过对顶点进行平移、旋转和缩放等操作,构建出复杂的三维模型。

(二)游戏开发领域

在游戏开发过程中,三维坐标系统用于确定游戏角色、场景物体以及摄像机的位置和方向。例如,通过对游戏角色进行平移变换使其在场景中移动,通过旋转变换改变角色的朝向,通过缩放变换实现角色的变大或变小等效果。

(三)虚拟现实(VR)和增强现实(AR)领域

在 VR 和 AR 应用中,三维坐标系统至关重要。它用于跟踪用户的头部和手部运动,并将这些运动转换为虚拟环境中的坐标变换,从而为用户带来沉浸式的体验。例如,当用户转动头部时,系统通过摄像机坐标系统的变换来更新虚拟场景的视角。

四、总结

三维坐标系统是计算机图形学的核心概念之一,平移、旋转和缩放等坐标变换操作是实现三维图形效果的基础。通过深入理解三维坐标系统及其变换原理,并结合 JavaScript 等编程语言进行实践,能够更好地在计算机图形学领域进行开发和创作。在实际应用中,需要根据具体的场景选择合适的坐标系统和变换方式,以实现预期的图形效果。

以上是关于计算机图形学三维坐标系统的教学内容。你对这篇文章的内容深度、案例选择等方面有什么看法或进一步需求,欢迎随时告诉我。

Three.js 中计算两个物体之间的距离

作者 Mintopia
2025年5月18日 10:40

在 3D 场景开发中,计算两个物体之间的距离是常见需求。无论是实现碰撞检测、AI 行为逻辑,还是创建视觉特效,距离计算都是基础且关键的功能。本文将详细介绍在 Three.js 中如何计算两个物体之间的距离。

基本概念

在 Three.js 中,物体之间的距离通常指的是它们位置 (position) 之间的欧几里得距离。每个 Three.js 对象都有一个 position 属性,它是一个 Vector3 实例,表示该对象在 3D 空间中的坐标 (x, y, z)。

计算距离的方法

Three.js 的 Vector3 类提供了多种计算距离的方法:

  1. distanceTo () - 计算当前向量到另一个向量的距离
  1. distanceToSquared () - 计算距离的平方 (性能更好,适用于比较距离大小的场景)

下面是一个简单的示例,展示如何使用这些方法:

// 假设我们有两个Three.js对象
const object1 = new THREE.Mesh(geometry, material);
const object2 = new THREE.Mesh(geometry, material);
// 设置它们的位置
object1.position.set(10, 5, 0);
object2.position.set(4, 1, 0);
// 计算它们之间的距离
const distance = object1.position.distanceTo(object2.position);
console.log('两个物体之间的距离是:', distance); // 输出约为7.21
// 如果只需要比较距离大小,可以使用distanceToSquared()
const distanceSquared = object1.position.distanceToSquared(object2.position);
console.log('距离的平方是:', distanceSquared); // 输出约为52

应用示例:距离检测系统

下面是一个完整的示例,展示如何实现一个简单的距离检测系统。当两个物体之间的距离小于某个阈值时,我们会改变它们的颜色。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js 距离检测示例</title>
    <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
</head>
<body>
    <script>
        // 创建场景、相机和渲染器
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        // 创建两个立方体
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        
        // 物体1 - 红色立方体
        const material1 = new THREE.MeshBasicMaterial({ color: 0xff0000 });
        const object1 = new THREE.Mesh(geometry, material1);
        object1.position.set(-3, 0, 0);
        scene.add(object1);
        
        // 物体2 - 蓝色立方体
        const material2 = new THREE.MeshBasicMaterial({ color: 0x0000ff });
        const object2 = new THREE.Mesh(geometry, material2);
        object2.position.set(3, 0, 0);
        scene.add(object2);
        // 添加坐标轴辅助
        const axesHelper = new THREE.AxesHelper(5);
        scene.add(axesHelper);
        // 设置相机位置
        camera.position.z = 5;
        // 距离阈值
        const distanceThreshold = 4;
        // 创建一个标签显示距离
        const distanceLabel = document.createElement('div');
        distanceLabel.style.position = 'absolute';
        distanceLabel.style.top = '10px';
        distanceLabel.style.left = '10px';
        distanceLabel.style.color = 'white';
        distanceLabel.style.fontFamily = 'Arial';
        distanceLabel.style.fontSize = '16px';
        document.body.appendChild(distanceLabel);
        // 动画循环
        function animate() {
            requestAnimationFrame(animate);
            // 让物体2向物体1移动
            object2.position.x -= 0.01;
            
            // 如果物体2移动到左边太远,则重置位置
            if (object2.position.x < -5) {
                object2.position.x = 3;
            }
            // 计算两个物体之间的距离
            const distance = object1.position.distanceTo(object2.position);
            
            // 更新标签显示
            distanceLabel.textContent = `距离: ${distance.toFixed(2)} (阈值: ${distanceThreshold})`;
            // 当距离小于阈值时,改变颜色
            if (distance < distanceThreshold) {
                object1.material.color.set(0x00ff00);
                object2.material.color.set(0x00ff00);
            } else {
                object1.material.color.set(0xff0000);
                object2.material.color.set(0x0000ff);
            }
            renderer.render(scene, camera);
        }
        animate();
    </script>
</body>
</html>

在这个示例中,我们创建了两个立方体,一个红色一个蓝色。蓝色立方体会自动向红色立方体移动,我们实时计算它们之间的距离并显示出来。当距离小于设定的阈值时,两个立方体都会变成绿色。

性能优化

在处理大量对象的场景中,计算每个对象之间的距离可能会影响性能。以下是一些优化建议:

  1. 使用 distanceToSquared () 代替 distanceTo (),避免开平方运算
  1. 实现空间分区算法 (如八叉树) 来减少需要计算距离的对象数量
  1. 限制距离计算的频率,不必每帧都计算

更复杂的距离计算

在某些情况下,你可能需要计算更复杂的距离,比如:

  1. 从一个点到一个物体表面的距离
  1. 两个物体边界框之间的距离
  1. 两个物体碰撞体之间的最小距离

对于这些情况,Three.js 提供了 Box3、Sphere 等类,它们也有类似的 distanceTo 方法。你还可以使用 Raycaster 来计算点到物体表面的距离。

通过掌握这些技术,你可以在 Three.js 中实现各种复杂的交互和效果,从简单的距离提示到高级的物理模拟。

一文读懂 SSE

2025年5月17日 19:01

什么是SSE

SSE(Server Sent Events)是一种基于HTTP的轻量级实时通信技术,允许服务端主动向客户端推送数据,适用于需要进行实时数据流的场景。

从表面看,SSE 和 WebSocket 似乎都能实现“双向通信” —— 毕竟数据能在客户端和服务器之间流动。

双向通信的错觉

实际上,SSE 的通信模式是 单向的(服务器 → 客户端),而所谓的“双向”需要额外配合其他技术(如客户端通过普通 HTTP 请求发送数据)。

每次客户端建立 EventSource 时(GET请求),携带自定义参数,服务端收到请求后,解析参数,只要当前的 EventSource 没有关闭,服务端就能持续通过这个通道向客户端发送数据。

流程如下:

sse流程.png

从上图可以看出,整个数据传输的过程,除了客户端开头主动建立 SEE 请求外,后续都是由服务端给客户端发送数据, 这种单向推送机制正是 SSE 的核心设计特点

为了更清晰地理解 SSE,从以下几个维度对 SSE 和 WebSocket 进行对比:

特性 SSE WebSocket
协议基础 HTTP 长连接 独立的 WebSocket 协议
连接开销 低(复用 HTTP ) 中(需升级协议)
通信模式 单向(服务器→客户端) 双向通信
二进制支持 仅文本传输 支持文本与二进制数据
客户端 API 浏览器内置 EventSource 浏览器内置 WebSocket 对象
典型场景 服务器主动推送(如新闻推送、ai智能回复) 双向交互(如聊天、协作)

至于具体要选择 SSE 还是 WebSocket,应根据业务需求决定——是否需要真正的双向通信

如何使用 SSE

客户端建立 SSE 连接

在客户端建立 SSE 连接十分简洁,借助浏览器内置的 EventSource API,只需数行代码,就能向服务器发起连接。

const eventSource = new EventSource('/xxxx?question=hi'); 

eventSource.onmessage = (event) => {
    console.log('收到消息:', event.data);
};

上述代码中,通过 new EventSource 创建一个 SSE 实例,并指定连接的服务器地址,同时可以在 URL 中携带参数。

然后通过 onmessage 事件监听服务器推送的数据。

服务端响应

服务器端在响应 SSE 请求时,必须包含特定的响应头。

这些响应头包括:

  • Content-Type : text/event-stream; :必须将该响应头设置为text/event-stream,这是 SSE 协议规定的内容类型。
  • Connection: keep-alive; :保持长连接,使得服务器能够持续向客户端推送数据。

以 express 框架为例,服务端的实现代码如下:

app.get('/xxxx', async (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Connection', 'keep-alive');

    const question = decodeURIComponent(req.query.question); // 获取参数
    res.write(`data: this is a test data\n\n`); 
    res.write('data: [DONE]\n\n');
});

在这段代码中,首先通过 res.setHeader 设置了必要的响应头,然后使用res.write方法向客户端发送数据。

服务器发送的消息必须严格遵循 SSE 协议格式,常见的格式有以下几种:

  • 单行文本
res.write(`data: 这是一条普通消息\n\n`)

在浏览器开发者工具中的network面板里查看 EventStream,可以清晰地看到每一次服务器返回的数据:

image.png

  • 多行文本
res.write(`data: 这是一条普通消息\n\n`)
res.write(`data: 这是一条普通消息\n\n`)

浏览器查看结果:

image.png

  • JSON字符串:
 res.write(`data: ${JSON.stringify({foo: "hello world"})}\n\n`);

浏览器查看结果:

image.png

注意:收到的是JSON字符串,需要进行JSON解析

  • 自定义事件:

服务端:

  res.write(`event: custom event name\n`)
  res.write(`data: 这是一条消息\n\n`)

前端:

 eventSource.addEventListener("custom event name", e => {
    console.log(e.data); // 输出这是一条消息
});

浏览器查看结果:

image.png

数据遵循 SSE 协议格式,以data:开头,并且每条消息以两个换行符\n\n结尾。

实践

在阅读完上文内容后,我们对 SSE 有了初步的了解,下面就来看看 SSE 的应用。

在 deepseek 的问答中,就使用了 SSE 来传输结果:

屏幕录制2025-05-14 17.48.48.gif

仔细看这个请求,发现 deepseek 在使用 SSE 和 理论上的有区别:它使用了POST请求,而并不是用 EventSource API 来实现

image.png

这是一个典型的 POST 请求 + SSE 响应 的组合,是现代 AI 聊天服务的主流实现方式。

这种组合解决了两个核心需求:

  1. 数据量和数据格式

    • POST 请求允许通过请求体发送大量数据,比 GET 请求灵活。
  2. 实时响应

    • SSE 流式响应允许服务器在生成回复的过程中逐步发送给客户端,提升用户体验。

下面笔者将介绍这种组合的实现方式。

服务端代码:

    app.post('/api/chat/completion', async (req, res) => {
      // 设置 SSE 响应头
      res.setHeader('Content-Type', 'text/event-stream');
      res.setHeader('Cache-Control', 'no-cache');
      res.setHeader('Connection', 'keep-alive');

      try {

        // 模拟 AI 生成回复
        const replys = 
            "刚进入大学想成为前端开发者,建议从HTML/CSS/JavaScript基础学起,先做静态网页练习。大二重点学习React或Vue框架,掌握组件开发和状态管理,同时熟悉Git和Webpack等工具。大三开始做完整项目,学习TypeScript和性能优化,尝试参与开源或团队协作。大四前要完成3-5个高质量作品,部署上线并整理成作品集。平时多逛GitHub、掘金等技术社区,保持每周20小时以上的编码时间。找实习时重点准备前端面试题,包括CSS布局、JS原理和框架特性等。记住动手实践比只看教程更重要,遇到问题先自己调试再求助。保持对新技术的敏感度,但先深入掌握核心技能再扩展知识面。坚持每天写代码,毕业时就能达到初级前端工程师的水平。"
            .split(''); 

        for (const reply of replys) {
          // 发送每个数据块
          res.write(`data: ${reply}\n\n`);

          // 模拟生成延迟
          await new Promise(resolve => setTimeout(resolve, 20));
        }

        // 结束响应
        res.end();
      } catch (error) {
        res.write(`data: error\n\n`);
        res.end();
      }
    });

前端请求和处理结果:

    const response = await fetch('http://localhost:3000/api/chat/completion', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            // 问题ID
            // 问题等参数
            .....
        })
    });

    // 处理流式响应
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let accumulatedText = '';

    const answerId = 'answer-' + Date.now();

    chatContainer.innerHTML += `<p><strong>问:</strong>${question}</p>`;
    chatContainer.innerHTML += `<p><strong>答:</strong><span id="${answerId}"></span></p>`;

    // 清空输入框
    questionInput.value = '';

    const answerElement = document.getElementById(answerId);

    while (true) {
        const { done, value } = await reader.read();

        if (done) break;

        // 解码数据块
        const chunk = decoder.decode(value, { stream: true });

        // 解析每个 SSE 消息
        const messages = chunk.split('\n\n');
        for (const message of messages) {
            if (message.startsWith('data: ')) {
                try {
                    accumulatedText += message.substring(6);
                    answerElement.textContent = accumulatedText;
                } catch (e) {
                    console.error('解析消息失败:', e);
                }
            }
        }
    }

效果:

屏幕录制2025-05-17 18.44.01.gif

当然,在实际业务中并不会使用这么简单的数据格式,一般会使用自定义事件数据格式或者JSON字符串数据格式,在数据中包含各种类型,方便前端根据类型做出判断。

总结

SSE作为基于 HTTP 的轻量级实时通信技术,凭借其单向推送的核心特性,在服务器主动传输数据的场景中展现出独特优势。

昨天以前首页

Vue 中 provide/inject 与传统状态管理的深度对比

2025年5月17日 21:00

一、provide/inject 基础原理

1. 基本用法

// 祖先组件提供数据
export default {
  provide() {
    return {
      theme: 'dark',
      toggleTheme: this.toggleTheme
    }
  },
  methods: {
    toggleTheme() {
      this.theme = this.theme === 'dark' ? 'light' : 'dark'
    }
  }
}

// 后代组件注入使用
export default {
  inject: ['theme', 'toggleTheme'],
  template: `
    <button @click="toggleTheme">
      当前主题: {{ theme }}
    </button>
  `
}

2. 响应式数据传递

// 使用 Vue 3 的 reactive/ref
import { ref, provide } from 'vue'

export default {
  setup() {
    const count = ref(0)
    provide('count', count)
    
    return { count }
  }
}

// 后代组件
export default {
  setup() {
    const count = inject('count')
    return { count }
  }
}

二、provide/inject 的优势

1. 组件树穿透能力

场景‌:多层嵌套组件共享配置

// 根组件
provide('appConfig', {
  apiBaseUrl: 'https://api.example.com',
  features: {
    analytics: true,
    notifications: false
  }
})

// 第5层子组件直接使用
const config = inject('appConfig')
console.log(config.apiBaseUrl) // 直接访问

2. 减少 props 传递

传统方式‌:

// 每层组件都需要传递props
<Parent :config="config">
  <Child :config="config">
    <GrandChild :config="config" />
  </Child>
</Parent>

provide/inject 方式‌:

// 根组件
provide('config', config)

// 任意层级子组件
const config = inject('config')

3. 动态上下文共享

场景‌:表单组件与表单项通信

// Form 组件
provide('formContext', {
  registerField: (field) => { /* 注册字段 */ },
  validate: () => { /* 验证表单 */ }
})

// FormItem 组件
const { registerField } = inject('formContext')
onMounted(() => registerField(this))

三、provide/inject 的劣势

1. 调试困难

// 当多个祖先提供同名key时
const data = inject('settings') // 无法直观确认数据来源

// 解决方案:使用Symbol作为key
const SettingsKey = Symbol()
provide(SettingsKey, { theme: 'dark' })
const settings = inject(SettingsKey)

2. 缺乏状态管理

// 简单的计数器示例
provide('counter', {
  count: 0,
  increment() { this.count++ }
})

// 问题:
// 1. 状态变更无法追踪
// 2. 多个组件修改时可能产生冲突

3. 类型安全缺失(JavaScript中)

// 无法像TypeScript那样进行类型检查
const user = inject('user') // 不知道user的结构

四、与传统状态管理(Vuex)对比

1. Vuex 基本示例

// store.js
export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    asyncIncrement({ commit }) {
      setTimeout(() => commit('increment'), 1000)
    }
  }
})

// 组件中使用
export default {
  computed: {
    count() {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
    }
  }
}

2. 对比表格

特性 provide/inject Vuex
作用范围 组件树局部 全局
调试工具 不可见 完整的时间旅行调试
响应式 自动响应式 自动响应式
代码组织 分散在各组件 集中式管理
类型安全 需要额外处理 需要类型定义
服务端渲染 天然支持 需要额外配置
性能 按需注入,内存友好 全局存储,初始加载稍慢
适用场景 组件库/局部状态共享 大型应用全局状态管理

五、实际场景选择指南

1. 适合 provide/inject 的场景

场景1:UI组件库开发

// 下拉菜单组件
provide('dropdown', {
  registerItem: (item) => { /* 注册菜单项 */ },
  close: () => { /* 关闭菜单 */ }
})

// 菜单项组件
const { registerItem, close } = inject('dropdown')
onMounted(() => registerItem(this))

场景2:主题切换

// 主题提供者
provide('theme', {
  current: 'light',
  colors: {
    light: { primary: '#fff' },
    dark: { primary: '#000' }
  }
})

// 任意子组件
const { current, colors } = inject('theme')
const bgColor = computed(() => colors[current].primary)

2. 适合 Vuex/Pinia 的场景

场景1:用户全局状态

// store/user.js (Pinia示例)
export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    token: ''
  }),
  actions: {
    async login(credentials) {
      const res = await api.login(credentials)
      this.name = res.name
      this.token = res.token
    }
  }
})

// 多个组件共享同一状态
const userStore = useUserStore()
userStore.login({...})

场景2:购物车管理

// store/cart.js (Vuex示例)
{
  state: {
    items: []
  },
  mutations: {
    ADD_ITEM(state, item) {
      state.items.push(item)
    }
  },
  getters: {
    totalPrice: (state) => {
      return state.items.reduce((sum, item) => sum + item.price, 0)
    }
  }
}

// 组件中使用
this.$store.commit('ADD_ITEM', product)
this.$store.getters.totalPrice

六、混合使用模式

1. 全局状态 + 局部增强

// 使用Pinia作为基础
const userStore = useUserStore()

// 在特定组件树中增强功能
provide('enhancedUser', {
  ...userStore,
  // 添加局部方法
  sendMessage() {
    console.log(`Message to ${userStore.name}`)
  }
})

2. 性能优化技巧

javascriptCopy Code
// 避免在provide中直接传递大对象
provide('heavyData', () => fetchHeavyData())

// 组件中按需获取
const getHeavyData = inject('heavyData')
const data = computed(() => getHeavyData())

七、决策流程图

graph TD
    A[需要共享状态?] -->|是| B{状态使用范围}
    B -->|全局多组件| C[Vuex/Pinia]
    B -->|特定组件树| D{状态复杂度}
    D -->|简单配置| E[provide/inject]
    D -->|复杂业务逻辑| C
    A -->|否| F[使用组件本地状态]

总结‌:

  • provide/inject 适合组件库开发和局部状态共享
  • Vuex/Pinia 适合大型应用全局状态管理
  • 在JavaScript项目中,注意通过命名规范和Symbol来避免注入冲突
  • 对于中型项目,可以考虑混合使用两种方案

❌
❌