普通视图

发现新文章,点击刷新页面。
昨天 — 2025年8月17日首页

前端必会:如何创建一个可随时取消的定时器

作者 烛阴
2025年8月17日 20:58

一、原生的取消方式

JavaScript 原生就提供了取消定时器的方法。setTimeoutsetInterval 在调用时都会返回一个数字类型的 ID,我们可以将这个 ID 传递给 clearTimeoutclearInterval 来取消它。

// 1. 设置一个定时器
const timerId: number = setTimeout(() => {
  console.log("这个消息可能永远不会被打印");
}, 2000);

// 2. 在它触发前取消它
clearTimeout(timerId);

常见痛点:

  • timerId 变量需要被保留在组件或模块的作用域中,状态分散。
  • 启动、暂停、取消的逻辑是割裂的,代码可读性和可维护性差。

二、封装一个可取消的定时器类

我们可以简单的封装一个 CancellableTimer 类,将定时器的状态和行为内聚在一起。后续可以扩展,把项目中的所有定时器进行统一管理。

// 定义定时器ID类型
type TimeoutId = ReturnType<typeof setTimeout>;

class CancellableTimer {
    private timerId: TimeoutId | null = null;

    constructor(private callback: () => void, private delay: number) {}

    public start(): void {
        // 防止重复启动
        if (this.timerId !== null) {
            this.cancel();
        }

        this.timerId = setTimeout(() => {
            this.callback();
            // 执行完毕后重置 timerId
            this.timerId = null;
        }, this.delay);
    }

    public cancel(): void {
        if (this.timerId !== null) {
            clearTimeout(this.timerId);
            this.timerId = null;
        }
    }
}

// 使用示例
console.log('定时器将在3秒后触发...');
const myTimer = new CancellableTimer(() => {
    console.log('定时器任务执行!');
}, 3000);

myTimer.start();

// 模拟在1秒后取消
setTimeout(() => {
    console.log('用户取消了定时器。');
    myTimer.cancel();
}, 1000);

三、实现可暂停和恢复的定时器

在很多场景下,我们需要的不仅仅是取消,还有暂停恢复

要实现这个功能,我们需要在暂停时记录剩余时间

type TimeoutId = ReturnType<typeof setTimeout>;

class AdvancedTimer {
    private timerId: TimeoutId | null = null;
    private startTime: number = 0;
    private remainingTime: number;
    private callback: () => void;
    private delay: number;


    constructor(callback: () => void, delay: number) {
        this.remainingTime = delay;
        this.callback = callback;
        this.delay = delay;
    }

    public resume(): void {
        if (this.timerId) {
            return; // 已经在运行
        }

        this.startTime = Date.now();
        this.timerId = setTimeout(() => {
            this.callback();
            // 任务完成,重置
            this.remainingTime = this.delay;
            this.timerId = null;
        }, this.remainingTime);
    }

    public pause(): void {
        if (!this.timerId) {
            return;
        }

        clearTimeout(this.timerId);
        this.timerId = null;
        // 计算并更新剩余时间
        const timePassed = Date.now() - this.startTime;
        this.remainingTime -= timePassed;
    }

    public cancel(): void {
        if (this.timerId) {
            clearTimeout(this.timerId);
        }
        this.timerId = null;
        this.remainingTime = this.delay; // 重置
    }
}

// 使用示例
console.log('定时器启动,5秒后执行...');
const advancedTimer = new AdvancedTimer(() => console.log('Done!'), 5000);
advancedTimer.resume();

setTimeout(() => {
    console.log('2秒后暂停定时器');
    advancedTimer.pause();
}, 2000);

setTimeout(() => {
    console.log('4秒后恢复定时器 , 应该还剩3秒');
    advancedTimer.resume();
}, 4000);

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

昨天以前首页

TypeScript 接口入门:定义代码的契约与形态

作者 烛阴
2025年8月16日 17:07

一、什么是接口?

用于描述一个对象的结构。

// 定义一个名为 User 的接口
interface User {
    id: number;
    name: string;
    email: string;
}

function printUserInfo(user: User) {
    console.log(`ID: ${user.id}, Name: ${user.name}, Email: ${user.email}`);
}

const myUser: User = {

    id: 1,
    name: 'Alice',
    email: 'alice@example.com',
};

printUserInfo(myUser); // OK

const invalidUser: User = {
    id: 2,
    username: 'Bob', // 属性名不匹配 编译时错误
    // 缺少 name,email 属性
};


二、接口的丰富特性

1. 可选属性(Optional Properties)

有时,对象的某些属性不是必需的。我们可以使用 ? 来标记它们。

interface UserProfile {
    id: number;
    username: string;
    bio?: string; // bio 是可选的
}

const user1: UserProfile = { id: 1, username: 'Alice' }; // OK
const user2: UserProfile = { id: 2, username: 'Bob', bio: 'Developer' }; // OK

2. 只读属性(Readonly Properties)

我们可以使用 readonly 关键字来防止对象属性在创建后被修改,这对于创建不可变数据非常有用。

interface Point {
    readonly x: number;
    readonly y: number;
}

const p1: Point = { x: 10, y: 20 };
p1.x = 5; // Error: 无法为“x”赋值,因为它是只读属性。

3. 函数类型

接口也能用来定义函数的签名(参数类型和返回值类型)。

interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc = function (src: string, sub: string) {
    let result = src.search(sub);
    return result > -1;
};

console.log(mySearch('hello', 'll'));

4. 可索引类型(Indexable Types)

接口可以描述那些可以通过索引得到的类型,比如数组和对象。

interface StringArray {
    [index: number]: string; // 索引是数字,值是字符串
}

let myArray: StringArray;
myArray = ['Bob', 'Fred'];
let myStr: string = myArray[0]; // OK
console.log(myStr);


interface Dictionary {
    [key: string]: any; // 索引是字符串,值是任意类型
}

let user: Dictionary = {
    name: '张三',
    age: 18,
    sex: '男',
}

console.log(user.name);

5. 类实现(Class Implementations)

接口可以被类(Class)implements(实现),强制一个类必须遵循接口定义的契约。

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date): void;
}

class Clock implements ClockInterface {
    currentTime: Date = new Date();
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) {
        this.currentTime.setHours(h);
        this.currentTime.setMinutes(m);
    }

    printTime() {
        console.log(this.currentTime.toLocaleTimeString());
    }
}


let clock = new Clock(12, 30);
clock.printTime(); //12:30:43
clock.setTime(new Date('2024-5-6 09:30:43'));
clock.printTime(); //09:30:43

三、接口的扩展与合并

1. 继承(Extends)

一个接口可以像类一样继承另一个接口,从而复用和扩展类型定义。

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

// Square 继承了 Shape 和 PenStroke
interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square: Square = {
    color: 'blue',
    penWidth: 5.0,
    sideLength: 10,
};

2. 声明合并(Declaration Merging)

这是一个接口独有的、非常强大的特性。如果你在同一个作用域内定义了两个同名的接口,它们会自动合并成一个单一的接口。

interface Box {
    height: number;
    width: number;
}

interface Box {
    scale: number;
}

// 合并后,Box 接口同时拥有 height, width, 和 scale 属性
const box: Box = { height: 5, width: 6, scale: 10 };

常用的用法 扩展第三方库的类型定义。例如,如果你想为 window 对象添加一个自定义属性,你可以这样做,而不会覆盖原有的定义:

// 在你的 .d.ts 文件中
declare global {
    interface Window {
        myAppConfig: object;
    }
}

// 现在你可以在代码中安全地访问它
window.myAppConfig = { version: '1.0' };

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多TypeScript开发干货

Clamp

作者 烛阴
2025年8月14日 21:47

Task

Write a GLSL program that draws a diagonal line from the bottom left corner of the texture to the top right corner. The line should have a width of 0.2 in normalized coordinates and be colored in (1.0, 0.3, 0.3). Additionally, ensure that the line is limited to values between 0.25 and 0.75 in Y coordinate.

编写一个GLSL程序,该程序绘制一条从纹理左下角到右上角的对角线。要求如下:

  • 线条在归一化坐标系中的宽度为 0.2
  • 线条颜色为 (1.0, 0.3, 0.3)
  • 此外,请确保该线条只在Y坐标

Theory

函数介绍

float clamp(float value, float minVal, float maxVal)

clamp 将一个值限制在一个闭区间 [min, max] 内,如果小于minVal则返回minVal,如果大于maxVal则返回maxVal,否则返回value

  • value: 你想要限制的原始值。
  • minVal: 区间的下限(最小值)。
  • maxVal: 区间的上限(最大值)。

Answer

uniform vec2 iResolution;

void main() {
  vec2 uv = gl_FragCoord.xy / iResolution.xy;

  float lineWidth = 0.2;
  vec3  lineColor = vec3(1.0, 0.3, 0.3);
  float value = clamp(uv.x, 0.25, 0.75);
  // 获得当前点的y坐标,用来下面进行判断颜色用,如果当前点的y坐标大于线条宽度的一半则舍弃这个点
  float dist  = abs(uv.y - value); 
  float line  = 1.0 - step(lineWidth * 0.5, dist);
  
  gl_FragColor = vec4(lineColor * line, 1.0);
}


效果

image.png

练习

Clamp

最后

如果你觉得这篇文章有用,记得点赞、关注、收藏,学Shader更轻松!!

告别 any!用联合类型打造更灵活、更安全的 TS 代码

作者 烛阴
2025年8月13日 23:08

一、什么是联合类型?

联合类型使用竖线 | 作为分隔符,表示一个值可以是列出的类型中的任意一种。

// ID 只接收数字或字符串作为参数
function printId(id: number | string) {
      console.log("Your ID is: " + id);
}

printId(101);       // OK
printId("202");     // OK
printId({ id: 303 }); // 类型“{ id: number; }”的参数不能赋给类型“string | number”的参数。

二、使用类型守卫收窄类型(断言类型)

1. typeof 类型守卫

typeof 是最常见的类型守卫,一般处理 string, number, boolean, symbol, bigint, undefined, function 这些基础类型时使用。

function printId(id: number | string) {
    if (typeof id === 'string') {
        // 在这个代码块内,TypeScript 知道 id 的类型是 string
        console.log(id.toUpperCase());
    } else {
        // 在这个代码块内,TypeScript 知 id 的类型是 number
        console.log(id);
    }
}

printId('good');
printId(10);

2. instanceof 类型守卫

当处理类的实例时,使用instanceof 判断类型

class User {
  constructor(public name: string) {
    this.name =  name;
   }
}

class Product {
  constructor(public title: string) { 
    this.title = title;
  }
}

function printEntity(entity: User | Product) {
  if (entity instanceof User) {
    // entity 被收窄为 User 类型
    console.log("User: " + entity.name);
  } else {
    // entity 被收窄为 Product 类型
    console.log("Product: " + entity.title);
  }
}

let user = new User('john')
printEntity(user)
let product = new Product('title')
printEntity(product)

3. in 操作符守卫

在判断对象的属性时,常常使用in

interface Fish {
  swim: () => void;
}

interface Bird {
  fly: () => void;
}

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    // animal 被收窄为 Fish 类型
    return animal.swim();
  }
  // animal 被收窄为 Bird 类型
  return animal.fly();
}

let fish = {
  swim:()=>{
    console.log('fish is swim');
  }
}

let bird = {
  fly:()=>{
    console.log('bird fly');
  }
}

move(fish);
move(bird);

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多TypeScript开发干货

Dot

作者 烛阴
2025年8月12日 22:54

Task

Write a program that draws a triangle in the center of the screen. The triangle should have an apex at (0.5, 0.75) in normalized device coordinates, and an apex angle of 120 degrees. The height of the triangle should be 0.5 times the height of the screen.

编写一个程序,在屏幕正中央绘制一个三角形。该三角形的顶点位于归一化设备坐标(NDC)的 (0.5, 0.75),顶角为 120 度,且三角形的高度为屏幕高度的 0.5 倍。

Theory

函数介绍

dot 函数用于计算两个向量的点积(数量积),返回一个标量值。

主要用途

  1. 计算夹角余弦值
vec2 a = normalize(vector1);
vec2 b = normalize(vector2);
float angle = dot(a, b);  // cos(夹角)

2. 计算投影长度

//投影长度 = dot(a, b) / length(b)
vec2 direction = normalize(dir);
vec2 toPoint = point - origin;
float projectionLength = dot(direction, toPoint);

3. 判断向量方向关系

  • dot(A, B) = 1:方向完全相同。
  • dot(A, B) = 0:方向互相垂直。
  • dot(A, B) = -1:方向完全相反。

Answer

uniform vec2 iResolution;

#define PI 3.14

void main() {
  vec2 uv = gl_FragCoord.xy / iResolution.xy;

  vec2 ratio = vec2(iResolution.x / iResolution.y, 1.0);
  uv -= 0.5;
  uv *= ratio;
 
  // 顶上的点(0.5, 0.75)
  vec2 p1 = vec2(0, 0.25);
  // 底部点
  vec2 p2 = vec2(0, -0.25);
  // 已知线的方向
  vec2 p1Dir = normalize(p2 - p1);
  // 当前点到顶点的向量
  vec2 dis = uv - p1;
   // 当前点到顶点的方向
  vec2 p2Dir = normalize(dis);
  // 计算夹角
  float t = dot(p1Dir, p2Dir);
  // 剔除大于60° cos(60) == 0.5
  float red = step(0.5, t);
  // 获取当前点在已知线上的投影长度(已知线的长度为0.5)
  float d = dot(p1Dir, dis); 
  red *= (1.0 - step(0.5, d));

  gl_FragColor = vec4(red, 0.0, 0.0, 1.0);
}

效果

image.png

练习

Dot

最后

如果你觉得这篇文章有用,记得点赞、关注、收藏,学Shader更轻松!!

Vector Normaliztion -- 向量归一化

作者 烛阴
2025年8月11日 22:40

Task

Write a shader program that divides the screen into two equal parts. The origin of the left part is at (0.25, 0.5), and the origin of the right part is at (0.75, 0.5). In the left part, display the cosine of the angle between the positive X-axis and the vector directed from the origin to the current pixel position. In the right part, display the sine of the angle between the positive X-axis and the vector directed from the origin to the current pixel position.

编写一个着色器程序,将屏幕分割成两个相等的部分。左侧部分的原点位于 (0.25, 0.5),右侧部分的原点位于 (0.75, 0.5)。在左侧部分,显示X轴正方向与从原点指向当前像素位置的向量之间夹角的余弦值。在右侧部分,显示X轴正方向与从原点指向当前像素位置的向量之间夹角的正弦值。

Theory

函数介绍

  • normalize(): 向量归一化(Vector Normalization)

它接收一个向量,然后返回一个指向相同方向,但长度恰好为 1 的单位向量(Unit Vector)

常常用于光照计算,方向相关内容

点积介绍

两个单位向量进行点积运算时,其结果等于它们之间夹角的余弦值(cosine)

  • dot(A, B) = 1:方向完全相同。
  • dot(A, B) = 0:方向互相垂直。
  • dot(A, B) = -1:方向完全相反。

Answer

uniform vec2 iResolution;

void main() {
  vec2 uv = gl_FragCoord.xy / iResolution.xy;

  //左边,和方向有关时需要向量归一化
  vec2 left = normalize(uv - vec2(0.25, 0.5));
  // 右边
  vec2 right = normalize(uv - vec2(0.75, 0.5));
  
  float condition = step(0.5, uv.x);
  // 因为是单位向量,所有cos = left.x
  float t = (1.0 - condition) * left.x + condition * right.y ;
  
  gl_FragColor = vec4(vec3(t), 1.0);
}

效果

image.png

练习

Vector Normaliztion

最后

如果你觉得这篇文章有用,记得点赞、关注、收藏,学Shader更轻松!!

❌
❌